#include "wifi_tools_app.h" #include #include #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 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(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(wifi_.listScan().size())); } requestRedraw(); } if (wifi_.listScanDone() && static_cast(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(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(list.size())) networks_.setCount(static_cast(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 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(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(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