#include "network.h" #include #include #include "config.h" #include "portal.h" #include "rtc.h" #include "settings.h" namespace { bool g_synced = false; uint32_t g_lastAttemptMs = 0; uint32_t g_lastSyncMs = 0; bool g_connecting = false; bool g_portalStarted = false; bool g_suspended = false; void startConnect() { const settings::Config &cfg = settings::get(); Serial.printf("[net] connecting to %s\n", cfg.ssid); WiFi.disconnect(); WiFi.begin(cfg.ssid, cfg.password); g_lastAttemptMs = millis(); g_connecting = true; } } // namespace namespace network { void begin() { // The timezone is applied to the system clock regardless of connectivity, so // the RTC-derived time renders correctly offline. setenv("TZ", settings::get().timezone, 1); tzset(); if (!settings::hasWifi()) { Serial.println("[net] no Wi-Fi configured"); return; // main() has already raised the setup portal } WiFi.mode(WIFI_STA); WiFi.setHostname(DEVICE_HOSTNAME); WiFi.setAutoReconnect(true); WiFi.setSleep(true); // the display is idle most of the time startConnect(); } void loop() { if (!settings::hasWifi() || g_suspended) { return; } const uint32_t nowMs = millis(); if (WiFi.status() == WL_CONNECTED) { if (g_connecting) { g_connecting = false; Serial.printf("[net] connected, ip=%s\n", WiFi.localIP().toString().c_str()); syncTimeNow(); } if (!g_portalStarted) { // Serve the settings page on the LAN so the outdoor URL and timezone can // be changed without re-provisioning. portal::startStation(); g_portalStarted = true; } if (g_synced && nowMs - g_lastSyncMs >= NTP_RESYNC_MS) { syncTimeNow(); } return; } // Not connected: retry, but never more often than WIFI_RETRY_MS. The device // deliberately does not fall back to the setup AP here — the clock and the // indoor reading must stay on screen through a router reboot. const uint32_t since = nowMs - g_lastAttemptMs; if (g_connecting && since < WIFI_CONNECT_TIMEOUT_MS) { return; } if (since >= WIFI_RETRY_MS || (g_connecting && since >= WIFI_CONNECT_TIMEOUT_MS)) { if (g_connecting) { Serial.println("[net] connect attempt timed out"); g_connecting = false; g_lastAttemptMs = nowMs; // hold off for WIFI_RETRY_MS before retrying return; } startConnect(); } } bool isConnected() { return WiFi.status() == WL_CONNECTED; } bool syncTimeNow() { if (!isConnected()) { return false; } const settings::Config &cfg = settings::get(); configTzTime(cfg.timezone, cfg.ntpServer, NTP_SERVER_2); // Wait briefly for the first SNTP response. A plausible year means the // system clock has been set from the network rather than left at the epoch. struct tm local = {}; const uint32_t deadline = millis() + 10000UL; bool got = false; while (millis() < deadline) { if (getLocalTime(&local, 200) && local.tm_year + 1900 >= 2024) { got = true; break; } delay(100); } if (!got) { Serial.println("[net] NTP sync failed"); return false; } // Persist UTC into the RTC so timekeeping survives the next outage. const time_t utcNow = time(nullptr); struct tm utc; gmtime_r(&utcNow, &utc); if (!rtc::setTime(utc)) { Serial.println("[net] NTP ok but RTC write failed"); } g_synced = true; g_lastSyncMs = millis(); Serial.printf("[net] time synced: %04d-%02d-%02d %02d:%02d local\n", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, local.tm_hour, local.tm_min); return true; } bool hasSyncedTime() { return g_synced; } void suspend() { if (g_suspended || !settings::hasWifi()) { return; } WiFi.disconnect(true); WiFi.mode(WIFI_OFF); g_suspended = true; g_connecting = false; } void resume() { if (!g_suspended) { return; } g_suspended = false; WiFi.mode(WIFI_STA); WiFi.setHostname(DEVICE_HOSTNAME); WiFi.setAutoReconnect(true); WiFi.setSleep(true); // The settings page and mDNS have to be re-announced on the new association. g_portalStarted = false; startConnect(); } } // namespace network