Debug Builds: the console over Wi-Fi (Debug Console, TCP 2323)

cardputer-adv-debug (-DRORO_DEBUG, version +debug) adds a Debug Console:
after a token line, a client gets the last 6 KB of console output, live
lines (ESP-IDF logs included) and the serial commands. The socket task
only queues lines; the main loop runs them. Release builds compile none
of it. The token lives in ~/.config/roro9stack/debug-token, created by
_docker.sh and passed into the container.

All output now goes through `console`, which never waits for USB: a host
that was attached but not reading stalled the main loop up to 2 s per
line. New commands everywhere: info (slots with their versions from NVS,
since the framework stamps its own into each image), tasks, reboot,
boot other, log level, help. scripts/rdbg.py is the client; flash.sh
--debug builds it; CI builds both variants. ADR 0004.

Verified on the device: USB-flashed, then updated over Wi-Fi to a Debug
Build that confirmed on Probation; both slots hold Debug Builds.

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-04 02:44:14 +02:00
co-authored by Claude Opus 5.5
parent 5b199c2436
commit 0fb7f4e9d5
18 changed files with 828 additions and 97 deletions
+143
View File
@@ -0,0 +1,143 @@
#ifdef RORO_DEBUG
#include "services/debug_console.h"
#include <WiFi.h>
#include "platform/console.h"
#include "version.h"
namespace roro {
namespace {
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();
uint32_t since = millis();
while (c.connected() && millis() - since < timeoutMs) {
int ch = c.read();
if (ch < 0) {
delay(10);
continue;
}
if (ch == '\n') {
if (!line.empty() && line.back() == '\r') line.pop_back();
return true;
}
if (line.size() >= kMaxLine) return false;
line += static_cast<char>(ch);
}
return false;
}
} // namespace
DebugConsole::DebugConsole(WifiService& wifi) : wifi_(wifi), lock_(xSemaphoreCreateMutex()) {}
void DebugConsole::start() {
console.captureEspLogs();
if (!task_) xTaskCreate(taskEntry, "debug", 6144, this, 1, &task_);
}
bool DebugConsole::takeCommand(std::string& line) {
xSemaphoreTake(lock_, portMAX_DELAY);
bool any = !commands_.empty();
if (any) {
line = std::move(commands_.front());
commands_.pop_front();
}
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) {
NetworkClient client = server.accept();
if (client) {
client.setNoDelay(true);
if (authenticate(client)) serve(client);
client.stop();
}
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
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());
delay(1000); // no quick retries
client.print("denied\n");
return false;
}
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 pos = console.oldest();
uint8_t buf[512];
std::string line;
while (client.connected()) {
// Console output since last time, including the replies to this client's commands.
uint32_t skipped = 0;
size_t n;
while ((n = console.readSince(pos, buf, sizeof buf, skipped)) > 0) {
if (skipped) client.printf("\n[... %u bytes lost: the console ran faster than the network]\n", (unsigned)skipped);
if (client.write(buf, n) != n) break;
}
// Commands, one line at a time.
while (client.available()) {
int ch = client.read();
if (ch == '\r') continue;
if (ch != '\n') {
if (line.size() < kMaxLine) line += static_cast<char>(ch);
continue;
}
if (line == "quit" || line == "exit") {
client.stop();
break;
}
xSemaphoreTake(lock_, portMAX_DELAY);
bool full = commands_.size() >= kMaxQueued;
if (!full && !line.empty()) commands_.push_back(line);
xSemaphoreGive(lock_);
if (full) client.print("debug: busy, command dropped\n");
line.clear();
}
vTaskDelay(pdMS_TO_TICKS(20));
}
connected_ = false;
console.println("debug: client disconnected");
}
} // namespace roro
#endif