M1 step 6: Wi-Fi Tools from ordinary scans

- lib/wifi (host-tested): channel occupancy (neighbour spill, signal
  weighting, quietest of 1/6/11) and a signal tracker (history, lost
  detection, click interval)
- WifiService: scan results carry BSSID, channel and security; a scan
  can target one channel for quick tracker refreshes; endListScans()
  turns the radio back off when Wi-Fi is disabled
- Wi-Fi Tools App: networks nearby, channel occupancy bars, signal
  tracker with clicks (m to mute)
- M1 plan: Monitoring-mode views and captures deferred

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 16:33:11 +02:00
co-authored by Claude Opus 5.5
parent 076baa77d1
commit 4cd6a63498
11 changed files with 512 additions and 16 deletions
@@ -0,0 +1,78 @@
#include <unity.h>
#include "channel_occupancy.h"
#include "signal_tracker.h"
using namespace roro;
void setUp() {}
void tearDown() {}
void test_empty_band_is_quiet_everywhere() {
auto load = channelLoad({});
for (int ch = 1; ch <= 13; ch++) TEST_ASSERT_EQUAL_FLOAT(0, load[ch]);
}
void test_an_access_point_also_loads_its_neighbours_less() {
auto load = channelLoad({{6, -50}});
TEST_ASSERT_TRUE(load[6] > load[5]);
TEST_ASSERT_TRUE(load[5] > load[4]);
TEST_ASSERT_TRUE(load[4] > 0);
TEST_ASSERT_EQUAL_FLOAT(0, load[3]);
TEST_ASSERT_EQUAL_FLOAT(load[5], load[7]);
}
void test_stronger_signals_weigh_more() {
auto strong = channelLoad({{6, -40}});
auto weak = channelLoad({{6, -85}});
TEST_ASSERT_TRUE(strong[6] > weak[6]);
TEST_ASSERT_TRUE(weak[6] > 0);
}
void test_recommends_the_quietest_of_1_6_11() {
TEST_ASSERT_EQUAL(11, quietestChannel(channelLoad({{1, -50}, {6, -50}, {6, -60}})));
TEST_ASSERT_EQUAL(1, quietestChannel(channelLoad({{11, -50}, {6, -50}})));
TEST_ASSERT_EQUAL(1, quietestChannel(channelLoad({}))); // ties: the lowest
}
void test_out_of_band_channels_are_ignored() {
auto load = channelLoad({{14, -40}, {0, -40}, {36, -40}});
for (int ch = 1; ch <= 13; ch++) TEST_ASSERT_EQUAL_FLOAT(0, load[ch]);
}
void test_tracker_keeps_recent_readings() {
SignalTracker t(4);
for (int r : {-80, -70, -60, -50, -40}) t.seen(r, 0);
TEST_ASSERT_EQUAL(4, t.history().size());
TEST_ASSERT_EQUAL(-70, t.history().front());
TEST_ASSERT_EQUAL(-40, t.latest());
}
void test_tracker_reports_lost_after_a_while_unseen() {
SignalTracker t(8);
t.seen(-60, 1000);
TEST_ASSERT_FALSE(t.lost(3000));
TEST_ASSERT_TRUE(t.lost(1000 + SignalTracker::kLostAfterMs));
t.missed();
TEST_ASSERT_EQUAL(SignalTracker::kNoSignal, t.history().back());
}
void test_clicks_speed_up_as_the_signal_gets_stronger() {
TEST_ASSERT_TRUE(SignalTracker::clickIntervalMs(-40) < SignalTracker::clickIntervalMs(-60));
TEST_ASSERT_TRUE(SignalTracker::clickIntervalMs(-60) < SignalTracker::clickIntervalMs(-85));
TEST_ASSERT_EQUAL_UINT32(SignalTracker::kFastestClickMs, SignalTracker::clickIntervalMs(-20));
TEST_ASSERT_EQUAL_UINT32(SignalTracker::kSlowestClickMs, SignalTracker::clickIntervalMs(-100));
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_empty_band_is_quiet_everywhere);
RUN_TEST(test_an_access_point_also_loads_its_neighbours_less);
RUN_TEST(test_stronger_signals_weigh_more);
RUN_TEST(test_recommends_the_quietest_of_1_6_11);
RUN_TEST(test_out_of_band_channels_are_ignored);
RUN_TEST(test_tracker_keeps_recent_readings);
RUN_TEST(test_tracker_reports_lost_after_a_while_unseen);
RUN_TEST(test_clicks_speed_up_as_the_signal_gets_stronger);
return UNITY_END();
}