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
+39 -7
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,15 +19,45 @@ 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() {
void WifiService::startListScan(int channel) {
if (scanRunning_) {
listScanPending_ = true; // results of the scan already running will do
return;
}
listScanPending_ = true;
startScan();
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();
if (n == WIFI_SCAN_RUNNING) return;
@@ -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));
}