Public Access
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:
@@ -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
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef RORO_DEBUG
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
#include "service.h"
|
||||
#include "services/wifi_service.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 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
|
||||
// through the console ring like everything else.
|
||||
class DebugConsole : public Service {
|
||||
public:
|
||||
static constexpr uint16_t kPort = 2323;
|
||||
|
||||
explicit DebugConsole(WifiService& wifi);
|
||||
const char* name() const override { return "debug"; }
|
||||
void start() override;
|
||||
|
||||
// The next command line a client sent, for the main loop to run.
|
||||
bool takeCommand(std::string& line);
|
||||
bool clientConnected() const { return connected_; }
|
||||
|
||||
private:
|
||||
static void taskEntry(void* self);
|
||||
void listen();
|
||||
void serve(::NetworkClient& client);
|
||||
bool authenticate(::NetworkClient& client);
|
||||
|
||||
WifiService& wifi_;
|
||||
TaskHandle_t task_ = nullptr;
|
||||
SemaphoreHandle_t lock_;
|
||||
std::deque<std::string> commands_;
|
||||
volatile bool connected_ = false;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
#endif
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "platform/identity.h"
|
||||
#include "platform/ota_device.h"
|
||||
#include "platform/system_info.h"
|
||||
#include "probation.h"
|
||||
#include "update_parser.h"
|
||||
#include "version.h"
|
||||
@@ -95,6 +96,7 @@ void UpdateService::bootGuard(KeyValueStore& store) {
|
||||
esp_ota_img_states_t state;
|
||||
bool probation = esp_ota_get_state_partition(esp_ota_get_running_partition(), &state) == ESP_OK &&
|
||||
state == ESP_OTA_IMG_PENDING_VERIFY;
|
||||
system_info::recordSlotVersion(store, esp_ota_get_running_partition(), versionString());
|
||||
int32_t attempts = 0;
|
||||
store.getInt("ota_attempts", attempts);
|
||||
if (Probation::rollBackAtBoot(probation, attempts)) {
|
||||
@@ -156,6 +158,7 @@ void UpdateService::install(UpdateSource& source, const char* via) {
|
||||
|
||||
if (ok) {
|
||||
store_.putString("ota_pending", parser.version());
|
||||
system_info::recordSlotVersion(store_, esp_ota_get_boot_partition(), parser.version().c_str());
|
||||
store_.putString("ota_from", versionString());
|
||||
source.reply("OK " + parser.version() + (parser.isDowngrade() ? " (older than the installed one)" : ""));
|
||||
notify("Update " + parser.version() + " installed (" + via + "), restarting", NotificationLevel::Info);
|
||||
|
||||
Reference in New Issue
Block a user