diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index 721ec9fc..ed93497a 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -400,6 +400,7 @@ enum class StrId : uint16_t { STR_INDEXING_STATUS_ICON, STR_SYNC_CLOCK, STR_TIME_SYNCED, + STR_AUTO_NTP_SYNC, STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK, diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index ca1a6163..f2fa07f4 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Text stavového řádku" STR_INDEXING_STATUS_ICON: "Ikona stavového řádku" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 263691ff..7ebbc04e 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -364,6 +364,7 @@ STR_INDEXING_STATUS_TEXT: "Status Bar Text" STR_INDEXING_STATUS_ICON: "Status Bar Icon" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" STR_MANAGE_BOOK: "Manage Book" STR_ARCHIVE_BOOK: "Archive Book" STR_UNARCHIVE_BOOK: "Unarchive Book" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index a2b09f17..5704cbcb 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Texte barre d'état" STR_INDEXING_STATUS_ICON: "Icône barre d'état" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index b0a8413b..e7aeb78b 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Statusleistentext" STR_INDEXING_STATUS_ICON: "Statusleistensymbol" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 310b282d..7f5329cb 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Texto da barra" STR_INDEXING_STATUS_ICON: "Ícone da barra" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index fa55025d..21d98283 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -318,3 +318,4 @@ STR_EMBEDDED_STYLE: "Stil încorporat" STR_OPDS_SERVER_URL: "URL server OPDS" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index e138f7c1..d6ebf82a 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Текст в строке" STR_INDEXING_STATUS_ICON: "Иконка в строке" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 21c4c060..de88e2fa 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Texto barra estado" STR_INDEXING_STATUS_ICON: "Icono barra estado" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 22afe609..8d4036bf 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -343,3 +343,4 @@ STR_INDEXING_STATUS_TEXT: "Statusfältstext" STR_INDEXING_STATUS_ICON: "Statusfältsikon" STR_SYNC_CLOCK: "Sync Clock" STR_TIME_SYNCED: "Time synced!" +STR_AUTO_NTP_SYNC: "Auto Sync on Boot" diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index efd9aa68..13ec78ce 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -144,6 +144,7 @@ uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const { writer.writeItem(file, timezone); writer.writeItem(file, timezoneOffsetHours); writer.writeItem(file, indexingDisplay); + writer.writeItem(file, autoNtpSync); return writer.item_count; } @@ -288,6 +289,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, indexingDisplay, INDEXING_DISPLAY_COUNT); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, autoNtpSync); + if (++settingsRead >= fileSettingsCount) break; } while (false); if (frontButtonMappingRead) { diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index edf7bfe7..a26dc427 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -227,6 +227,9 @@ class CrossPointSettings { // Custom timezone offset in hours from UTC (-12 to +14) int8_t timezoneOffsetHours = 0; + // Automatically sync time via NTP on boot using saved WiFi credentials + uint8_t autoNtpSync = 0; + ~CrossPointSettings() = default; // Get singleton instance diff --git a/src/SettingsList.h b/src/SettingsList.h index 6ad2f193..f65c7d1c 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -91,6 +91,8 @@ inline std::vector getSettingsList() { {StrId::STR_TZ_UTC, StrId::STR_TZ_EASTERN, StrId::STR_TZ_CENTRAL, StrId::STR_TZ_MOUNTAIN, StrId::STR_TZ_PACIFIC, StrId::STR_TZ_ALASKA, StrId::STR_TZ_HAWAII, StrId::STR_TZ_CUSTOM}, "timezone", StrId::STR_CAT_CLOCK), + SettingInfo::Toggle(StrId::STR_AUTO_NTP_SYNC, &CrossPointSettings::autoNtpSync, "autoNtpSync", + StrId::STR_CAT_CLOCK), // --- Reader --- SettingInfo::DynamicEnum( diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index d326c842..2e5d4153 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -13,6 +13,7 @@ #include "MappedInputManager.h" #include "NetworkModeSelectionActivity.h" #include "WifiSelectionActivity.h" +#include "util/BootNtpSync.h" #include "activities/network/CalibreConnectActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -35,6 +36,8 @@ constexpr uint16_t DNS_PORT = 53; void CrossPointWebServerActivity::onEnter() { ActivityWithSubactivity::onEnter(); + BootNtpSync::cancel(); + LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); // Reset state diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index b5fa85e8..a74e450a 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -12,11 +12,14 @@ #include "activities/util/KeyboardEntryActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/BootNtpSync.h" #include "util/TimeSync.h" void WifiSelectionActivity::onEnter() { Activity::onEnter(); + BootNtpSync::cancel(); + // Load saved WiFi credentials - 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 d959232f..788042f6 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -11,6 +11,7 @@ #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/BootNtpSync.h" #include "util/TimeSync.h" void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { @@ -155,6 +156,8 @@ void KOReaderSyncActivity::performUpload() { void KOReaderSyncActivity::onEnter() { ActivityWithSubactivity::onEnter(); + BootNtpSync::cancel(); + // Check for credentials first if (!KOREADER_STORE.hasCredentials()) { state = NO_CREDENTIALS; diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index 1a487636..aba89c3b 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -8,6 +8,7 @@ #include "KOReaderSyncClient.h" #include "MappedInputManager.h" #include "activities/network/WifiSelectionActivity.h" +#include "util/BootNtpSync.h" #include "components/UITheme.h" #include "fontIds.h" @@ -53,6 +54,8 @@ void KOReaderAuthActivity::performAuthentication() { void KOReaderAuthActivity::onEnter() { ActivityWithSubactivity::onEnter(); + BootNtpSync::cancel(); + // Turn on WiFi WiFi.mode(WIFI_STA); diff --git a/src/activities/settings/NtpSyncActivity.cpp b/src/activities/settings/NtpSyncActivity.cpp index f4b2116c..ba449134 100644 --- a/src/activities/settings/NtpSyncActivity.cpp +++ b/src/activities/settings/NtpSyncActivity.cpp @@ -10,6 +10,7 @@ #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/BootNtpSync.h" #include "util/TimeSync.h" static constexpr unsigned long AUTO_DISMISS_MS = 5000; @@ -52,6 +53,7 @@ void NtpSyncActivity::onWifiSelectionComplete(const bool success) { void NtpSyncActivity::onEnter() { ActivityWithSubactivity::onEnter(); + BootNtpSync::cancel(); LOG_DBG("NTP", "Turning on WiFi..."); WiFi.mode(WIFI_STA); diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index 8efc3013..db2a9dc6 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -9,6 +9,7 @@ #include "components/UITheme.h" #include "fontIds.h" #include "network/OtaUpdater.h" +#include "util/BootNtpSync.h" void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { exitActivity(); @@ -58,6 +59,8 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { void OtaUpdateActivity::onEnter() { ActivityWithSubactivity::onEnter(); + BootNtpSync::cancel(); + // Turn on WiFi immediately LOG_DBG("OTA", "Turning on WiFi..."); WiFi.mode(WIFI_STA); diff --git a/src/main.cpp b/src/main.cpp index 5e183183..e5dfaec3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ #include "activities/util/FullScreenMessageActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/BootNtpSync.h" #include "util/ButtonNavigator.h" HalDisplay display; @@ -342,6 +343,7 @@ void setup() { I18N.loadSettings(); KOREADER_STORE.loadFromFile(); + BootNtpSync::start(); UITheme::getInstance().reload(); ButtonNavigator::setMappedInputManager(mappedInputManager); @@ -459,14 +461,23 @@ void loop() { // Refresh screen when the displayed minute changes (clock in header) if (SETTINGS.clockFormat != CrossPointSettings::CLOCK_OFF && currentActivity) { static int lastRenderedMinute = -1; + static bool sawInvalidTime = false; 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) { + if (lastRenderedMinute < 0) { + lastRenderedMinute = currentMinute; + if (sawInvalidTime) { + // Time just became valid (e.g. background NTP sync completed) + currentActivity->requestUpdate(); + } + } else if (currentMinute != lastRenderedMinute) { currentActivity->requestUpdate(); + lastRenderedMinute = currentMinute; } - lastRenderedMinute = currentMinute; + } else { + sawInvalidTime = true; } } diff --git a/src/util/BootNtpSync.cpp b/src/util/BootNtpSync.cpp new file mode 100644 index 00000000..c4627201 --- /dev/null +++ b/src/util/BootNtpSync.cpp @@ -0,0 +1,163 @@ +#include "BootNtpSync.h" + +#include +#include +#include +#include + +#include +#include + +#include "CrossPointSettings.h" +#include "WifiCredentialStore.h" +#include "util/TimeSync.h" + +namespace BootNtpSync { + +static volatile bool running = false; +static TaskHandle_t taskHandle = nullptr; + +struct TaskParams { + std::vector credentials; + std::string lastConnectedSsid; +}; + +static bool tryConnectToSavedNetwork(const TaskParams& params) { + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + vTaskDelay(100 / portTICK_PERIOD_MS); + + LOG_DBG("BNTP", "Scanning WiFi networks..."); + int16_t count = WiFi.scanNetworks(); + if (count <= 0) { + LOG_DBG("BNTP", "Scan returned %d networks", count); + WiFi.scanDelete(); + return false; + } + + LOG_DBG("BNTP", "Found %d networks, matching against %zu saved credentials", count, params.credentials.size()); + + // Find best match: prefer lastConnectedSsid, otherwise first saved match + const WifiCredential* bestMatch = nullptr; + for (int i = 0; i < count; i++) { + std::string ssid = WiFi.SSID(i).c_str(); + for (const auto& cred : params.credentials) { + if (cred.ssid == ssid) { + if (!bestMatch || cred.ssid == params.lastConnectedSsid) { + bestMatch = &cred; + } + if (cred.ssid == params.lastConnectedSsid) { + break; // Can't do better than lastConnected + } + } + } + if (bestMatch && bestMatch->ssid == params.lastConnectedSsid) { + break; + } + } + + WiFi.scanDelete(); + + if (!bestMatch) { + LOG_DBG("BNTP", "No saved network found in scan results"); + return false; + } + + LOG_DBG("BNTP", "Connecting to %s", bestMatch->ssid.c_str()); + if (!bestMatch->password.empty()) { + WiFi.begin(bestMatch->ssid.c_str(), bestMatch->password.c_str()); + } else { + WiFi.begin(bestMatch->ssid.c_str()); + } + + const unsigned long start = millis(); + constexpr unsigned long CONNECT_TIMEOUT_MS = 10000; + while (WiFi.status() != WL_CONNECTED && millis() - start < CONNECT_TIMEOUT_MS) { + if (!running) return false; + vTaskDelay(100 / portTICK_PERIOD_MS); + + wl_status_t status = WiFi.status(); + if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { + LOG_DBG("BNTP", "Connection failed (status=%d)", status); + return false; + } + } + + if (WiFi.status() == WL_CONNECTED) { + LOG_DBG("BNTP", "Connected to %s", bestMatch->ssid.c_str()); + return true; + } + + LOG_DBG("BNTP", "Connection timed out"); + return false; +} + +static void taskFunc(void* param) { + auto* params = static_cast(param); + + bool connected = tryConnectToSavedNetwork(*params); + + if (!connected && running) { + LOG_DBG("BNTP", "First scan failed, retrying in 3s..."); + vTaskDelay(3000 / portTICK_PERIOD_MS); + if (running) { + connected = tryConnectToSavedNetwork(*params); + } + } + + if (connected && running) { + LOG_DBG("BNTP", "Starting NTP sync..."); + bool synced = TimeSync::waitForNtpSync(5000); + TimeSync::stopNtpSync(); + if (synced) { + LOG_DBG("BNTP", "NTP sync successful"); + } else { + LOG_DBG("BNTP", "NTP sync timed out, continuing without time"); + } + } + + WiFi.disconnect(false); + vTaskDelay(100 / portTICK_PERIOD_MS); + WiFi.mode(WIFI_OFF); + vTaskDelay(100 / portTICK_PERIOD_MS); + + delete params; + running = false; + taskHandle = nullptr; + LOG_DBG("BNTP", "Boot NTP task complete"); + vTaskDelete(nullptr); +} + +void start() { + if (!SETTINGS.autoNtpSync) { + return; + } + + WIFI_STORE.loadFromFile(); + const auto& creds = WIFI_STORE.getCredentials(); + if (creds.empty()) { + LOG_DBG("BNTP", "No saved WiFi credentials, skipping boot NTP sync"); + return; + } + + auto* params = new TaskParams{creds, WIFI_STORE.getLastConnectedSsid()}; + + running = true; + xTaskCreate(taskFunc, "BootNTP", 4096, params, 1, &taskHandle); + LOG_DBG("BNTP", "Boot NTP sync task started"); +} + +void cancel() { + if (!running) return; + LOG_DBG("BNTP", "Cancelling boot NTP sync..."); + running = false; + // Wait for the task to notice and clean up (up to 2s) + for (int i = 0; i < 20 && taskHandle != nullptr; i++) { + delay(100); + } + LOG_DBG("BNTP", "Boot NTP sync cancelled"); +} + +bool isRunning() { return running; } + +} // namespace BootNtpSync diff --git a/src/util/BootNtpSync.h b/src/util/BootNtpSync.h new file mode 100644 index 00000000..9c55a2f7 --- /dev/null +++ b/src/util/BootNtpSync.h @@ -0,0 +1,16 @@ +#pragma once + +namespace BootNtpSync { + +// Spawn a background FreeRTOS task that scans for saved WiFi networks, +// connects, syncs NTP, then tears down WiFi. Non-blocking; does nothing +// if autoNtpSync is disabled or no credentials are stored. +void start(); + +// Signal the background task to abort and wait for it to finish. +// Call before starting any other WiFi operation. +void cancel(); + +bool isRunning(); + +} // namespace BootNtpSync