#include "app_keys.h" #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; constexpr int kWeakRssi = -75; } // namespace void WifiToolsApp::onEnter() { view_ = View::Menu; menu_.setCount(3); } void WifiToolsApp::onExit() { logging_ = false; // logging only runs while this foreground App is scanning wifi_.endListScans(); } void WifiToolsApp::open(View v) { view_ = v; nextScanMs_ = 0; // scan right away requestRedraw(); } void WifiToolsApp::refreshOrder() { order_ = orderNetworks(wifi_.listScan(), sort_, filter_); networks_.setCount(static_cast(order_.size())); } 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: open(menu_.selected() == 1 ? View::Channels : View::Networks); return true; default: return false; } case View::Networks: switch (e.key) { case Key::Up: networks_.up(); return true; case Key::Down: networks_.down(); return true; case Key::Back: view_ = View::Menu; return true; case Key::Select: { if (networks_.selected() < 0 || networks_.selected() >= static_cast(order_.size())) return true; const ScanEntry& ap = wifi_.listScan()[order_[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); return true; } case Key::Char: switch (e.ch) { case 's': sort_ = nextSort(sort_); break; case 'o': filter_.openOnly = !filter_.openOnly; break; case 'h': filter_.hideHidden = !filter_.hideHidden; break; case 'w': filter_.minRssi = filter_.minRssi == kWeakRssi ? -100 : kWeakRssi; break; case 'l': logging_ = !logging_; logThrottle_.reset(); if (logging_) loggedRows_ = 0; break; default: return true; } refreshOrder(); return true; default: 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::logScan(uint32_t nowMs) { std::string date = clock_.localDate(); if (date.empty()) return; // no timestamp without a clock if (!logThrottle_.due(nowMs)) return; std::string path = "/wifi/scans/" + date + ".csv"; if (date != headerDate_) { storage_.appendLine(path, scanlog::header()); headerDate_ = date; } std::string stamp = date + " " + clock_.displayTime(); for (auto& ap : wifi_.listScan()) { storage_.appendLine(path, scanlog::row(stamp, ap)); loggedRows_++; } logThrottle_.logged(nowMs); } 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 { refreshOrder(); if (logging_) logScan(nowMs); } requestRedraw(); } if (wifi_.listScanDone() && static_cast(nowMs - nextScanMs_) >= 0) { bool tracking = view_ == View::Tracker; 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::help(std::vector& out) const { switch (view_) { case View::Menu: keys::add(out, keys::kWifiTools); break; case View::Networks: keys::add(out, keys::kWifiNetworks); break; case View::Channels: break; // nothing but the keys that work everywhere case View::Tracker: keys::add(out, keys::kWifiTracker); break; } } const char* WifiToolsApp::helpTitle() const { switch (view_) { case View::Networks: return "Networks nearby"; case View::Channels: return "Channel occupancy"; case View::Tracker: return "Signal tracker"; default: return nullptr; } } void WifiToolsApp::drawNetworks(Canvas& c) { const auto& area = theme::kContent; const auto& scan = wifi_.listScan(); // Status line: sort, filters, logging. c.setFont(&fonts::small); std::string top = std::string("sort:") + sortName(sort_); if (filter_.openOnly) top += " open"; if (filter_.hideHidden) top += " !hid"; if (filter_.minRssi != -100) top += " strong"; c.setTextColor(theme::kMuted); c.drawString(top.c_str(), 4, area.y + 1); if (logging_) { c.setTextColor(theme::kWarning); c.setTextDatum(top_right); c.drawString(("LOG " + std::to_string(loggedRows_)).c_str(), area.w - 4, area.y + 1); c.setTextDatum(top_left); } theme::Rect list{area.x, area.y + 13, area.w, area.h - 13}; if (order_.empty()) { bool any = !scan.empty(); widgets::textLines(c, {wifi_.listScanDone() ? (any ? "Nothing matches the filter." : "No networks found yet.") : "Scanning...", }, 0, list); return; } widgets::list( c, networks_, list, [&](int i) { const ScanEntry& ap = scan[order_[i]]; return ap.ssid.empty() ? std::string("(hidden)") : ap.ssid; }, [&](int i) { const ScanEntry& ap = scan[order_[i]]; return "ch" + std::to_string(ap.channel) + " " + std::to_string(ap.rssi) + " " + ap.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_) + " clicks " + (clicks_ ? "on" : "off"); c.drawString(sub.c_str(), 4, area.y + 17); 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); 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); 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