#include "HalClock.h" #include #include #include #include #include HalClock halClock; // Singleton instance // DS3231 register layout (BCD encoded): // 0x00: Seconds (bits 6-4 = tens, bits 3-0 = ones) // 0x01: Minutes (bits 6-4 = tens, bits 3-0 = ones) // 0x02: Hours (bit 6 = 12/24 mode, bits 5-4 = tens, bits 3-0 = ones) static uint8_t bcdToDec(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F); } static uint8_t decToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); } void HalClock::begin() { if (!gpio.deviceIsX3()) { _available = false; return; } // I2C is already initialised by HalPowerManager::begin() for X3. // Probe the DS3231 by reading the seconds register. Wire.beginTransmission(I2C_ADDR_DS3231); Wire.write(DS3231_SEC_REG); if (Wire.endTransmission(false) != 0) { LOG_INF("CLK", "DS3231 RTC not found"); _available = false; return; } Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)1); if (Wire.available() < 1) { _available = false; return; } Wire.read(); // discard — just testing connectivity _available = true; LOG_INF("CLK", "DS3231 RTC found"); // Prime the cache with an initial read uint8_t h, m; getTime(h, m); } bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const { if (!_available) return false; const unsigned long now = millis(); if (_lastPollMs != 0 && (now - _lastPollMs) < CLOCK_POLL_MS) { hour = _cachedHour; minute = _cachedMinute; return true; } // Read 3 bytes starting at register 0x00: seconds, minutes, hours Wire.beginTransmission(I2C_ADDR_DS3231); Wire.write(DS3231_SEC_REG); if (Wire.endTransmission(false) != 0) { if (!_hasCachedTime) return false; _lastPollMs = now; hour = _cachedHour; minute = _cachedMinute; return true; } Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)3); if (Wire.available() < 3) { if (!_hasCachedTime) return false; _lastPollMs = now; hour = _cachedHour; minute = _cachedMinute; return true; } Wire.read(); // seconds — not needed const uint8_t rawMin = Wire.read(); const uint8_t rawHour = Wire.read(); _cachedMinute = bcdToDec(rawMin & 0x7F); // Handle 12/24h mode: bit 6 high = 12h mode if (rawHour & 0x40) { // 12h mode: bit 5 = PM, bits 4-0 = hours (1-12) uint8_t h12 = bcdToDec(rawHour & 0x1F); bool pm = rawHour & 0x20; if (h12 == 12) h12 = 0; _cachedHour = pm ? (h12 + 12) : h12; } else { // 24h mode: bits 5-0 = hours (0-23) _cachedHour = bcdToDec(rawHour & 0x3F); } _lastPollMs = now; _hasCachedTime = true; hour = _cachedHour; minute = _cachedMinute; return true; } bool HalClock::formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased, bool use12Hour) const { if (bufSize < (use12Hour ? 9u : 6u)) return false; uint8_t h, m; if (!getTime(h, m)) return false; // Apply UTC offset: convert biased value to signed quarter-hours. // Clamp against corrupted persisted values so display time can't drift outside [-12:00, +14:00]. if (utcOffsetQuarterHoursBiased > 104) utcOffsetQuarterHoursBiased = 104; int offsetQuarterHours = static_cast(utcOffsetQuarterHoursBiased) - 48; int totalMinutes = static_cast(h) * 60 + static_cast(m) + offsetQuarterHours * 15; // Wrap around 24 hours totalMinutes = ((totalMinutes % 1440) + 1440) % 1440; const int hour24 = totalMinutes / 60; const int min = totalMinutes % 60; if (use12Hour) { const bool pm = hour24 >= 12; int hour12 = hour24 % 12; if (hour12 == 0) hour12 = 12; snprintf(buf, bufSize, "%d:%02d %s", hour12, min, pm ? "PM" : "AM"); } else { snprintf(buf, bufSize, "%02d:%02d", hour24, min); } return true; } bool HalClock::writeTimeToRTC(uint8_t hour, uint8_t minute, uint8_t second) { assert(hour < 24); assert(minute < 60); assert(second < 60); Wire.beginTransmission(I2C_ADDR_DS3231); Wire.write(DS3231_SEC_REG); // Start at register 0x00 Wire.write(decToBcd(second)); // 0x00: Seconds Wire.write(decToBcd(minute)); // 0x01: Minutes Wire.write(decToBcd(hour)); // 0x02: Hours (24h mode, bit 6 = 0) if (Wire.endTransmission() != 0) { LOG_ERR("CLK", "Failed to write time to DS3231"); return false; } // Invalidate cache so next read fetches fresh data _lastPollMs = 0; _cachedHour = hour; _cachedMinute = minute; _hasCachedTime = true; return true; } bool HalClock::syncFromNTP() { if (!_available) return false; if (WiFi.status() != WL_CONNECTED) { LOG_ERR("CLK", "WiFi not connected, cannot sync NTP"); return false; } LOG_INF("CLK", "Starting NTP sync..."); configTzTime("UTC0", "pool.ntp.org", "time.nist.gov"); // Wait for SNTP sync to complete (up to 5 seconds) constexpr int maxAttempts = 50; for (int i = 0; i < maxAttempts; i++) { if (sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) { time_t now = time(nullptr); struct tm timeinfo; gmtime_r(&now, &timeinfo); if (writeTimeToRTC(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)) { LOG_INF("CLK", "RTC set to %02d:%02d:%02d UTC", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); return true; } return false; } delay(100); } LOG_ERR("CLK", "NTP sync timed out"); return false; }