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:
+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()); }
|
||||
|
||||
Reference in New Issue
Block a user