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