M1 step 2: Wi-Fi Service, Saved Networks, NTP, Settings page

- lib/wifi (host-tested): SavedNetworks (up to 8, validated, hidden
  flag, persisted) and WifiController (joins the strongest Saved
  Network, tries hidden ones in turn, backoff 10/30/60 s, connect
  timeout, Monitoring override, radio off without Saved Networks)
- WifiService carries out the controller's actions, sets the EU
  country code, and syncs the Clock over SNTP without touching the TZ
- Wi-Fi On/Off setting; Settings > Wi-Fi page: status, add from a scan
  or a hidden network, forget
- Status Bar: W + signal bars, W? while searching, MON while monitoring
- Serial: wifi add / wifi status (replaces the step 1 heap probe)

Verified on the device: joins the test network ~5 s after boot, clock
set over NTP, ~129 KB free heap while connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-02 14:46:07 +02:00
co-authored by Claude Opus 5.5
parent d8fe676c65
commit 1a0baf4ac7
22 changed files with 1108 additions and 48 deletions
+116
View File
@@ -0,0 +1,116 @@
#include "wifi_service.h"
#include <WiFi.h>
#include <esp_sntp.h>
#include <esp_wifi.h>
namespace roro {
int WifiService::rssi() const {
return controller_.state() == WifiController::State::Connected ? WiFi.RSSI() : 0;
}
void WifiService::startScan() {
if (!radioInitialised_) {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(false); // the controller decides when to reconnect
esp_wifi_set_country_code("EU", true);
radioInitialised_ = true;
}
if (scanRunning_) return;
if (WiFi.scanNetworks(true /* async */, false /* no hidden */) == WIFI_SCAN_FAILED) return;
scanRunning_ = true;
}
void WifiService::startListScan() {
listScanPending_ = true;
startScan();
}
void WifiService::finishScan(uint32_t nowMs) {
int n = WiFi.scanComplete();
if (n == WIFI_SCAN_RUNNING) return;
scanRunning_ = false;
std::vector<ScanResult> results;
std::vector<ScanEntry> entries;
for (int i = 0; i < n; i++) {
std::string ssid = WiFi.SSID(i).c_str();
if (ssid.empty()) continue;
results.push_back({ssid, WiFi.RSSI(i)});
entries.push_back({ssid, WiFi.RSSI(i), WiFi.encryptionType(i) == WIFI_AUTH_OPEN});
}
WiFi.scanDelete();
if (listScanPending_) {
listScan_ = entries;
listScanPending_ = false;
}
apply(controller_.scanDone(results, nowMs));
}
void WifiService::startNtp() {
// Not configTime(): it would overwrite the POSIX TZ the Clock applies.
if (!esp_sntp_enabled()) {
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_init();
} else {
esp_sntp_restart();
}
ntpWaiting_ = true;
}
void WifiService::apply(const WifiController::Step& step) {
using Action = WifiController::Action;
switch (step.action) {
case Action::None: break;
case Action::StartScan: startScan(); break;
case Action::Connect: WiFi.begin(step.ssid.c_str(), step.password.empty() ? nullptr : step.password.c_str()); break;
case Action::Disconnect: WiFi.disconnect(); break;
case Action::RadioOff:
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
radioInitialised_ = scanRunning_ = false;
break;
case Action::StartMonitor:
startScan(); // makes sure the radio is up
WiFi.disconnect();
break;
case Action::StopMonitorAndScan:
esp_wifi_set_promiscuous(false);
startScan();
break;
case Action::StopMonitorAndOff:
esp_wifi_set_promiscuous(false);
WiFi.mode(WIFI_OFF);
radioInitialised_ = scanRunning_ = false;
break;
}
}
void WifiService::tick(uint32_t nowMs) {
using State = WifiController::State;
if (scanRunning_) finishScan(nowMs);
wl_status_t status = WiFi.status();
if (controller_.state() == State::Connecting) {
if (status == WL_CONNECTED) {
apply(controller_.connected(nowMs));
startNtp();
} else if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) {
apply(controller_.disconnected(nowMs));
}
} else if (controller_.state() == State::Connected && status != WL_CONNECTED) {
apply(controller_.disconnected(nowMs));
}
apply(controller_.update(nowMs, settings_.getBool(Setting::WifiEnabled)));
if (ntpWaiting_ && sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) {
ntpWaiting_ = false;
clock_.set(static_cast<int64_t>(time(nullptr)), TimeSource::Ntp);
}
}
} // namespace roro