diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index f8bf7765..581166f2 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -192,6 +192,19 @@ void HomeActivity::freeCoverBuffer() { } void HomeActivity::loop() { + // Refresh the screen when the displayed minute changes (clock update) + if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) { + time_t now = time(nullptr); + struct tm* t = localtime(&now); + if (t != nullptr && t->tm_year > 100) { + const int currentMinute = t->tm_hour * 60 + t->tm_min; + if (lastRenderedMinute >= 0 && currentMinute != lastRenderedMinute) { + requestUpdate(); + } + lastRenderedMinute = currentMinute; + } + } + const int menuCount = getMenuItemCount(); buttonNavigator.onNext([this, menuCount] { @@ -240,23 +253,6 @@ void HomeActivity::render(Activity::RenderLock&&) { GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr); - // Draw clock in the header area (left side) if enabled - if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) { - time_t now = time(nullptr); - struct tm* t = localtime(&now); - if (t != nullptr && t->tm_year > 100) { - char timeBuf[16]; - if (SETTINGS.homeScreenClock == CrossPointSettings::CLOCK_24H) { - snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min); - } else { - int hour12 = t->tm_hour % 12; - if (hour12 == 0) hour12 = 12; - snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM"); - } - renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, metrics.topPadding, timeBuf, true); - } - } - GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, std::bind(&HomeActivity::storeCoverBuffer, this)); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 5359bf79..57e111cb 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -16,6 +16,7 @@ class HomeActivity final : public Activity { bool recentsLoaded = false; bool firstRenderDone = false; bool hasOpdsUrl = false; + int lastRenderedMinute = -1; // Track displayed minute for clock auto-update bool coverRendered = false; // Track if cover has been rendered once bool coverBufferStored = false; // Track if cover buffer is stored uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index deefd781..00d5bbae 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -12,6 +12,7 @@ #include "activities/util/KeyboardEntryActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/TimeSync.h" void WifiSelectionActivity::onEnter() { Activity::onEnter(); @@ -243,6 +244,9 @@ void WifiSelectionActivity::checkConnectionStatus() { connectedIP = ipStr; autoConnecting = false; + // Start NTP time sync in the background (non-blocking) + TimeSync::startNtpSync(); + // Save this as the last connected network - SD card operations need lock as // we use SPI for both { diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 6d6d88f6..05dc30ad 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include "KOReaderCredentialStore.h" #include "KOReaderDocumentId.h" @@ -12,34 +11,7 @@ #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #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"); - } -} -} // namespace +#include "util/TimeSync.h" void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { exitActivity(); @@ -59,8 +31,8 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { } requestUpdate(); - // Sync time with NTP before making API requests - syncTimeWithNTP(); + // Wait for NTP sync before making API requests (blocks up to 5s) + TimeSync::waitForNtpSync(); { RenderLock lock(*this); @@ -199,8 +171,8 @@ void KOReaderSyncActivity::onEnter() { xTaskCreate( [](void* param) { auto* self = static_cast(param); - // Sync time first - syncTimeWithNTP(); + // Wait for NTP sync before making API requests + TimeSync::waitForNtpSync(); { RenderLock lock(*self); self->statusMessage = tr(STR_CALC_HASH); diff --git a/src/activities/settings/SetTimeActivity.cpp b/src/activities/settings/SetTimeActivity.cpp index 63648d8b..af7626a0 100644 --- a/src/activities/settings/SetTimeActivity.cpp +++ b/src/activities/settings/SetTimeActivity.cpp @@ -34,25 +34,25 @@ void SetTimeActivity::onExit() { Activity::onExit(); } void SetTimeActivity::loop() { // Back button: discard and exit - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onBack(); return; } // Confirm button: apply time and exit - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { applyTime(); onBack(); return; } // Left/Right: switch between hour and minute fields - if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { + if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { selectedField = 0; requestUpdate(); return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { selectedField = 1; requestUpdate(); return; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index ade43ef0..88e918d4 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -6,9 +6,11 @@ #include #include +#include #include #include "Battery.h" +#include "CrossPointSettings.h" #include "I18n.h" #include "RecentBooksStore.h" #include "components/UITheme.h" @@ -260,6 +262,23 @@ 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 on the left side (symmetric with battery on the right) + if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) { + time_t now = time(nullptr); + struct tm* t = localtime(&now); + if (t != nullptr && t->tm_year > 100) { + char timeBuf[16]; + if (SETTINGS.homeScreenClock == CrossPointSettings::CLOCK_24H) { + snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min); + } else { + int hour12 = t->tm_hour % 12; + if (hour12 == 0) hour12 = 12; + snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM"); + } + renderer.drawText(SMALL_FONT_ID, rect.x + 12, rect.y + 5, timeBuf, true); + } + } + if (title) { int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth; auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index e6206639..50d3c4f1 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -6,10 +6,12 @@ #include #include +#include #include #include #include "Battery.h" +#include "CrossPointSettings.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" @@ -113,6 +115,23 @@ 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 on the left side (symmetric with battery on the right) + if (SETTINGS.homeScreenClock != CrossPointSettings::CLOCK_OFF) { + time_t now = time(nullptr); + struct tm* t = localtime(&now); + if (t != nullptr && t->tm_year > 100) { + char timeBuf[16]; + if (SETTINGS.homeScreenClock == CrossPointSettings::CLOCK_24H) { + snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min); + } else { + int hour12 = t->tm_hour % 12; + if (hour12 == 0) hour12 = 12; + snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM"); + } + renderer.drawText(SMALL_FONT_ID, rect.x + 12, rect.y + 5, timeBuf, true); + } + } + if (title) { auto truncatedTitle = renderer.truncatedText( UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD); diff --git a/src/main.cpp b/src/main.cpp index 01ed076d..7fd81f18 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ #include #include +#include #include "Battery.h" #include "CrossPointSettings.h" @@ -350,6 +351,18 @@ void setup() { // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION); + // Log RTC time to verify persistence across deep sleep + { + time_t now = time(nullptr); + struct tm* t = localtime(&now); + if (t != nullptr && t->tm_year > 100) { + LOG_DBG("MAIN", "RTC time: %04d-%02d-%02d %02d:%02d:%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec); + } else { + LOG_DBG("MAIN", "RTC time not set (epoch)"); + } + } + setupDisplayAndFonts(); exitActivity(); diff --git a/src/util/TimeSync.cpp b/src/util/TimeSync.cpp new file mode 100644 index 00000000..b545a875 --- /dev/null +++ b/src/util/TimeSync.cpp @@ -0,0 +1,50 @@ +#include "TimeSync.h" + +#include +#include +#include +#include + +namespace TimeSync { + +void startNtpSync() { + if (esp_sntp_enabled()) { + esp_sntp_stop(); + } + + esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); + esp_sntp_setservername(0, "pool.ntp.org"); + esp_sntp_init(); + + LOG_DBG("NTP", "SNTP service started"); +} + +bool waitForNtpSync(int timeoutMs) { + startNtpSync(); + + const int intervalMs = 100; + const int maxRetries = timeoutMs / intervalMs; + int retry = 0; + + while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) { + vTaskDelay(intervalMs / portTICK_PERIOD_MS); + retry++; + } + + if (retry < maxRetries) { + LOG_DBG("NTP", "Time synced after %d ms", retry * intervalMs); + return true; + } + + LOG_DBG("NTP", "Sync timeout after %d ms", timeoutMs); + return false; +} + +void stopNtpSync() { + if (esp_sntp_enabled()) { + esp_sntp_stop(); + LOG_DBG("NTP", "SNTP service stopped"); + } +} + +} // namespace TimeSync diff --git a/src/util/TimeSync.h b/src/util/TimeSync.h new file mode 100644 index 00000000..700a9d39 --- /dev/null +++ b/src/util/TimeSync.h @@ -0,0 +1,17 @@ +#pragma once + +namespace TimeSync { + +// Start NTP time synchronization (non-blocking). +// Configures and starts the SNTP service; time will be updated +// automatically when the NTP response arrives. +void startNtpSync(); + +// Start NTP sync and block until complete or timeout. +// Returns true if time was synced, false on timeout. +bool waitForNtpSync(int timeoutMs = 5000); + +// Stop the SNTP service. Call before disconnecting WiFi. +void stopNtpSync(); + +} // namespace TimeSync