One firmware: the Debug Console in every build, off until switched on, with the device's own token
CI / build (pull_request) Successful in 7m20s
Site / build (pull_request) Successful in 9s

There is no Debug Build any more (ADR 0010, issue #68, Q188 to Q195). The
console and the test commands are compiled into every firmware. It listens
only while Settings > Debug Console is on, which isn't the default; off,
neither its task nor its 4 KB ring exists. The token is made by the device
and shown on that page; a client proves it knows it by answering a challenge
with an HMAC, so it never crosses the network, and five wrong answers close
the console for a minute. DBG in the Status Bar while it listens.

Over USB serial only: debug on, debug token <value>, debug token new.
scripts/flash.sh --debug uses them to set a device up with the developer's
token. scripts/rdbg.py takes the token from -t, $RORO_DEBUG_TOKEN or the
file, answers the challenge, and fetches a release's ELF to decode a crash.

Gone: the cardputer-adv-debug environment, RORO_DEBUG, the +debug version,
scripts/debug_flags.py, update install ... force, and the rule that a Debug
Build doesn't install releases. Old clients and old firmwares don't talk to
each other.

Against the builds it replaces: 30 KB more flash and 88 bytes more static
RAM than the release, 4 KB less RAM than the Debug Build. 468 host tests.
Checked on the device: off by default, login, the pause after wrong tokens,
Safe Mode with the console, the setting surviving an update, debug off.

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-06 22:59:35 +02:00
co-authored by Claude Opus 5.5
parent 1353e6a5f9
commit c68741cc46
65 changed files with 1245 additions and 374 deletions
+144
View File
@@ -0,0 +1,144 @@
#include "apps/debug_console_page.h"
#include "debug_auth.h"
#include "services/debug_console.h"
#include "ui/fonts.h"
#include "ui/widgets.h"
namespace roro {
void DebugConsolePage::enter() {
list_.setCount(kRows);
confirm_.reset();
typing_ = false;
refusal_.clear();
}
bool DebugConsolePage::onKey(const KeyEvent& e) {
if (confirm_) {
confirm_->onKey(e);
if (confirm_->result() == 1) {
if (ask_ == Ask::SwitchOn) DebugConsole::switchOn(settings_);
else settings_.setString(Setting::DebugToken, DebugConsole::freshToken());
}
if (confirm_->result() != DialogModel::kPending) confirm_.reset();
return true;
}
if (typing_) {
switch (e.key) {
case Key::Char: editor_.insert(e.ch); break;
case Key::Delete: editor_.backspace(); break;
case Key::Left: editor_.left(); break;
case Key::Right: editor_.right(); break;
case Key::Back: typing_ = false; break;
case Key::Select: {
std::string token = debug::tidyToken(editor_.text());
if (debug::validToken(token) && settings_.setString(Setting::DebugToken, token)) typing_ = false;
else refusal_ = "16 to 64 characters, please";
break;
}
default: break;
}
return true;
}
switch (e.key) {
case Key::Up: list_.up(); break;
case Key::Down: list_.down(); break;
case Key::Back: return false;
case Key::Left:
case Key::Right:
case Key::Select:
if (e.key != Key::Select && list_.selected() != kSwitch) break;
switch (list_.selected()) {
case kSwitch:
if (settings_.getBool(Setting::DebugConsole)) settings_.setBool(Setting::DebugConsole, false);
else { // on is the one to think about
ask_ = Ask::SwitchOn;
confirm_.reset(new DialogModel({"Cancel", "Switch on"}));
}
break;
case kNewToken:
ask_ = Ask::NewToken;
confirm_.reset(new DialogModel({"Cancel", "New token"}));
break;
case kTypeToken:
editor_ = LineEditor(64);
refusal_.clear();
typing_ = true;
break;
default: break;
}
break;
default: break;
}
return true;
}
void DebugConsolePage::draw(Canvas& c) {
const auto& area = theme::kContent;
c.setTextDatum(top_left);
if (typing_) {
c.setFont(&fonts::body);
c.setTextColor(theme::kMuted);
c.drawString("A token of your own", 4, area.y + 4);
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
c.setTextColor(refusal_.empty() ? theme::kMuted : theme::kWarning);
c.drawString(refusal_.empty() ? "16 to 64 characters. Capitals or not, it's the same." : refusal_.c_str(), 4, area.y + 44);
c.setTextColor(theme::kMuted);
c.drawString("Enter: save `: cancel", 4, area.y + 44 + theme::kLineHeight);
return;
}
bool on = settings_.getBool(Setting::DebugConsole);
std::string ip = wifi_.ip();
widgets::list(
c, list_, {area.x, area.y, area.w, kRows * theme::kLineHeight},
[](int i) -> std::string {
switch (i) {
case kSwitch: return "Debug Console";
case kAddress: return "Connect to";
case kNewToken: return "New token";
default: return "Type a token";
}
},
[&](int i) -> std::string {
switch (i) {
case kSwitch: return on ? "On" : "Off";
case kAddress: return !on ? "-" : ip.empty() ? "Wi-Fi not connected" : ip + ":" + std::to_string(DebugConsole::kPort);
default: return ">";
}
});
// The token: here, in full, and nowhere else (it's never printed on a console). One the device
// made is drawn at twice the size, three groups then two: it has to be read and typed.
int y = area.y + kRows * theme::kLineHeight + 4;
c.drawFastHLine(4, y - 2, area.w - 8, theme::kMuted);
const std::string& token = settings_.getString(Setting::DebugToken);
std::string shown = debug::groupToken(token);
c.setFont(&fonts::body);
if (token.empty()) {
c.setTextColor(theme::kMuted);
c.drawString("No token yet: one is made when", 4, y + 4);
c.drawString("you switch the console on.", 4, y + 4 + theme::kLineHeight);
} else if (token.size() <= debug::kTokenChars) {
c.setTextColor(theme::kText);
c.setTextSize(2);
c.drawString(shown.substr(0, 14).c_str(), 4, y + 2);
if (shown.size() > 15) c.drawString(shown.substr(15).c_str(), 4, y + 2 + 2 * theme::kLineHeight - 3);
c.setTextSize(1);
} else {
c.setTextColor(theme::kText);
for (size_t at = 0; at < shown.size(); at += 35, y += theme::kLineHeight) c.drawString(shown.substr(at, 35).c_str(), 4, y + 2);
}
c.setFont(&fonts::small);
c.setTextColor(on ? theme::kWarning : theme::kMuted);
c.drawString(on ? "On this Wi-Fi, the token is full control." : "Off: nothing listens.", 4, area.y + area.h - 9);
if (confirm_) {
if (ask_ == Ask::SwitchOn)
widgets::dialog(c, "Switch it on?", "With the token, anyone on this Wi-Fi can read the console, press keys and copy files.", *confirm_);
else widgets::dialog(c, "A new token?", "The one in use stops working, and whoever is connected is cut off.", *confirm_);
}
}
} // namespace roro
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <string>
#include "dialog_model.h"
#include "key_event.h"
#include "line_editor.h"
#include "list_model.h"
#include "services/wifi_service.h"
#include "settings.h"
#include "ui/canvas.h"
#include "ui/theme.h"
namespace roro {
// Settings → Debug Console (ADR 0010): the switch, where to connect, and the token, which is shown
// here and nowhere else. Switching on makes a token if there's none; a new one, or one typed by
// hand, ends the connection of whoever holds the old one.
class DebugConsolePage {
public:
DebugConsolePage(Settings& settings, WifiService& wifi) : settings_(settings), wifi_(wifi) {}
void enter();
bool onKey(const KeyEvent& e); // false: leave the page
bool textEntryActive() const { return typing_; }
void draw(Canvas& c);
private:
enum Row { kSwitch, kAddress, kNewToken, kTypeToken, kRows };
enum class Ask { None, SwitchOn, NewToken };
Settings& settings_;
WifiService& wifi_;
ListModel list_{kRows};
std::unique_ptr<DialogModel> confirm_;
Ask ask_ = Ask::None;
bool typing_ = false;
LineEditor editor_{64};
std::string refusal_;
};
} // namespace roro
-1
View File
@@ -80,7 +80,6 @@ bool FirmwarePage::installable(std::string& why) const {
std::string running = update_.runningVersion();
if (!shown_.usable()) why = "Nothing to install in it";
else if (!release::versionNewer(shown_.tag, running) && !versionOlder(shown_.tag, running)) why = "That's the version running";
else if (release::isDebugBuild(running)) why = "Debug Build: update from the PC"; // it keeps its console (Q171)
else if (wifi_.state() != WifiController::State::Connected) why = "Not connected to Wi-Fi";
else return true;
return false;
+9 -1
View File
@@ -34,6 +34,9 @@ bool SettingsApp::onKey(const KeyEvent& e) {
case Page::Firmware:
if (!firmwarePage_.onKey(e)) page_ = Page::Menu;
return true;
case Page::Debug:
if (!debugPage_.onKey(e)) page_ = Page::Menu;
return true;
}
return false;
}
@@ -75,6 +78,10 @@ bool SettingsApp::onMenuKey(const KeyEvent& e) {
page_ = Page::Firmware;
firmwarePage_.enter();
break;
case Row::DebugConsole:
page_ = Page::Debug;
debugPage_.enter();
break;
default: page_ = Page::About; break;
}
break;
@@ -124,7 +131,7 @@ bool SettingsApp::onAboutKey(const KeyEvent& e) {
void SettingsApp::update(uint32_t nowMs) {
// Live values on About and Firmware.
bool live = page_ == Page::About || page_ == Page::Firmware ||
bool live = page_ == Page::About || page_ == Page::Firmware || page_ == Page::Debug ||
(page_ == Page::Wifi && wifiPage_.live());
if (live && nowMs - lastRefreshMs_ >= 500) {
lastRefreshMs_ = nowMs;
@@ -184,6 +191,7 @@ void SettingsApp::draw(Canvas& c) {
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
case Page::Wifi: wifiPage_.draw(c); break;
case Page::Firmware: firmwarePage_.draw(c); break;
case Page::Debug: debugPage_.draw(c); break;
}
}
+8 -4
View File
@@ -13,6 +13,7 @@
#include "services/battery_service.h"
#include "services/clock_service.h"
#include "services/storage_service.h"
#include "apps/debug_console_page.h"
#include "apps/firmware_page.h"
#include "apps/wifi_settings_page.h"
#include "settings_menu.h"
@@ -32,24 +33,26 @@ struct SettingsAppDeps {
UpdateService& update;
};
// Settings: every user-facing setting, plus the Wi-Fi, Firmware and About pages.
// Settings: every user-facing setting, plus the Wi-Fi, Firmware, Debug Console and About pages.
class SettingsApp : public App {
public:
explicit SettingsApp(const SettingsAppDeps& deps)
: d_(deps),
menu_(deps.settings),
wifiPage_(deps.settings, deps.savedNetworks, deps.wifi, deps.bus),
firmwarePage_(deps.update, deps.wifi, deps.storage) {}
firmwarePage_(deps.update, deps.wifi, deps.storage),
debugPage_(deps.settings, deps.wifi) {}
void onEnter() override;
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
bool textEntryActive() const override {
return page_ == Page::Text || (page_ == Page::Wifi && wifiPage_.textEntryActive());
return page_ == Page::Text || (page_ == Page::Wifi && wifiPage_.textEntryActive()) ||
(page_ == Page::Debug && debugPage_.textEntryActive());
}
void draw(Canvas& c) override;
private:
enum class Page { Menu, Text, Choice, About, Wifi, Firmware };
enum class Page { Menu, Text, Choice, About, Wifi, Firmware, Debug };
bool onMenuKey(const KeyEvent& e);
bool onTextKey(const KeyEvent& e);
@@ -62,6 +65,7 @@ class SettingsApp : public App {
SettingsMenu menu_;
WifiSettingsPage wifiPage_;
FirmwarePage firmwarePage_;
DebugConsolePage debugPage_;
Page page_ = Page::Menu;
ListModel list_{theme::kContent.h / theme::kLineHeight};
ListModel choices_{theme::kContent.h / theme::kLineHeight};
+40 -57
View File
@@ -74,17 +74,13 @@ static ClockService* clockService;
static GnssService* gnssService;
static RadioService* radioService;
static LoraCaptureService* loraCapture;
#ifdef RORO_DEBUG
static NoiseTest* noiseTest;
#endif
static GeminiService* geminiService;
static SavedNetworks* savedNetworks;
static WifiService* wifi;
static IrcService* irc;
static UpdateService* update;
#ifdef RORO_DEBUG
static DebugConsole* debugConsole;
#endif
static Notifier* notifier;
static LauncherApp launcher;
static AppManager* apps;
@@ -131,6 +127,7 @@ static StatusInfo currentStatus() {
s.radio = last && millis() - last < 400 ? StatusInfo::Radio::Packet : StatusInfo::Radio::Listening;
}
s.capturing = loraCapture && loraCapture->capturing();
s.debug = !debugConsole->on() ? StatusInfo::Debug::Off : debugConsole->clientConnected() ? StatusInfo::Debug::Client : StatusInfo::Debug::On;
using WifiState = WifiController::State;
switch (wifi->state()) {
case WifiState::Connected: {
@@ -194,18 +191,14 @@ void setup() {
services.add(*gnssService);
services.add(*radioService);
loraCapture = new LoraCaptureService(*radioService, *storageService, *clockService, bus);
#ifdef RORO_DEBUG
noiseTest = new NoiseTest(*radioService);
#endif
services.add(*loraCapture);
services.add(*storageService);
services.add(*wifi);
services.add(*irc);
services.add(*update);
#ifdef RORO_DEBUG
debugConsole = new DebugConsole(*wifi, *storageService);
debugConsole = new DebugConsole(*wifi, *storageService, settings);
services.add(*debugConsole);
#endif
apps = new AppManager(launcher);
launcher.setManager(*apps);
@@ -228,9 +221,7 @@ void setup() {
bus.subscribe(EventType::Notification, [](const Event& e) { console.printf("notification: %s\n", e.text); });
if (!screen.begin()) console.println("frame buffer allocation failed");
#ifdef RORO_DEBUG
debugConsole->setFrame(screen.canvas()); // for `screenshot`
#endif
services.startAll(millis());
apps->begin();
if (!settings.getBool(Setting::SetupDone)) apps->openModal("setup");
@@ -246,7 +237,7 @@ void setup() {
enableLoopWDT();
}
// Safe Mode: Wi-Fi, the clock, Firmware Updates and (in a Debug Build) the Debug Console. No Apps,
// Safe Mode: Wi-Fi, the clock, Firmware Updates and (if it's switched on) the Debug Console. No Apps,
// no IRC, no SD card: whatever crashed three times in a row is most likely among them.
static void setupSafeMode(int crashes) {
clockService = new ClockService(settings, bus);
@@ -258,15 +249,11 @@ static void setupSafeMode(int crashes) {
services.add(*clockService);
services.add(*wifi);
services.add(*update);
#ifdef RORO_DEBUG
debugConsole = new DebugConsole(*wifi, *storageService);
debugConsole = new DebugConsole(*wifi, *storageService, settings);
services.add(*debugConsole);
#endif
bus.subscribe(EventType::Notification, [](const Event& e) { console.printf("notification: %s\n", e.text); });
screen.begin();
#ifdef RORO_DEBUG
debugConsole->setFrame(screen.canvas());
#endif
services.startAll(millis());
console.printf("%s %s in SAFE MODE: %d crash restarts in a row. Only Wi-Fi and Firmware Updates run.\n",
kProductName, versionString(), crashes);
@@ -385,7 +372,6 @@ static void uploadStep() {
}
}
#ifdef RORO_DEBUG
// `wifi ip ... try <seconds>`: a trial IP setting that reverts unless `wifi ip keep` arrives, so a
// wrong address tried over Wi-Fi doesn't cut the device off for good.
static struct {
@@ -404,7 +390,6 @@ static void ipTrialStep() {
ipTrial.wasFixed ? net::formatFixed(ipTrial.was).c_str() : "Automatic (DHCP)");
wifi->ipSettingChanged(ipTrial.ssid);
}
#endif
// `tasks`: the first sample, and when to take the second and print.
static std::vector<TaskSample> tasksBefore;
@@ -467,13 +452,11 @@ static void printReleases(bool list) {
static void updateStep() {
if (!updateWatch || update->giteaBusy()) return;
#ifdef RORO_DEBUG
if (updateWatch == 3) {
console.printf("update: probe: %s\n", update->probeResult().c_str());
updateWatch = 0;
return;
}
#endif
printReleases(updateWatch == 2);
updateWatch = 0;
}
@@ -492,14 +475,8 @@ static void updateCommand(const String& args) {
printReleases(false);
} else if (args.startsWith("install ")) {
String rest = args.substring(8);
bool force = rest.endsWith(" force");
if (force) rest.remove(rest.length() - 6);
#ifndef RORO_DEBUG
force = false; // only the Debug Console may install on a Debug Build, which a release isn't
#endif
std::string why = update->requestInstall(rest.c_str(), force);
std::string why = update->requestInstall(rest.c_str());
console.printf("update: %s\n", why.empty() ? "installing" : why.c_str());
#ifdef RORO_DEBUG
} else if (args.startsWith("probe ")) { // update probe <host> [path]: is that server's certificate accepted?
String rest = args.substring(6);
int space = rest.indexOf(' ');
@@ -516,7 +493,6 @@ static void updateCommand(const String& args) {
} else if (args.startsWith("pretend ")) { // update pretend v0.9.0 | off: what the comparisons take as running
update->pretendVersion(args.substring(8) == "off" ? "" : args.substring(8).c_str());
console.printf("update: running %s\n", update->runningVersion().c_str());
#endif
} else {
console.println("update: check | list | status | install <tag>");
}
@@ -553,9 +529,7 @@ static void fileCommand(const String& line) {
}
static uint32_t loopPasses = 0; // counted in loop(), for `tasks` (issue #40)
#ifdef RORO_DEBUG
static bool loopSpin = false; // `loop spin on`: no rest between passes, to compare load and radio noise
#endif
static uint32_t tasksPasses = 0;
static void tasksStep() {
@@ -590,7 +564,8 @@ static const char* const kHelp =
"install <path.ota> Update from SD\n"
"update check | list | status | install <tag> the project's releases on Gitea\n"
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
#ifdef RORO_DEBUG
"debug status | debug off the Debug Console over Wi-Fi (Settings > Debug Console)\n"
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
"crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n"
"wifi ip ... try <seconds> | wifi ip keep a trial IP setting: back to the previous one unless kept\n"
"loop spin on|off make the main loop spin without resting, to compare load and radio noise\n"
@@ -601,34 +576,58 @@ static const char* const kHelp =
"reset (Debug Console only) restart at once, even if the main loop is stuck\n"
"get <path> | put <path> <size> <sha256> | screenshot (Debug Console only) binary, see rdbg.py\n"
"quit close the Debug Console connection\n"
#endif
;
// Commands that only touch what Safe Mode starts.
static bool safeModeCommand(const String& line) {
return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
line.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") ||
line == "wifi status" || line.startsWith("wifi add ");
line == "wifi status" || line.startsWith("wifi add ") || line.startsWith("debug ");
}
static void runCommand(String line) {
// `debug ...`: the Debug Console's switch and token (ADR 0010). Switching it on and setting its token
// are for USB serial only (Q192): whoever holds the cable holds the device anyway, and the console
// can't be used to open itself wider. The token is never printed.
static void debugCommand(const String& args, bool fromSerial) {
if (args == "status") {
console.printf("debug: %s, token %s, client %s\n", settings.getBool(Setting::DebugConsole) ? "on" : "off",
settings.getString(Setting::DebugToken).empty() ? "not set" : "set", debugConsole->clientConnected() ? "connected" : "none");
} else if (args == "off") {
settings.setBool(Setting::DebugConsole, false);
console.println("debug: off");
} else if (!fromSerial) {
console.println("debug: over USB serial only (or Settings > Debug Console)");
} else if (args == "on") {
DebugConsole::switchOn(settings);
console.println("debug: on (the token is in Settings > Debug Console)");
} else if (args == "token new") {
settings.setString(Setting::DebugToken, DebugConsole::freshToken());
console.println("debug: a new token (it is in Settings > Debug Console)");
} else if (args.startsWith("token ")) {
std::string token = debug::tidyToken(args.substring(6).c_str());
bool ok = debug::validToken(token) && settings.setString(Setting::DebugToken, token);
console.println(ok ? "debug: token set" : "debug: a token is 16 to 64 characters");
} else console.println("debug: status | off | on | token <16 to 64 characters> | token new");
}
static void runCommand(String line, bool fromSerial = false) {
line.trim();
if (line.isEmpty()) return;
if (safeMode && !safeModeCommand(line)) return (void)console.println("not available in Safe Mode");
if (line == "help") console.print(kHelp);
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
if (line == "info") {
system_info::printSystem(console);
console.printf("wifi: %s, ip %s, rssi %d | sd: %s, %u write faults\n", wifi->ssid().c_str(), wifi->ip().c_str(),
wifi->rssi(), storageService->state().present ? "present" : "none", (unsigned)sdLastFault().count);
console.printf("update: %s\n", update->onProbation() ? "on probation" : "confirmed");
console.printf("update: %s | debug console: %s\n", update->onProbation() ? "on probation" : "confirmed",
!debugConsole->on() ? "off" : debugConsole->clientConnected() ? "on, a client connected" : "on");
system_info::printSlots(console, nvs);
}
#ifdef RORO_DEBUG
if (line == "loop spin on" || line == "loop spin off") {
loopSpin = line.endsWith("on");
console.printf("loop: %s\n", loopSpin ? "spinning, no rest" : "resting between passes");
}
#endif
if (line == "tasks") { // sampled now, printed a second later by tasksStep(): the loop must run in between
tasksTotal = system_info::sampleTasks(tasksBefore);
tasksDueMs = millis() + 1000;
@@ -670,7 +669,6 @@ static void runCommand(String line) {
});
}
if (line.startsWith("install ")) update->installFromSd(line.substring(8).c_str()); // Update from SD
#ifdef RORO_DEBUG
if (line.startsWith("sd fill ")) { // sd fill <folder> <count>: small files, to test a crowded folder
int space = line.lastIndexOf(' ');
std::string folder = line.substring(8, space).c_str();
@@ -689,11 +687,9 @@ static void runCommand(String line) {
console.printf("sd fill: %d files in %s\n", made, folder.c_str());
});
}
#endif
if (line == "lora probe" && radioService) radioService->probe(console);
if (line == "lora status" && radioService) radioService->printStatus(console);
if ((line == "lora rx on" || line == "lora rx off") && radioService) radioService->setEcho(line.endsWith("on"));
#ifdef RORO_DEBUG
if (line == "lora noise report" && noiseTest) noiseTest->printReport(console);
if (line == "lora noise test" && noiseTest && !noiseTest->running()) {
// One thing changed at a time, each put back before the next (issue #20). Wi-Fi off cuts the
@@ -758,7 +754,6 @@ static void runCommand(String line) {
};
noiseTest->start(std::move(conditions));
}
#endif
if (line.startsWith("lora sweep") && radioService) { // on [from MHz] [to MHz] [step kHz] | off | dump
if (line == "lora sweep dump") radioService->printSweep(console);
else if (line == "lora sweep off") {
@@ -776,7 +771,6 @@ static void runCommand(String line) {
console.printf("lora capture: %s\n", why.empty() ? loraCapture->path().c_str() : why.c_str());
}
if (line == "lora capture stop" && loraCapture) loraCapture->stop();
#ifdef RORO_DEBUG
if (line.startsWith("lora inject ") && radioService) { // <hex> [rssi] [snr]: as if received
String hex = line.substring(12);
int space = hex.indexOf(' ');
@@ -787,7 +781,6 @@ static void runCommand(String line) {
for (size_t i = 0; i + 1 < hex.length() && n < sizeof data; i += 2) data[n++] = strtoul(hex.substring(i, i + 2).c_str(), nullptr, 16);
radioService->inject(data, n, rssi, snr);
}
#endif
if (line.startsWith("lora custom ") && radioService) {
double mhz = 0, bw = 0;
unsigned sf = 0, cr = 0, sync = 0, preamble = 8;
@@ -822,11 +815,9 @@ static void runCommand(String line) {
console.println(gnssService->send(line.substring(10).c_str()) ? "gnss: sent" : "gnss: off");
if (line == "crash") crash_report::print(console, nvs);
if (line == "coredump erase") console.println(crash_report::erase() ? "coredump: erased" : "coredump: nothing to erase");
#ifdef RORO_DEBUG
if (line == "crash abort") abort();
if (line == "crash wdt")
for (;;) {} // the main loop never yields: the task watchdog fires
#endif
if (line == "burst")
for (int i = 1; i <= 5; i++)
bus.publish(Event::withText(EventType::Notification, ("Burst " + String(i)).c_str(), 0));
@@ -933,7 +924,6 @@ static void runCommand(String line) {
}
if (line.startsWith("wifi ip ")) { // wifi ip <ssid> dhcp | <address>/<prefix> [gateway] (the SSID may hold spaces)
std::string rest = line.substring(8).c_str();
#ifdef RORO_DEBUG
if (rest == "keep") { // the trial setting stays
console.println(ipTrial.active ? "wifi ip: kept" : "wifi ip: no trial running");
ipTrial.active = false;
@@ -945,7 +935,6 @@ static void runCommand(String line) {
trialS = strtoul(rest.c_str() + tryAt + 5, nullptr, 10);
rest = rest.substr(0, tryAt);
}
#endif
std::string ssid, why;
net::FixedIp fixed;
bool dhcp = rest.size() > 5 && rest.compare(rest.size() - 5, 5, " dhcp") == 0;
@@ -959,9 +948,7 @@ static void runCommand(String line) {
}
}
const SavedNetwork* before = why.empty() ? savedNetworks->find(ssid) : nullptr;
#ifdef RORO_DEBUG
if (before && trialS) ipTrial = {true, millis() + trialS * 1000, ssid, before->fixed, before->ip};
#endif
if (why.empty()) why = savedNetworks->setIp(ssid, dhcp ? nullptr : &fixed);
if (!why.empty()) return (void)console.printf("wifi ip: %s\n", why.c_str());
console.printf("wifi ip: %s is now %s\n", ssid.c_str(), dhcp ? "Automatic (DHCP)" : net::formatFixed(fixed).c_str());
@@ -1007,18 +994,18 @@ static void serialCommands() {
line += c;
continue;
}
runCommand(line);
runCommand(line, true);
line = "";
}
}
static void remoteCommands() {
#ifdef RORO_DEBUG
for (std::string remote; debugConsole->takeCommand(remote);) {
console.printf("> %s\n", remote.c_str()); // so the transcript reads the same on both ends
runCommand(remote.c_str());
}
#endif
for (std::string alert; debugConsole->takeAlert(alert);)
bus.publish(Event::withText(EventType::Notification, alert.c_str(), static_cast<int32_t>(NotificationLevel::Warning)));
}
// After a minute up, the crash streak is over (SafeMode counts starts that crash in a row).
@@ -1069,9 +1056,7 @@ void loop() {
return;
}
loopPass();
#ifdef RORO_DEBUG
if (loopSpin) return;
#endif
if (upload.active()) return; // a serial file transfer: every byte is read promptly
delay(power->screen() == ScreenState::Off ? kLoopRestScreenOffMs : kLoopRestScreenOnMs);
}
@@ -1088,10 +1073,8 @@ static void loopPass() {
serialCommands();
remoteCommands();
tasksStep();
#ifdef RORO_DEBUG
ipTrialStep();
if (noiseTest) noiseTest->step(now);
#endif
noteStableOnce(now);
updateStep();
fileOpsStep();
+42 -21
View File
@@ -2,19 +2,16 @@
#include <Arduino.h>
#ifdef RORO_DEBUG
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <algorithm>
#include <cstdio>
#endif
namespace roro {
Console console;
#ifdef RORO_DEBUG
namespace {
// Any task may write (ESP-IDF logs come from everywhere), so the ring is behind a spinlock, held
@@ -34,7 +31,28 @@ int teeEspLog(const char* format, va_list args) {
} // namespace
void Console::captureEspLogs() { espLogNext = esp_log_set_vprintf(teeEspLog); }
void Console::captureEspLogs() {
if (!espLogNext) espLogNext = esp_log_set_vprintf(teeEspLog); // once: it stays, and costs nothing with the ring closed
}
bool Console::openRing() {
if (ring_) return true;
auto* fresh = static_cast<uint8_t*>(malloc(kRingBytes));
if (!fresh) return false;
portENTER_CRITICAL(&ringLock);
head_ = 0;
ring_ = fresh;
portEXIT_CRITICAL(&ringLock);
return true;
}
void Console::closeRing() {
portENTER_CRITICAL(&ringLock);
uint8_t* old = ring_;
ring_ = nullptr;
portEXIT_CRITICAL(&ringLock);
free(old);
}
void Console::toRing(const uint8_t* data, size_t len) {
if (len > kRingBytes) {
@@ -42,11 +60,13 @@ void Console::toRing(const uint8_t* data, size_t len) {
len = kRingBytes;
}
portENTER_CRITICAL(&ringLock);
size_t at = head_ % kRingBytes;
size_t first = std::min(len, kRingBytes - at);
memcpy(ring_ + at, data, first);
memcpy(ring_, data + first, len - first);
head_ += len;
if (ring_) {
size_t at = head_ % kRingBytes;
size_t first = std::min(len, kRingBytes - at);
memcpy(ring_ + at, data, first);
memcpy(ring_, data + first, len - first);
head_ += len;
}
portEXIT_CRITICAL(&ringLock);
}
@@ -59,24 +79,25 @@ uint32_t Console::oldest() const {
size_t Console::readSince(uint32_t& pos, uint8_t* out, size_t max, uint32_t& skipped) {
portENTER_CRITICAL(&ringLock);
uint32_t from = head_ > kRingBytes ? head_ - kRingBytes : 0;
skipped = pos < from ? from - pos : 0;
if (pos < from) pos = from;
size_t n = std::min<size_t>(max, head_ - pos);
size_t at = pos % kRingBytes;
size_t first = std::min(n, kRingBytes - at);
memcpy(out, ring_ + at, first);
memcpy(out + first, ring_, n - first);
pos += n;
size_t n = 0;
skipped = 0;
if (ring_) {
uint32_t from = head_ > kRingBytes ? head_ - kRingBytes : 0;
skipped = pos < from ? from - pos : 0;
if (pos < from) pos = from;
n = std::min<size_t>(max, head_ - pos);
size_t at = pos % kRingBytes;
size_t first = std::min(n, kRingBytes - at);
memcpy(out, ring_ + at, first);
memcpy(out + first, ring_, n - first);
pos += n;
}
portEXIT_CRITICAL(&ringLock);
return n;
}
#endif
size_t Console::write(const uint8_t* data, size_t len) {
#ifdef RORO_DEBUG
toRing(data, len);
#endif
// Never wait for the USB host. One that's attached but not reading (a VM, a closed terminal)
// would stall the caller for up to 2 s per write while HWCDC retries; the ring keeps it anyway.
if (Serial.availableForWrite() >= static_cast<int>(len)) Serial.write(data, len);
+11 -7
View File
@@ -7,18 +7,23 @@
namespace roro {
// Where the firmware's console output goes: the USB serial port, and in a Debug Build also a ring
// buffer the Debug Console sends over Wi-Fi (with what came before the connection, so boot messages
// aren't lost). Use `console` instead of Serial for anything a human should be able to read remotely.
// Where the firmware's console output goes: the USB serial port, and while the Debug Console is
// switched on (ADR 0010) also a ring buffer it sends over Wi-Fi, with what came before the connection.
// Use `console` instead of Serial for anything a human should be able to read remotely.
class Console : public Print {
public:
size_t write(uint8_t c) override { return write(&c, 1); }
size_t write(const uint8_t* data, size_t len) override;
using Print::write;
#ifdef RORO_DEBUG
static constexpr size_t kRingBytes = 4096;
// The ring exists only while the Debug Console is on: 4 KB of heap a device that never uses it
// keeps. False if there's no memory for it.
bool openRing();
void closeRing();
bool ringOpen() const { return ring_ != nullptr; }
// Also copies ESP-IDF's own log lines into the ring (they still reach the serial port).
void captureEspLogs();
// Copies bytes written since `pos` into `out`, and advances `pos`. A reader that fell more than
@@ -30,9 +35,8 @@ class Console : public Print {
void toRing(const uint8_t* data, size_t len);
private:
uint8_t ring_[kRingBytes];
uint32_t head_ = 0; // total bytes ever written; the ring holds the last kRingBytes of them
#endif
uint8_t* ring_ = nullptr;
uint32_t head_ = 0; // total bytes written since the ring opened; it holds the last kRingBytes of them
};
extern Console console;
+100 -36
View File
@@ -1,4 +1,3 @@
#ifdef RORO_DEBUG
#include "services/debug_console.h"
@@ -7,6 +6,7 @@
#include <algorithm>
#include <esp_core_dump.h>
#include <esp_flash.h>
#include <esp_random.h>
#include <SD.h>
#include <unistd.h>
@@ -29,14 +29,6 @@ constexpr uint32_t kAuthTimeoutMs = 10000;
constexpr size_t kMaxLine = 240;
constexpr size_t kMaxQueued = 8;
// Compares without stopping at the first difference, so timing says nothing about the token.
bool sameToken(const std::string& a, const char* b) {
size_t n = strlen(b);
uint8_t diff = a.size() != n;
for (size_t i = 0; i < n; i++) diff |= (i < a.size() ? a[i] : 0) ^ b[i];
return diff == 0;
}
// Reads one line (without its \r\n) within `timeoutMs`; false on a timeout, a drop or an overlong line.
bool readLine(NetworkClient& c, std::string& line, uint32_t timeoutMs) {
line.clear();
@@ -77,12 +69,40 @@ void sendCoreDump(NetworkClient& client) {
} // namespace
DebugConsole::DebugConsole(WifiService& wifi, StorageService& storage)
: wifi_(wifi), storage_(storage), lock_(xSemaphoreCreateMutex()) {}
DebugConsole::DebugConsole(WifiService& wifi, StorageService& storage, Settings& settings)
: wifi_(wifi), storage_(storage), settings_(settings), lock_(xSemaphoreCreateMutex()) {}
void DebugConsole::start() {
std::string DebugConsole::freshToken() {
uint8_t random[debug::kTokenRandom];
esp_fill_random(random, sizeof random); // the hardware generator: true random with the radio on
return debug::makeToken(random);
}
void DebugConsole::switchOn(Settings& settings) {
if (settings.getString(Setting::DebugToken).empty()) settings.setString(Setting::DebugToken, freshToken());
settings.setBool(Setting::DebugConsole, true);
}
void DebugConsole::tick(uint32_t) { apply(); }
// The main loop's side. The task frees what it holds and clears task_ when it sees wanted_ go:
// until then a new one isn't started, so switching off and on again quickly takes a tick or two.
void DebugConsole::apply() {
const std::string& token = settings_.getString(Setting::DebugToken);
bool want = settings_.getBool(Setting::DebugConsole) && !token.empty(); // no token, nobody could get in: stay closed
xSemaphoreTake(lock_, portMAX_DELAY);
if (token != token_) {
token_ = token;
tokenSeq_ = tokenSeq_ + 1;
}
xSemaphoreGive(lock_);
wanted_ = want;
if (!want || task_) return;
if (!console.openRing()) return (void)console.println("debug: no memory for the console");
console.captureEspLogs();
if (!task_) xTaskCreate(taskEntry, "debug", 6144, this, 1, &task_);
TaskHandle_t made = nullptr;
if (xTaskCreate(taskEntry, "debug", 6144, this, 1, &made) == pdPASS) task_ = made;
else console.closeRing();
}
bool DebugConsole::takeCommand(std::string& line) {
@@ -96,38 +116,82 @@ bool DebugConsole::takeCommand(std::string& line) {
return any;
}
bool DebugConsole::takeAlert(std::string& text) {
xSemaphoreTake(lock_, portMAX_DELAY);
bool any = !alert_.empty();
if (any) text = std::move(alert_);
alert_.clear();
xSemaphoreGive(lock_);
return any;
}
void DebugConsole::taskEntry(void* self) { static_cast<DebugConsole*>(self)->listen(); }
void DebugConsole::listen() {
NetworkServer server(kPort);
bool listening = false;
for (;;) {
bool up = wifi_.state() == WifiController::State::Connected;
if (up && !listening) {
server.begin();
listening = true;
} else if (!up && listening) {
server.end();
listening = false;
}
if (listening) {
Counted<NetworkClient> client(server.accept(), net::User::DebugConsole);
if (client) {
client.setNoDelay(true);
if (authenticate(client)) serve(client);
client.stop();
{
NetworkServer server(kPort);
bool listening = false;
while (wanted_) {
bool up = wifi_.state() == WifiController::State::Connected;
if (up && !listening) {
server.begin();
listening = true;
} else if (!up && listening) {
server.end();
listening = false;
}
if (listening) {
Counted<NetworkClient> client(server.accept(), net::User::DebugConsole);
if (client) {
client.setNoDelay(true);
if (authenticate(client)) serve(client);
client.stop();
}
}
vTaskDelay(pdMS_TO_TICKS(200));
}
vTaskDelay(pdMS_TO_TICKS(200));
if (listening) server.end();
}
// Switched off: nothing is left behind (Q189). The commands a client queued die with it.
xSemaphoreTake(lock_, portMAX_DELAY);
commands_.clear();
xSemaphoreGive(lock_);
console.closeRing();
task_ = nullptr;
vTaskDelete(nullptr);
}
// The token never crosses the network (Q193): the device sends 16 random bytes, the client sends
// back their HMAC-SHA256 keyed by the token. A recorded answer is no use for the next challenge.
bool DebugConsole::authenticate(NetworkClient& client) {
std::string token;
if (readLine(client, token, kAuthTimeoutMs) && sameToken(token, RORO_DEBUG_TOKEN)) return true;
console.printf("debug: refused a client from %s\n", client.remoteIP().toString().c_str());
if (gate_.locked(millis())) {
client.print("locked\n");
return false;
}
xSemaphoreTake(lock_, portMAX_DELAY);
std::string token = token_;
xSemaphoreGive(lock_);
uint8_t nonce[debug::kNonceBytes];
esp_fill_random(nonce, sizeof nonce);
client.printf("%s debug console, challenge %s\n", kProductName, debug::toHex(nonce, sizeof nonce).c_str());
std::string answer;
bool answered = readLine(client, answer, kAuthTimeoutMs);
if (answered && !token.empty() && debug::sameText(answer, debug::answerFor(token, nonce))) {
gate_.succeeded();
return true;
}
std::string from = client.remoteIP().toString().c_str();
console.printf("debug: refused a client from %s\n", from.c_str());
delay(1000); // no quick retries
client.print("denied\n");
if (answered && gate_.failed(millis())) { // a connection that says nothing isn't a guess
console.printf("debug: %d wrong tokens in a row, closed for %lu s\n", debug::AuthGate::kMaxFailures,
(unsigned long)(debug::AuthGate::kLockMs / 1000));
xSemaphoreTake(lock_, portMAX_DELAY);
alert_ = "Console closed, wrong tokens: " + from; // an Event's text holds 47 characters
xSemaphoreGive(lock_);
}
return false;
}
@@ -135,10 +199,11 @@ void DebugConsole::serve(NetworkClient& client) {
console.printf("debug: client %s connected\n", client.remoteIP().toString().c_str());
client.printf("%s %s debug console. 'help' lists the commands. Backlog follows.\n", kProductName, versionString());
connected_ = true;
uint32_t tokenSeq = tokenSeq_;
uint32_t pos = console.oldest();
uint8_t buf[512];
std::string line;
while (client.connected()) {
while (client.connected() && wanted_ && tokenSeq == tokenSeq_) { // switched off, or a new token: out
// Console output since last time, including the replies to this client's commands.
uint32_t skipped = 0;
size_t n;
@@ -313,4 +378,3 @@ void DebugConsole::screenshot(NetworkClient& client) {
} // namespace roro
#endif
+27 -11
View File
@@ -1,7 +1,5 @@
#pragma once
#ifdef RORO_DEBUG
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
@@ -9,18 +7,22 @@
#include <functional>
#include <string>
#include "debug_auth.h"
#include "service.h"
#include "services/storage_service.h"
#include "services/wifi_service.h"
#include "settings.h"
#include "ui/canvas.h"
class NetworkClient;
namespace roro {
// Debug Builds only (ADR 0004): the console over Wi-Fi, on TCP 2323 while Wi-Fi is Connected. A
// client sends the debug token as its first line, then gets the recent console backlog, every new
// console line, and runs the same commands as the serial port. One client at a time.
// The console over Wi-Fi, on TCP 2323 while Wi-Fi is Connected, in every build but only while it's
// switched on in Settings (ADR 0010): off, nothing listens, and neither its task nor the console's
// ring exists. A client answers a challenge with its token (debug_auth.h), then gets the recent
// console backlog, every new console line, and runs the same commands as the serial port. One
// client at a time.
//
// The socket lives on this Service's own task; commands are handed to the main loop (takeCommand),
// which runs them where touching Apps and Services is safe. Their output reaches the client
@@ -29,18 +31,29 @@ class DebugConsole : public Service {
public:
static constexpr uint16_t kPort = 2323;
DebugConsole(WifiService& wifi, StorageService& storage);
DebugConsole(WifiService& wifi, StorageService& storage, Settings& settings);
// The off-screen frame the UI composes into: `screenshot` sends it as it stands.
void setFrame(Canvas& frame) { frame_ = &frame; }
const char* name() const override { return "debug"; }
void start() override;
void start() override { apply(); }
// Follows the settings: starts or stops listening, and takes a changed token (which drops a client).
void tick(uint32_t nowMs) override;
// Switches the console on, making a token first if there's none (Q191). The settings are what
// counts: this only writes them.
static void switchOn(Settings& settings);
static std::string freshToken();
// The next command line a client sent, for the main loop to run.
bool takeCommand(std::string& line);
// News for the user that the task can't publish itself (a pause after wrong tokens).
bool takeAlert(std::string& text);
bool on() const { return wanted_; }
bool clientConnected() const { return connected_; }
private:
static void taskEntry(void* self);
void apply();
void listen();
void serve(::NetworkClient& client);
bool authenticate(::NetworkClient& client);
@@ -55,13 +68,16 @@ class DebugConsole : public Service {
WifiService& wifi_;
StorageService& storage_;
Settings& settings_;
Canvas* frame_ = nullptr;
TaskHandle_t task_ = nullptr;
SemaphoreHandle_t lock_;
volatile TaskHandle_t task_ = nullptr; // null once the task has freed everything and gone
SemaphoreHandle_t lock_; // commands_, token_, alert_
std::deque<std::string> commands_;
std::string token_, alert_;
volatile uint32_t tokenSeq_ = 0; // changes with the token: an open connection ends
volatile bool wanted_ = false;
volatile bool connected_ = false;
debug::AuthGate gate_; // the task's own
};
} // namespace roro
#endif
-2
View File
@@ -1,4 +1,3 @@
#ifdef RORO_DEBUG
#include "services/noise_test.h"
@@ -107,4 +106,3 @@ void NoiseTest::printReport(Print& out) const {
} // namespace roro
#endif // RORO_DEBUG
-2
View File
@@ -1,6 +1,5 @@
#pragma once
#ifdef RORO_DEBUG
#include <Print.h>
@@ -56,4 +55,3 @@ class NoiseTest {
} // namespace roro
#endif // RORO_DEBUG
-2
View File
@@ -190,7 +190,6 @@ void RadioService::store(RadioPacket& p) {
lastPacketMs_ = p.ms;
}
#ifdef RORO_DEBUG
void RadioService::debugAntenna(bool on) {
auto& i2c = M5.In_I2C;
uint8_t out = i2c.readRegister8(kExpander, kOutput, kI2cFreq);
@@ -208,7 +207,6 @@ void RadioService::inject(const uint8_t* data, size_t len, float rssi, float snr
store(p);
console.printf("lora inject: #%lu, %u B\n", (unsigned long)p.seq, p.len);
}
#endif
// Continuous receive. Only RX done raises DIO1; preambles and headers are only recorded in the
// IRQ status, which sampleNoise() reads.
-2
View File
@@ -99,7 +99,6 @@ class RadioService : public Service {
void printStatus(Print& out) const;
void setEcho(bool echo);
void probe(Print& out); // `lora probe`: runs on the radio task
#ifdef RORO_DEBUG
// For the noise self-test (issue #20): the chip's regulator as an LDO instead of its DC-DC
// converter, and receive gain boosted or not. Used the next time the radio is set up.
void debugOptions(bool ldo, bool boostedGain) {
@@ -112,7 +111,6 @@ class RadioService : public Service {
// `lora inject`: a packet into the ring as if received, to test the App and Captures with no
// transmitter in range. Nothing goes on air.
void inject(const uint8_t* data, size_t len, float rssi, float snr);
#endif
private:
enum Notify : uint32_t { kIrq = 1, kRequest = 2 };
+1 -8
View File
@@ -215,9 +215,8 @@ std::string UpdateService::failedVersion() const {
return failed;
}
std::string UpdateService::requestInstall(const std::string& tag, bool force) {
std::string UpdateService::requestInstall(const std::string& tag) {
if (phase_ != Phase::Idle || giteaBusy()) return "Busy with something else";
if (!force && release::isDebugBuild(runningVersion())) return "Debug Build: update from the PC"; // short: it is drawn in one line at the screen's foot
if (wifi_.state() != WifiController::State::Connected) return "No Wi-Fi";
release::Release r;
if (!gitea_.find(tag, r)) return "Look for releases first";
@@ -295,14 +294,12 @@ void UpdateService::serve() {
if (gitea_.find(installTag_, release)) installFromGitea(release);
break;
}
#ifdef RORO_DEBUG
case Request::Probe: {
HttpsGet get(net::User::Updates);
std::string why = get.open(probeHost_, probePath_, "*/*");
probeResult_ = why.empty() ? "accepted, the server answered 200" : why;
break;
}
#endif
case Request::None: break;
}
// A check or a list someone asked for that failed says so; the daily one stays quiet.
@@ -328,12 +325,8 @@ void UpdateService::installFromGitea(const release::Release& r) {
notify("Update refused: " + why, NotificationLevel::Warning);
return;
}
#ifdef RORO_DEBUG
GiteaSource source(get, damageCut_, damageFlip_);
damageCut_ = damageFlip_ = -1;
#else
GiteaSource source(get, -1, -1);
#endif
install(source, "Gitea");
}
+2 -7
View File
@@ -50,9 +50,8 @@ class UpdateService : public Service {
void requestCheck() { request_ = Request::Check; }
void requestList() { request_ = Request::List; }
// Downloads `tag` (a release known from the last check or list) into the inactive slot and
// installs it. "" when it started, or why not. A Debug Build doesn't install a release (Q171,
// it would take its Debug Console away) unless `force`, which only the Debug Console passes.
std::string requestInstall(const std::string& tag, bool force = false);
// installs it. "" when it started, or why not.
std::string requestInstall(const std::string& tag);
bool giteaBusy() const { return request_ != Request::None || serving_; }
// Asked to make room for a TLS connection (R1, Q172): IRC steps aside while the Update Service
// talks to Gitea. Set by the main loop; `true` to step aside, `false` to come back.
@@ -63,14 +62,12 @@ class UpdateService : public Service {
std::string runningVersion() const { return fakeVersion_.empty() ? versionString() : fakeVersion_; }
void pretendVersion(const std::string& v) { fakeVersion_ = v; }
std::string failedVersion() const; // a release that rolled back here, "" if none
#ifdef RORO_DEBUG
// Debug Builds only, to try the refusals on the real network path: one HTTPS GET to any host
// (is its certificate accepted?), and a download cut short or with one byte flipped.
void requestProbe(const std::string& host, const std::string& path) { probeHost_ = host; probePath_ = path; probeResult_ = "..."; request_ = Request::Probe; }
std::string probeResult() const { return probeResult_; }
void damageNextDownload(long cutAfter, long flipAt) { damageCut_ = cutAfter; damageFlip_ = flipAt; }
void forgetToday() { store_.putInt("rel_day", 0); nextCheckMs_ = 0; announced_.clear(); } // the daily check runs at the next tick
#endif
// The main loop calls this once it has drawn a frame (part of Probation).
void firstFrameDrawn() { firstFrame_ = true; }
@@ -109,11 +106,9 @@ class UpdateService : public Service {
std::string installTag_, fakeVersion_, announced_;
bool dailyCheck_ = false;
uint32_t nextCheckMs_ = 90000; // not before the device has settled
#ifdef RORO_DEBUG
std::string probeHost_, probePath_;
volatile long damageCut_ = -1, damageFlip_ = -1;
std::string probeResult_;
#endif
};
} // namespace roro
-2
View File
@@ -56,11 +56,9 @@ class WifiService : public Service {
void ipSettingChanged(const std::string& ssid);
// The DNS or NTP settings changed: use them now.
void serversChanged() { applyServers(Why::SettingsChanged); }
#ifdef RORO_DEBUG
// The noise self-test switches the radio off for a few seconds. Not saved anywhere: a restart
// during the test brings Wi-Fi back, which a changed setting wouldn't.
void debugPause(bool paused) { paused_ = paused; }
#endif
// A Saved Network was added: try it now rather than after the retry delay.
void savedNetworksChanged() { controller_.retryNow(millis()); }
+5
View File
@@ -48,6 +48,11 @@ void statusBar(Canvas& c, const StatusInfo& info) {
case StatusInfo::Wifi::Monitoring: right("MON", kAccent); break;
case StatusInfo::Wifi::None: break;
}
switch (info.debug) { // Q190: there while the console listens, bright with someone connected
case StatusInfo::Debug::On: right("DBG", kMuted); break;
case StatusInfo::Debug::Client: right("DBG", kAccent); break;
case StatusInfo::Debug::Off: break;
}
if (info.tracking) right("REC", kWarning);
if (info.capturing) right("CAP", kWarning);
switch (info.radio) { // Q101: muted while listening, bright for a moment on each packet
+2 -1
View File
@@ -29,12 +29,13 @@ struct StatusInfo {
bool tracking = false; // a Track is recording (Q63)
enum class Radio { None, Listening, Packet, Sweep } radio = Radio::None; // M3, Q101: Packet flashes
bool capturing = false; // a LoRa Capture is recording (Q97)
enum class Debug { Off, On, Client } debug = Debug::Off; // the Debug Console listens (ADR 0010, Q190)
bool operator==(const StatusInfo& o) const {
return title == o.title && batteryPercent == o.batteryPercent && clock == o.clock &&
sdPresent == o.sdPresent && sdLevel == o.sdLevel && compose == o.compose && wifi == o.wifi &&
wifiBars == o.wifiBars && unread == o.unread && gnss == o.gnss && gnssSatellites == o.gnssSatellites &&
tracking == o.tracking && radio == o.radio && capturing == o.capturing;
tracking == o.tracking && radio == o.radio && capturing == o.capturing && debug == o.debug;
}
bool operator!=(const StatusInfo& o) const { return !(*this == o); }
};