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:
@@ -47,5 +47,7 @@ sudo usermod -aG dialout "$USER"
|
|||||||
| `key up\|down\|left\|right\|select\|back\|home` | Injects a logical key press |
|
| `key up\|down\|left\|right\|select\|back\|home` | Injects a logical key press |
|
||||||
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
| `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
|
||||||
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
|
||||||
|
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
|
||||||
|
| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap |
|
||||||
|
|
||||||
`scripts/flash.sh` stops a running serial log first, since it would hold the port.
|
`scripts/flash.sh` stops a running serial log first, since it would hold the port.
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ const RowDef kRows[] = {
|
|||||||
{Row::Region, Kind::Choice, "Region"}, {Row::Timezone, Kind::Choice, "Timezone"},
|
{Row::Region, Kind::Choice, "Region"}, {Row::Timezone, Kind::Choice, "Timezone"},
|
||||||
{Row::Brightness, Kind::Slider, "Brightness"}, {Row::DimTimeout, Kind::Choice, "Dim after"},
|
{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::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"},
|
{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::OffTimeout: return formatSeconds(settings_.getInt(Setting::OffTimeoutS));
|
||||||
case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off";
|
case Row::Sound: return settings_.getBool(Setting::Sound) ? "On" : "Off";
|
||||||
case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised";
|
case Row::ProbeMacs: return settings_.getBool(Setting::ProbeMacRaw) ? "Raw" : "Pseudonymised";
|
||||||
|
case Row::Wifi: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
||||||
default: return "";
|
default: return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace roro {
|
|||||||
// values, choice lists and validation messages. Rendering and navigation live in the App.
|
// values, choice lists and validation messages. Rendering and navigation live in the App.
|
||||||
class SettingsMenu {
|
class SettingsMenu {
|
||||||
public:
|
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 };
|
enum class Kind { Text, Choice, Toggle, Slider, Page };
|
||||||
|
|
||||||
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
explicit SettingsMenu(Settings& settings) : settings_(settings) {}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const Definition kDefinitions[] = {
|
|||||||
{"off_s", Kind::Int, 60, nullptr, 10, 3600},
|
{"off_s", Kind::Int, 60, nullptr, 10, 3600},
|
||||||
{"sound", Kind::Bool, 1, nullptr, 0, 1},
|
{"sound", Kind::Bool, 1, nullptr, 0, 1},
|
||||||
{"probe_mac_raw", 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),
|
static_assert(sizeof(kDefinitions) / sizeof(kDefinitions[0]) == static_cast<size_t>(Setting::Count),
|
||||||
"every Setting needs a definition");
|
"every Setting needs a definition");
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ enum class Setting : uint8_t {
|
|||||||
OffTimeoutS, // int, 10..3600, above DimTimeoutS
|
OffTimeoutS, // int, 10..3600, above DimTimeoutS
|
||||||
Sound, // bool
|
Sound, // bool
|
||||||
ProbeMacRaw, // bool: probe-request Logs keep raw MAC addresses
|
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
|
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
|
||||||
@@ -30,6 +30,9 @@ bool SettingsApp::onKey(const KeyEvent& e) {
|
|||||||
case Page::Choice: return onChoiceKey(e);
|
case Page::Choice: return onChoiceKey(e);
|
||||||
case Page::Storage: return onStorageKey(e);
|
case Page::Storage: return onStorageKey(e);
|
||||||
case Page::About: return onAboutKey(e);
|
case Page::About: return onAboutKey(e);
|
||||||
|
case Page::Wifi:
|
||||||
|
if (!wifiPage_.onKey(e)) page_ = Page::Menu;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -61,7 +64,16 @@ bool SettingsApp::onMenuKey(const KeyEvent& e) {
|
|||||||
break;
|
break;
|
||||||
case Kind::Toggle: menu_.toggle(i); break;
|
case Kind::Toggle: menu_.toggle(i); break;
|
||||||
case Kind::Slider: break;
|
case Kind::Slider: break;
|
||||||
case Kind::Page: page_ = menu_.row(i) == Row::Storage ? Page::Storage : Page::About; break;
|
case Kind::Page:
|
||||||
|
switch (menu_.row(i)) {
|
||||||
|
case Row::Storage: page_ = Page::Storage; break;
|
||||||
|
case Row::Wifi:
|
||||||
|
page_ = Page::Wifi;
|
||||||
|
wifiPage_.enter();
|
||||||
|
break;
|
||||||
|
default: page_ = Page::About; break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -123,7 +135,8 @@ bool SettingsApp::onAboutKey(const KeyEvent& e) {
|
|||||||
|
|
||||||
void SettingsApp::update(uint32_t nowMs) {
|
void SettingsApp::update(uint32_t nowMs) {
|
||||||
// Live values on Storage and About.
|
// Live values on Storage and About.
|
||||||
if ((page_ == Page::Storage || page_ == Page::About) && nowMs - lastRefreshMs_ >= 1000) {
|
bool live = page_ == Page::Storage || page_ == Page::About || (page_ == Page::Wifi && wifiPage_.live());
|
||||||
|
if (live && nowMs - lastRefreshMs_ >= 500) {
|
||||||
lastRefreshMs_ = nowMs;
|
lastRefreshMs_ = nowMs;
|
||||||
requestRedraw();
|
requestRedraw();
|
||||||
}
|
}
|
||||||
@@ -204,6 +217,7 @@ void SettingsApp::draw(Canvas& c) {
|
|||||||
*eraseDialog_);
|
*eraseDialog_);
|
||||||
break;
|
break;
|
||||||
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
|
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
|
||||||
|
case Page::Wifi: wifiPage_.draw(c); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -13,6 +13,7 @@
|
|||||||
#include "services/battery_service.h"
|
#include "services/battery_service.h"
|
||||||
#include "services/clock_service.h"
|
#include "services/clock_service.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
|
#include "apps/wifi_settings_page.h"
|
||||||
#include "settings_menu.h"
|
#include "settings_menu.h"
|
||||||
#include "ui/theme.h"
|
#include "ui/theme.h"
|
||||||
|
|
||||||
@@ -25,20 +26,25 @@ struct SettingsAppDeps {
|
|||||||
BatteryService& battery;
|
BatteryService& battery;
|
||||||
StorageService& storage;
|
StorageService& storage;
|
||||||
ClockService& clock;
|
ClockService& clock;
|
||||||
|
WifiService& wifi;
|
||||||
|
SavedNetworks& savedNetworks;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Settings: every user-facing setting, plus the Storage and About pages.
|
// Settings: every user-facing setting, plus the Storage and About pages.
|
||||||
class SettingsApp : public App {
|
class SettingsApp : public App {
|
||||||
public:
|
public:
|
||||||
explicit SettingsApp(const SettingsAppDeps& deps) : d_(deps), menu_(deps.settings) {}
|
explicit SettingsApp(const SettingsAppDeps& deps)
|
||||||
|
: d_(deps), menu_(deps.settings), wifiPage_(deps.settings, deps.savedNetworks, deps.wifi, deps.bus) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
bool textEntryActive() const override { return page_ == Page::Text; }
|
bool textEntryActive() const override {
|
||||||
|
return page_ == Page::Text || (page_ == Page::Wifi && wifiPage_.textEntryActive());
|
||||||
|
}
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class Page { Menu, Text, Choice, Storage, About };
|
enum class Page { Menu, Text, Choice, Storage, About, Wifi };
|
||||||
|
|
||||||
bool onMenuKey(const KeyEvent& e);
|
bool onMenuKey(const KeyEvent& e);
|
||||||
bool onTextKey(const KeyEvent& e);
|
bool onTextKey(const KeyEvent& e);
|
||||||
@@ -51,6 +57,7 @@ class SettingsApp : public App {
|
|||||||
|
|
||||||
SettingsAppDeps d_;
|
SettingsAppDeps d_;
|
||||||
SettingsMenu menu_;
|
SettingsMenu menu_;
|
||||||
|
WifiSettingsPage wifiPage_;
|
||||||
Page page_ = Page::Menu;
|
Page page_ = Page::Menu;
|
||||||
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
||||||
ListModel choices_{theme::kContent.h / theme::kLineHeight};
|
ListModel choices_{theme::kContent.h / theme::kLineHeight};
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
#include "wifi_settings_page.h"
|
||||||
|
|
||||||
|
#include "ui/fonts.h"
|
||||||
|
#include "ui/widgets.h"
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
using State = WifiController::State;
|
||||||
|
|
||||||
|
void WifiSettingsPage::enter() {
|
||||||
|
view_ = View::Main;
|
||||||
|
forgetDialog_.reset();
|
||||||
|
refreshMain();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WifiSettingsPage::refreshMain() { main_.setCount(kFixedRows + saved_.count()); }
|
||||||
|
|
||||||
|
void WifiSettingsPage::warn(const std::string& text, NotificationLevel level) {
|
||||||
|
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(level)));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WifiSettingsPage::statusText() const {
|
||||||
|
switch (wifi_.state()) {
|
||||||
|
case State::Off: return "Off";
|
||||||
|
case State::Scanning: return "Searching...";
|
||||||
|
case State::Waiting: return "No saved network in range";
|
||||||
|
case State::Connecting: return "Joining " + wifi_.ssid() + "...";
|
||||||
|
case State::Connected: return wifi_.ssid() + " (" + std::to_string(wifi_.rssi()) + " dBm)";
|
||||||
|
case State::Monitoring: return "Monitoring";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WifiSettingsPage::onKey(const KeyEvent& e) {
|
||||||
|
if (forgetDialog_) {
|
||||||
|
forgetDialog_->onKey(e);
|
||||||
|
if (forgetDialog_->result() == 1) {
|
||||||
|
saved_.forget(saved_.at(main_.selected() - kFixedRows).ssid);
|
||||||
|
refreshMain();
|
||||||
|
}
|
||||||
|
if (forgetDialog_->result() != DialogModel::kPending) forgetDialog_.reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (view_) {
|
||||||
|
case View::Main:
|
||||||
|
switch (e.key) {
|
||||||
|
case Key::Up: main_.up(); return true;
|
||||||
|
case Key::Down: main_.down(); return true;
|
||||||
|
case Key::Back: return false;
|
||||||
|
case Key::Left:
|
||||||
|
case Key::Right:
|
||||||
|
if (main_.selected() != kToggle) return true;
|
||||||
|
[[fallthrough]];
|
||||||
|
case Key::Select:
|
||||||
|
switch (main_.selected()) {
|
||||||
|
case kToggle: settings_.setBool(Setting::WifiEnabled, !settings_.getBool(Setting::WifiEnabled)); break;
|
||||||
|
case kStatus: break;
|
||||||
|
case kAddScanned:
|
||||||
|
wifi_.startListScan();
|
||||||
|
scan_.setCount(0);
|
||||||
|
view_ = View::Scan;
|
||||||
|
break;
|
||||||
|
case kAddHidden:
|
||||||
|
newHidden_ = true;
|
||||||
|
editor_ = LineEditor(32);
|
||||||
|
view_ = View::Ssid;
|
||||||
|
break;
|
||||||
|
default: forgetDialog_.reset(new DialogModel({"Cancel", "Forget"})); break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case View::Scan: {
|
||||||
|
const auto& found = wifi_.listScan();
|
||||||
|
if (wifi_.listScanDone()) scan_.setCount(static_cast<int>(found.size()));
|
||||||
|
switch (e.key) {
|
||||||
|
case Key::Up: scan_.up(); break;
|
||||||
|
case Key::Down: scan_.down(); break;
|
||||||
|
case Key::Back: view_ = View::Main; break;
|
||||||
|
case Key::Select:
|
||||||
|
if (scan_.selected() < 0) break;
|
||||||
|
newSsid_ = found[scan_.selected()].ssid;
|
||||||
|
newHidden_ = false;
|
||||||
|
if (found[scan_.selected()].open) {
|
||||||
|
std::string error = saved_.add(newSsid_, "", false);
|
||||||
|
if (!error.empty()) warn(error);
|
||||||
|
wifi_.savedNetworksChanged();
|
||||||
|
enter();
|
||||||
|
} else {
|
||||||
|
editor_ = LineEditor(63);
|
||||||
|
view_ = View::Password;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case View::Ssid:
|
||||||
|
case View::Password:
|
||||||
|
switch (e.key) {
|
||||||
|
case Key::Char: editor_.insert(e.ch); break;
|
||||||
|
case Key::Delete: editor_.backspace(); break;
|
||||||
|
case Key::Left: editor_.left(); break;
|
||||||
|
case Key::Right: editor_.right(); break;
|
||||||
|
case Key::Back: enter(); break;
|
||||||
|
case Key::Select:
|
||||||
|
if (view_ == View::Ssid) {
|
||||||
|
newSsid_ = editor_.text();
|
||||||
|
editor_ = LineEditor(63);
|
||||||
|
view_ = View::Password;
|
||||||
|
} else {
|
||||||
|
std::string error = saved_.add(newSsid_, editor_.text(), newHidden_);
|
||||||
|
if (!error.empty()) {
|
||||||
|
warn(error);
|
||||||
|
} else {
|
||||||
|
warn("Saved " + newSsid_, NotificationLevel::Info);
|
||||||
|
wifi_.savedNetworksChanged();
|
||||||
|
enter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WifiSettingsPage::draw(Canvas& c) {
|
||||||
|
const auto& area = theme::kContent;
|
||||||
|
switch (view_) {
|
||||||
|
case View::Main:
|
||||||
|
widgets::list(
|
||||||
|
c, main_, area,
|
||||||
|
[this](int i) -> std::string {
|
||||||
|
switch (i) {
|
||||||
|
case kToggle: return "Wi-Fi";
|
||||||
|
case kStatus: return "Status";
|
||||||
|
case kAddScanned: return "Add a network";
|
||||||
|
case kAddHidden: return "Add a hidden network";
|
||||||
|
default: {
|
||||||
|
const auto& n = saved_.at(i - kFixedRows);
|
||||||
|
return " " + n.ssid + (n.hidden ? " (hidden)" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[this](int i) -> std::string {
|
||||||
|
switch (i) {
|
||||||
|
case kToggle: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
||||||
|
case kStatus: return statusText();
|
||||||
|
case kAddScanned:
|
||||||
|
case kAddHidden: return ">";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (forgetDialog_ && main_.selected() >= kFixedRows)
|
||||||
|
widgets::dialog(c, "Forget network?", saved_.at(main_.selected() - kFixedRows).ssid, *forgetDialog_);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case View::Scan: {
|
||||||
|
if (!wifi_.listScanDone()) {
|
||||||
|
widgets::textLines(c, {"Scanning..."}, 0, area);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto& found = wifi_.listScan();
|
||||||
|
if (scan_.count() != static_cast<int>(found.size())) scan_.setCount(static_cast<int>(found.size()));
|
||||||
|
if (found.empty()) {
|
||||||
|
widgets::textLines(c, {"No networks found.", "", "`: back"}, 0, area);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
widgets::list(
|
||||||
|
c, scan_, area, [&](int i) { return found[i].ssid + (found[i].open ? " (open)" : ""); },
|
||||||
|
[&](int i) { return std::to_string(found[i].rssi) + " dBm"; });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case View::Ssid:
|
||||||
|
case View::Password: {
|
||||||
|
c.setFont(&fonts::body);
|
||||||
|
c.setTextColor(theme::kMuted);
|
||||||
|
std::string title = view_ == View::Ssid ? "Hidden network name" : "Password for " + newSsid_;
|
||||||
|
c.drawString(title.c_str(), 4, area.y + 4);
|
||||||
|
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||||
|
c.drawString(view_ == View::Ssid ? "Enter: next `: cancel" : "Enter: save (empty = open) `: cancel", 4,
|
||||||
|
area.y + 44);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "dialog_model.h"
|
||||||
|
#include "event_bus.h"
|
||||||
|
#include "key_event.h"
|
||||||
|
#include "line_editor.h"
|
||||||
|
#include "list_model.h"
|
||||||
|
#include "saved_networks.h"
|
||||||
|
#include "services/wifi_service.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "ui/canvas.h"
|
||||||
|
#include "ui/theme.h"
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
// Settings → Wi-Fi: the On/Off switch, current connection, Saved Networks, and adding networks
|
||||||
|
// from a scan or by name (hidden). Owned by the Settings App.
|
||||||
|
class WifiSettingsPage {
|
||||||
|
public:
|
||||||
|
WifiSettingsPage(Settings& settings, SavedNetworks& saved, WifiService& wifi, EventBus& bus)
|
||||||
|
: settings_(settings), saved_(saved), wifi_(wifi), bus_(bus) {}
|
||||||
|
|
||||||
|
void enter();
|
||||||
|
bool onKey(const KeyEvent& e); // false: leave the page
|
||||||
|
bool textEntryActive() const { return view_ == View::Ssid || view_ == View::Password; }
|
||||||
|
bool live() const { return view_ == View::Main || view_ == View::Scan; } // redraw periodically
|
||||||
|
void draw(Canvas& c);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class View { Main, Scan, Ssid, Password };
|
||||||
|
enum Fixed { kToggle, kStatus, kAddScanned, kAddHidden, kFixedRows };
|
||||||
|
|
||||||
|
void refreshMain();
|
||||||
|
void warn(const std::string& text, NotificationLevel level = NotificationLevel::Warning);
|
||||||
|
std::string statusText() const;
|
||||||
|
|
||||||
|
Settings& settings_;
|
||||||
|
SavedNetworks& saved_;
|
||||||
|
WifiService& wifi_;
|
||||||
|
EventBus& bus_;
|
||||||
|
View view_ = View::Main;
|
||||||
|
ListModel main_{theme::kContent.h / theme::kLineHeight};
|
||||||
|
ListModel scan_{theme::kContent.h / theme::kLineHeight};
|
||||||
|
LineEditor editor_{63};
|
||||||
|
std::string newSsid_;
|
||||||
|
bool newHidden_ = false;
|
||||||
|
std::unique_ptr<DialogModel> forgetDialog_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
+30
-40
@@ -1,7 +1,5 @@
|
|||||||
// roro9stack firmware entry point: wires Services, Apps and the UI loop together.
|
// roro9stack firmware entry point: wires Services, Apps and the UI loop together.
|
||||||
#include <M5Cardputer.h>
|
#include <M5Cardputer.h>
|
||||||
#include <NetworkClientSecure.h>
|
|
||||||
#include <WiFi.h>
|
|
||||||
|
|
||||||
#include "app_manager.h"
|
#include "app_manager.h"
|
||||||
#include "apps/demo_app.h"
|
#include "apps/demo_app.h"
|
||||||
@@ -16,6 +14,7 @@
|
|||||||
#include "services/clock_service.h"
|
#include "services/clock_service.h"
|
||||||
#include "services/power_service.h"
|
#include "services/power_service.h"
|
||||||
#include "services/storage_service.h"
|
#include "services/storage_service.h"
|
||||||
|
#include "services/wifi_service.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "ui/notifier.h"
|
#include "ui/notifier.h"
|
||||||
#include "ui/screen.h"
|
#include "ui/screen.h"
|
||||||
@@ -35,6 +34,8 @@ static BatteryService* battery;
|
|||||||
static StorageService* storage;
|
static StorageService* storage;
|
||||||
static PowerService* power;
|
static PowerService* power;
|
||||||
static ClockService* clockService;
|
static ClockService* clockService;
|
||||||
|
static SavedNetworks* savedNetworks;
|
||||||
|
static WifiService* wifi;
|
||||||
static Notifier* notifier;
|
static Notifier* notifier;
|
||||||
static LauncherApp launcher;
|
static LauncherApp launcher;
|
||||||
static AppManager* apps;
|
static AppManager* apps;
|
||||||
@@ -64,6 +65,18 @@ static StatusInfo currentStatus() {
|
|||||||
s.sdPresent = storageState.present;
|
s.sdPresent = storageState.present;
|
||||||
s.sdLevel = storageState.level;
|
s.sdLevel = storageState.level;
|
||||||
s.compose = keyMapper.pendingCompose();
|
s.compose = keyMapper.pendingCompose();
|
||||||
|
using WifiState = WifiController::State;
|
||||||
|
switch (wifi->state()) {
|
||||||
|
case WifiState::Connected: {
|
||||||
|
s.wifi = StatusInfo::Wifi::Connected;
|
||||||
|
int rssi = wifi->rssi();
|
||||||
|
s.wifiBars = rssi >= -60 ? 3 : rssi >= -70 ? 2 : rssi >= -80 ? 1 : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WifiState::Monitoring: s.wifi = StatusInfo::Wifi::Monitoring; break;
|
||||||
|
case WifiState::Off: s.wifi = StatusInfo::Wifi::None; break;
|
||||||
|
default: s.wifi = StatusInfo::Wifi::Searching; break;
|
||||||
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,17 +93,21 @@ void setup() {
|
|||||||
storage = new StorageService(bus);
|
storage = new StorageService(bus);
|
||||||
power = new PowerService(settings);
|
power = new PowerService(settings);
|
||||||
clockService = new ClockService(settings, bus);
|
clockService = new ClockService(settings, bus);
|
||||||
|
savedNetworks = new SavedNetworks(nvs);
|
||||||
|
savedNetworks->load();
|
||||||
|
wifi = new WifiService(settings, *savedNetworks, *clockService);
|
||||||
notifier = new Notifier(bus, settings);
|
notifier = new Notifier(bus, settings);
|
||||||
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
|
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
|
||||||
services.add(*power);
|
services.add(*power);
|
||||||
services.add(*clockService);
|
services.add(*clockService);
|
||||||
services.add(*battery);
|
services.add(*battery);
|
||||||
services.add(*storage);
|
services.add(*storage);
|
||||||
|
services.add(*wifi);
|
||||||
|
|
||||||
apps = new AppManager(launcher);
|
apps = new AppManager(launcher);
|
||||||
launcher.setManager(*apps);
|
launcher.setManager(*apps);
|
||||||
apps->registerApp({"settings", "Settings", false,
|
apps->registerApp({"settings", "Settings", false,
|
||||||
new SettingsApp({settings, bus, *apps, *battery, *storage, *clockService})});
|
new SettingsApp({settings, bus, *apps, *battery, *storage, *clockService, *wifi, *savedNetworks})});
|
||||||
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
|
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
|
||||||
apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)});
|
apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)});
|
||||||
|
|
||||||
@@ -103,41 +120,6 @@ void setup() {
|
|||||||
Serial.printf("%s %s ready, free heap %u\n", kProductName, versionString(), ESP.getFreeHeap());
|
Serial.printf("%s %s ready, free heap %u\n", kProductName, versionString(), ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dev aid (M1 step 1): heap cost of Wi-Fi and one TLS connection. Credentials come over serial only.
|
|
||||||
static void memoryProbe(const String& ssid, const String& pass) {
|
|
||||||
auto heap = [](const char* stage) {
|
|
||||||
Serial.printf("probe %-18s heap %6u min %6u largest block %6u\n", stage, ESP.getFreeHeap(),
|
|
||||||
ESP.getMinFreeHeap(), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
|
|
||||||
};
|
|
||||||
heap("before wifi");
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
|
||||||
uint32_t start = millis();
|
|
||||||
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) delay(100);
|
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
|
||||||
Serial.printf("probe wifi failed, status %d\n", WiFi.status());
|
|
||||||
heap("wifi failed");
|
|
||||||
WiFi.disconnect(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Serial.printf("probe wifi connected, rssi %d, ip %s\n", WiFi.RSSI(), WiFi.localIP().toString().c_str());
|
|
||||||
heap("wifi connected");
|
|
||||||
{
|
|
||||||
NetworkClientSecure tls;
|
|
||||||
tls.setInsecure(); // measurement only; M1 verifies certificates
|
|
||||||
if (tls.connect("irc.libera.chat", 6697)) {
|
|
||||||
heap("tls connected");
|
|
||||||
tls.stop();
|
|
||||||
} else {
|
|
||||||
Serial.println("probe tls connect failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
heap("tls closed");
|
|
||||||
WiFi.disconnect(true);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
heap("wifi off");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dev aid: serial commands to drive the UI without the keyboard.
|
// Dev aid: serial commands to drive the UI without the keyboard.
|
||||||
static void serialCommands() {
|
static void serialCommands() {
|
||||||
static String line;
|
static String line;
|
||||||
@@ -157,10 +139,18 @@ static void serialCommands() {
|
|||||||
: k == "right" ? Key::Right : k == "back" ? Key::Back : k == "home" ? Key::Home : Key::Select;
|
: k == "right" ? Key::Right : k == "back" ? Key::Back : k == "home" ? Key::Home : Key::Select;
|
||||||
if (!power->onKey(millis())) apps->handleKey(KeyEvent::of(key));
|
if (!power->onKey(millis())) apps->handleKey(KeyEvent::of(key));
|
||||||
}
|
}
|
||||||
if (line.startsWith("probe ")) { // probe <ssid>\t<password>
|
if (line.startsWith("wifi add ")) { // wifi add <ssid>\t<password>: credentials never touch the repo
|
||||||
int tab = line.indexOf('\t');
|
int tab = line.indexOf('\t');
|
||||||
if (tab > 0) memoryProbe(line.substring(6, tab), line.substring(tab + 1));
|
std::string ssid = (tab > 0 ? line.substring(9, tab) : line.substring(9)).c_str();
|
||||||
|
std::string pass = tab > 0 ? line.substring(tab + 1).c_str() : "";
|
||||||
|
std::string error = savedNetworks->add(ssid, pass);
|
||||||
|
Serial.printf("wifi add: %s\n", error.empty() ? "ok" : error.c_str());
|
||||||
|
wifi->savedNetworksChanged();
|
||||||
}
|
}
|
||||||
|
if (line == "wifi status")
|
||||||
|
Serial.printf("wifi: state %d ssid '%s' rssi %d clock %s heap %u min %u\n", (int)wifi->state(),
|
||||||
|
wifi->ssid().c_str(), wifi->rssi(), clockService->displayTime().c_str(), ESP.getFreeHeap(),
|
||||||
|
ESP.getMinFreeHeap());
|
||||||
if (line == "sound off") settings.setBool(Setting::Sound, false);
|
if (line == "sound off") settings.setBool(Setting::Sound, false);
|
||||||
if (line == "sound on") settings.setBool(Setting::Sound, true);
|
if (line == "sound on") settings.setBool(Setting::Sound, true);
|
||||||
if (line == "short") {
|
if (line == "short") {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "event_bus.h"
|
||||||
|
#include "saved_networks.h"
|
||||||
|
#include "service.h"
|
||||||
|
#include "services/clock_service.h"
|
||||||
|
#include "settings.h"
|
||||||
|
#include "wifi_controller.h"
|
||||||
|
|
||||||
|
namespace roro {
|
||||||
|
|
||||||
|
struct ScanEntry {
|
||||||
|
std::string ssid;
|
||||||
|
int rssi;
|
||||||
|
bool open;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Owns the Wi-Fi radio: carries out the WifiController's decisions with the ESP32 Wi-Fi stack,
|
||||||
|
// syncs the clock over NTP once Connected, and runs scans for the Settings network list.
|
||||||
|
class WifiService : public Service {
|
||||||
|
public:
|
||||||
|
WifiService(Settings& settings, SavedNetworks& saved, ClockService& clock)
|
||||||
|
: settings_(settings), saved_(saved), clock_(clock), controller_(saved) {}
|
||||||
|
const char* name() const override { return "wifi"; }
|
||||||
|
uint32_t tickIntervalMs() const override { return 250; }
|
||||||
|
void tick(uint32_t nowMs) override;
|
||||||
|
|
||||||
|
WifiController::State state() const { return controller_.state(); }
|
||||||
|
const std::string& ssid() const { return controller_.ssid(); }
|
||||||
|
int rssi() const;
|
||||||
|
|
||||||
|
// Scan for the "add a network" list; results arrive in listScan() when listScanDone().
|
||||||
|
void startListScan();
|
||||||
|
bool listScanDone() const { return !listScanPending_; }
|
||||||
|
const std::vector<ScanEntry>& listScan() const { return listScan_; }
|
||||||
|
|
||||||
|
// A Saved Network was added: try it now rather than after the retry delay.
|
||||||
|
void savedNetworksChanged() { controller_.retryNow(millis()); }
|
||||||
|
|
||||||
|
void requestMonitor() { apply(controller_.requestMonitor()); }
|
||||||
|
void releaseMonitor() { apply(controller_.releaseMonitor(settings_.getBool(Setting::WifiEnabled))); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void apply(const WifiController::Step& step);
|
||||||
|
void startScan();
|
||||||
|
void finishScan(uint32_t nowMs);
|
||||||
|
void startNtp();
|
||||||
|
|
||||||
|
Settings& settings_;
|
||||||
|
SavedNetworks& saved_;
|
||||||
|
ClockService& clock_;
|
||||||
|
WifiController controller_;
|
||||||
|
bool scanRunning_ = false;
|
||||||
|
bool listScanPending_ = false;
|
||||||
|
std::vector<ScanEntry> listScan_;
|
||||||
|
bool ntpWaiting_ = false;
|
||||||
|
bool radioInitialised_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace roro
|
||||||
@@ -32,6 +32,21 @@ void statusBar(Canvas& c, const StatusInfo& info) {
|
|||||||
if (info.batteryPercent >= 0)
|
if (info.batteryPercent >= 0)
|
||||||
right(std::to_string(info.batteryPercent) + "%", info.batteryPercent <= 15 ? kWarning : kText);
|
right(std::to_string(info.batteryPercent) + "%", info.batteryPercent <= 15 ? kWarning : kText);
|
||||||
if (info.sdPresent) right("SD", info.sdLevel >= 80 ? kWarning : kMuted);
|
if (info.sdPresent) right("SD", info.sdLevel >= 80 ? kWarning : kMuted);
|
||||||
|
switch (info.wifi) {
|
||||||
|
case StatusInfo::Wifi::Connected: {
|
||||||
|
// Three signal bars, then the label.
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int h = 2 + 2 * i;
|
||||||
|
c.fillRect(x - 10 + 4 * i, 9 - h, 3, h, i < info.wifiBars ? kText : kMuted);
|
||||||
|
}
|
||||||
|
x -= 14;
|
||||||
|
right("W", kText);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case StatusInfo::Wifi::Searching: right("W?", kMuted); break;
|
||||||
|
case StatusInfo::Wifi::Monitoring: right("MON", kAccent); break;
|
||||||
|
case StatusInfo::Wifi::None: break;
|
||||||
|
}
|
||||||
if (info.compose) right(std::string("opt ") + (info.compose == '*' ? "" : std::string(1, info.compose)), kAccent);
|
if (info.compose) right(std::string("opt ") + (info.compose == '*' ? "" : std::string(1, info.compose)), kAccent);
|
||||||
c.setTextDatum(top_left);
|
c.setTextDatum(top_left);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -21,10 +21,13 @@ struct StatusInfo {
|
|||||||
bool sdPresent = false;
|
bool sdPresent = false;
|
||||||
int sdLevel = 0; // StorageMonitor level
|
int sdLevel = 0; // StorageMonitor level
|
||||||
char compose = 0; // pending Compose Key state
|
char compose = 0; // pending Compose Key state
|
||||||
|
enum class Wifi { None, Searching, Connected, Monitoring } wifi = Wifi::None;
|
||||||
|
int wifiBars = 0; // 0..3 when Connected
|
||||||
|
|
||||||
bool operator==(const StatusInfo& o) const {
|
bool operator==(const StatusInfo& o) const {
|
||||||
return title == o.title && batteryPercent == o.batteryPercent && clock == o.clock &&
|
return title == o.title && batteryPercent == o.batteryPercent && clock == o.clock &&
|
||||||
sdPresent == o.sdPresent && sdLevel == o.sdLevel && compose == o.compose;
|
sdPresent == o.sdPresent && sdLevel == o.sdLevel && compose == o.compose && wifi == o.wifi &&
|
||||||
|
wifiBars == o.wifiBars;
|
||||||
}
|
}
|
||||||
bool operator!=(const StatusInfo& o) const { return !(*this == o); }
|
bool operator!=(const StatusInfo& o) const { return !(*this == o); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include "../memory_store.h"
|
||||||
|
#include "saved_networks.h"
|
||||||
|
|
||||||
|
using namespace roro;
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
void test_empty_store_has_no_networks() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
TEST_ASSERT_EQUAL(0, n.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_added_networks_survive_reload() {
|
||||||
|
MemoryStore store;
|
||||||
|
{
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
TEST_ASSERT_TRUE(n.add("home", "password1").empty());
|
||||||
|
TEST_ASSERT_TRUE(n.add("cafe", "").empty()); // open network
|
||||||
|
}
|
||||||
|
SavedNetworks again(store);
|
||||||
|
again.load();
|
||||||
|
TEST_ASSERT_EQUAL(2, again.count());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("home", again.at(0).ssid.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("password1", again.at(0).password.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", again.at(1).password.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_adding_an_existing_ssid_updates_its_password() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
n.add("home", "password1");
|
||||||
|
n.add("home", "password2");
|
||||||
|
TEST_ASSERT_EQUAL(1, n.count());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("password2", n.at(0).password.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_at_most_eight_networks() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
for (int i = 0; i < 8; i++) TEST_ASSERT_TRUE(n.add("net" + std::to_string(i), "").empty());
|
||||||
|
TEST_ASSERT_FALSE(n.add("ninth", "").empty());
|
||||||
|
TEST_ASSERT_EQUAL(8, n.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_forget_removes_and_persists() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
n.add("a", "");
|
||||||
|
n.add("b", "");
|
||||||
|
n.add("c", "");
|
||||||
|
n.forget("b");
|
||||||
|
SavedNetworks again(store);
|
||||||
|
again.load();
|
||||||
|
TEST_ASSERT_EQUAL(2, again.count());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("a", again.at(0).ssid.c_str());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("c", again.at(1).ssid.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_validation_follows_wifi_limits() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
TEST_ASSERT_FALSE(n.add("", "").empty()); // SSID 1..32 bytes
|
||||||
|
TEST_ASSERT_FALSE(n.add(std::string(33, 's'), "").empty());
|
||||||
|
TEST_ASSERT_FALSE(n.add("home", "short").empty()); // WPA: 8..63, or empty for open
|
||||||
|
TEST_ASSERT_FALSE(n.add("home", std::string(64, 'p')).empty());
|
||||||
|
TEST_ASSERT_TRUE(n.add(std::string(32, 's'), std::string(63, 'p')).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_hidden_flag_survives_reload() {
|
||||||
|
MemoryStore store;
|
||||||
|
{
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
n.add("secret", "password1", true);
|
||||||
|
}
|
||||||
|
SavedNetworks again(store);
|
||||||
|
again.load();
|
||||||
|
TEST_ASSERT_TRUE(again.at(0).hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_find() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks n(store);
|
||||||
|
n.load();
|
||||||
|
n.add("home", "password1");
|
||||||
|
TEST_ASSERT_NOT_NULL(n.find("home"));
|
||||||
|
TEST_ASSERT_NULL(n.find("other"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_empty_store_has_no_networks);
|
||||||
|
RUN_TEST(test_added_networks_survive_reload);
|
||||||
|
RUN_TEST(test_adding_an_existing_ssid_updates_its_password);
|
||||||
|
RUN_TEST(test_at_most_eight_networks);
|
||||||
|
RUN_TEST(test_forget_removes_and_persists);
|
||||||
|
RUN_TEST(test_validation_follows_wifi_limits);
|
||||||
|
RUN_TEST(test_hidden_flag_survives_reload);
|
||||||
|
RUN_TEST(test_find);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ void test_defaults_when_store_is_empty() {
|
|||||||
TEST_ASSERT_EQUAL(60, s.getInt(Setting::OffTimeoutS));
|
TEST_ASSERT_EQUAL(60, s.getInt(Setting::OffTimeoutS));
|
||||||
TEST_ASSERT_TRUE(s.getBool(Setting::Sound));
|
TEST_ASSERT_TRUE(s.getBool(Setting::Sound));
|
||||||
TEST_ASSERT_TRUE(s.getBool(Setting::ProbeMacRaw));
|
TEST_ASSERT_TRUE(s.getBool(Setting::ProbeMacRaw));
|
||||||
|
TEST_ASSERT_TRUE(s.getBool(Setting::WifiEnabled));
|
||||||
TEST_ASSERT_EQUAL_STRING("EU868", s.getString(Setting::Region).c_str());
|
TEST_ASSERT_EQUAL_STRING("EU868", s.getString(Setting::Region).c_str());
|
||||||
TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", s.getString(Setting::Timezone).c_str());
|
TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", s.getString(Setting::Timezone).c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ void test_brightness_steps_with_left_right_within_bounds() {
|
|||||||
TEST_ASSERT_EQUAL(10, f.settings.getInt(Setting::Brightness));
|
TEST_ASSERT_EQUAL(10, f.settings.getInt(Setting::Brightness));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_wifi_is_a_page() {
|
||||||
|
Fixture f;
|
||||||
|
int wifi = f.row(SettingsMenu::Row::Wifi);
|
||||||
|
TEST_ASSERT_TRUE(wifi >= 0);
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(SettingsMenu::Kind::Page), static_cast<int>(f.menu.kind(wifi)));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("On", f.menu.value(wifi).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
void test_names_are_text_rows_with_their_byte_limits() {
|
void test_names_are_text_rows_with_their_byte_limits() {
|
||||||
Fixture f;
|
Fixture f;
|
||||||
int shortName = f.row(SettingsMenu::Row::ShortName);
|
int shortName = f.row(SettingsMenu::Row::ShortName);
|
||||||
@@ -108,6 +116,7 @@ int main() {
|
|||||||
RUN_TEST(test_choosing_applies_the_setting);
|
RUN_TEST(test_choosing_applies_the_setting);
|
||||||
RUN_TEST(test_choice_that_breaks_a_rule_is_refused_with_a_reason);
|
RUN_TEST(test_choice_that_breaks_a_rule_is_refused_with_a_reason);
|
||||||
RUN_TEST(test_brightness_steps_with_left_right_within_bounds);
|
RUN_TEST(test_brightness_steps_with_left_right_within_bounds);
|
||||||
|
RUN_TEST(test_wifi_is_a_page);
|
||||||
RUN_TEST(test_names_are_text_rows_with_their_byte_limits);
|
RUN_TEST(test_names_are_text_rows_with_their_byte_limits);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include "../memory_store.h"
|
||||||
|
#include "wifi_controller.h"
|
||||||
|
|
||||||
|
using namespace roro;
|
||||||
|
using State = WifiController::State;
|
||||||
|
using Action = WifiController::Action;
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks saved{store};
|
||||||
|
WifiController wifi{saved};
|
||||||
|
Fixture() {
|
||||||
|
saved.load();
|
||||||
|
saved.add("home", "password1");
|
||||||
|
saved.add("work", "password2");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setUp() {}
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
static void assertState(State s, const WifiController& w) {
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(s), static_cast<int>(w.state()));
|
||||||
|
}
|
||||||
|
static void assertAction(Action expected, const WifiController::Step& step) {
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(expected), static_cast<int>(step.action));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_disabled_wifi_stays_off() {
|
||||||
|
Fixture f;
|
||||||
|
assertAction(Action::None, f.wifi.update(0, false));
|
||||||
|
assertState(State::Off, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_enabling_starts_a_scan() {
|
||||||
|
Fixture f;
|
||||||
|
assertAction(Action::StartScan, f.wifi.update(0, true));
|
||||||
|
assertState(State::Scanning, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_scan_joins_the_strongest_saved_network() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
auto step = f.wifi.scanDone({{"cafe", -30}, {"home", -70}, {"work", -55}, {"home", -50}}, 100);
|
||||||
|
assertAction(Action::Connect, step);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("home", step.ssid.c_str()); // -50 beats work's -55
|
||||||
|
TEST_ASSERT_EQUAL_STRING("password1", step.password.c_str());
|
||||||
|
assertState(State::Connecting, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_no_saved_network_in_range_waits_then_rescans_with_growing_delay() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
assertAction(Action::None, f.wifi.scanDone({{"cafe", -30}}, 1000));
|
||||||
|
assertState(State::Waiting, f.wifi);
|
||||||
|
assertAction(Action::None, f.wifi.update(10999, true));
|
||||||
|
assertAction(Action::StartScan, f.wifi.update(11000, true)); // 10 s
|
||||||
|
f.wifi.scanDone({}, 12000);
|
||||||
|
assertAction(Action::None, f.wifi.update(41999, true));
|
||||||
|
assertAction(Action::StartScan, f.wifi.update(42000, true)); // then 30 s
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_connected_resets_the_retry_delay() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({}, 0);
|
||||||
|
f.wifi.update(10000, true);
|
||||||
|
f.wifi.scanDone({{"home", -50}}, 10000);
|
||||||
|
f.wifi.connected(11000);
|
||||||
|
assertState(State::Connected, f.wifi);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("home", f.wifi.ssid().c_str());
|
||||||
|
f.wifi.disconnected(20000);
|
||||||
|
assertState(State::Scanning, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_losing_the_connection_rescans_immediately() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({{"home", -50}}, 0);
|
||||||
|
f.wifi.connected(100);
|
||||||
|
auto step = f.wifi.disconnected(5000);
|
||||||
|
assertAction(Action::StartScan, step);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_connect_timeout_counts_as_failure() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({{"home", -50}}, 0);
|
||||||
|
assertAction(Action::None, f.wifi.update(14999, true));
|
||||||
|
assertAction(Action::Disconnect, f.wifi.update(15000, true));
|
||||||
|
assertState(State::Waiting, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_disabling_disconnects_and_turns_off() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({{"home", -50}}, 0);
|
||||||
|
f.wifi.connected(100);
|
||||||
|
assertAction(Action::RadioOff, f.wifi.update(200, false));
|
||||||
|
assertState(State::Off, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_monitoring_takes_over_and_reconnects_afterwards() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({{"home", -50}}, 0);
|
||||||
|
f.wifi.connected(100);
|
||||||
|
assertAction(Action::StartMonitor, f.wifi.requestMonitor());
|
||||||
|
assertState(State::Monitoring, f.wifi);
|
||||||
|
assertAction(Action::None, f.wifi.update(60000, true)); // stays monitoring
|
||||||
|
assertAction(Action::StopMonitorAndScan, f.wifi.releaseMonitor(true));
|
||||||
|
assertState(State::Scanning, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_monitoring_works_even_when_wifi_is_disabled() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, false);
|
||||||
|
assertAction(Action::StartMonitor, f.wifi.requestMonitor());
|
||||||
|
assertAction(Action::StopMonitorAndOff, f.wifi.releaseMonitor(false));
|
||||||
|
assertState(State::Off, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_late_scan_or_connect_results_are_ignored_while_monitoring() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.requestMonitor();
|
||||||
|
assertAction(Action::None, f.wifi.scanDone({{"home", -50}}, 10));
|
||||||
|
f.wifi.connected(20);
|
||||||
|
assertState(State::Monitoring, f.wifi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_hidden_networks_are_tried_when_no_visible_one_is_in_range() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks saved(store);
|
||||||
|
saved.load();
|
||||||
|
saved.add("home", "password1");
|
||||||
|
saved.add("hideA", "password2", true);
|
||||||
|
saved.add("hideB", "password3", true);
|
||||||
|
WifiController wifi(saved);
|
||||||
|
wifi.update(0, true);
|
||||||
|
auto step = wifi.scanDone({{"cafe", -30}}, 0);
|
||||||
|
assertAction(Action::Connect, step);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("hideA", step.ssid.c_str());
|
||||||
|
wifi.disconnected(100); // failed
|
||||||
|
wifi.update(10100, true);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("hideB", wifi.scanDone({}, 10100).ssid.c_str()); // the next one in turn
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_visible_saved_network_beats_hidden_ones() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks saved(store);
|
||||||
|
saved.load();
|
||||||
|
saved.add("hideA", "password2", true);
|
||||||
|
saved.add("home", "password1");
|
||||||
|
WifiController wifi(saved);
|
||||||
|
wifi.update(0, true);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("home", wifi.scanDone({{"home", -80}}, 0).ssid.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_retry_now_cuts_the_wait_short() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({}, 0);
|
||||||
|
f.wifi.retryNow(500);
|
||||||
|
assertAction(Action::StartScan, f.wifi.update(500, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_no_saved_networks_keeps_the_radio_off() {
|
||||||
|
MemoryStore store;
|
||||||
|
SavedNetworks saved(store);
|
||||||
|
saved.load();
|
||||||
|
WifiController wifi(saved);
|
||||||
|
assertAction(Action::None, wifi.update(0, true));
|
||||||
|
assertState(State::Off, wifi);
|
||||||
|
saved.add("home", "password1");
|
||||||
|
assertAction(Action::StartScan, wifi.update(100, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_forgetting_the_last_network_turns_the_radio_off() {
|
||||||
|
Fixture f;
|
||||||
|
f.wifi.update(0, true);
|
||||||
|
f.wifi.scanDone({{"home", -50}}, 0);
|
||||||
|
f.wifi.connected(100);
|
||||||
|
f.saved.forget("home");
|
||||||
|
f.saved.forget("work");
|
||||||
|
assertAction(Action::RadioOff, f.wifi.update(200, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_disabled_wifi_stays_off);
|
||||||
|
RUN_TEST(test_enabling_starts_a_scan);
|
||||||
|
RUN_TEST(test_scan_joins_the_strongest_saved_network);
|
||||||
|
RUN_TEST(test_no_saved_network_in_range_waits_then_rescans_with_growing_delay);
|
||||||
|
RUN_TEST(test_connected_resets_the_retry_delay);
|
||||||
|
RUN_TEST(test_losing_the_connection_rescans_immediately);
|
||||||
|
RUN_TEST(test_connect_timeout_counts_as_failure);
|
||||||
|
RUN_TEST(test_disabling_disconnects_and_turns_off);
|
||||||
|
RUN_TEST(test_monitoring_takes_over_and_reconnects_afterwards);
|
||||||
|
RUN_TEST(test_monitoring_works_even_when_wifi_is_disabled);
|
||||||
|
RUN_TEST(test_late_scan_or_connect_results_are_ignored_while_monitoring);
|
||||||
|
RUN_TEST(test_hidden_networks_are_tried_when_no_visible_one_is_in_range);
|
||||||
|
RUN_TEST(test_visible_saved_network_beats_hidden_ones);
|
||||||
|
RUN_TEST(test_retry_now_cuts_the_wait_short);
|
||||||
|
RUN_TEST(test_no_saved_networks_keeps_the_radio_off);
|
||||||
|
RUN_TEST(test_forgetting_the_last_network_turns_the_radio_off);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user