Public Access
One firmware: the Debug Console in every build, off until switched on, with the device's own token
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef RORO_DEBUG
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
@@ -56,4 +55,3 @@ class NoiseTest {
|
||||
|
||||
} // namespace roro
|
||||
|
||||
#endif // RORO_DEBUG
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()); }
|
||||
|
||||
@@ -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
@@ -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); }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user