#include "HalClock.h" #include #include #include #include #include #include #include #include #include // ---- RTC-memory state (survives deep sleep, not cold boot) ---------------- static constexpr uint32_t CLOCK_RTC_MAGIC = 0xC10C4B1D; static constexpr uint32_t CLOCK_RTC_FLAG_LP_VALID = 0x00000001u; RTC_NOINIT_ATTR static uint32_t rtcClockMagic; RTC_NOINIT_ATTR static uint32_t rtcClockFlags; RTC_NOINIT_ATTR static time_t rtcEpoch; // last-known unix epoch RTC_NOINIT_ATTR static uint64_t rtcLpTimeUs; // esp_clk_rtc_time() at capture RTC_NOINIT_ATTR static uint32_t rtcSlowCal; // esp_clk_slowclk_cal_get() at capture static bool clockApproximate = true; struct TimeZoneEntry { const char* tz; }; static constexpr TimeZoneEntry TIMEZONES[] = { {"GMT0BST,M3.5.0/1,M10.5.0/2"}, {"CET-1CEST,M3.5.0/2,M10.5.0/3"}, {"EET-2EEST,M3.5.0/3,M10.5.0/4"}, {"MSK-3"}, {"UTC-4"}, {"UTC-5:30"}, {"UTC-7"}, {"UTC-8"}, {"UTC-9"}, {"AEST-10AEDT,M10.1.0/2,M4.1.0/3"}, {"NZST-12NZDT,M9.5.0/2,M4.1.0/3"}, {"UTC+3"}, {"EST5EDT,M3.2.0/2,M11.1.0/2"}, {"CST6CDT,M3.2.0/2,M11.1.0/2"}, {"MST7MDT,M3.2.0/2,M11.1.0/2"}, {"PST8PDT,M3.2.0/2,M11.1.0/2"}, }; // ---- NVS helpers ---------------------------------------------------------- // If the last NTP sync is older than this, treat a cold-boot restore as // unsynced rather than showing a potentially very wrong time. static constexpr int64_t STALE_THRESHOLD_S = 72 * 3600; // 72 hours static constexpr char NVS_NAMESPACE[] = "halclock"; static constexpr char NVS_KEY[] = "epoch"; static constexpr char NVS_SYNC_KEY[] = "lastsync"; static void nvsWrite(time_t epoch) { Preferences prefs; if (prefs.begin(NVS_NAMESPACE, false)) { prefs.putLong64(NVS_KEY, (int64_t)epoch); prefs.end(); } } static void nvsWriteSyncTime(time_t syncEpoch) { Preferences prefs; if (prefs.begin(NVS_NAMESPACE, false)) { prefs.putLong64(NVS_SYNC_KEY, (int64_t)syncEpoch); prefs.end(); } } static time_t nvsRead() { Preferences prefs; time_t epoch = 0; if (prefs.begin(NVS_NAMESPACE, true)) { epoch = (time_t)prefs.getLong64(NVS_KEY, 0); prefs.end(); } return epoch; } static time_t nvsReadSyncTime() { Preferences prefs; time_t syncEpoch = 0; if (prefs.begin(NVS_NAMESPACE, true)) { syncEpoch = (time_t)prefs.getLong64(NVS_SYNC_KEY, 0); prefs.end(); } return syncEpoch; } // ---- internal helpers ----------------------------------------------------- static void setSystemClock(time_t epoch) { struct timeval tv = {}; tv.tv_sec = epoch; settimeofday(&tv, nullptr); } static bool rtcValid() { return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0; } /// Capture current time + LP timer into RTC memory, and epoch into NVS. static void capture(bool lpValid) { rtcEpoch = time(nullptr); rtcLpTimeUs = esp_clk_rtc_time(); rtcSlowCal = esp_clk_slowclk_cal_get(); rtcClockMagic = CLOCK_RTC_MAGIC; rtcClockFlags = lpValid ? CLOCK_RTC_FLAG_LP_VALID : 0; nvsWrite(rtcEpoch); } // ---- public API ----------------------------------------------------------- namespace HalClock { void applyTimezone(uint8_t timeZoneSetting) { const size_t index = timeZoneSetting < (sizeof(TIMEZONES) / sizeof(TIMEZONES[0])) ? timeZoneSetting : 0; setenv("TZ", TIMEZONES[index].tz, 1); tzset(); LOG_DBG("CLK", "Timezone applied: %s", TIMEZONES[index].tz); } bool syncNtp() { if (esp_sntp_enabled()) { esp_sntp_stop(); } esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); esp_sntp_setservername(0, "pool.ntp.org"); esp_sntp_init(); int retry = 0; constexpr int maxRetries = 50; // 5 seconds while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) { vTaskDelay(100 / portTICK_PERIOD_MS); retry++; } if (retry >= maxRetries) { LOG_ERR("CLK", "NTP sync timeout"); return false; } capture(false); nvsWriteSyncTime(rtcEpoch); clockApproximate = false; LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch); return true; } void saveBeforeSleep(bool keepLpAlive) { if (!isSynced()) { return; } capture(keepLpAlive); LOG_DBG("CLK", "Saved epoch %lld before sleep", (long long)rtcEpoch); } void restore() { const bool lpValid = (rtcClockFlags & CLOCK_RTC_FLAG_LP_VALID) != 0; if (rtcValid() && lpValid) { // RTC memory survived — we woke from deep sleep. // Use the LP timer to compute how much time elapsed during sleep. // Apply calibration correction: the slow-clock frequency may have // drifted (temperature) between when we captured and now. The fresh // boot-time calibration (calNow) is our best estimate of the actual // frequency during sleep. uint64_t lpNow = esp_clk_rtc_time(); time_t estimated = rtcEpoch; if (lpNow > rtcLpTimeUs) { uint32_t calNow = esp_clk_slowclk_cal_get(); uint64_t elapsedUs; if (rtcSlowCal != 0 && calNow != 0) { // rtcLpTimeUs was computed with rtcSlowCal; convert it to the // current calibration basis so the subtraction is consistent. uint64_t lpThenCorrected = (uint64_t)((double)rtcLpTimeUs * calNow / rtcSlowCal); elapsedUs = lpNow - lpThenCorrected; } else { elapsedUs = lpNow - rtcLpTimeUs; } estimated += (time_t)(elapsedUs / 1000000LL); } setSystemClock(estimated); // Re-capture with current LP baseline rtcEpoch = estimated; rtcLpTimeUs = lpNow; rtcSlowCal = esp_clk_slowclk_cal_get(); clockApproximate = true; LOG_INF("CLK", "Restored from RTC + LP timer, epoch %lld", (long long)estimated); return; } // Cold boot — try NVS. No elapsed correction possible. time_t epoch = nvsRead(); if (epoch > 0) { time_t lastSync = nvsReadSyncTime(); if (lastSync > 0 && (epoch - lastSync) > STALE_THRESHOLD_S) { LOG_ERR("CLK", "NVS epoch %lld is stale (last NTP sync %lld, %lld h ago), discarding", (long long)epoch, (long long)lastSync, (long long)((epoch - lastSync) / 3600)); return; } setSystemClock(epoch); rtcEpoch = epoch; rtcLpTimeUs = esp_clk_rtc_time(); rtcSlowCal = esp_clk_slowclk_cal_get(); rtcClockMagic = CLOCK_RTC_MAGIC; rtcClockFlags = 0; clockApproximate = true; LOG_INF("CLK", "Restored from NVS, epoch %lld (no elapsed correction)", (long long)epoch); } } time_t now() { if (!isSynced()) { return 0; } return time(nullptr); } bool isSynced() { return time(nullptr) > 1577836800; // > 2020-01-01 } bool isApproximate() { return clockApproximate; } void formatTime(char* buf, size_t bufSize, bool use24h) { if (!isSynced()) { snprintf(buf, bufSize, "--:--"); return; } time_t t = time(nullptr); struct tm timeinfo; localtime_r(&t, &timeinfo); const char* prefix = isApproximate() ? "~" : ""; if (use24h) { snprintf(buf, bufSize, "%s%02d:%02d", prefix, timeinfo.tm_hour, timeinfo.tm_min); } else { int hour = timeinfo.tm_hour % 12; if (hour == 0) hour = 12; const char* ampm = timeinfo.tm_hour < 12 ? "am" : "pm"; snprintf(buf, bufSize, "%s%d:%02d%s", prefix, hour, timeinfo.tm_min, ampm); } } void formatLogTime(char* buf, size_t bufSize) { if (!isSynced()) { buf[0] = '\0'; return; } time_t t = time(nullptr); struct tm timeinfo; localtime_r(&t, &timeinfo); snprintf(buf, bufSize, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); } void wifiOff(bool skipNtpSync) { if (!skipNtpSync && isApproximate() && WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) { syncNtp(); } if (esp_sntp_enabled()) { esp_sntp_stop(); } WiFi.disconnect(false); delay(100); WiFi.mode(WIFI_OFF); delay(100); } } // namespace HalClock