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
+99
View File
@@ -0,0 +1,99 @@
#include "wifi_controller.h"
namespace roro {
namespace {
const uint32_t kRetryDelaysMs[] = {10000, 30000, 60000};
const int kRetrySteps = sizeof(kRetryDelaysMs) / sizeof(kRetryDelaysMs[0]);
} // namespace
WifiController::Step WifiController::wait(uint32_t nowMs) {
state_ = State::Waiting;
retryAtMs_ = nowMs + kRetryDelaysMs[retries_ < kRetrySteps ? retries_ : kRetrySteps - 1];
retries_++;
return act(Action::None);
}
WifiController::Step WifiController::update(uint32_t nowMs, bool enabled) {
if (state_ == State::Monitoring) return act(Action::None);
if (!enabled || saved_.count() == 0) { // nothing to join: keep the radio off
if (state_ == State::Off) return act(Action::None);
state_ = State::Off;
ssid_.clear();
return act(Action::RadioOff);
}
switch (state_) {
case State::Off:
state_ = State::Scanning;
return act(Action::StartScan);
case State::Waiting:
if (static_cast<int32_t>(nowMs - retryAtMs_) < 0) return act(Action::None);
state_ = State::Scanning;
return act(Action::StartScan);
case State::Connecting:
if (nowMs - sinceMs_ < kConnectTimeoutMs) return act(Action::None);
wait(nowMs);
return act(Action::Disconnect);
default:
return act(Action::None);
}
}
WifiController::Step WifiController::scanDone(const std::vector<ScanResult>& results, uint32_t nowMs) {
if (state_ != State::Scanning) return act(Action::None);
const ScanResult* best = nullptr;
for (auto& r : results)
if (saved_.find(r.ssid) && (!best || r.rssi > best->rssi)) best = &r;
const SavedNetwork* net = best ? saved_.find(best->ssid) : nullptr;
if (!net) {
// Hidden networks never appear in scans: try them one at a time.
for (int i = 0; i < saved_.count() && !net; i++) {
const SavedNetwork& candidate = saved_.at((nextHidden_ + i) % saved_.count());
if (candidate.hidden) {
net = &candidate;
nextHidden_ = (nextHidden_ + i + 1) % saved_.count();
}
}
}
if (!net) return wait(nowMs);
state_ = State::Connecting;
ssid_ = net->ssid;
sinceMs_ = nowMs;
return Step{Action::Connect, net->ssid, net->password};
}
WifiController::Step WifiController::connected(uint32_t) {
if (state_ != State::Connecting) return act(Action::None);
state_ = State::Connected;
retries_ = 0;
return act(Action::None);
}
WifiController::Step WifiController::disconnected(uint32_t nowMs) {
if (state_ == State::Connected) {
state_ = State::Scanning;
return act(Action::StartScan);
}
if (state_ == State::Connecting) return wait(nowMs); // e.g. wrong password
return act(Action::None);
}
WifiController::Step WifiController::requestMonitor() {
state_ = State::Monitoring;
ssid_.clear();
return act(Action::StartMonitor);
}
WifiController::Step WifiController::releaseMonitor(bool enabled) {
if (state_ != State::Monitoring) return act(Action::None);
retries_ = 0;
if (enabled) {
state_ = State::Scanning;
return act(Action::StopMonitorAndScan);
}
state_ = State::Off;
return act(Action::StopMonitorAndOff);
}
} // namespace roro