Public Access
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:
@@ -20,7 +20,8 @@ const RowDef kRows[] = {
|
||||
{Row::Region, Kind::Choice, "Region"}, {Row::Timezone, Kind::Choice, "Timezone"},
|
||||
{Row::Brightness, Kind::Slider, "Brightness"}, {Row::DimTimeout, Kind::Choice, "Dim after"},
|
||||
{Row::OffTimeout, Kind::Choice, "Screen off after"}, {Row::Sound, Kind::Toggle, "Sound & LED"},
|
||||
{Row::ProbeMacs, Kind::Toggle, "Probe MACs"}, {Row::Storage, Kind::Page, "Storage"},
|
||||
{Row::ProbeMacs, Kind::Toggle, "Probe MACs"}, {Row::Wifi, Kind::Page, "Wi-Fi"},
|
||||
{Row::Storage, Kind::Page, "Storage"},
|
||||
{Row::About, Kind::Page, "About"},
|
||||
};
|
||||
|
||||
@@ -78,6 +79,7 @@ std::string SettingsMenu::value(int i) const {
|
||||
case Row::OffTimeout: return formatSeconds(settings_.getInt(Setting::OffTimeoutS));
|
||||
case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off";
|
||||
case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised";
|
||||
case Row::Wifi: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace roro {
|
||||
// values, choice lists and validation messages. Rendering and navigation live in the App.
|
||||
class SettingsMenu {
|
||||
public:
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, ProbeMacs, Storage, About };
|
||||
enum class Row { LongName, ShortName, Region, Timezone, Brightness, DimTimeout, OffTimeout, Sound, ProbeMacs, Wifi, Storage, About };
|
||||
enum class Kind { Text, Choice, Toggle, Slider, Page };
|
||||
|
||||
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
||||
|
||||
@@ -28,6 +28,7 @@ const Definition kDefinitions[] = {
|
||||
{"off_s", Kind::Int, 60, nullptr, 10, 3600},
|
||||
{"sound", Kind::Bool, 1, nullptr, 0, 1},
|
||||
{"probe_mac_raw", Kind::Bool, 1, nullptr, 0, 1},
|
||||
{"wifi_on", Kind::Bool, 1, nullptr, 0, 1},
|
||||
};
|
||||
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||
"every Setting needs a definition");
|
||||
|
||||
@@ -20,6 +20,7 @@ enum class Setting : uint8_t {
|
||||
OffTimeoutS, // int, 10..3600, above DimTimeoutS
|
||||
Sound, // bool
|
||||
ProbeMacRaw, // bool: probe-request Logs keep raw MAC addresses
|
||||
WifiEnabled, // bool: the Wi-Fi Service stays Connected when a Saved Network is in range
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "saved_networks.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
std::string key(int i, const char* field) { return "net" + std::to_string(i) + "_" + field; }
|
||||
} // namespace
|
||||
|
||||
void SavedNetworks::load() {
|
||||
networks_.clear();
|
||||
int32_t n = 0;
|
||||
store_.getInt("net_count", n);
|
||||
for (int i = 0; i < n && i < kMax; i++) {
|
||||
SavedNetwork net;
|
||||
if (!store_.getString(key(i, "ssid").c_str(), net.ssid)) break;
|
||||
store_.getString(key(i, "pass").c_str(), net.password);
|
||||
int32_t hidden = 0;
|
||||
store_.getInt(key(i, "hid").c_str(), hidden);
|
||||
net.hidden = hidden != 0;
|
||||
networks_.push_back(net);
|
||||
}
|
||||
}
|
||||
|
||||
const SavedNetwork* SavedNetworks::find(const std::string& ssid) const {
|
||||
for (auto& n : networks_)
|
||||
if (n.ssid == ssid) return &n;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string SavedNetworks::add(const std::string& ssid, const std::string& password, bool hidden) {
|
||||
if (ssid.empty() || ssid.size() > 32) return "Network name must be 1 to 32 bytes";
|
||||
if (!password.empty() && (password.size() < 8 || password.size() > 63))
|
||||
return "Password must be 8 to 63 characters (or empty for an open network)";
|
||||
for (auto& n : networks_) {
|
||||
if (n.ssid == ssid) {
|
||||
n.password = password;
|
||||
n.hidden = hidden;
|
||||
save();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (count() >= kMax) return "Already 8 saved networks: forget one first";
|
||||
networks_.push_back({ssid, password, hidden});
|
||||
save();
|
||||
return "";
|
||||
}
|
||||
|
||||
void SavedNetworks::forget(const std::string& ssid) {
|
||||
for (auto it = networks_.begin(); it != networks_.end(); ++it) {
|
||||
if (it->ssid == ssid) {
|
||||
networks_.erase(it);
|
||||
save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SavedNetworks::save() {
|
||||
for (int i = 0; i < count(); i++) {
|
||||
store_.putString(key(i, "ssid").c_str(), networks_[i].ssid);
|
||||
store_.putString(key(i, "pass").c_str(), networks_[i].password);
|
||||
store_.putInt(key(i, "hid").c_str(), networks_[i].hidden ? 1 : 0);
|
||||
}
|
||||
store_.putInt("net_count", count());
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "key_value_store.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
struct SavedNetwork {
|
||||
std::string ssid;
|
||||
std::string password; // empty for an open network
|
||||
bool hidden = false; // doesn't broadcast its name, so never shows up in scans
|
||||
};
|
||||
|
||||
// The Saved Networks the Wi-Fi Service may join (see CONTEXT.md), persisted in internal flash.
|
||||
class SavedNetworks {
|
||||
public:
|
||||
static constexpr int kMax = 8;
|
||||
|
||||
explicit SavedNetworks(KeyValueStore& store) : store_(store) {}
|
||||
|
||||
void load();
|
||||
int count() const { return static_cast<int>(networks_.size()); }
|
||||
const SavedNetwork& at(int i) const { return networks_[i]; }
|
||||
const SavedNetwork* find(const std::string& ssid) const;
|
||||
|
||||
// Adds, or updates the password of an existing SSID. Empty on success, otherwise why not.
|
||||
std::string add(const std::string& ssid, const std::string& password, bool hidden = false);
|
||||
void forget(const std::string& ssid);
|
||||
|
||||
private:
|
||||
void save();
|
||||
|
||||
KeyValueStore& store_;
|
||||
std::vector<SavedNetwork> networks_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -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
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "saved_networks.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
struct ScanResult {
|
||||
std::string ssid;
|
||||
int rssi;
|
||||
};
|
||||
|
||||
// Decides what the Wi-Fi radio should do next. The Wi-Fi Service feeds it what happened (scan
|
||||
// results, connected, disconnected, Monitoring requests) and carries out the returned Action.
|
||||
// Stays Connected to the strongest Saved Network in range while enabled (radio off when there are
|
||||
// no Saved Networks); Monitoring overrides.
|
||||
class WifiController {
|
||||
public:
|
||||
enum class State { Off, Scanning, Connecting, Connected, Waiting, Monitoring };
|
||||
enum class Action { None, StartScan, Connect, Disconnect, RadioOff, StartMonitor, StopMonitorAndScan, StopMonitorAndOff };
|
||||
|
||||
struct Step {
|
||||
Action action = Action::None;
|
||||
std::string ssid;
|
||||
std::string password;
|
||||
};
|
||||
|
||||
static constexpr uint32_t kConnectTimeoutMs = 15000;
|
||||
|
||||
explicit WifiController(const SavedNetworks& saved) : saved_(saved) {}
|
||||
|
||||
Step update(uint32_t nowMs, bool enabled);
|
||||
Step scanDone(const std::vector<ScanResult>& results, uint32_t nowMs);
|
||||
Step connected(uint32_t nowMs);
|
||||
Step disconnected(uint32_t nowMs);
|
||||
// A Saved Network was just added: stop waiting and scan now.
|
||||
void retryNow(uint32_t nowMs) {
|
||||
if (state_ == State::Waiting) retryAtMs_ = nowMs;
|
||||
retries_ = 0;
|
||||
}
|
||||
|
||||
Step requestMonitor();
|
||||
Step releaseMonitor(bool enabled);
|
||||
|
||||
State state() const { return state_; }
|
||||
const std::string& ssid() const { return ssid_; } // network joined or being joined
|
||||
|
||||
private:
|
||||
Step wait(uint32_t nowMs);
|
||||
static Step act(Action a) { return Step{a, "", ""}; }
|
||||
|
||||
const SavedNetworks& saved_;
|
||||
State state_ = State::Off;
|
||||
std::string ssid_;
|
||||
uint32_t sinceMs_ = 0; // when Connecting started
|
||||
uint32_t retryAtMs_ = 0; // when Waiting ends
|
||||
int retries_ = 0;
|
||||
int nextHidden_ = 0; // round-robin over hidden Saved Networks
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user