feat: add silent NTP time sync on boot via saved WiFi credentials
New "Auto Sync on Boot" toggle in Clock Settings. When enabled, a background FreeRTOS task scans for saved WiFi networks at boot, connects, syncs time via NTP, then tears down WiFi — all without blocking boot or requiring user interaction. If no saved network is found after two scan attempts (with a 3-second retry gap), it bails silently. Conflict guards (BootNtpSync::cancel()) added to all WiFi-using activities so the background task cleans up before any user-initiated WiFi flow. Also fixes clock not appearing in the header until a button press by detecting the invalid→valid time transition after NTP sync. Made-with: Cursor
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -91,6 +91,8 @@ inline std::vector<SettingInfo> 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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
15
src/main.cpp
15
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
163
src/util/BootNtpSync.cpp
Normal file
163
src/util/BootNtpSync.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include "BootNtpSync.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<WifiCredential> 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<TaskParams*>(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
|
||||
16
src/util/BootNtpSync.h
Normal file
16
src/util/BootNtpSync.h
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user