Public Access
Wi-Fi Tools: scan logging to CSV, plus sort and filter
- lib/wifi (host-tested): scan_log (CSV field quoting, header/row, a once-per-interval throttle) and network_list_view (sort by signal / channel / name, filter open-only / hide-hidden / strong-only) - ScanEntry moved to lib/wifi so both are testable on the PC - Wi-Fi Tools networks view: s sort, o/h/w filters, l toggles logging to /wifi/scans/<date>.csv (header + one row per AP per logged scan, throttled 30 s, needs the clock for timestamps); foreground-only - Wi-Fi scan logs replace probe-request logs as a clean-up category - Serial: cat <path>, and key <char> injects a character Verified on the device: CSV written with header, timestamps, BSSID, channel, RSSI, security and SSID; hidden networks marked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -18,7 +18,7 @@ struct CleanupCategory {
|
||||
};
|
||||
inline const CleanupCategory kCleanupCategories[] = {
|
||||
{"IRC logs", "/irc"},
|
||||
{"Probe-request logs", "/wifi/probes"},
|
||||
{"Wi-Fi scan logs", "/wifi/scans"},
|
||||
{"Wi-Fi captures", "/captures/wifi"},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "network_list_view.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
std::string lower(std::string s) {
|
||||
for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<int> orderNetworks(const std::vector<ScanEntry>& entries, WifiSort sort, const NetworkFilter& filter) {
|
||||
std::vector<int> idx;
|
||||
for (int i = 0; i < static_cast<int>(entries.size()); i++) {
|
||||
const ScanEntry& e = entries[i];
|
||||
if (filter.openOnly && !e.open) continue;
|
||||
if (filter.hideHidden && e.ssid.empty()) continue;
|
||||
if (e.rssi < filter.minRssi) continue;
|
||||
idx.push_back(i);
|
||||
}
|
||||
std::stable_sort(idx.begin(), idx.end(), [&](int a, int b) {
|
||||
const ScanEntry& x = entries[a];
|
||||
const ScanEntry& y = entries[b];
|
||||
switch (sort) {
|
||||
case WifiSort::Signal: return x.rssi > y.rssi; // strongest first
|
||||
case WifiSort::Channel: return x.channel != y.channel ? x.channel < y.channel : x.rssi > y.rssi;
|
||||
case WifiSort::Name: {
|
||||
// Hidden networks (no name) sort to the end.
|
||||
if (x.ssid.empty() != y.ssid.empty()) return y.ssid.empty();
|
||||
return lower(x.ssid) < lower(y.ssid);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return idx;
|
||||
}
|
||||
|
||||
const char* sortName(WifiSort sort) {
|
||||
switch (sort) {
|
||||
case WifiSort::Signal: return "signal";
|
||||
case WifiSort::Channel: return "channel";
|
||||
case WifiSort::Name: return "name";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
WifiSort nextSort(WifiSort sort) {
|
||||
switch (sort) {
|
||||
case WifiSort::Signal: return WifiSort::Channel;
|
||||
case WifiSort::Channel: return WifiSort::Name;
|
||||
case WifiSort::Name: return WifiSort::Signal;
|
||||
}
|
||||
return WifiSort::Signal;
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "scan_entry.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
enum class WifiSort { Signal, Channel, Name };
|
||||
|
||||
struct NetworkFilter {
|
||||
bool openOnly = false;
|
||||
bool hideHidden = false;
|
||||
int minRssi = -100; // drop anything weaker
|
||||
};
|
||||
|
||||
// The order and selection the network list shows: indices into `entries`, sorted and filtered.
|
||||
// Stable, so equal rows keep their scan order.
|
||||
std::vector<int> orderNetworks(const std::vector<ScanEntry>& entries, WifiSort sort, const NetworkFilter& filter);
|
||||
|
||||
const char* sortName(WifiSort sort);
|
||||
WifiSort nextSort(WifiSort sort);
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace roro {
|
||||
|
||||
// One access point an ordinary Wi-Fi scan reported (a network announcing itself). No client or
|
||||
// monitor-mode data: just what a beacon broadcasts.
|
||||
struct ScanEntry {
|
||||
std::string ssid; // empty for a hidden network
|
||||
std::string bssid; // the access point's own address, from its beacon
|
||||
int channel;
|
||||
int rssi;
|
||||
bool open;
|
||||
std::string security; // "open", "WPA2", "WPA3"...
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "scan_log.h"
|
||||
|
||||
namespace roro::scanlog {
|
||||
|
||||
std::string csvField(const std::string& value) {
|
||||
bool needsQuote = false;
|
||||
for (char c : value)
|
||||
if (c == ',' || c == '"' || c == '\n' || c == '\r') needsQuote = true;
|
||||
if (!needsQuote) return value;
|
||||
std::string out = "\"";
|
||||
for (char c : value) {
|
||||
if (c == '"') out += '"'; // escape by doubling
|
||||
out += c;
|
||||
}
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string header() { return "time,bssid,channel,rssi,security,ssid"; }
|
||||
|
||||
std::string row(const std::string& timestamp, const ScanEntry& ap) {
|
||||
return csvField(timestamp) + "," + csvField(ap.bssid) + "," + std::to_string(ap.channel) + "," +
|
||||
std::to_string(ap.rssi) + "," + csvField(ap.security) + "," +
|
||||
csvField(ap.ssid.empty() ? "(hidden)" : ap.ssid);
|
||||
}
|
||||
|
||||
} // namespace roro::scanlog
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "scan_entry.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Writes what Wi-Fi Tools already sees (access points, channel, signal) to a CSV Log, so the user
|
||||
// can track their own Wi-Fi environment over time. One row per access point per logged scan.
|
||||
namespace scanlog {
|
||||
|
||||
// A CSV field, quoted when it contains a comma, quote or newline.
|
||||
std::string csvField(const std::string& value);
|
||||
|
||||
// The header row (no trailing newline).
|
||||
std::string header();
|
||||
|
||||
// One data row for an access point seen at `timestamp` (a preformatted local time string).
|
||||
std::string row(const std::string& timestamp, const ScanEntry& ap);
|
||||
|
||||
} // namespace scanlog
|
||||
|
||||
// Decides when to write a scan to the Log: at most once per interval.
|
||||
class ScanLogThrottle {
|
||||
public:
|
||||
explicit ScanLogThrottle(uint32_t intervalMs) : intervalMs_(intervalMs) {}
|
||||
bool due(uint32_t nowMs) const { return !logged_ || nowMs - lastMs_ >= intervalMs_; }
|
||||
void logged(uint32_t nowMs) {
|
||||
lastMs_ = nowMs;
|
||||
logged_ = true;
|
||||
}
|
||||
void reset() { logged_ = false; }
|
||||
|
||||
private:
|
||||
uint32_t intervalMs_;
|
||||
uint32_t lastMs_ = 0;
|
||||
bool logged_ = false;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user