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
+3 -3
View File
@@ -8,7 +8,7 @@
|---|---|
| Q40 | Up to 8 **Saved Networks**, joined strongest-first. Added from a scan or as a hidden network. Open networks are allowed; enterprise (802.1X) is not. |
| Q41 / Q48 | The Wi-Fi Service stays **Connected** whenever enabled and a Saved Network is in range. Settings has a Wi-Fi On/Off switch. **Monitoring** happens only while Wi-Fi Tools is open; the device reconnects on exit. |
| Q42 | Captures record **full frames**, from a fixed Wi-Fi channel or hopping, to `/captures/wifi/YYYYMMDD-HHMMSS.pcap`. Band-wide views hop across channels 1–13. |
| Q42 | *Revised 2026-10-02:* Wi-Fi Tools is built from ordinary scans only (access points, channel occupancy, signal tracker). Monitoring-mode views and captures are deferred, to be revisited later. |
| Q43 | The signal tracker clicks faster as the signal gets stronger. On by default, mutable. |
| Q44 | IRC shows one Buffer at a time; Tab cycles Buffers. Commands: `/join /part /msg /me /nick /topic /names /quit /raw`. Logs go to `/irc/<network>/<buffer>/YYYY-MM-DD.log`. |
| Q45 | TLS verifies server certificates against the bundled certificate authorities. Per server, a self-signed certificate can be pinned on first use. |
@@ -26,7 +26,7 @@
- Settings → Wi-Fi: On/Off, scan and add a network (with password), add a hidden network, forget a network.
- The IRC App configures one server (host, port, TLS, nick, SASL or NickServ, auto-join IRC channels). It connects and keeps running after you leave the App. Mentions notify.
- IRC reconnects after Wi-Fi loss or a server drop, and rejoins its IRC channels. Logs are written to the SD card (and stop past 90% usage).
- Wi-Fi Tools: access-point list, channel occupancy, signal tracker, packet counters, probe requests (logged), and pcap capture that opens in Wireshark.
- Wi-Fi Tools: access-point list, channel occupancy with the quietest of channels 1/6/11, and a signal tracker that clicks faster as the signal gets stronger. None of it interrupts the connection or IRC.
- Settings → Storage → Clean-up deletes IRC Logs, probe Logs and Wi-Fi Captures older than a chosen age, with a preview of the space freed.
- Free heap stays above about 40 KB with Wi-Fi, TLS-connected IRC and the UI running.
@@ -50,4 +50,4 @@
Then the TLS transport with the certificate bundle.
5. **IRC App:** Buffer view, input line, command parser (host-tested), server settings page.
6. **Wi-Fi Tools App:** the six views; a pcap writer (host-tested); channel hopper; clicker for the signal tracker.
6. **Wi-Fi Tools App:** access-point list, channel occupancy and signal tracker from scans (occupancy and tracker logic host-tested). Monitoring mode is deferred.
+35
View File
@@ -0,0 +1,35 @@
#include "channel_occupancy.h"
namespace roro {
namespace {
// -90 dBm (barely there) -> 0.1, -40 dBm and stronger -> 1.
float weight(int rssi) {
float w = (rssi + 90) / 50.0f;
if (w < 0.1f) w = 0.1f;
if (w > 1.0f) w = 1.0f;
return w;
}
const float kSpill[] = {1.0f, 0.5f, 0.2f}; // same channel, one away, two away
} // namespace
std::array<float, 14> channelLoad(const std::vector<ChannelSighting>& sightings) {
std::array<float, 14> load{};
for (auto& s : sightings) {
if (s.channel < 1 || s.channel > 13) continue;
for (int d = -2; d <= 2; d++) {
int ch = s.channel + d;
if (ch >= 1 && ch <= 13) load[ch] += weight(s.rssi) * kSpill[d < 0 ? -d : d];
}
}
return load;
}
int quietestChannel(const std::array<float, 14>& load) {
int best = 1;
for (int ch : {6, 11})
if (load[ch] < load[best]) best = ch;
return best;
}
} // namespace roro
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <array>
#include <vector>
namespace roro {
struct ChannelSighting {
int channel;
int rssi;
};
// How crowded each 2.4 GHz Wi-Fi channel (1..13) is, from the access points a scan saw. A 20 MHz
// network spills onto the channels up to two away, less the further it is; a strong signal
// counts more than a faint one. Index 0 is unused.
std::array<float, 14> channelLoad(const std::vector<ChannelSighting>& sightings);
// The least crowded of the non-overlapping channels 1, 6 and 11 (the lowest on a tie).
int quietestChannel(const std::array<float, 14>& load);
} // namespace roro
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <deque>
namespace roro {
// Follows one access point's signal over repeated scans, for finding where it is.
class SignalTracker {
public:
static constexpr int kNoSignal = -127;
static constexpr uint32_t kLostAfterMs = 5000;
static constexpr uint32_t kFastestClickMs = 60;
static constexpr uint32_t kSlowestClickMs = 1200;
explicit SignalTracker(size_t historySize) : size_(historySize) {}
void seen(int rssi, uint32_t nowMs) {
push(rssi);
latest_ = rssi;
lastSeenMs_ = nowMs;
everSeen_ = true;
}
void missed() { push(kNoSignal); } // a scan that didn't see it
bool lost(uint32_t nowMs) const { return !everSeen_ || nowMs - lastSeenMs_ >= kLostAfterMs; }
int latest() const { return latest_; }
const std::deque<int>& history() const { return history_; }
// Time between clicks: fast when the signal is strong (close), slow when faint.
static uint32_t clickIntervalMs(int rssi) {
if (rssi >= -35) return kFastestClickMs;
if (rssi <= -95) return kSlowestClickMs;
float t = (rssi + 95) / 60.0f; // 0 at -95 dBm, 1 at -35 dBm
return static_cast<uint32_t>(kSlowestClickMs - t * (kSlowestClickMs - kFastestClickMs));
}
private:
void push(int v) {
history_.push_back(v);
if (history_.size() > size_) history_.pop_front();
}
size_t size_;
std::deque<int> history_;
int latest_ = kNoSignal;
uint32_t lastSeenMs_ = 0;
bool everSeen_ = false;
};
} // namespace roro
+6 -2
View File
@@ -73,7 +73,9 @@ bool WifiSettingsPage::onKey(const KeyEvent& e) {
}
case View::Scan: {
const auto& found = wifi_.listScan();
std::vector<ScanEntry> found;
for (auto& n : wifi_.listScan())
if (!n.ssid.empty()) found.push_back(n);
if (wifi_.listScanDone()) scan_.setCount(static_cast<int>(found.size()));
switch (e.key) {
case Key::Up: scan_.up(); break;
@@ -165,7 +167,9 @@ void WifiSettingsPage::draw(Canvas& c) {
widgets::textLines(c, {"Scanning..."}, 0, area);
break;
}
const auto& found = wifi_.listScan();
std::vector<ScanEntry> found;
for (auto& n : wifi_.listScan())
if (!n.ssid.empty()) found.push_back(n);
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);
+215
View File
@@ -0,0 +1,215 @@
#include "wifi_tools_app.h"
#include <M5Cardputer.h>
#include <algorithm>
#include "channel_occupancy.h"
#include "ui/fonts.h"
#include "ui/widgets.h"
namespace roro {
namespace {
const char* const kMenu[] = {"Networks nearby", "Channel occupancy", "Signal tracker"};
constexpr uint32_t kListRescanMs = 4000;
constexpr uint32_t kTrackRescanMs = 400;
} // namespace
void WifiToolsApp::onEnter() {
view_ = View::Menu;
menu_.setCount(3);
}
void WifiToolsApp::onExit() { wifi_.endListScans(); }
void WifiToolsApp::open(View v) {
view_ = v;
nextScanMs_ = 0; // scan right away
requestRedraw();
}
std::vector<ScanEntry> WifiToolsApp::sortedScan() const {
auto list = wifi_.listScan();
std::sort(list.begin(), list.end(), [](const ScanEntry& a, const ScanEntry& b) { return a.rssi > b.rssi; });
return list;
}
bool WifiToolsApp::onKey(const KeyEvent& e) {
requestRedraw();
switch (view_) {
case View::Menu:
switch (e.key) {
case Key::Up: menu_.up(); return true;
case Key::Down: menu_.down(); return true;
case Key::Select:
if (menu_.selected() == 2) {
// The tracker follows an access point picked from the list.
open(View::Networks);
} else {
open(menu_.selected() == 0 ? View::Networks : View::Channels);
}
return true;
default: return false;
}
case View::Networks:
switch (e.key) {
case Key::Up: networks_.up(); break;
case Key::Down: networks_.down(); break;
case Key::Back: view_ = View::Menu; break;
case Key::Select: {
auto list = sortedScan();
if (networks_.selected() < 0 || networks_.selected() >= static_cast<int>(list.size())) break;
const ScanEntry& ap = list[networks_.selected()];
targetBssid_ = ap.bssid;
targetName_ = ap.ssid.empty() ? "(hidden network)" : ap.ssid;
targetChannel_ = ap.channel;
tracker_ = SignalTracker(110);
tracker_.seen(ap.rssi, millis());
open(View::Tracker);
break;
}
default: break;
}
return true;
case View::Channels:
if (e.key == Key::Back) view_ = View::Menu;
return true;
case View::Tracker:
if (e.key == Key::Back) view_ = View::Networks;
if (e.key == Key::Char && (e.ch == 'm' || e.ch == 'M')) clicks_ = !clicks_;
return true;
}
return false;
}
void WifiToolsApp::update(uint32_t nowMs) {
if (view_ == View::Menu) return;
if (wifi_.listScanSeq() != seenSeq_) {
seenSeq_ = wifi_.listScanSeq();
if (view_ == View::Tracker) {
bool found = false;
for (auto& ap : wifi_.listScan()) {
if (ap.bssid == targetBssid_) {
tracker_.seen(ap.rssi, nowMs);
targetChannel_ = ap.channel;
found = true;
}
}
if (!found) tracker_.missed();
} else {
networks_.setCount(static_cast<int>(wifi_.listScan().size()));
}
requestRedraw();
}
if (wifi_.listScanDone() && static_cast<int32_t>(nowMs - nextScanMs_) >= 0) {
bool tracking = view_ == View::Tracker;
// A lost access point may have moved channel: fall back to a full scan.
wifi_.startListScan(tracking && !tracker_.lost(nowMs) ? targetChannel_ : 0);
nextScanMs_ = nowMs + (tracking ? kTrackRescanMs : kListRescanMs);
}
if (view_ == View::Tracker && clicks_ && !tracker_.lost(nowMs) && static_cast<int32_t>(nowMs - nextClickMs_) >= 0) {
M5Cardputer.Speaker.tone(2600, 8);
nextClickMs_ = nowMs + SignalTracker::clickIntervalMs(tracker_.latest());
}
}
void WifiToolsApp::draw(Canvas& c) {
switch (view_) {
case View::Menu:
widgets::list(c, menu_, theme::kContent, [](int i) { return std::string(kMenu[i]); });
break;
case View::Networks: drawNetworks(c); break;
case View::Channels: drawChannels(c); break;
case View::Tracker: drawTracker(c); break;
}
}
void WifiToolsApp::drawNetworks(Canvas& c) {
auto list = sortedScan();
if (list.empty()) {
widgets::textLines(c, {wifi_.listScanDone() ? "No networks found yet." : "Scanning..."}, 0, theme::kContent);
return;
}
if (networks_.count() != static_cast<int>(list.size())) networks_.setCount(static_cast<int>(list.size()));
widgets::list(
c, networks_, theme::kContent,
[&](int i) { return list[i].ssid.empty() ? std::string("(hidden)") : list[i].ssid; },
[&](int i) {
return "ch" + std::to_string(list[i].channel) + " " + std::to_string(list[i].rssi) + " " + list[i].security;
});
}
void WifiToolsApp::drawChannels(Canvas& c) {
const auto& area = theme::kContent;
std::vector<ChannelSighting> sightings;
for (auto& ap : wifi_.listScan()) sightings.push_back({ap.channel, ap.rssi});
auto load = channelLoad(sightings);
int best = quietestChannel(load);
float maxLoad = 1.0f;
for (int ch = 1; ch <= 13; ch++) maxLoad = std::max(maxLoad, load[ch]);
c.setFont(&fonts::body);
c.setTextColor(theme::kText);
std::string summary = std::to_string(sightings.size()) + " networks. Quietest of 1/6/11: " + std::to_string(best);
c.drawString(summary.c_str(), 4, area.y + 2);
const int baseY = area.y + area.h - 14, barMaxH = area.h - 34, slot = 17, x0 = 10;
c.setFont(&fonts::small);
for (int ch = 1; ch <= 13; ch++) {
int x = x0 + (ch - 1) * slot;
int h = static_cast<int>(load[ch] / maxLoad * barMaxH);
uint16_t color = ch == best ? theme::kMessage : (ch == 1 || ch == 6 || ch == 11) ? theme::kAccent : theme::kMuted;
if (h > 0) c.fillRect(x, baseY - h, slot - 4, h, color);
c.setTextColor(ch == best ? theme::kMessage : theme::kText);
c.setTextDatum(top_center);
c.drawString(std::to_string(ch).c_str(), x + (slot - 4) / 2, baseY + 3);
c.setTextDatum(top_left);
}
c.drawFastHLine(x0 - 2, baseY, 13 * slot, theme::kMuted);
}
void WifiToolsApp::drawTracker(Canvas& c) {
const auto& area = theme::kContent;
uint32_t now = millis();
bool lost = tracker_.lost(now);
c.setFont(&fonts::bold);
c.setTextColor(theme::kText);
c.drawString(targetName_.c_str(), 4, area.y + 2);
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
std::string sub = targetBssid_ + " ch" + std::to_string(targetChannel_) + " m: clicks " + (clicks_ ? "on" : "off");
c.drawString(sub.c_str(), 4, area.y + 17);
// Big reading
c.setFont(&fonts::bold);
c.setTextSize(2);
c.setTextColor(lost ? theme::kWarning : theme::kText);
std::string reading = lost ? "lost" : std::to_string(tracker_.latest()) + " dBm";
c.drawString(reading.c_str(), 4, area.y + 30);
c.setTextSize(1);
// Strength bar: -95 dBm empty, -35 dBm full
int strength = lost ? 0 : std::min(100, std::max(0, (tracker_.latest() + 95) * 100 / 60));
int barW = area.w - 8;
c.drawRect(4, area.y + 60, barW, 8, theme::kMuted);
c.fillRect(5, area.y + 61, (barW - 2) * strength / 100, 6, theme::kMessage);
// History, newest at the right
const int gy = area.y + 74, gh = area.h - 76;
const auto& h = tracker_.history();
int n = static_cast<int>(h.size());
for (int i = 0; i < n; i++) {
int v = h[i];
if (v == SignalTracker::kNoSignal) continue;
int bh = std::min(gh, std::max(1, (v + 95) * gh / 60));
int x = area.w - 4 - (n - i) * 2;
c.fillRect(x, gy + gh - bh, 2, bh, theme::kAccent);
}
}
} // namespace roro
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <string>
#include <vector>
#include "app.h"
#include "list_model.h"
#include "services/wifi_service.h"
#include "signal_tracker.h"
#include "ui/theme.h"
namespace roro {
// Wi-Fi diagnostics from ordinary scans: nearby access points, how crowded each channel is, and
// a signal tracker for finding one access point. None of it interrupts the connection or IRC.
class WifiToolsApp : public App {
public:
explicit WifiToolsApp(WifiService& wifi) : wifi_(wifi) {}
void onEnter() override;
void onExit() override;
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
void draw(Canvas& c) override;
private:
enum class View { Menu, Networks, Channels, Tracker };
void open(View v);
void drawNetworks(Canvas& c);
void drawChannels(Canvas& c);
void drawTracker(Canvas& c);
std::vector<ScanEntry> sortedScan() const;
WifiService& wifi_;
View view_ = View::Menu;
ListModel menu_{theme::kContent.h / theme::kLineHeight};
ListModel networks_{theme::kContent.h / theme::kLineHeight};
uint32_t seenSeq_ = 0;
uint32_t nextScanMs_ = 0;
// Tracker
std::string targetBssid_, targetName_;
int targetChannel_ = 0;
SignalTracker tracker_{110};
bool clicks_ = true;
uint32_t nextClickMs_ = 0;
};
} // namespace roro
+2
View File
@@ -6,6 +6,7 @@
#include "apps/irc_app.h"
#include "apps/launcher_app.h"
#include "apps/settings_app.h"
#include "apps/wifi_tools_app.h"
#include "apps/setup_app.h"
#include "event_bus.h"
#include "key_mapper.h"
@@ -115,6 +116,7 @@ void setup() {
apps = new AppManager(launcher);
launcher.setManager(*apps);
apps->registerApp({"irc", "IRC", false, new IrcApp(*irc, *clockService, bus)});
apps->registerApp({"wifi-tools", "Wi-Fi Tools", false, new WifiToolsApp(*wifi)});
apps->registerApp({"settings", "Settings", false,
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks})});
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
+40 -8
View File
@@ -11,7 +11,7 @@ int WifiService::rssi() const {
return controller_.state() == WifiController::State::Connected ? WiFi.RSSI() : 0;
}
void WifiService::startScan() {
void WifiService::startScan(int channel) {
if (!radioInitialised_) {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(false); // the controller decides when to reconnect
@@ -19,14 +19,44 @@ void WifiService::startScan() {
radioInitialised_ = true;
}
if (scanRunning_) return;
if (WiFi.scanNetworks(true /* async */, false /* no hidden */) == WIFI_SCAN_FAILED) return;
// Hidden networks are listed too (with an empty name); the controller skips them.
if (WiFi.scanNetworks(true /* async */, true /* hidden */, false, channel ? 120 : 300, channel) == WIFI_SCAN_FAILED)
return;
scanRunning_ = true;
}
void WifiService::startListScan() {
listScanPending_ = true;
startScan();
void WifiService::startListScan(int channel) {
if (scanRunning_) {
listScanPending_ = true; // results of the scan already running will do
return;
}
listScanPending_ = true;
startScan(channel);
}
void WifiService::endListScans() {
listScanPending_ = false;
if (controller_.state() == WifiController::State::Off && radioInitialised_ && !scanRunning_) {
WiFi.mode(WIFI_OFF);
radioInitialised_ = false;
}
}
namespace {
const char* security(wifi_auth_mode_t auth) {
switch (auth) {
case WIFI_AUTH_OPEN: return "open";
case WIFI_AUTH_WEP: return "WEP";
case WIFI_AUTH_WPA_PSK: return "WPA";
case WIFI_AUTH_WPA2_PSK:
case WIFI_AUTH_WPA_WPA2_PSK: return "WPA2";
case WIFI_AUTH_WPA2_ENTERPRISE: return "WPA2-E";
case WIFI_AUTH_WPA3_PSK:
case WIFI_AUTH_WPA2_WPA3_PSK: return "WPA3";
default: return "?";
}
}
} // namespace
void WifiService::finishScan(uint32_t nowMs) {
int n = WiFi.scanComplete();
@@ -37,14 +67,16 @@ void WifiService::finishScan(uint32_t nowMs) {
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_auth_mode_t auth = WiFi.encryptionType(i);
entries.push_back({ssid, WiFi.BSSIDstr(i).c_str(), WiFi.channel(i), WiFi.RSSI(i), auth == WIFI_AUTH_OPEN,
security(auth)});
if (!ssid.empty()) results.push_back({ssid, WiFi.RSSI(i)});
}
WiFi.scanDelete();
if (listScanPending_) {
listScan_ = entries;
listScanPending_ = false;
listScanSeq_++;
}
apply(controller_.scanDone(results, nowMs));
}
+12 -4
View File
@@ -12,9 +12,12 @@
namespace roro {
struct ScanEntry {
std::string ssid;
std::string ssid; // empty for a hidden network
std::string bssid;
int channel;
int rssi;
bool open;
std::string security; // "open", "WPA2", "WPA3"...
};
// Owns the Wi-Fi radio: carries out the WifiController's decisions with the ESP32 Wi-Fi stack,
@@ -31,10 +34,14 @@ class WifiService : public Service {
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();
// Scans for a list of nearby networks (all channels, or just one for a quick refresh); results
// arrive in listScan() when listScanDone(), and listScanSeq() counts completed scans.
void startListScan(int channel = 0);
bool listScanDone() const { return !listScanPending_; }
const std::vector<ScanEntry>& listScan() const { return listScan_; }
uint32_t listScanSeq() const { return listScanSeq_; }
// Done with list scans: switch the radio back off if Wi-Fi itself is off.
void endListScans();
// A Saved Network was added: try it now rather than after the retry delay.
void savedNetworksChanged() { controller_.retryNow(millis()); }
@@ -44,7 +51,7 @@ class WifiService : public Service {
private:
void apply(const WifiController::Step& step);
void startScan();
void startScan(int channel = 0);
void finishScan(uint32_t nowMs);
void startNtp();
@@ -55,6 +62,7 @@ class WifiService : public Service {
bool scanRunning_ = false;
bool listScanPending_ = false;
std::vector<ScanEntry> listScan_;
uint32_t listScanSeq_ = 0;
bool ntpWaiting_ = false;
bool radioInitialised_ = false;
};
@@ -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();
}