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};