Files
roro9stack/src/main.cpp
T
twislaandClaude Opus 4.8 28d025fb13 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
2026-10-03 20:52:43 +02:00

277 lines
11 KiB
C++

// roro9stack firmware entry point: wires Services, Apps and the UI loop together.
#include <M5Cardputer.h>
#include <SD.h>
#include "app_manager.h"
#include "apps/demo_app.h"
#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"
#include "platform/nvs_store.h"
#include "service_manager.h"
#include "platform/identity.h"
#include "services/battery_service.h"
#include "services/irc_service.h"
#include "services/clock_service.h"
#include "services/power_service.h"
#include "services/storage_service.h"
#include "services/wifi_service.h"
#include "settings.h"
#include "storage_paths.h"
#include "ui/notifier.h"
#include "ui/screen.h"
#include "version.h"
using namespace roro;
static EventBus bus;
static NvsStore nvs;
static Settings settings(nvs, bus);
static ServiceManager services;
static KeyMapper keyMapper;
static Screen screen;
// Constructed in setup(), after the hardware and Settings are ready.
static BatteryService* battery;
static StorageService* storageService;
static PowerService* power;
static ClockService* clockService;
static SavedNetworks* savedNetworks;
static WifiService* wifi;
static IrcService* irc;
static Notifier* notifier;
static LauncherApp launcher;
static AppManager* apps;
static RawKeys readKeys() {
auto state = M5Cardputer.Keyboard.keysState();
RawKeys raw;
raw.chars.assign(state.word.begin(), state.word.end());
raw.fn = state.fn;
raw.opt = state.opt;
raw.ctrl = state.ctrl;
raw.alt = state.alt;
raw.shift = state.shift;
raw.enter = state.enter;
raw.del = state.del;
raw.tab = state.tab;
return raw;
}
static StatusInfo currentStatus() {
StatusInfo s;
s.title = kProductName;
if (apps->foregroundTitle()) s.title = apps->foregroundTitle();
if (battery->estimator().hasReading()) s.batteryPercent = battery->estimator().percent();
s.clock = clockService->displayTime();
auto storageState = storageService->state();
s.sdPresent = storageState.present;
s.sdLevel = storageState.level;
s.compose = keyMapper.pendingCompose();
s.unread = irc->totalUnread();
using WifiState = WifiController::State;
switch (wifi->state()) {
case WifiState::Connected: {
s.wifi = StatusInfo::Wifi::Connected;
int rssi = wifi->rssi();
s.wifiBars = rssi >= -60 ? 3 : rssi >= -70 ? 2 : rssi >= -80 ? 1 : 0;
break;
}
case WifiState::Monitoring: s.wifi = StatusInfo::Wifi::Monitoring; break;
case WifiState::Off: s.wifi = StatusInfo::Wifi::None; break;
default: s.wifi = StatusInfo::Wifi::Searching; break;
}
return s;
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5Cardputer.Display.setRotation(1);
Serial.begin(115200);
nvs.begin();
settings.load();
battery = new BatteryService(bus);
storageService = new StorageService(bus);
power = new PowerService(settings);
clockService = new ClockService(settings, bus);
savedNetworks = new SavedNetworks(nvs);
savedNetworks->load();
wifi = new WifiService(settings, *savedNetworks, *clockService);
irc = new IrcService(nvs, "roro_" + identity::defaultShortName(), *wifi, *storageService, *clockService, bus);
notifier = new Notifier(bus, settings);
notifier->onShow = [](uint32_t now, uint32_t until) { power->onNotification(now, until); };
services.add(*power);
services.add(*clockService);
services.add(*battery);
services.add(*storageService);
services.add(*wifi);
services.add(*irc);
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, *storageService, *clockService)});
apps->registerApp({"settings", "Settings", false,
new SettingsApp({settings, bus, *apps, *battery, *storageService, *clockService, *wifi, *savedNetworks})});
apps->registerApp({"demo", "Widget demo", true, new DemoApp(bus)});
apps->registerApp({"setup", "Setup", true, new SetupApp(settings, *apps)});
bus.subscribe(EventType::Notification, [](const Event& e) { Serial.printf("notification: %s\n", e.text); });
if (!screen.begin()) Serial.println("frame buffer allocation failed");
services.startAll(millis());
apps->begin();
if (!settings.getBool(Setting::SetupDone)) apps->openModal("setup");
Serial.printf("%s %s ready, free heap %u\n", kProductName, versionString(), ESP.getFreeHeap());
}
// Dev aid: serial commands to drive the UI without the keyboard.
static bool listingWanted = false;
static void printListingWhenReady() {
if (!listingWanted || !storageService->listingReady()) return;
listingWanted = false;
auto listing = storageService->listing();
for (size_t i = 0; i < listing.size(); i++) {
uint64_t bytes = 0;
for (auto& f : listing[i]) bytes += f.bytes;
Serial.printf("sd list: %-20s %u files, %s\n", kCleanupCategories[i].label, (unsigned)listing[i].size(),
formatBytes(bytes).c_str());
for (auto& f : listing[i]) Serial.printf(" %s (%u B)\n", f.path.c_str(), (unsigned)f.bytes);
}
}
static void serialCommands() {
static String line;
while (Serial.available()) {
char c = Serial.read();
if (c != '\n') {
line += c;
continue;
}
line.trim();
if (line == "burst")
for (int i = 1; i <= 5; i++)
bus.publish(Event::withText(EventType::Notification, ("Burst " + String(i)).c_str(), 0));
if (line.startsWith("key ")) { // key up|down|left|right|select|back|home
String k = line.substring(4);
Key key = k == "up" ? Key::Up : k == "down" ? Key::Down : k == "left" ? Key::Left
: k == "right" ? Key::Right : k == "back" ? Key::Back : k == "home" ? Key::Home : Key::Select;
KeyEvent ev = k.length() == 1 && k[0] > ' ' ? KeyEvent::character((unsigned char)k[0]) : KeyEvent::of(key);
if (!power->onKey(millis())) apps->handleKey(ev);
}
if (line.startsWith("wifi add ")) { // wifi add <ssid>\t<password>: credentials never touch the repo
int tab = line.indexOf('\t');
std::string ssid = (tab > 0 ? line.substring(9, tab) : line.substring(9)).c_str();
std::string pass = tab > 0 ? line.substring(tab + 1).c_str() : "";
std::string error = savedNetworks->add(ssid, pass);
Serial.printf("wifi add: %s\n", error.empty() ? "ok" : error.c_str());
wifi->savedNetworksChanged();
}
if (line.startsWith("log ")) { // appends to a test IRC Log
std::string path = storage::dailyLogPath({"irc", "dev", "#test"}, clockService->localDate());
storageService->appendLine(path, clockService->displayTime() + " " + line.substring(4).c_str());
Serial.printf("log: queued to %s\n", path.c_str());
}
if (line.startsWith("cat ")) {
std::string path = line.substring(4).c_str();
File f = SD.open(path.c_str());
if (!f) Serial.printf("cat: cannot open %s\n", path.c_str());
else {
Serial.printf("--- %s (%u B) ---\n", path.c_str(), (unsigned)f.size());
int shown = 0;
while (f.available() && shown < 1200) { Serial.write(f.read()); shown++; }
Serial.println("\n--- end ---");
f.close();
}
}
if (line == "sd list") {
storageService->requestListing();
listingWanted = true;
}
if (line == "irc start") irc->connect();
if (line.startsWith("irc say ")) { // irc say <buffer index> <text>
String rest = line.substring(8);
int space = rest.indexOf(' ');
int b = rest.substring(0, space).toInt();
std::string text = rest.substring(space + 1).c_str();
irc->withSession([&](IrcSession& s) { s.input(b, text, clockService->utcNow()); });
}
if (line == "irc dump") {
Serial.printf("irc: status %d, unread %d, heap %u min %u\n", (int)irc->status(), irc->totalUnread(),
ESP.getFreeHeap(), ESP.getMinFreeHeap());
IrcConfig c = irc->draftConfig(); // which login is configured, never the secrets
Serial.printf("irc: nick %s, sasl %s, nickserv %s, autojoin %u channels\n", c.nick.c_str(),
c.saslUser.empty() || c.saslPassword.empty() ? "off" : "on",
c.nickservPassword.empty() ? "off" : "on", (unsigned)c.autojoin.size());
irc->withSession([](IrcSession& s) {
for (int i = 0; i < s.bufferCount(); i++) {
const auto& b = s.buffer(i);
Serial.printf("irc buffer %d %s unread %d%s\n", i, b.name.c_str(), b.unread, b.joined ? " joined" : "");
size_t from = 0;
for (size_t j = from; j < b.lines.size(); j++)
Serial.printf(" <%s> %s\n", b.lines[j].nick.c_str(), b.lines[j].text.c_str());
}
});
}
if (line == "wifi status")
Serial.printf("wifi: state %d ssid '%s' rssi %d clock %s heap %u min %u\n", (int)wifi->state(),
wifi->ssid().c_str(), wifi->rssi(), clockService->displayTime().c_str(), ESP.getFreeHeap(),
ESP.getMinFreeHeap());
if (line == "sound off") settings.setBool(Setting::Sound, false);
if (line == "sound on") settings.setBool(Setting::Sound, true);
if (line == "short") {
settings.setInt(Setting::DimTimeoutS, 5);
settings.setInt(Setting::OffTimeoutS, 10);
}
if (line == "normal") {
settings.setInt(Setting::OffTimeoutS, 60);
settings.setInt(Setting::DimTimeoutS, 30);
}
line = "";
}
}
void loop() {
static StatusInfo lastStatus;
static uint32_t lastLog = 0;
uint32_t now = millis();
serialCommands();
printListingWhenReady();
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange()) {
bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen
keyMapper.setTextEntry(apps->foreground().textEntryActive());
auto events = keyMapper.update(readKeys()); // always, so held keys are tracked
if (!swallow)
for (auto& e : events) apps->handleKey(e);
}
services.tick(now);
bus.dispatch();
notifier->update(now);
apps->update(now);
if (now - lastLog > 10000) {
lastLog = now;
Serial.printf("status: heap %u min %u\n", ESP.getFreeHeap(), ESP.getMinFreeHeap());
}
if (power->screen() == ScreenState::Off) return;
StatusInfo status = currentStatus();
const Toast* toast = notifier->toasts().current(now);
bool toastChanged = notifier->toasts().takeChanged();
if (apps->takeRedraw() || toastChanged || status != lastStatus) {
lastStatus = status;
screen.render(*apps, status, toast);
}
}