From dd9c9e6defb16e7433f03d4dbc154045f23c354d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 16:43:30 +0100 Subject: [PATCH 01/16] Add basic clock support --- lib/I18n/translations/english.yaml | 9 + lib/Logging/Logging.cpp | 11 +- lib/hal/HalClock.cpp | 181 ++++++++++++++++++ lib/hal/HalClock.h | 64 +++++++ lib/hal/HalPowerManager.cpp | 20 +- lib/hal/HalPowerManager.h | 8 +- src/CrossPointSettings.h | 7 + src/SettingsList.h | 6 + .../reader/KOReaderSyncActivity.cpp | 28 +-- src/activities/settings/SettingsActivity.cpp | 5 + src/activities/settings/SettingsActivity.h | 1 + .../settings/StatusBarSettingsActivity.cpp | 10 +- src/activities/settings/SyncTimeActivity.cpp | 119 ++++++++++++ src/activities/settings/SyncTimeActivity.h | 21 ++ src/components/UITheme.cpp | 2 +- src/components/themes/BaseTheme.cpp | 22 ++- src/components/themes/lyra/LyraTheme.cpp | 8 + src/main.cpp | 5 +- 18 files changed, 484 insertions(+), 43 deletions(-) create mode 100644 lib/hal/HalClock.cpp create mode 100644 lib/hal/HalClock.h create mode 100644 src/activities/settings/SyncTimeActivity.cpp create mode 100644 src/activities/settings/SyncTimeActivity.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index df91c1a1..7a7e4b81 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -82,6 +82,15 @@ STR_PARA_ALIGNMENT: "Reader Paragraph Alignment" STR_HYPHENATION: "Hyphenation" STR_TIME_TO_SLEEP: "Time to Sleep" STR_SHOW_HIDDEN_FILES: "Show Hidden Files" +STR_KEEP_CLOCK_ALIVE: "Keep Clock Alive" +STR_CLOCK: "Clock" +STR_CLOCK_FORMAT: "Clock Format" +STR_24H: "24h" +STR_12H: "12h" +STR_SYNC_TIME: "Sync Time" +STR_SYNCING_CLOCK: "Syncing clock..." +STR_TIME_SYNCED: "Time synced" +STR_TIME_SYNC_FAILED: "Time sync failed" STR_REFRESH_FREQ: "Refresh Frequency" STR_KOREADER_SYNC: "KOReader Sync" STR_CHECK_UPDATES: "Check for updates" diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index d670f5fb..2aec1081 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -1,5 +1,7 @@ #include "Logging.h" +#include + #include #define MAX_ENTRY_LEN 256 @@ -41,7 +43,14 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) { // add the timestamp { unsigned long ms = millis(); - int len = snprintf(c, sizeof(buf), "[%lu] ", ms); + char wallClock[12]; + HalClock::formatLogTime(wallClock, sizeof(wallClock)); + int len; + if (wallClock[0] != '\0') { + len = snprintf(c, sizeof(buf), "[%lu %s] ", ms, wallClock); + } else { + len = snprintf(c, sizeof(buf), "[%lu] ", ms); + } if (len < 0) { return; // encoding error, skip logging } diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp new file mode 100644 index 00000000..1ff29364 --- /dev/null +++ b/lib/hal/HalClock.cpp @@ -0,0 +1,181 @@ +#include "HalClock.h" + +#include +#include +#include +#include +#include +#include + +// ---- RTC-memory state (survives deep sleep, not cold boot) ---------------- + +static constexpr uint32_t CLOCK_RTC_MAGIC = 0xC10C4B1D; + +RTC_NOINIT_ATTR static uint32_t rtcClockMagic; +RTC_NOINIT_ATTR static time_t rtcEpoch; // last-known unix epoch +RTC_NOINIT_ATTR static uint64_t rtcLpTimeUs; // esp_clk_rtc_time() at capture + +static bool clockApproximate = true; + +// ---- NVS helpers ---------------------------------------------------------- + +static constexpr char NVS_NAMESPACE[] = "halclock"; +static constexpr char NVS_KEY[] = "epoch"; + +static void nvsWrite(time_t epoch) { + Preferences prefs; + if (prefs.begin(NVS_NAMESPACE, false)) { + prefs.putLong64(NVS_KEY, (int64_t)epoch); + 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; +} + +// ---- 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() { + rtcEpoch = time(nullptr); + rtcLpTimeUs = esp_clk_rtc_time(); + rtcClockMagic = CLOCK_RTC_MAGIC; + nvsWrite(rtcEpoch); +} + +// ---- public API ----------------------------------------------------------- + +namespace HalClock { + +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(); + clockApproximate = false; + LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch); + return true; +} + +void saveBeforeSleep() { + if (!isSynced()) { + return; + } + capture(); + LOG_DBG("CLK", "Saved epoch %lld before sleep", (long long)rtcEpoch); +} + +void restore() { + if (rtcValid()) { + // RTC memory survived — we woke from deep sleep. + // Use the LP timer to compute how much time elapsed during sleep. + uint64_t lpNow = esp_clk_rtc_time(); + time_t estimated = rtcEpoch; + if (lpNow > rtcLpTimeUs) { + estimated += (time_t)((lpNow - rtcLpTimeUs) / 1000000LL); + } + setSystemClock(estimated); + // Re-capture with current LP baseline + rtcEpoch = estimated; + rtcLpTimeUs = lpNow; + 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) { + setSystemClock(epoch); + rtcEpoch = epoch; + rtcLpTimeUs = esp_clk_rtc_time(); + rtcClockMagic = CLOCK_RTC_MAGIC; + 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); +} + +} // namespace HalClock diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h new file mode 100644 index 00000000..68d4eb38 --- /dev/null +++ b/lib/hal/HalClock.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +/// Lightweight wall-clock facade. +/// +/// The ESP32-C3 has no battery-backed RTC, so wall-clock time is lost on every +/// deep-sleep / power cycle. HalClock bridges this gap using three layers: +/// +/// - **LP timer** (`esp_clk_rtc_time()`) — keeps running during deep sleep +/// when `keepClockAlive` is enabled (GPIO13 stays HIGH). Used to compute +/// elapsed time and correct the stored epoch on wake. +/// - **RTC memory** (`RTC_NOINIT_ATTR`) — survives deep sleep, lost on cold +/// boot. Stores the epoch + LP timer value captured before sleep. +/// - **NVS** (flash key-value store) — survives power cycles. Fallback when +/// RTC memory is unavailable (cold boot). +/// +/// Usage: +/// 1. On boot, call `restore()` to seed the system clock from the best +/// available source (RTC memory + LP correction > NVS). +/// 2. After a successful NTP sync, call `onNtpSynced()`. +/// 3. Before entering deep sleep, call `saveBeforeSleep()`. +/// +/// `now()` returns the best-effort epoch (0 if never synced). +namespace HalClock { + +/// Perform an NTP sync (requires WiFi to be connected). Starts SNTP, +/// waits up to 5 seconds for completion, then captures the result. +/// Returns true if the sync succeeded. +bool syncNtp(); + +/// Call just before deep sleep. Snapshots the current system time to RTC +/// memory and NVS so it can be restored on wake / cold boot. +void saveBeforeSleep(); + +/// Call on boot to seed the system clock from the best available stored +/// value. When RTC memory is valid (deep-sleep wake) and the LP timer was +/// running, the restored time includes elapsed-time correction. Falls back +/// to NVS for cold boot (stale, but better than nothing). +void restore(); + +/// Returns the current best-effort wall-clock epoch, or 0 if the clock was +/// never set. +time_t now(); + +/// True if the clock has been set at least once (NTP or restore). +bool isSynced(); + +/// True if the last restore was from a backup (not NTP) — i.e. the clock +/// may have drifted. Cleared on NTP sync. +bool isApproximate(); + +/// Format the current time for display. Returns "--:--" if the clock was +/// never synced, prefixes with "~" if approximate. +/// When use24h is false, formats as "2:05pm" / "12:30am". +/// Output is written to `buf` (must be at least 16 bytes). +void formatTime(char* buf, size_t bufSize, bool use24h); + +/// Format the current time for log timestamps. Returns "HH:MM:SS" if +/// synced, or an empty string if not. +void formatLogTime(char* buf, size_t bufSize); + +} // namespace HalClock diff --git a/lib/hal/HalPowerManager.cpp b/lib/hal/HalPowerManager.cpp index c25b1a28..989376ab 100644 --- a/lib/hal/HalPowerManager.cpp +++ b/lib/hal/HalPowerManager.cpp @@ -52,26 +52,30 @@ void HalPowerManager::setPowerSaving(bool enabled) { // Otherwise, no change needed } -void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { +void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const { // Ensure that the power button has been released to avoid immediately turning back on if you're holding it while (gpio.isPressed(HalGPIO::BTN_POWER)) { delay(50); gpio.update(); } - // Pre-sleep routines from the original firmware - // GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep - // Note that this means the MCU will be completely powered off during sleep, including RTC + // GPIO13 is connected to the battery latch MOSFET. + // When keepClockAlive is false (default): GPIO13 goes LOW, the MCU is + // completely powered off during sleep (including the LP timer / RTC memory). + // When keepClockAlive is true: GPIO13 stays HIGH, the MCU remains powered + // at ~3-4 mA so the LP timer keeps running and RTC memory is preserved. + // This allows HalClock to accurately compute elapsed sleep time on wake. constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13; gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT); - gpio_set_level(GPIO_SPIWP, 0); + gpio_set_level(GPIO_SPIWP, keepClockAlive ? 1 : 0); esp_sleep_config_gpio_isolate(); gpio_deep_sleep_hold_en(); gpio_hold_en(GPIO_SPIWP); pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP); // Arm the wakeup trigger *after* the button is released - // Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the - // power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source - // configuration + // Note: when keepClockAlive is false, this is only useful for waking up on USB power. On battery, the MCU will be + // completely powered off, so the power button is hard-wired to briefly provide power to the MCU, waking it up + // regardless of the wakeup source configuration. + // When keepClockAlive is true, this is the actual wakeup mechanism since the MCU stays powered. esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Enter Deep Sleep esp_deep_sleep_start(); diff --git a/lib/hal/HalPowerManager.h b/lib/hal/HalPowerManager.h index 74cff1bf..4fdc1652 100644 --- a/lib/hal/HalPowerManager.h +++ b/lib/hal/HalPowerManager.h @@ -29,9 +29,11 @@ class HalPowerManager { // Control CPU frequency for power saving void setPowerSaving(bool enabled); - // Setup wake up GPIO and enter deep sleep - // Should be called inside main loop() to handle the currentLockMode - void startDeepSleep(HalGPIO& gpio) const; + // Setup wake up GPIO and enter deep sleep. + // When keepClockAlive is true, GPIO13 stays HIGH so the LP timer keeps + // running during sleep (~3-4 mA extra). This allows HalClock to compute + // elapsed sleep time and restore the wall clock accurately on wake. + void startDeepSleep(HalGPIO& gpio, bool keepClockAlive = false) const; // Get battery percentage (range 0-100) uint16_t getBatteryPercentage() const; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 9a0e298b..65858128 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -199,6 +199,13 @@ class CrossPointSettings { uint8_t showHiddenFiles = 0; // Image rendering mode in EPUB reader uint8_t imageRendering = IMAGES_DISPLAY; + // Show clock in the reader status bar + uint8_t statusBarClock = 0; + // Clock format: 0 = 24h (14:00), 1 = 12h (2:00pm) + uint8_t clockFormat12h = 0; + // Keep the LP timer running during deep sleep (GPIO13 HIGH) so the clock + // can be accurately restored on wake. Increases sleep current by ~3-4 mA. + uint8_t keepClockAlive = 0; ~CrossPointSettings() = default; diff --git a/src/SettingsList.h b/src/SettingsList.h index cdbee372..8f6d3af0 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -80,6 +80,10 @@ inline const std::vector& getSettingsList() { "sleepTimeout", StrId::STR_CAT_SYSTEM), SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", StrId::STR_CAT_SYSTEM), + SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H}, + "clockFormat12h", StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_KEEP_CLOCK_ALIVE, &CrossPointSettings::keepClockAlive, "keepClockAlive", + StrId::STR_CAT_SYSTEM), // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- SettingInfo::DynamicString( @@ -136,6 +140,8 @@ inline const std::vector& getSettingsList() { StrId::STR_CUSTOMISE_STATUS_BAR), SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", + StrId::STR_CUSTOMISE_STATUS_BAR), }; return list; } diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 4df71e93..e76116d2 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -1,6 +1,7 @@ #include "KOReaderSyncActivity.h" #include +#include #include #include #include @@ -14,31 +15,6 @@ #include "fontIds.h" namespace { -void syncTimeWithNTP() { - // Stop SNTP if already running (can't reconfigure while running) - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } - - // Configure SNTP - esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); - esp_sntp_setservername(0, "pool.ntp.org"); - esp_sntp_init(); - - // Wait for time to sync (with timeout) - int retry = 0; - const int maxRetries = 50; // 5 seconds max - while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) { - vTaskDelay(100 / portTICK_PERIOD_MS); - retry++; - } - - if (retry < maxRetries) { - LOG_DBG("KOSync", "NTP time synced"); - } else { - LOG_DBG("KOSync", "NTP sync timeout, using fallback"); - } -} void wifiOff() { if (esp_sntp_enabled()) { esp_sntp_stop(); @@ -70,7 +46,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { requestUpdate(true); // Sync time with NTP before making API requests - syncTimeWithNTP(); + HalClock::syncNtp(); { RenderLock lock(*this); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index e5824a89..455d56f5 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -13,6 +13,7 @@ #include "OtaUpdateActivity.h" #include "SettingsList.h" #include "StatusBarSettingsActivity.h" +#include "SyncTimeActivity.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -46,6 +47,7 @@ void SettingsActivity::onEnter() { // Append device-only ACTION items controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_TIME, SettingAction::SyncTime)); systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); @@ -192,6 +194,9 @@ void SettingsActivity::toggleCurrentSetting() { case SettingAction::Language: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; + case SettingAction::SyncTime: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + break; case SettingAction::None: // Do nothing break; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 9d5778a9..06baeae8 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -21,6 +21,7 @@ enum class SettingAction { ClearCache, CheckForUpdates, Language, + SyncTime, }; struct SettingInfo { diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index 6ff0b34d..979041b1 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -11,13 +11,14 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 6; +constexpr int MENU_ITEMS = 7; const StrId menuNames[MENU_ITEMS] = {StrId::STR_CHAPTER_PAGE_COUNT, StrId::STR_BOOK_PROGRESS_PERCENTAGE, StrId::STR_PROGRESS_BAR, StrId::STR_PROGRESS_BAR_THICKNESS, StrId::STR_TITLE, - StrId::STR_BATTERY}; + StrId::STR_BATTERY, + StrId::STR_CLOCK}; constexpr int PROGRESS_BAR_ITEMS = 3; const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}; @@ -110,6 +111,9 @@ void StatusBarSettingsActivity::handleSelection() { } else if (selectedIndex == 5) { // Show Battery SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2; + } else if (selectedIndex == 6) { + // Show Clock + SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % 2; } SETTINGS.saveToFile(); } @@ -143,6 +147,8 @@ void StatusBarSettingsActivity::render(RenderLock&&) { return I18N.get(titleNames[SETTINGS.statusBarTitle]); } else if (index == 5) { return SETTINGS.statusBarBattery ? tr(STR_SHOW) : tr(STR_HIDE); + } else if (index == 6) { + return SETTINGS.statusBarClock ? tr(STR_SHOW) : tr(STR_HIDE); } else { return tr(STR_HIDE); } diff --git a/src/activities/settings/SyncTimeActivity.cpp b/src/activities/settings/SyncTimeActivity.cpp new file mode 100644 index 00000000..6cd0d3d0 --- /dev/null +++ b/src/activities/settings/SyncTimeActivity.cpp @@ -0,0 +1,119 @@ +#include "SyncTimeActivity.h" + +#include +#include + +#include "CrossPointSettings.h" +#include +#include +#include +#include + +#include "MappedInputManager.h" +#include "activities/network/WifiSelectionActivity.h" +#include "components/UITheme.h" +#include "fontIds.h" + +namespace { +void wifiOff() { + if (esp_sntp_enabled()) { + esp_sntp_stop(); + } + WiFi.disconnect(false); + delay(100); + WiFi.mode(WIFI_OFF); + delay(100); +} +} // namespace + +void SyncTimeActivity::onEnter() { + Activity::onEnter(); + + if (WiFi.status() == WL_CONNECTED) { + onWifiSelectionComplete(true); + return; + } + + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); +} + +void SyncTimeActivity::onExit() { + Activity::onExit(); + wifiOff(); +} + +void SyncTimeActivity::onWifiSelectionComplete(bool success) { + if (!success) { + state = FAILED; + requestUpdate(); + return; + } + + { + RenderLock lock(*this); + state = SYNCING; + } + requestUpdateAndWait(); + + performSync(); +} + +void SyncTimeActivity::performSync() { + bool ok = HalClock::syncNtp(); + wifiOff(); + + state = ok ? SUCCESS : FAILED; + requestUpdate(); +} + +void SyncTimeActivity::render(RenderLock&&) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SYNC_TIME)); + + if (state == SYNCING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_SYNCING_CLOCK), true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return; + } + + if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD); + + time_t now = HalClock::now(); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + char timePart[16]; + HalClock::formatTime(timePart, sizeof(timePart), !SETTINGS.clockFormat12h); + char timeStr[32]; + snprintf(timeStr, sizeof(timeStr), "%s %04d-%02d-%02d", timePart, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, + timeinfo.tm_mday); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, timeStr); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIME_SYNC_FAILED), true, EpdFontFamily::BOLD); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } +} + +void SyncTimeActivity::loop() { + if (state == SUCCESS || state == FAILED) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + finish(); + } + } +} diff --git a/src/activities/settings/SyncTimeActivity.h b/src/activities/settings/SyncTimeActivity.h new file mode 100644 index 00000000..5a25b3ec --- /dev/null +++ b/src/activities/settings/SyncTimeActivity.h @@ -0,0 +1,21 @@ +#pragma once + +#include "activities/Activity.h" + +class SyncTimeActivity final : public Activity { + public: + explicit SyncTimeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("SyncTime", renderer, mappedInput) {} + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + private: + enum State { CONNECTING, SYNCING, SUCCESS, FAILED }; + State state = CONNECTING; + + void onWifiSelectionComplete(bool success); + void performSync(); +}; diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 57e49484..79fb603a 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -96,7 +96,7 @@ int UITheme::getStatusBarHeight() { // Add status bar margin const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || - SETTINGS.statusBarBattery; + SETTINGS.statusBarBattery || SETTINGS.statusBarClock; const bool showProgressBar = SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) + diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 9c563eb1..c2b8d229 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -1,6 +1,7 @@ #include "BaseTheme.h" #include +#include #include #include #include @@ -294,6 +295,13 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight}, showBatteryPercentage); + // Draw clock in header + { + char clockStr[16]; + HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h); + renderer.drawText(SMALL_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, rect.y + 5, clockStr); + } + if (title) { int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth; auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, @@ -715,6 +723,17 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c showBatteryPercentage); } + // Draw Clock + int clockTextWidth = 0; + if (SETTINGS.statusBarClock) { + char clockStr[16]; + HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h); + clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, clockStr); + const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0; + renderer.drawText(SMALL_FONT_ID, metrics.statusBarHorizontalMargin + orientedMarginLeft + batterySize + 8, textY, + clockStr); + } + // Draw Title if (!title.empty()) { textY -= textYOffset; @@ -724,7 +743,8 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight; const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0; - const int titleMarginLeft = batterySize + 30; + const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0; + const int titleMarginLeft = batterySize + clockSize + 30; const int titleMarginRight = progressTextWidth + 30; // Attempt to center title on the screen, but if title is too wide then later we will center it within the diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 58dabeab..7e62f5b4 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -1,6 +1,7 @@ #include "LyraTheme.h" #include +#include #include #include #include @@ -167,6 +168,13 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight}, showBatteryPercentage); + // Draw clock in header + { + char clockStr[16]; + HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h); + renderer.drawText(SMALL_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, rect.y + 5, clockStr); + } + int maxTitleWidth = rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0); diff --git a/src/main.cpp b/src/main.cpp index 75bf69c5..2e8cff70 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -184,6 +185,7 @@ void waitForPowerRelease() { void enterDeepSleep() { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); + HalClock::saveBeforeSleep(); APP_STATE.saveToFile(); activityManager.goToSleep(); @@ -192,7 +194,7 @@ void enterDeepSleep() { LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1); LOG_DBG("MAIN", "Entering deep sleep"); - powerManager.startDeepSleep(gpio); + powerManager.startDeepSleep(gpio, SETTINGS.keepClockAlive); } void setupDisplayAndFonts() { @@ -289,6 +291,7 @@ void setup() { activityManager.goToBoot(); APP_STATE.loadFromFile(); + HalClock::restore(); RECENT_BOOKS.loadFromFile(); // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity From 2bcfa50a7ce3b5da8abd85551658a35d744c8e3f Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 17:03:30 +0100 Subject: [PATCH 02/16] yaclf --- lib/hal/HalClock.cpp | 8 ++------ src/activities/settings/SyncTimeActivity.cpp | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 1ff29364..bd39fcf4 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -48,9 +48,7 @@ static void setSystemClock(time_t epoch) { settimeofday(&tv, nullptr); } -static bool rtcValid() { - return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0; -} +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() { @@ -140,9 +138,7 @@ bool isSynced() { return time(nullptr) > 1577836800; // > 2020-01-01 } -bool isApproximate() { - return clockApproximate; -} +bool isApproximate() { return clockApproximate; } void formatTime(char* buf, size_t bufSize, bool use24h) { if (!isSynced()) { diff --git a/src/activities/settings/SyncTimeActivity.cpp b/src/activities/settings/SyncTimeActivity.cpp index 6cd0d3d0..7a99f9a4 100644 --- a/src/activities/settings/SyncTimeActivity.cpp +++ b/src/activities/settings/SyncTimeActivity.cpp @@ -2,13 +2,12 @@ #include #include - -#include "CrossPointSettings.h" #include #include #include #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" From 166ce73e2d60e8c47806d71dd9c848dd796b073e Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 17:34:00 +0100 Subject: [PATCH 03/16] Addressing review comments --- lib/hal/HalClock.cpp | 15 ++++++++++----- lib/hal/HalClock.h | 5 +++-- src/activities/settings/SyncTimeActivity.cpp | 10 +++++++++- src/activities/settings/SyncTimeActivity.h | 2 +- src/main.cpp | 2 +- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index bd39fcf4..7edad342 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -10,8 +10,10 @@ // ---- 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 @@ -51,10 +53,11 @@ static void setSystemClock(time_t epoch) { 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() { +static void capture(bool lpValid) { rtcEpoch = time(nullptr); rtcLpTimeUs = esp_clk_rtc_time(); rtcClockMagic = CLOCK_RTC_MAGIC; + rtcClockFlags = lpValid ? CLOCK_RTC_FLAG_LP_VALID : 0; nvsWrite(rtcEpoch); } @@ -83,22 +86,23 @@ bool syncNtp() { return false; } - capture(); + capture(false); clockApproximate = false; LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch); return true; } -void saveBeforeSleep() { +void saveBeforeSleep(bool keepLpAlive) { if (!isSynced()) { return; } - capture(); + capture(keepLpAlive); LOG_DBG("CLK", "Saved epoch %lld before sleep", (long long)rtcEpoch); } void restore() { - if (rtcValid()) { + 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. uint64_t lpNow = esp_clk_rtc_time(); @@ -122,6 +126,7 @@ void restore() { rtcEpoch = epoch; rtcLpTimeUs = esp_clk_rtc_time(); rtcClockMagic = CLOCK_RTC_MAGIC; + rtcClockFlags = 0; clockApproximate = true; LOG_INF("CLK", "Restored from NVS, epoch %lld (no elapsed correction)", (long long)epoch); } diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index 68d4eb38..e5dd5d7a 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -31,8 +31,9 @@ namespace HalClock { bool syncNtp(); /// Call just before deep sleep. Snapshots the current system time to RTC -/// memory and NVS so it can be restored on wake / cold boot. -void saveBeforeSleep(); +/// memory and NVS so it can be restored on wake / cold boot. Pass true when +/// the LP timer is kept alive during sleep. +void saveBeforeSleep(bool keepLpAlive); /// Call on boot to seed the system clock from the best available stored /// value. When RTC memory is valid (deep-sleep wake) and the LP timer was diff --git a/src/activities/settings/SyncTimeActivity.cpp b/src/activities/settings/SyncTimeActivity.cpp index 7a99f9a4..66ca2683 100644 --- a/src/activities/settings/SyncTimeActivity.cpp +++ b/src/activities/settings/SyncTimeActivity.cpp @@ -34,7 +34,13 @@ void SyncTimeActivity::onEnter() { } startActivityForResult(std::make_unique(renderer, mappedInput), - [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); + [this](const ActivityResult& result) { + if (result.isCancelled) { + onWifiSelectionCancelled(); + return; + } + onWifiSelectionComplete(true); + }); } void SyncTimeActivity::onExit() { @@ -58,6 +64,8 @@ void SyncTimeActivity::onWifiSelectionComplete(bool success) { performSync(); } +void SyncTimeActivity::onWifiSelectionCancelled() { finish(); } + void SyncTimeActivity::performSync() { bool ok = HalClock::syncNtp(); wifiOff(); diff --git a/src/activities/settings/SyncTimeActivity.h b/src/activities/settings/SyncTimeActivity.h index 5a25b3ec..43a419fb 100644 --- a/src/activities/settings/SyncTimeActivity.h +++ b/src/activities/settings/SyncTimeActivity.h @@ -15,7 +15,7 @@ class SyncTimeActivity final : public Activity { private: enum State { CONNECTING, SYNCING, SUCCESS, FAILED }; State state = CONNECTING; - void onWifiSelectionComplete(bool success); + void onWifiSelectionCancelled(); void performSync(); }; diff --git a/src/main.cpp b/src/main.cpp index 2e8cff70..fa02523a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -185,7 +185,7 @@ void waitForPowerRelease() { void enterDeepSleep() { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); - HalClock::saveBeforeSleep(); + HalClock::saveBeforeSleep(SETTINGS.keepClockAlive); APP_STATE.saveToFile(); activityManager.goToSleep(); From 6e8254b81537773173302601721e185306122bff Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 18:46:28 +0100 Subject: [PATCH 04/16] Adding timezone support --- lib/I18n/translations/english.yaml | 24 ++ lib/hal/HalClock.cpp | 33 ++ lib/hal/HalClock.h | 3 + src/CrossPointSettings.h | 23 ++ src/SettingsList.h | 6 + src/activities/ActivityManager.cpp | 13 + .../settings/DetectTimezoneActivity.cpp | 315 ++++++++++++++++++ .../settings/DetectTimezoneActivity.h | 28 ++ src/activities/settings/SettingsActivity.cpp | 10 + src/activities/settings/SettingsActivity.h | 1 + src/main.cpp | 1 + 11 files changed, 457 insertions(+) create mode 100644 src/activities/settings/DetectTimezoneActivity.cpp create mode 100644 src/activities/settings/DetectTimezoneActivity.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 7a7e4b81..9047748c 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -85,12 +85,36 @@ STR_SHOW_HIDDEN_FILES: "Show Hidden Files" STR_KEEP_CLOCK_ALIVE: "Keep Clock Alive" STR_CLOCK: "Clock" STR_CLOCK_FORMAT: "Clock Format" +STR_TIMEZONE: "Timezone" STR_24H: "24h" STR_12H: "12h" +STR_TZ_UTC: "UTC (GMT/BST)" +STR_TZ_CET: "Central Europe (CET/CEST)" +STR_TZ_EET: "Eastern Europe (EET/EEST)" +STR_TZ_EST: "US Eastern (EST/EDT)" +STR_TZ_CST: "US Central (CST/CDT)" +STR_TZ_MST: "US Mountain (MST/MDT)" +STR_TZ_PST: "US Pacific (PST/PDT)" +STR_TZ_AEST: "Australia Eastern (AEST/AEDT)" +STR_TZ_NZST: "New Zealand (NZST/NZDT)" +STR_TZ_MSK: "Russia (MSK)" +STR_TZ_UTC_MINUS3: "South America (UTC-3)" +STR_TZ_UTC_PLUS4: "Gulf (UTC+4)" +STR_TZ_IST: "India (UTC+5:30)" +STR_TZ_UTC_PLUS7: "SE Asia (UTC+7)" +STR_TZ_UTC_PLUS8: "China/SE Asia (UTC+8)" +STR_TZ_UTC_PLUS9: "Japan/Korea (UTC+9)" STR_SYNC_TIME: "Sync Time" +STR_DETECT_TIMEZONE: "Detect Timezone" STR_SYNCING_CLOCK: "Syncing clock..." +STR_DETECTING_TIMEZONE: "Detecting timezone..." STR_TIME_SYNCED: "Time synced" STR_TIME_SYNC_FAILED: "Time sync failed" +STR_TIMEZONE_DETECTED: "Timezone detected" +STR_TIMEZONE_DETECT_FAILED: "Timezone detect failed" +STR_DST_ACTIVE: "DST: active" +STR_DST_INACTIVE: "DST: inactive" +STR_DST_UNKNOWN: "DST: unknown" STR_REFRESH_FREQ: "Refresh Frequency" STR_KOREADER_SYNC: "KOReader Sync" STR_CHECK_UPDATES: "Check for updates" diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 7edad342..4d32dcbb 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -6,6 +6,9 @@ #include #include #include +#include + +#include // ---- RTC-memory state (survives deep sleep, not cold boot) ---------------- @@ -19,6 +22,29 @@ RTC_NOINIT_ATTR static uint64_t rtcLpTimeUs; // esp_clk_rtc_time() 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 ---------------------------------------------------------- static constexpr char NVS_NAMESPACE[] = "halclock"; @@ -65,6 +91,13 @@ static void capture(bool lpValid) { 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(); diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index e5dd5d7a..05cc3101 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -30,6 +30,9 @@ namespace HalClock { /// Returns true if the sync succeeded. bool syncNtp(); +/// Apply timezone/DST rules via the POSIX TZ string for the given setting. +void applyTimezone(uint8_t timeZoneSetting); + /// Call just before deep sleep. Snapshots the current system time to RTC /// memory and NVS so it can be restored on wake / cold boot. Pass true when /// the LP timer is kept alive during sleep. diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 65858128..269a866d 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -137,6 +137,27 @@ class CrossPointSettings { // Image rendering in EPUB reader enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT }; + // Timezone options (POSIX TZ rules for DST support) + enum TIMEZONE { + TZ_UTC = 0, + TZ_CET = 1, + TZ_EET = 2, + TZ_MSK = 3, + TZ_UTC_PLUS4 = 4, + TZ_IST = 5, + TZ_UTC_PLUS7 = 6, + TZ_UTC_PLUS8 = 7, + TZ_UTC_PLUS9 = 8, + TZ_AEST = 9, + TZ_NZST = 10, + TZ_UTC_MINUS3 = 11, + TZ_EST = 12, + TZ_CST = 13, + TZ_MST = 14, + TZ_PST = 15, + TIMEZONE_COUNT + }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -203,6 +224,8 @@ class CrossPointSettings { uint8_t statusBarClock = 0; // Clock format: 0 = 24h (14:00), 1 = 12h (2:00pm) uint8_t clockFormat12h = 0; + // Timezone selection (applies POSIX TZ rules for DST) + uint8_t timeZone = TZ_UTC; // Keep the LP timer running during deep sleep (GPIO13 HIGH) so the clock // can be accurately restored on wake. Increases sleep current by ~3-4 mA. uint8_t keepClockAlive = 0; diff --git a/src/SettingsList.h b/src/SettingsList.h index 8f6d3af0..d01119d1 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -82,6 +82,12 @@ inline const std::vector& getSettingsList() { StrId::STR_CAT_SYSTEM), SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H}, "clockFormat12h", StrId::STR_CAT_SYSTEM), + SettingInfo::Enum(StrId::STR_TIMEZONE, &CrossPointSettings::timeZone, + {StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, + StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, + StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, + StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, + "timeZone", StrId::STR_CAT_SYSTEM), SettingInfo::Toggle(StrId::STR_KEEP_CLOCK_ALIVE, &CrossPointSettings::keepClockAlive, "keepClockAlive", StrId::STR_CAT_SYSTEM), diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 3cf115b1..e1a01cea 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -1,5 +1,6 @@ #include "ActivityManager.h" +#include #include #include "boot_sleep/BootActivity.h" @@ -56,6 +57,18 @@ void ActivityManager::loop() { currentActivity->loop(); } + if (SETTINGS.statusBarClock && HalClock::isSynced()) { + static time_t lastClockMinute = 0; + time_t now = HalClock::now(); + if (now > 0) { + time_t minute = now / 60; + if (minute != lastClockMinute) { + lastClockMinute = minute; + requestUpdate(); + } + } + } + while (pendingAction != PendingAction::None) { if (pendingAction == PendingAction::Pop) { RenderLock lock; diff --git a/src/activities/settings/DetectTimezoneActivity.cpp b/src/activities/settings/DetectTimezoneActivity.cpp new file mode 100644 index 00000000..686c2545 --- /dev/null +++ b/src/activities/settings/DetectTimezoneActivity.cpp @@ -0,0 +1,315 @@ +#include "DetectTimezoneActivity.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "activities/network/WifiSelectionActivity.h" +#include "components/UITheme.h" +#include "fontIds.h" +#include "network/HttpDownloader.h" + +namespace { +bool mapIanaTimezone(const std::string& tz, uint8_t& outSetting) { + using TZ = CrossPointSettings::TIMEZONE; + if (tz == "UTC" || tz == "Etc/UTC") { + outSetting = TZ::TZ_UTC; + return true; + } + if (tz == "Europe/London" || tz == "Europe/Guernsey" || tz == "Europe/Isle_of_Man" || + tz == "Europe/Jersey") { + outSetting = TZ::TZ_UTC; + return true; + } + if (tz == "Europe/Athens" || tz == "Europe/Bucharest" || tz == "Europe/Helsinki" || tz == "Europe/Kiev" || + tz == "Europe/Vilnius" || tz == "Europe/Riga" || tz == "Europe/Tallinn") { + outSetting = TZ::TZ_EET; + return true; + } + if (tz.rfind("Europe/", 0) == 0) { + outSetting = TZ::TZ_CET; + return true; + } + if (tz == "Europe/Moscow") { + outSetting = TZ::TZ_MSK; + return true; + } + if (tz == "America/New_York" || tz == "America/Toronto") { + outSetting = TZ::TZ_EST; + return true; + } + if (tz == "America/Chicago") { + outSetting = TZ::TZ_CST; + return true; + } + if (tz == "America/Denver") { + outSetting = TZ::TZ_MST; + return true; + } + if (tz == "America/Los_Angeles" || tz == "America/Vancouver") { + outSetting = TZ::TZ_PST; + return true; + } + if (tz == "America/Sao_Paulo" || tz == "America/Argentina/Buenos_Aires" || tz == "America/Montevideo") { + outSetting = TZ::TZ_UTC_MINUS3; + return true; + } + if (tz == "Asia/Dubai" || tz == "Asia/Muscat") { + outSetting = TZ::TZ_UTC_PLUS4; + return true; + } + if (tz == "Asia/Kolkata") { + outSetting = TZ::TZ_IST; + return true; + } + if (tz == "Asia/Bangkok" || tz == "Asia/Ho_Chi_Minh" || tz == "Asia/Jakarta" || tz == "Asia/Phnom_Penh" || + tz == "Asia/Vientiane") { + outSetting = TZ::TZ_UTC_PLUS7; + return true; + } + if (tz == "Asia/Shanghai" || tz == "Asia/Hong_Kong" || tz == "Asia/Singapore" || tz == "Asia/Taipei" || + tz == "Asia/Kuala_Lumpur" || tz == "Asia/Manila") { + outSetting = TZ::TZ_UTC_PLUS8; + return true; + } + if (tz == "Asia/Tokyo" || tz == "Asia/Seoul") { + outSetting = TZ::TZ_UTC_PLUS9; + return true; + } + if (tz == "Australia/Sydney" || tz == "Australia/Melbourne" || tz == "Australia/Hobart") { + outSetting = TZ::TZ_AEST; + return true; + } + if (tz == "Pacific/Auckland") { + outSetting = TZ::TZ_NZST; + return true; + } + return false; +} + +bool fetchTimezonePayload(const char* url, std::string& payload) { + // Give DNS a moment after WiFi connect. + delay(500); + for (int attempt = 0; attempt < 2; ++attempt) { + if (HttpDownloader::fetchUrl(url, payload)) { + return true; + } + delay(300); + } + return false; +} + +bool fetchPublicIp(std::string& ip) { + std::string payload; + if (!fetchTimezonePayload("https://api.ipify.org", payload)) { + return false; + } + + // Payload is plain text: IP address + ip = payload; + // Trim any whitespace + while (!ip.empty() && (ip.back() == '\n' || ip.back() == '\r' || ip.back() == ' ' || ip.back() == '\t')) { + ip.pop_back(); + } + if (!ip.empty()) { + LOG_DBG("CLK", "Public IP: %s", ip.c_str()); + return true; + } + return false; +} + +bool detectTimezoneSetting(uint8_t& outSetting, std::string& outIana, bool& outDstKnown, bool& outDstActive) { + std::string payload; + std::string publicIp; + if (fetchPublicIp(publicIp)) { + std::string timeUrl = std::string("https://timeapi.io/api/Time/current/ip?ipAddress=") + publicIp; + LOG_DBG("CLK", "Timezone detect via TimeAPI: %s", timeUrl.c_str()); + if (fetchTimezonePayload(timeUrl.c_str(), payload)) { + // Continue to parse TimeAPI payload below. + } + } + + if (payload.empty() && !fetchTimezonePayload("http://ip-api.com/json/?fields=timezone,dst", payload)) { + LOG_ERR("CLK", "Timezone detect failed: fetch error"); + return false; + } + + JsonDocument doc; + const auto err = deserializeJson(doc, payload); + if (err) { + LOG_ERR("CLK", "Timezone detect failed: %s", err.c_str()); + return false; + } + + const char* tz = doc["timezone"] | ""; + if (!tz || tz[0] == '\0') { + tz = doc["timeZone"] | ""; + } + if (!tz || tz[0] == '\0') { + tz = doc["time_zone"] | ""; + } + if (!tz || tz[0] == '\0') { + LOG_ERR("CLK", "Timezone detect failed: missing timezone (payload: %s)", payload.c_str()); + return false; + } + + outIana = tz; + if (!doc["dst_active"].isNull()) { + outDstKnown = true; + outDstActive = doc["dst_active"].as(); + } else if (!doc["dstActive"].isNull()) { + outDstKnown = true; + outDstActive = doc["dstActive"].as(); + } else if (!doc["dst"].isNull()) { + outDstKnown = true; + outDstActive = doc["dst"].as(); + } else { + outDstKnown = false; + outDstActive = false; + } + + if (!mapIanaTimezone(tz, outSetting)) { + LOG_ERR("CLK", "Timezone detect unsupported: %s", tz); + return false; + } + + if (outDstKnown) { + LOG_DBG("CLK", "Timezone detected: %s (dst=%d)", tz, outDstActive ? 1 : 0); + } else { + LOG_DBG("CLK", "Timezone detected: %s (dst=unknown)", tz); + } + return true; +} + +void wifiOff() { + if (esp_sntp_enabled()) { + esp_sntp_stop(); + } + WiFi.disconnect(false); + delay(100); + WiFi.mode(WIFI_OFF); + delay(100); +} +} // namespace + +void DetectTimezoneActivity::onEnter() { + Activity::onEnter(); + + if (WiFi.status() == WL_CONNECTED) { + onWifiSelectionComplete(true); + return; + } + + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (result.isCancelled) { + onWifiSelectionCancelled(); + return; + } + onWifiSelectionComplete(true); + }); +} + +void DetectTimezoneActivity::onExit() { + Activity::onExit(); + wifiOff(); +} + +void DetectTimezoneActivity::onWifiSelectionComplete(bool success) { + if (!success) { + state = FAILED; + requestUpdate(); + return; + } + + { + RenderLock lock(*this); + state = DETECTING; + } + requestUpdateAndWait(); + + performDetect(); +} + +void DetectTimezoneActivity::onWifiSelectionCancelled() { finish(); } + +void DetectTimezoneActivity::performDetect() { + uint8_t detected = SETTINGS.timeZone; + detectedTimezone.clear(); + dstKnown = false; + dstActive = false; + if (detectTimezoneSetting(detected, detectedTimezone, dstKnown, dstActive)) { + SETTINGS.timeZone = detected; + HalClock::applyTimezone(SETTINGS.timeZone); + SETTINGS.saveToFile(); + state = SUCCESS; + } else { + state = FAILED; + } + + wifiOff(); + requestUpdate(); +} + +void DetectTimezoneActivity::render(RenderLock&&) { + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_DETECT_TIMEZONE)); + + if (state == DETECTING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_DETECTING_TIMEZONE), true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return; + } + + if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIMEZONE_DETECTED), true, + EpdFontFamily::BOLD); + + if (!detectedTimezone.empty()) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 5, detectedTimezone.c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 25, dstStatusLabel()); + } + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIMEZONE_DETECT_FAILED), true, + EpdFontFamily::BOLD); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } +} + +const char* DetectTimezoneActivity::dstStatusLabel() const { + if (!dstKnown) { + return tr(STR_DST_UNKNOWN); + } + return dstActive ? tr(STR_DST_ACTIVE) : tr(STR_DST_INACTIVE); +} + +void DetectTimezoneActivity::loop() { + if (state == SUCCESS || state == FAILED) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + finish(); + } + } +} diff --git a/src/activities/settings/DetectTimezoneActivity.h b/src/activities/settings/DetectTimezoneActivity.h new file mode 100644 index 00000000..103a0736 --- /dev/null +++ b/src/activities/settings/DetectTimezoneActivity.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "activities/Activity.h" + +class DetectTimezoneActivity final : public Activity { + public: + explicit DetectTimezoneActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("DetectTimezone", renderer, mappedInput) {} + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; + + private: + enum State { CONNECTING, DETECTING, SUCCESS, FAILED }; + State state = CONNECTING; + std::string detectedTimezone; + bool dstKnown = false; + bool dstActive = false; + + void onWifiSelectionComplete(bool success); + void onWifiSelectionCancelled(); + void performDetect(); + const char* dstStatusLabel() const; +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 455d56f5..783c5e50 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -1,12 +1,14 @@ #include "SettingsActivity.h" #include +#include #include #include "ButtonRemapActivity.h" #include "CalibreSettingsActivity.h" #include "ClearCacheActivity.h" #include "CrossPointSettings.h" +#include "DetectTimezoneActivity.h" #include "KOReaderSettingsActivity.h" #include "LanguageSelectActivity.h" #include "MappedInputManager.h" @@ -48,6 +50,7 @@ void SettingsActivity::onEnter() { controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); systemSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_TIME, SettingAction::SyncTime)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_DETECT_TIMEZONE, SettingAction::DetectTimezone)); systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); @@ -197,6 +200,9 @@ void SettingsActivity::toggleCurrentSetting() { case SettingAction::SyncTime: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; + case SettingAction::DetectTimezone: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + break; case SettingAction::None: // Do nothing break; @@ -206,6 +212,10 @@ void SettingsActivity::toggleCurrentSetting() { return; } + if (setting.nameId == StrId::STR_TIMEZONE) { + HalClock::applyTimezone(SETTINGS.timeZone); + } + SETTINGS.saveToFile(); } diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 06baeae8..28116197 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -21,6 +21,7 @@ enum class SettingAction { ClearCache, CheckForUpdates, Language, + DetectTimezone, SyncTime, }; diff --git a/src/main.cpp b/src/main.cpp index fa02523a..fa24aa8e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -260,6 +260,7 @@ void setup() { HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info SETTINGS.loadFromFile(); + HalClock::applyTimezone(SETTINGS.timeZone); I18N.loadSettings(); KOREADER_STORE.loadFromFile(); UITheme::getInstance().reload(); From cfd79d32cab8e1325193f2bd7cd1429eb3db1d1d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 19:55:56 +0100 Subject: [PATCH 05/16] Updates for better setting grouping --- lib/I18n/translations/english.yaml | 3 +- src/CrossPointSettings.h | 6 +- src/SettingsList.h | 3 +- src/activities/ActivityManager.cpp | 9 +- .../settings/ClockSettingsActivity.cpp | 117 ++++++++++++++++++ .../settings/ClockSettingsActivity.h | 20 +++ src/activities/settings/SettingsActivity.cpp | 16 ++- src/activities/settings/SettingsActivity.h | 1 + .../settings/StatusBarSettingsActivity.cpp | 20 ++- src/components/UITheme.cpp | 2 +- src/components/themes/BaseTheme.cpp | 4 +- src/components/themes/lyra/LyraTheme.cpp | 2 +- src/main.cpp | 4 +- 13 files changed, 183 insertions(+), 24 deletions(-) create mode 100644 src/activities/settings/ClockSettingsActivity.cpp create mode 100644 src/activities/settings/ClockSettingsActivity.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 9047748c..9a99039e 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -82,7 +82,8 @@ STR_PARA_ALIGNMENT: "Reader Paragraph Alignment" STR_HYPHENATION: "Hyphenation" STR_TIME_TO_SLEEP: "Time to Sleep" STR_SHOW_HIDDEN_FILES: "Show Hidden Files" -STR_KEEP_CLOCK_ALIVE: "Keep Clock Alive" +STR_USE_CLOCK: "Use Clock" +STR_CLOCK_SETTINGS: "Clock Settings" STR_CLOCK: "Clock" STR_CLOCK_FORMAT: "Clock Format" STR_TIMEZONE: "Timezone" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 269a866d..3280b063 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -226,9 +226,9 @@ class CrossPointSettings { uint8_t clockFormat12h = 0; // Timezone selection (applies POSIX TZ rules for DST) uint8_t timeZone = TZ_UTC; - // Keep the LP timer running during deep sleep (GPIO13 HIGH) so the clock - // can be accurately restored on wake. Increases sleep current by ~3-4 mA. - uint8_t keepClockAlive = 0; + // Use clock and keep the LP timer running during deep sleep (GPIO13 HIGH) + // so time can be accurately restored on wake. Increases sleep current by ~3-4 mA. + uint8_t useClock = 0; ~CrossPointSettings() = default; diff --git a/src/SettingsList.h b/src/SettingsList.h index d01119d1..4bcbc754 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -88,8 +88,7 @@ inline const std::vector& getSettingsList() { StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, "timeZone", StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_KEEP_CLOCK_ALIVE, &CrossPointSettings::keepClockAlive, "keepClockAlive", - StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM), // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- SettingInfo::DynamicString( diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index e1a01cea..a94e1e52 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -1,5 +1,6 @@ #include "ActivityManager.h" +#include #include #include @@ -57,13 +58,13 @@ void ActivityManager::loop() { currentActivity->loop(); } - if (SETTINGS.statusBarClock && HalClock::isSynced()) { - static time_t lastClockMinute = 0; + if (SETTINGS.useClock && HalClock::isSynced()) { + static time_t lastMinute = -1; time_t now = HalClock::now(); if (now > 0) { time_t minute = now / 60; - if (minute != lastClockMinute) { - lastClockMinute = minute; + if (minute != lastMinute) { + lastMinute = minute; requestUpdate(); } } diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp new file mode 100644 index 00000000..abd70ccb --- /dev/null +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -0,0 +1,117 @@ +#include "ClockSettingsActivity.h" + +#include +#include +#include + +#include "CrossPointSettings.h" +#include "DetectTimezoneActivity.h" +#include "MappedInputManager.h" +#include "SyncTimeActivity.h" +#include "components/UITheme.h" +#include "fontIds.h" + +namespace { +constexpr int MENU_ITEMS = 5; +const StrId menuNames[MENU_ITEMS] = {StrId::STR_USE_CLOCK, StrId::STR_CLOCK_FORMAT, StrId::STR_TIMEZONE, + StrId::STR_SYNC_TIME, StrId::STR_DETECT_TIMEZONE}; + +const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { + StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, + StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, + StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, + StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; +} // namespace + +void ClockSettingsActivity::onEnter() { + Activity::onEnter(); + selectedIndex = 0; + requestUpdate(); +} + +void ClockSettingsActivity::onExit() { Activity::onExit(); } + +void ClockSettingsActivity::loop() { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + finish(); + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + handleSelection(); + requestUpdate(); + return; + } + + buttonNavigator.onNextRelease([this] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + requestUpdate(); + }); + + buttonNavigator.onPreviousRelease([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + requestUpdate(); + }); +} + +void ClockSettingsActivity::handleSelection() { + if (selectedIndex == 0) { + SETTINGS.useClock = (SETTINGS.useClock + 1) % 2; + if (!SETTINGS.useClock) { + SETTINGS.statusBarClock = 0; + } + SETTINGS.saveToFile(); + } else if (selectedIndex == 1) { + SETTINGS.clockFormat12h = (SETTINGS.clockFormat12h + 1) % 2; + SETTINGS.saveToFile(); + } else if (selectedIndex == 2) { + SETTINGS.timeZone = (SETTINGS.timeZone + 1) % CrossPointSettings::TIMEZONE_COUNT; + HalClock::applyTimezone(SETTINGS.timeZone); + SETTINGS.saveToFile(); + } else if (selectedIndex == 3) { + auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); }; + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + } else if (selectedIndex == 4) { + auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); }; + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + } +} + +void ClockSettingsActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_SETTINGS)); + + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, MENU_ITEMS, selectedIndex, + [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, nullptr, + [](int index) { + if (index == 0) { + return std::string(SETTINGS.useClock ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + } + if (index == 1) { + return std::string(SETTINGS.clockFormat12h ? tr(STR_12H) : tr(STR_24H)); + } + if (index == 2) { + const auto tzIndex = static_cast(SETTINGS.timeZone); + if (tzIndex < (sizeof(timeZoneNames) / sizeof(timeZoneNames[0]))) { + return std::string(I18N.get(timeZoneNames[tzIndex])); + } + return std::string(tr(STR_TZ_UTC)); + } + return std::string(""); + }, + true); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h new file mode 100644 index 00000000..e1f491bc --- /dev/null +++ b/src/activities/settings/ClockSettingsActivity.h @@ -0,0 +1,20 @@ +#pragma once +#include + +#include "activities/Activity.h" +#include "util/ButtonNavigator.h" + +class ClockSettingsActivity final : public Activity { + ButtonNavigator buttonNavigator; + int selectedIndex = 0; + + void handleSelection(); + + public: + explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("ClockSettings", renderer, mappedInput) {} + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 783c5e50..d0be195a 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -7,6 +7,7 @@ #include "ButtonRemapActivity.h" #include "CalibreSettingsActivity.h" #include "ClearCacheActivity.h" +#include "ClockSettingsActivity.h" #include "CrossPointSettings.h" #include "DetectTimezoneActivity.h" #include "KOReaderSettingsActivity.h" @@ -34,6 +35,11 @@ void SettingsActivity::onEnter() { for (const auto& setting : getSettingsList()) { if (setting.category == StrId::STR_NONE_OPT) continue; + if (setting.category == StrId::STR_CAT_SYSTEM && + (setting.nameId == StrId::STR_USE_CLOCK || setting.nameId == StrId::STR_CLOCK_FORMAT || + setting.nameId == StrId::STR_TIMEZONE)) { + continue; + } if (setting.category == StrId::STR_CAT_DISPLAY) { displaySettings.push_back(setting); } else if (setting.category == StrId::STR_CAT_READER) { @@ -49,8 +55,7 @@ void SettingsActivity::onEnter() { // Append device-only ACTION items controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_TIME, SettingAction::SyncTime)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_DETECT_TIMEZONE, SettingAction::DetectTimezone)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)); systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); @@ -179,6 +184,9 @@ void SettingsActivity::toggleCurrentSetting() { case SettingAction::CustomiseStatusBar: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; + case SettingAction::ClockSettings: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + break; case SettingAction::KOReaderSync: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; @@ -216,6 +224,10 @@ void SettingsActivity::toggleCurrentSetting() { HalClock::applyTimezone(SETTINGS.timeZone); } + if (setting.nameId == StrId::STR_USE_CLOCK && !SETTINGS.useClock) { + SETTINGS.statusBarClock = 0; + } + SETTINGS.saveToFile(); } diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 28116197..ef9a4dbb 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -15,6 +15,7 @@ enum class SettingAction { None, RemapFrontButtons, CustomiseStatusBar, + ClockSettings, KOReaderSync, OPDSBrowser, Network, diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index 979041b1..990a47b8 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -38,6 +38,9 @@ void StatusBarSettingsActivity::onEnter() { Activity::onEnter(); selectedIndex = 0; + if (!SETTINGS.useClock && selectedIndex >= MENU_ITEMS - 1) { + selectedIndex = 0; + } // Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) { @@ -71,22 +74,26 @@ void StatusBarSettingsActivity::loop() { // Handle navigation buttonNavigator.onNextRelease([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1; + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, menuCount); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1; + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, menuCount); requestUpdate(); }); buttonNavigator.onNextContinuous([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1; + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, menuCount); requestUpdate(); }); buttonNavigator.onPreviousContinuous([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1; + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, menuCount); requestUpdate(); }); } @@ -111,7 +118,7 @@ void StatusBarSettingsActivity::handleSelection() { } else if (selectedIndex == 5) { // Show Battery SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2; - } else if (selectedIndex == 6) { + } else if (selectedIndex == 6 && SETTINGS.useClock) { // Show Clock SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % 2; } @@ -129,8 +136,9 @@ void StatusBarSettingsActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEMS), + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuCount), static_cast(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, nullptr, [this](int index) { diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 79fb603a..a600145e 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -96,7 +96,7 @@ int UITheme::getStatusBarHeight() { // Add status bar margin const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || - SETTINGS.statusBarBattery || SETTINGS.statusBarClock; + SETTINGS.statusBarBattery || (SETTINGS.useClock && SETTINGS.statusBarClock); const bool showProgressBar = SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) + diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index c2b8d229..67ea1757 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -296,7 +296,7 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t showBatteryPercentage); // Draw clock in header - { + if (SETTINGS.useClock) { char clockStr[16]; HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h); renderer.drawText(SMALL_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, rect.y + 5, clockStr); @@ -725,7 +725,7 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c // Draw Clock int clockTextWidth = 0; - if (SETTINGS.statusBarClock) { + if (SETTINGS.useClock && SETTINGS.statusBarClock) { char clockStr[16]; HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h); clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, clockStr); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 7e62f5b4..77e00e9b 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -169,7 +169,7 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t showBatteryPercentage); // Draw clock in header - { + if (SETTINGS.useClock) { char clockStr[16]; HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h); renderer.drawText(SMALL_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, rect.y + 5, clockStr); diff --git a/src/main.cpp b/src/main.cpp index fa24aa8e..4cc63aaa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -185,7 +185,7 @@ void waitForPowerRelease() { void enterDeepSleep() { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); - HalClock::saveBeforeSleep(SETTINGS.keepClockAlive); + HalClock::saveBeforeSleep(SETTINGS.useClock); APP_STATE.saveToFile(); activityManager.goToSleep(); @@ -194,7 +194,7 @@ void enterDeepSleep() { LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1); LOG_DBG("MAIN", "Entering deep sleep"); - powerManager.startDeepSleep(gpio, SETTINGS.keepClockAlive); + powerManager.startDeepSleep(gpio, SETTINGS.useClock); } void setupDisplayAndFonts() { From 868e527bcdb0bc76b3eee3b02a0de5d55a576ead Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 19:59:55 +0100 Subject: [PATCH 06/16] Add warnings --- lib/I18n/translations/english.yaml | 1 + src/activities/settings/ClockSettingsActivity.cpp | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 9a99039e..c9a18ab0 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -84,6 +84,7 @@ STR_TIME_TO_SLEEP: "Time to Sleep" STR_SHOW_HIDDEN_FILES: "Show Hidden Files" STR_USE_CLOCK: "Use Clock" STR_CLOCK_SETTINGS: "Clock Settings" +STR_CLOCK_SETTINGS_WARNING: "Uses more battery; clock may drift" STR_CLOCK: "Clock" STR_CLOCK_FORMAT: "Clock Format" STR_TIMEZONE: "Timezone" diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index abd70ccb..9db165f9 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -85,8 +85,10 @@ void ClockSettingsActivity::render(RenderLock&&) { const auto pageHeight = renderer.getScreenHeight(); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_SETTINGS)); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + tr(STR_CLOCK_SETTINGS_WARNING)); - const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( From 8e4fa4d392ec9e9c29924eff609ed3ccd97561b5 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 20:04:06 +0100 Subject: [PATCH 07/16] Add warning --- src/activities/settings/ClockSettingsActivity.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 9db165f9..21aab09a 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -17,10 +17,10 @@ const StrId menuNames[MENU_ITEMS] = {StrId::STR_USE_CLOCK, StrId::STR_CLOCK_FORM StrId::STR_SYNC_TIME, StrId::STR_DETECT_TIMEZONE}; const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { - StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, - StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, - StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, - StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; + StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, + StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, + StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, + StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; } // namespace void ClockSettingsActivity::onEnter() { From 1f9b9038770d268f50d38413dff9f381b6674573 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 20:07:45 +0100 Subject: [PATCH 08/16] yaclf --- src/activities/settings/DetectTimezoneActivity.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/activities/settings/DetectTimezoneActivity.cpp b/src/activities/settings/DetectTimezoneActivity.cpp index 686c2545..0052913b 100644 --- a/src/activities/settings/DetectTimezoneActivity.cpp +++ b/src/activities/settings/DetectTimezoneActivity.cpp @@ -24,8 +24,7 @@ bool mapIanaTimezone(const std::string& tz, uint8_t& outSetting) { outSetting = TZ::TZ_UTC; return true; } - if (tz == "Europe/London" || tz == "Europe/Guernsey" || tz == "Europe/Isle_of_Man" || - tz == "Europe/Jersey") { + if (tz == "Europe/London" || tz == "Europe/Guernsey" || tz == "Europe/Isle_of_Man" || tz == "Europe/Jersey") { outSetting = TZ::TZ_UTC; return true; } @@ -274,8 +273,7 @@ void DetectTimezoneActivity::render(RenderLock&&) { } if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIMEZONE_DETECTED), true, - EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIMEZONE_DETECTED), true, EpdFontFamily::BOLD); if (!detectedTimezone.empty()) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 5, detectedTimezone.c_str()); @@ -289,8 +287,7 @@ void DetectTimezoneActivity::render(RenderLock&&) { } if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIMEZONE_DETECT_FAILED), true, - EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIMEZONE_DETECT_FAILED), true, EpdFontFamily::BOLD); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); From ec6835ee794f64e05342f6af7e546f38c71e166b Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 20:12:46 +0100 Subject: [PATCH 09/16] Address cppcheck --- src/activities/ActivityManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index a94e1e52..f99bcdc1 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -59,9 +59,9 @@ void ActivityManager::loop() { } if (SETTINGS.useClock && HalClock::isSynced()) { - static time_t lastMinute = -1; time_t now = HalClock::now(); if (now > 0) { + static time_t lastMinute = -1; time_t minute = now / 60; if (minute != lastMinute) { lastMinute = minute; From 1112a368065b9387598af1e39e2f9b5237cab183 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 20:31:41 +0100 Subject: [PATCH 10/16] Review comments --- lib/hal/HalClock.h | 2 +- .../settings/ClockSettingsActivity.cpp | 10 ++++++++++ .../settings/DetectTimezoneActivity.cpp | 16 +++++++++++----- src/activities/settings/SettingsActivity.cpp | 8 -------- .../settings/StatusBarSettingsActivity.cpp | 4 ++-- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index 05cc3101..63428116 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -19,7 +19,7 @@ /// Usage: /// 1. On boot, call `restore()` to seed the system clock from the best /// available source (RTC memory + LP correction > NVS). -/// 2. After a successful NTP sync, call `onNtpSynced()`. +/// 2. After a successful NTP sync, call `syncNtp()`. /// 3. Before entering deep sleep, call `saveBeforeSleep()`. /// /// `now()` returns the best-effort epoch (0 if never synced). diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 21aab09a..b79a7bc8 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -52,6 +52,16 @@ void ClockSettingsActivity::loop() { selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); requestUpdate(); }); + + buttonNavigator.onNextContinuous([this] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + requestUpdate(); + }); + + buttonNavigator.onPreviousContinuous([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + requestUpdate(); + }); } void ClockSettingsActivity::handleSelection() { diff --git a/src/activities/settings/DetectTimezoneActivity.cpp b/src/activities/settings/DetectTimezoneActivity.cpp index 0052913b..a969616d 100644 --- a/src/activities/settings/DetectTimezoneActivity.cpp +++ b/src/activities/settings/DetectTimezoneActivity.cpp @@ -33,14 +33,14 @@ bool mapIanaTimezone(const std::string& tz, uint8_t& outSetting) { outSetting = TZ::TZ_EET; return true; } - if (tz.rfind("Europe/", 0) == 0) { - outSetting = TZ::TZ_CET; - return true; - } if (tz == "Europe/Moscow") { outSetting = TZ::TZ_MSK; return true; } + if (tz.rfind("Europe/", 0) == 0) { + outSetting = TZ::TZ_CET; + return true; + } if (tz == "America/New_York" || tz == "America/Toronto") { outSetting = TZ::TZ_EST; return true; @@ -136,7 +136,7 @@ bool detectTimezoneSetting(uint8_t& outSetting, std::string& outIana, bool& outD } } - if (payload.empty() && !fetchTimezonePayload("http://ip-api.com/json/?fields=timezone,dst", payload)) { + if (payload.empty() && !fetchTimezonePayload("https://ip-api.com/json/?fields=timezone,dst", payload)) { LOG_ERR("CLK", "Timezone detect failed: fetch error"); return false; } @@ -266,6 +266,12 @@ void DetectTimezoneActivity::render(RenderLock&&) { renderer.clearScreen(); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_DETECT_TIMEZONE)); + if (state == CONNECTING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CONNECTING), true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return; + } + if (state == DETECTING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_DETECTING_TIMEZONE), true, EpdFontFamily::BOLD); renderer.displayBuffer(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d0be195a..64e755b0 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -220,14 +220,6 @@ void SettingsActivity::toggleCurrentSetting() { return; } - if (setting.nameId == StrId::STR_TIMEZONE) { - HalClock::applyTimezone(SETTINGS.timeZone); - } - - if (setting.nameId == StrId::STR_USE_CLOCK && !SETTINGS.useClock) { - SETTINGS.statusBarClock = 0; - } - SETTINGS.saveToFile(); } diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index 990a47b8..7e5ab68c 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -37,8 +37,8 @@ const int verticalPreviewTextPadding = 40; void StatusBarSettingsActivity::onEnter() { Activity::onEnter(); - selectedIndex = 0; - if (!SETTINGS.useClock && selectedIndex >= MENU_ITEMS - 1) { + const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1; + if (selectedIndex >= menuCount) { selectedIndex = 0; } From 7f9456e8c6a025b03daa2a563b31611c8717c44f Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 21:41:39 +0100 Subject: [PATCH 11/16] Add calibration --- lib/hal/HalClock.cpp | 50 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 4d32dcbb..83042731 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -19,6 +19,7 @@ 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; @@ -47,8 +48,13 @@ static constexpr TimeZoneEntry TIMEZONES[] = { // ---- 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; @@ -58,6 +64,14 @@ static void nvsWrite(time_t epoch) { } } +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; @@ -68,6 +82,16 @@ static time_t nvsRead() { 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) { @@ -82,6 +106,7 @@ static bool rtcValid() { return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0 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); @@ -120,6 +145,7 @@ bool syncNtp() { } capture(false); + nvsWriteSyncTime(rtcEpoch); clockApproximate = false; LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch); return true; @@ -138,15 +164,30 @@ void restore() { 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) { - estimated += (time_t)((lpNow - rtcLpTimeUs) / 1000000LL); + 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; @@ -155,9 +196,16 @@ void restore() { // 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; From 92877f9c3656e453aacb59bb3b6e289229b5d0a1 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 26 Mar 2026 21:55:28 +0100 Subject: [PATCH 12/16] Use ntp time sync at end of wifi activities --- lib/hal/HalClock.cpp | 14 ++++++++++++++ lib/hal/HalClock.h | 5 +++++ .../browser/OpdsBookBrowserActivity.cpp | 4 ++-- .../network/CrossPointWebServerActivity.cpp | 12 +++++------- .../reader/KOReaderSyncActivity.cpp | 19 +++---------------- .../settings/DetectTimezoneActivity.cpp | 14 ++------------ .../settings/KOReaderAuthActivity.cpp | 7 ++----- src/activities/settings/OtaUpdateActivity.cpp | 7 ++----- src/activities/settings/SyncTimeActivity.cpp | 17 ++--------------- 9 files changed, 37 insertions(+), 62 deletions(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 83042731..f7fec425 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -260,4 +261,17 @@ void formatLogTime(char* buf, size_t bufSize) { 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 diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index 63428116..cdb49604 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -65,4 +65,9 @@ void formatTime(char* buf, size_t bufSize, bool use24h); /// synced, or an empty string if not. void formatLogTime(char* buf, size_t bufSize); +/// Tear down WiFi cleanly. When skipNtpSync is false (default) and the +/// clock is approximate, performs an opportunistic NTP sync before +/// disconnecting — essentially free since we already have a connection. +void wifiOff(bool skipNtpSync = false); + } // namespace HalClock diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index f6a58385..6c7b1d78 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -39,8 +40,7 @@ void OpdsBookBrowserActivity::onEnter() { void OpdsBookBrowserActivity::onExit() { Activity::onExit(); - // Turn off WiFi when exiting - WiFi.mode(WIFI_OFF); + HalClock::wifiOff(); entries.clear(); navigationHistory.clear(); diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index b3cc8642..fe12b505 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -86,15 +87,12 @@ void CrossPointWebServerActivity::onExit() { if (isApMode) { LOG_DBG("WEBACT", "Stopping WiFi AP..."); WiFi.softAPdisconnect(true); + delay(30); + WiFi.mode(WIFI_OFF); + delay(30); } else { - LOG_DBG("WEBACT", "Disconnecting WiFi (graceful)..."); - WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame + HalClock::wifiOff(); } - delay(30); // Allow disconnect frame to be sent - - LOG_DBG("WEBACT", "Setting WiFi mode OFF..."); - WiFi.mode(WIFI_OFF); - delay(30); // Allow WiFi hardware to power down LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); } diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index e76116d2..e76e2dba 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include "KOReaderCredentialStore.h" #include "KOReaderDocumentId.h" @@ -14,18 +13,6 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -void wifiOff() { - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } - WiFi.disconnect(false); - delay(100); - WiFi.mode(WIFI_OFF); - delay(100); -} -} // namespace - void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { if (!success) { LOG_DBG("KOSync", "WiFi connection failed, exiting"); @@ -149,7 +136,7 @@ void KOReaderSyncActivity::performUpload() { const auto result = KOReaderSyncClient::updateProgress(progress); if (result != KOReaderSyncClient::OK) { - wifiOff(); + HalClock::wifiOff(true); { RenderLock lock(*this); state = SYNC_FAILED; @@ -159,7 +146,7 @@ void KOReaderSyncActivity::performUpload() { return; } - wifiOff(); + HalClock::wifiOff(true); { RenderLock lock(*this); state = UPLOAD_COMPLETE; @@ -193,7 +180,7 @@ void KOReaderSyncActivity::onEnter() { void KOReaderSyncActivity::onExit() { Activity::onExit(); - wifiOff(); + HalClock::wifiOff(true); } void KOReaderSyncActivity::render(RenderLock&&) { diff --git a/src/activities/settings/DetectTimezoneActivity.cpp b/src/activities/settings/DetectTimezoneActivity.cpp index a969616d..70859b41 100644 --- a/src/activities/settings/DetectTimezoneActivity.cpp +++ b/src/activities/settings/DetectTimezoneActivity.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include @@ -188,15 +187,6 @@ bool detectTimezoneSetting(uint8_t& outSetting, std::string& outIana, bool& outD return true; } -void wifiOff() { - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } - WiFi.disconnect(false); - delay(100); - WiFi.mode(WIFI_OFF); - delay(100); -} } // namespace void DetectTimezoneActivity::onEnter() { @@ -219,7 +209,7 @@ void DetectTimezoneActivity::onEnter() { void DetectTimezoneActivity::onExit() { Activity::onExit(); - wifiOff(); + HalClock::wifiOff(); } void DetectTimezoneActivity::onWifiSelectionComplete(bool success) { @@ -254,7 +244,7 @@ void DetectTimezoneActivity::performDetect() { state = FAILED; } - wifiOff(); + HalClock::wifiOff(); requestUpdate(); } diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index 2240ac6e..66ea2f3b 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -1,6 +1,7 @@ #include "KOReaderAuthActivity.h" #include +#include #include #include @@ -65,11 +66,7 @@ void KOReaderAuthActivity::onEnter() { void KOReaderAuthActivity::onExit() { Activity::onExit(); - // Turn off wifi - WiFi.disconnect(false); - delay(100); - WiFi.mode(WIFI_OFF); - delay(100); + HalClock::wifiOff(); } void KOReaderAuthActivity::render(RenderLock&&) { diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index c661a618..73082529 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -1,6 +1,7 @@ #include "OtaUpdateActivity.h" #include +#include #include #include @@ -66,11 +67,7 @@ void OtaUpdateActivity::onEnter() { void OtaUpdateActivity::onExit() { Activity::onExit(); - // Turn off wifi - WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame - delay(100); // Allow disconnect frame to be sent - WiFi.mode(WIFI_OFF); - delay(100); // Allow WiFi hardware to fully power down + HalClock::wifiOff(); } void OtaUpdateActivity::render(RenderLock&&) { diff --git a/src/activities/settings/SyncTimeActivity.cpp b/src/activities/settings/SyncTimeActivity.cpp index 66ca2683..fc81f3aa 100644 --- a/src/activities/settings/SyncTimeActivity.cpp +++ b/src/activities/settings/SyncTimeActivity.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include "CrossPointSettings.h" #include "MappedInputManager.h" @@ -13,18 +12,6 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -void wifiOff() { - if (esp_sntp_enabled()) { - esp_sntp_stop(); - } - WiFi.disconnect(false); - delay(100); - WiFi.mode(WIFI_OFF); - delay(100); -} -} // namespace - void SyncTimeActivity::onEnter() { Activity::onEnter(); @@ -45,7 +32,7 @@ void SyncTimeActivity::onEnter() { void SyncTimeActivity::onExit() { Activity::onExit(); - wifiOff(); + HalClock::wifiOff(true); } void SyncTimeActivity::onWifiSelectionComplete(bool success) { @@ -68,7 +55,7 @@ void SyncTimeActivity::onWifiSelectionCancelled() { finish(); } void SyncTimeActivity::performSync() { bool ok = HalClock::syncNtp(); - wifiOff(); + HalClock::wifiOff(true); state = ok ? SUCCESS : FAILED; requestUpdate(); From 316a1ac3abc90399e931885cf1ed501912c8aead Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 27 Mar 2026 11:49:13 +0100 Subject: [PATCH 13/16] Display drifitng stats --- lib/I18n/translations/english.yaml | 2 + lib/hal/HalClock.cpp | 2 + lib/hal/HalClock.h | 4 + src/activities/settings/SyncTimeActivity.cpp | 88 +++++++++++++++++++- src/activities/settings/SyncTimeActivity.h | 4 + 5 files changed, 98 insertions(+), 2 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c9a18ab0..774c0ad7 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -112,6 +112,8 @@ STR_SYNCING_CLOCK: "Syncing clock..." STR_DETECTING_TIMEZONE: "Detecting timezone..." STR_TIME_SYNCED: "Time synced" STR_TIME_SYNC_FAILED: "Time sync failed" +STR_CLOCK_DRIFT: "Drift: %s" +STR_LAST_NTP_SYNC: "Last sync: %s" STR_TIMEZONE_DETECTED: "Timezone detected" STR_TIMEZONE_DETECT_FAILED: "Timezone detect failed" STR_DST_ACTIVE: "DST: active" diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index f7fec425..43806689 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -227,6 +227,8 @@ bool isSynced() { bool isApproximate() { return clockApproximate; } +time_t lastSyncTime() { return nvsReadSyncTime(); } + void formatTime(char* buf, size_t bufSize, bool use24h) { if (!isSynced()) { snprintf(buf, bufSize, "--:--"); diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index cdb49604..a96d230f 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -55,6 +55,10 @@ bool isSynced(); /// may have drifted. Cleared on NTP sync. bool isApproximate(); +/// Returns the epoch of the last successful NTP sync (from NVS), or 0 if +/// no sync has ever been recorded. +time_t lastSyncTime(); + /// Format the current time for display. Returns "--:--" if the clock was /// never synced, prefixes with "~" if approximate. /// When use24h is false, formats as "2:05pm" / "12:30am". diff --git a/src/activities/settings/SyncTimeActivity.cpp b/src/activities/settings/SyncTimeActivity.cpp index fc81f3aa..53ae4f28 100644 --- a/src/activities/settings/SyncTimeActivity.cpp +++ b/src/activities/settings/SyncTimeActivity.cpp @@ -6,12 +6,48 @@ #include #include +#include + #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" +static void formatDuration(char* buf, size_t bufSize, int32_t totalSeconds) { + const char* sign = totalSeconds < 0 ? "-" : "+"; + int32_t abs = totalSeconds < 0 ? -totalSeconds : totalSeconds; + + int32_t days = abs / 86400; + int32_t hours = (abs % 86400) / 3600; + int32_t mins = (abs % 3600) / 60; + int32_t secs = abs % 60; + + if (days > 0) { + snprintf(buf, bufSize, "%s%ldd %ldh %ldm", sign, (long)days, (long)hours, (long)mins); + } else if (hours > 0) { + snprintf(buf, bufSize, "%s%ldh %ldm %lds", sign, (long)hours, (long)mins, (long)secs); + } else if (mins > 0) { + snprintf(buf, bufSize, "%s%ldm %lds", sign, (long)mins, (long)secs); + } else { + snprintf(buf, bufSize, "%s%lds", sign, (long)secs); + } +} + +static void formatElapsed(char* buf, size_t bufSize, int32_t totalSeconds) { + int32_t days = totalSeconds / 86400; + int32_t hours = (totalSeconds % 86400) / 3600; + int32_t mins = (totalSeconds % 3600) / 60; + + if (days > 0) { + snprintf(buf, bufSize, "%ldd %ldh ago", (long)days, (long)hours); + } else if (hours > 0) { + snprintf(buf, bufSize, "%ldh %ldm ago", (long)hours, (long)mins); + } else { + snprintf(buf, bufSize, "%ldm ago", (long)mins); + } +} + void SyncTimeActivity::onEnter() { Activity::onEnter(); @@ -54,7 +90,16 @@ void SyncTimeActivity::onWifiSelectionComplete(bool success) { void SyncTimeActivity::onWifiSelectionCancelled() { finish(); } void SyncTimeActivity::performSync() { + hadTimeBeforeSync = HalClock::isSynced(); + preSyncTime = hadTimeBeforeSync ? time(nullptr) : 0; + prevSyncTime = HalClock::lastSyncTime(); + bool ok = HalClock::syncNtp(); + + if (ok && hadTimeBeforeSync) { + driftSeconds = (int32_t)(time(nullptr) - preSyncTime); + } + HalClock::wifiOff(true); state = ok ? SUCCESS : FAILED; @@ -76,7 +121,8 @@ void SyncTimeActivity::render(RenderLock&&) { } if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD); + int y = pageHeight / 2 - 40; + renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD); time_t now = HalClock::now(); struct tm timeinfo; @@ -86,7 +132,45 @@ void SyncTimeActivity::render(RenderLock&&) { char timeStr[32]; snprintf(timeStr, sizeof(timeStr), "%s %04d-%02d-%02d", timePart, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, timeStr); + y += 30; + renderer.drawCenteredText(UI_10_FONT_ID, y, timeStr); + + int32_t elapsedSinceSync = -1; + if (prevSyncTime > 0) { + elapsedSinceSync = (int32_t)(now - prevSyncTime); + } + + char driftStr[80]; + if (hadTimeBeforeSync) { + char driftFmt[24]; + formatDuration(driftFmt, sizeof(driftFmt), driftSeconds); + + if (elapsedSinceSync > 0) { + double hours = (double)elapsedSinceSync / 3600.0; + double rate = (double)driftSeconds / hours; + char rateFmt[16]; + snprintf(rateFmt, sizeof(rateFmt), "%+.2f", rate); + + char driftWithRate[48]; + snprintf(driftWithRate, sizeof(driftWithRate), "%s (%s s/hr)", driftFmt, rateFmt); + snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), driftWithRate); + } else { + snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), driftFmt); + } + } else { + snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), "N/A"); + } + y += 30; + renderer.drawCenteredText(UI_10_FONT_ID, y, driftStr); + + if (elapsedSinceSync > 0) { + char elapsedFmt[24]; + formatElapsed(elapsedFmt, sizeof(elapsedFmt), elapsedSinceSync); + char lastSyncStr[48]; + snprintf(lastSyncStr, sizeof(lastSyncStr), tr(STR_LAST_NTP_SYNC), elapsedFmt); + y += 25; + renderer.drawCenteredText(UI_10_FONT_ID, y, lastSyncStr); + } const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/SyncTimeActivity.h b/src/activities/settings/SyncTimeActivity.h index 43a419fb..08f2eecd 100644 --- a/src/activities/settings/SyncTimeActivity.h +++ b/src/activities/settings/SyncTimeActivity.h @@ -15,6 +15,10 @@ class SyncTimeActivity final : public Activity { private: enum State { CONNECTING, SYNCING, SUCCESS, FAILED }; State state = CONNECTING; + time_t preSyncTime = 0; + time_t prevSyncTime = 0; + int32_t driftSeconds = 0; + bool hadTimeBeforeSync = false; void onWifiSelectionComplete(bool success); void onWifiSelectionCancelled(); void performSync(); From ba5b0b91916ed0b72ce85c54fd0ae99ef702344e Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 27 Mar 2026 18:10:45 +0100 Subject: [PATCH 14/16] Capitalize am / pm --- lib/hal/HalClock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 43806689..4f234f51 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -246,7 +246,7 @@ void formatTime(char* buf, size_t bufSize, bool use24h) { } else { int hour = timeinfo.tm_hour % 12; if (hour == 0) hour = 12; - const char* ampm = timeinfo.tm_hour < 12 ? "am" : "pm"; + const char* ampm = timeinfo.tm_hour < 12 ? "AM" : "PM"; snprintf(buf, bufSize, "%s%d:%02d%s", prefix, hour, timeinfo.tm_min, ampm); } } From 4f92a9935e5502769ebc5a852ea3909e04b12897 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 29 Mar 2026 18:19:35 +0200 Subject: [PATCH 15/16] Adding experimental RTC temperature compensation --- lib/hal/HalClock.cpp | 272 +++++++++++++++++++++++++++++++++++++++---- lib/hal/HalClock.h | 8 ++ src/main.cpp | 1 + 3 files changed, 259 insertions(+), 22 deletions(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index 4f234f51..a03576af 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -9,6 +9,7 @@ #include #include +#include #include // ---- RTC-memory state (survives deep sleep, not cold boot) ---------------- @@ -16,14 +17,45 @@ static constexpr uint32_t CLOCK_RTC_MAGIC = 0xC10C4B1D; static constexpr uint32_t CLOCK_RTC_FLAG_LP_VALID = 0x00000001u; +// Temperature drift model for ESP32 RTC-based timekeeping. +// +// The chip's low-power (slow) clock frequency depends on temperature. +// ESP32 variants can drift by about 2 minutes per day per °C from the +// initial captured operating temperature. +// +// - dt_drift ≈ 120 seconds/day/°C +// - relative frequency error per second per °C = 120 / 86400 +// +// At restore() we apply a first-order correction over the sleep interval: +// corrected_interval = raw_interval × (1 + ΔT × drift_factor), where +// drift_factor = 120 / 86400. +// +// Experimental source: https://www.reddit.com/r/esp32/comments/11cikkp/the_clock_on_the_esp_is_wrong/ +static constexpr float CLOCK_TEMP_DRIFT_SECONDS_PER_SECOND_PER_DEG = 120.0f / 86400.0f; + 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 +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 +RTC_NOINIT_ATTR static float rtcTemperatureC; // captured chip temperature at save static bool clockApproximate = true; +// Drift correction scale factor (learned from NTP sync results). +// +// Raw temp drift model uses 2 min/day/°C -> factor = 120/86400. This is a +// generic base model. The actual board may behave a bit differently. On each +// NTP sync we estimate how the local clock error compares to the model and +// update this scale factor slightly to converge toward real world behavior. +// +// rtcDriftScale = 1.0 means we trust 2 min/day/°C exactly. If the device is +// slower/faster than that, NTP drift calibration adjusts this factor. +static float rtcDriftScale = 1.0f; + +static unsigned long lastPeriodicUpdateMs = 0; +static constexpr unsigned long PERIODIC_UPDATE_INTERVAL_MS = 10UL * 60UL * 1000UL; + struct TimeZoneEntry { const char* tz; }; @@ -56,6 +88,8 @@ 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 constexpr char NVS_DRIFT_KEY[] = "driftcoef"; +static constexpr char NVS_TEMP_KEY[] = "lasttemp"; static void nvsWrite(time_t epoch) { Preferences prefs; @@ -65,6 +99,46 @@ static void nvsWrite(time_t epoch) { } } +static void nvsWriteDriftScale(float driftScale) { + Preferences prefs; + if (prefs.begin(NVS_NAMESPACE, false)) { + prefs.putFloat(NVS_DRIFT_KEY, driftScale); + prefs.end(); + } +} + +static float nvsReadDriftScale() { + Preferences prefs; + float result = 1.0f; + if (prefs.begin(NVS_NAMESPACE, true)) { + result = prefs.getFloat(NVS_DRIFT_KEY, 1.0f); + prefs.end(); + } + // Guard against NaN, Inf, or out-of-range values from corrupted NVS. + if (!std::isfinite(result) || result < 0.1f || result > 5.0f) { + result = 1.0f; + } + return result; +} + +static void nvsWriteLastSyncTemp(float tempC) { + Preferences prefs; + if (prefs.begin(NVS_NAMESPACE, false)) { + prefs.putFloat(NVS_TEMP_KEY, tempC); + prefs.end(); + } +} + +static float nvsReadLastSyncTemp() { + Preferences prefs; + float result = 0.0f; + if (prefs.begin(NVS_NAMESPACE, true)) { + result = prefs.getFloat(NVS_TEMP_KEY, 0.0f); + prefs.end(); + } + return result; +} + static void nvsWriteSyncTime(time_t syncEpoch) { Preferences prefs; if (prefs.begin(NVS_NAMESPACE, false)) { @@ -95,6 +169,11 @@ static time_t nvsReadSyncTime() { // ---- internal helpers ----------------------------------------------------- +static float readChipTemperatureC() { + // ESP32 and ESP32-C3 use the internal ADC temperature sensor. + return (float)temperatureRead(); +} + static void setSystemClock(time_t epoch) { struct timeval tv = {}; tv.tv_sec = epoch; @@ -103,11 +182,53 @@ static void setSystemClock(time_t epoch) { static bool rtcValid() { return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0; } +/// Compute temperature-corrected elapsed seconds from LP timer delta. +/// Uses the trapezoidal rule (average of start + end temperature) as a +/// first-order approximation of the temperature integral over the interval. +/// Returns the corrected elapsed seconds and updates lpNowOut/calNowOut +/// for the caller to re-baseline. +static double computeCorrectedElapsedSec(uint64_t lpNow, float tempNow) { + 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; + } + + // Use the full temperature delta between the average over the interval + // and the calibration reference (which is the capture-time temperature). + // avgTemp approximates the mean temperature during the interval. + // The drift model says the RTC runs (1 + deltaT * driftRate) times + // faster/slower than nominal, so the true elapsed wall-clock time + // differs from the raw LP-derived time by that factor. + float avgTemp = (rtcTemperatureC + tempNow) * 0.5f; + float tempDelta = avgTemp - rtcTemperatureC; // = (tempNow - rtcTemperatureC) / 2 + float tempFactor = 1.0f + tempDelta * CLOCK_TEMP_DRIFT_SECONDS_PER_SECOND_PER_DEG * rtcDriftScale; + if (tempFactor < 0.5f) { + tempFactor = 0.5f; + } else if (tempFactor > 1.5f) { + tempFactor = 1.5f; + } + + double elapsedSec = (double)elapsedUs / 1000000.0; + double correctedSec = elapsedSec * (double)tempFactor; + + LOG_DBG("CLK", "Drift calc: startT=%.1fC nowT=%.1fC dT=%.3f factor=%.6f raw=%.3fs corr=%.3fs", rtcTemperatureC, + tempNow, tempDelta, tempFactor, elapsedSec, correctedSec); + + return correctedSec; +} + /// 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(); + rtcTemperatureC = readChipTemperatureC(); rtcClockMagic = CLOCK_RTC_MAGIC; rtcClockFlags = lpValid ? CLOCK_RTC_FLAG_LP_VALID : 0; nvsWrite(rtcEpoch); @@ -125,6 +246,10 @@ void applyTimezone(uint8_t timeZoneSetting) { } bool syncNtp() { + time_t preSyncTime = time(nullptr); + time_t prevSyncTime = nvsReadSyncTime(); + float prevSyncTemp = nvsReadLastSyncTemp(); + if (esp_sntp_enabled()) { esp_sntp_stop(); } @@ -147,6 +272,48 @@ bool syncNtp() { capture(false); nvsWriteSyncTime(rtcEpoch); + + float currentTemp = rtcTemperatureC; + if (currentTemp != 0.0f) { + nvsWriteLastSyncTemp(currentTemp); + } + + if (prevSyncTime > 0 && preSyncTime > 0 && rtcEpoch > prevSyncTime) { + float interval = (float)(rtcEpoch - prevSyncTime); + // error = how far the local clock was off before NTP corrected it. + // Positive means local clock was behind (NTP jumped us forward). + // Negative means local clock was ahead (NTP pulled us back). + float error = (float)(preSyncTime - rtcEpoch); + if (interval >= 60.0f) { + // Convert to seconds-of-drift per day. + float observedDriftPerDay = error * 86400.0f / interval; + + // Adaptive model calibration: + // - Observed drift is derived from the difference between local clock + // reading just before NTP and the true time reported by NTP, scaled + // to a per-day rate over the interval since the previous sync. + // - The baseline model expects 120 sec/day per °C. + // - Measure temp delta since last sync (from stored NVS temp). + // - If large enough, compute an empirical scale to apply to the model + // so future drift corrections are better aligned with actual hardware. + // - The scale is persisted to NVS via saveBeforeSleep(). + float effectiveScale = rtcDriftScale; + float tempDelta = currentTemp - prevSyncTemp; + if (std::fabs(tempDelta) > 0.1f) { + float modelDriftPerDay = 120.0f * tempDelta; + if (std::fabs(modelDriftPerDay) > 0.01f) { + float measuredScale = observedDriftPerDay / modelDriftPerDay; + effectiveScale = 0.9f * rtcDriftScale + 0.1f * measuredScale; + effectiveScale = std::max(0.1f, std::min(5.0f, effectiveScale)); + rtcDriftScale = effectiveScale; + } + } + + LOG_DBG("CLK", "NTP drift: interval=%.0fs error=%.3fs perDay=%.3f scale=%.3f deltaT=%.2f", interval, error, + observedDriftPerDay, rtcDriftScale, tempDelta); + } + } + clockApproximate = false; LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch); return true; @@ -157,38 +324,51 @@ void saveBeforeSleep(bool keepLpAlive) { return; } capture(keepLpAlive); - LOG_DBG("CLK", "Saved epoch %lld before sleep", (long long)rtcEpoch); + // Persist learned drift scale and last temperature to NVS so they survive + // cold boot. We only write here (not periodically) to minimise flash wear. + nvsWriteDriftScale(rtcDriftScale); + nvsWriteLastSyncTemp(rtcTemperatureC); + LOG_DBG("CLK", "Saved epoch %lld before sleep (driftScale=%.3f)", (long long)rtcEpoch, rtcDriftScale); } void restore() { + rtcDriftScale = nvsReadDriftScale(); + 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. + // + // We restore the wall clock by computing elapsed real time from the + // LP timer delta and applying both frequency calibration and temperature + // drift correction. + // + // Steps: + // 1) Read current LP timer and slow-clock calibration. + // 2) Compute raw elapsed LP ticks, on the same calibration basis used + // when capture() was called. + // 3) Convert elapsed ticks to seconds. + // 4) Apply temperature drift correction based on measured RTC memory + // capture temperature and current chip temp. + // 5) Set system time to rtcEpoch + corrected elapsed seconds. + // + // This is an approximation: we use the average of start/end measured + // temperature as a simple integral proxy. More advanced models could + // sample temperature continuously, but this is a good tradeoff for low + // cost and better accuracy vs no temperature compensation. 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); + float tempNow = readChipTemperatureC(); + double correctedSec = computeCorrectedElapsedSec(lpNow, tempNow); + estimated += (time_t)correctedSec; } + setSystemClock(estimated); - // Re-capture with current LP baseline + // Re-baseline LP timer and temperature for next interval. rtcEpoch = estimated; - rtcLpTimeUs = lpNow; + rtcLpTimeUs = esp_clk_rtc_time(); rtcSlowCal = esp_clk_slowclk_cal_get(); + rtcTemperatureC = readChipTemperatureC(); clockApproximate = true; LOG_INF("CLK", "Restored from RTC + LP timer, epoch %lld", (long long)estimated); return; @@ -207,6 +387,10 @@ void restore() { rtcEpoch = epoch; rtcLpTimeUs = esp_clk_rtc_time(); rtcSlowCal = esp_clk_slowclk_cal_get(); + rtcTemperatureC = nvsReadLastSyncTemp(); + if (rtcTemperatureC == 0.0f) { + rtcTemperatureC = readChipTemperatureC(); + } rtcClockMagic = CLOCK_RTC_MAGIC; rtcClockFlags = 0; clockApproximate = true; @@ -221,6 +405,50 @@ time_t now() { return time(nullptr); } +void updatePeriodic() { + if (!isSynced()) { + return; + } + unsigned long nowMs = millis(); + if (nowMs - lastPeriodicUpdateMs < PERIODIC_UPDATE_INTERVAL_MS) { + return; + } + lastPeriodicUpdateMs = nowMs; + + // Compute temperature-corrected elapsed time since last baseline and apply + // only the drift delta (correction - raw) to the system clock. The kernel + // clock already advanced by the raw amount, so we must not re-add it. + uint64_t lpNow = esp_clk_rtc_time(); + if (lpNow <= rtcLpTimeUs) { + return; + } + + float tempNow = readChipTemperatureC(); + double correctedSec = computeCorrectedElapsedSec(lpNow, tempNow); + + // Raw elapsed seconds (what the kernel clock already counted). + uint64_t rawElapsedUs = lpNow - rtcLpTimeUs; + double rawSec = (double)rawElapsedUs / 1000000.0; + + // The drift delta is the difference between what really elapsed + // (temperature-corrected) and what the kernel counted (raw). + double driftDeltaSec = correctedSec - rawSec; + + // Re-baseline LP timer and temperature for the next interval. + rtcLpTimeUs = lpNow; + rtcSlowCal = esp_clk_slowclk_cal_get(); + rtcTemperatureC = tempNow; + + // Only nudge the system clock if the drift delta is meaningful (>50 ms). + // This avoids unnecessary settimeofday calls for negligible corrections. + if (std::fabs(driftDeltaSec) > 0.05) { + rtcEpoch = time(nullptr) + (time_t)driftDeltaSec; + setSystemClock(rtcEpoch); + LOG_DBG("CLK", "Periodic drift nudge: raw=%.3fs corr=%.3fs delta=%.3fs scale=%.3f", rawSec, correctedSec, + driftDeltaSec, rtcDriftScale); + } +} + bool isSynced() { return time(nullptr) > 1577836800; // > 2020-01-01 } diff --git a/lib/hal/HalClock.h b/lib/hal/HalClock.h index a96d230f..976003e2 100644 --- a/lib/hal/HalClock.h +++ b/lib/hal/HalClock.h @@ -51,6 +51,14 @@ time_t now(); /// True if the clock has been set at least once (NTP or restore). bool isSynced(); +/// Periodic callback (called from main loop) to compensate temperature-induced +/// RTC drift while the device is awake. Runs at a 10-minute interval. +/// Computes the drift delta since the last baseline using the temperature +/// model and nudges the system clock by only that delta (the kernel clock +/// already advanced the raw amount). Drift state is persisted to NVS only +/// in saveBeforeSleep() to minimise flash wear. +void updatePeriodic(); + /// True if the last restore was from a backup (not NTP) — i.e. the clock /// may have drifted. Cleared on NTP sync. bool isApproximate(); diff --git a/src/main.cpp b/src/main.cpp index 4cc63aaa..e8a00a1a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -319,6 +319,7 @@ void loop() { static unsigned long lastMemPrint = 0; gpio.update(); + HalClock::updatePeriodic(); renderer.setFadingFix(SETTINGS.fadingFix); From 174bdf1f3d04575bce35910aaaafe6403cabeca5 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 30 Mar 2026 09:10:04 +0200 Subject: [PATCH 16/16] Fix correction direction --- lib/hal/HalClock.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/hal/HalClock.cpp b/lib/hal/HalClock.cpp index a03576af..5ae8d6af 100644 --- a/lib/hal/HalClock.cpp +++ b/lib/hal/HalClock.cpp @@ -206,7 +206,12 @@ static double computeCorrectedElapsedSec(uint64_t lpNow, float tempNow) { // faster/slower than nominal, so the true elapsed wall-clock time // differs from the raw LP-derived time by that factor. float avgTemp = (rtcTemperatureC + tempNow) * 0.5f; - float tempDelta = avgTemp - rtcTemperatureC; // = (tempNow - rtcTemperatureC) / 2 + // Positive when COOLED DOWN relative to capture temperature. + // ESP32 RC oscillator has a positive temperature coefficient: it runs faster + // when hotter, causing the LP timer to over-count. To recover true elapsed + // time we must REDUCE the raw LP-derived seconds when the device is warmer + // than at capture (and INCREASE them when cooler). Hence the sign inversion. + float tempDelta = rtcTemperatureC - avgTemp; // = (rtcTemperatureC - tempNow) / 2 float tempFactor = 1.0f + tempDelta * CLOCK_TEMP_DRIFT_SECONDS_PER_SECOND_PER_DEG * rtcDriftScale; if (tempFactor < 0.5f) { tempFactor = 0.5f; @@ -218,7 +223,7 @@ static double computeCorrectedElapsedSec(uint64_t lpNow, float tempNow) { double correctedSec = elapsedSec * (double)tempFactor; LOG_DBG("CLK", "Drift calc: startT=%.1fC nowT=%.1fC dT=%.3f factor=%.6f raw=%.3fs corr=%.3fs", rtcTemperatureC, - tempNow, tempDelta, tempFactor, elapsedSec, correctedSec); + tempNow, rtcTemperatureC - avgTemp, tempFactor, elapsedSec, correctedSec); return correctedSec; } @@ -281,8 +286,8 @@ bool syncNtp() { if (prevSyncTime > 0 && preSyncTime > 0 && rtcEpoch > prevSyncTime) { float interval = (float)(rtcEpoch - prevSyncTime); // error = how far the local clock was off before NTP corrected it. - // Positive means local clock was behind (NTP jumped us forward). - // Negative means local clock was ahead (NTP pulled us back). + // Negative means local clock was behind (NTP jumped us forward). + // Positive means local clock was ahead (NTP pulled us back). float error = (float)(preSyncTime - rtcEpoch); if (interval >= 60.0f) { // Convert to seconds-of-drift per day.