Public Access
- 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
211 lines
7.0 KiB
C++
211 lines
7.0 KiB
C++
#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();
|
|
}
|