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:
cottongin
2026-02-26 18:21:13 -05:00
parent 2eae521b6a
commit 19b6ad047b
22 changed files with 227 additions and 2 deletions

163
src/util/BootNtpSync.cpp Normal file
View 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
View 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