diff --git a/docs/adr/0004-debug-console-in-debug-builds.md b/docs/adr/0004-debug-console-in-debug-builds.md index 7c942a7..572793d 100644 --- a/docs/adr/0004-debug-console-in-debug-builds.md +++ b/docs/adr/0004-debug-console-in-debug-builds.md @@ -1,6 +1,6 @@ # A Debug Console over Wi-Fi, in Debug Builds only -The goal of Firmware Updates is to manage the device without a cable, and that includes finding out what went wrong. So a **Debug Build** (`cardputer-adv-debug`, `-DRORO_DEBUG`, version suffix `+debug`) adds a **Debug Console** on TCP 2323: the serial console, over Wi-Fi. A client sends a token as its first line, then gets the last 6 KB of console output (boot messages included), every new line live, and runs the same commands as the serial port, plus a few that only make sense remotely. ESP-IDF's own log lines are teed into it. +The goal of Firmware Updates is to manage the device without a cable, and that includes finding out what went wrong. So a **Debug Build** (`cardputer-adv-debug`, `-DRORO_DEBUG`, version suffix `+debug`) adds a **Debug Console** on TCP 2323: the serial console, over Wi-Fi. A client sends a token as its first line, then gets the last 4 KB of console output (boot messages included), every new line live, and runs the same commands as the serial port, plus a few that only make sense remotely. ESP-IDF's own log lines are teed into it. It's compiled out of release builds entirely, rather than switched off by a setting. A console that runs commands is a remote control: in a release build, nothing listens. @@ -9,7 +9,7 @@ It's compiled out of release builds entirely, rather than switched off by a sett - **Console, not Serial.** All human-readable output goes through `console`, which writes to the USB port and, in a Debug Build, to a ring buffer the Debug Console drains. Writes never wait for USB: a host that's attached but not reading used to stall the main loop for up to 2 s per line. - **Commands run on the main loop.** The socket lives on the Debug Console's own task, which only queues command lines. The main loop runs them, as it does serial commands, so they touch Apps and Services from the one task allowed to. - **The token** is 128 random bits in `~/.config/roro9stack/debug-token`, made by the first build and passed into the container. It's never committed; a Debug Build refuses to compile without one. Like the OTA key, it guards against the network, not against someone holding the device. -- **One client at a time**, to keep memory flat (about 6 KB for the ring, 6 KB of task stack). +- **One client at a time**, to keep memory flat (4 KB for the ring since M2, 6 KB of task stack). - **Binary commands are answered on the console's own task**, not queued: `get`/`put` (SD card files, run as one Storage Service job each so card access stays on the storage task, with TCP doing the flow control), `screenshot` (the 32 KB RGB332 frame the UI composes into, read as it stands, so it may tear), `coredump get` and `reset`. These keep working when the main loop is stuck. A failed `put` closes the connection, so the rest of the file is never read as commands. ## Keep a Debug Build in the fallback slot diff --git a/docs/milestones/M2.md b/docs/milestones/M2.md index 522f8be..54359ee 100644 --- a/docs/milestones/M2.md +++ b/docs/milestones/M2.md @@ -1,12 +1,12 @@ # M2 — GNSS -**Status:** steps 1–6 done on the device (branch `m2`). Open: the heap floor (below). +**Status:** steps 1–6 done on the device (branch `m2`). Open: the heap floor during a TLS handshake (below). ## Measured - **Cold start** (`$PCAS10,2`) to a 3D Fix, by a window: **73 s**, 5 satellites used of 8 in view. A restart of the ESP32 alone keeps the receiver's Fix (the Cap stays powered). - By a window: 3D Fix from GPS, GLONASS, Galileo and BeiDou, up to 14 of 17 satellites used, HDOP 1.0–1.3. -- **Heap, Debug Build, GNSS on:** 82 KB free with Wi-Fi up; with IRC on TLS, **33 KB free and a 12.6 KB low** during the handshake. The floor is 40 KB, and v0.2.1 (before OTA) measured a 79 KB low. Not a GNSS cost: the OTA and Debug Build work added the `update` (8 KB) and `debug` (6 KB) task stacks, the 6 KB console ring, a larger `storage` stack (10 KB, for SD signature checks), 4 KB of serial buffers, mDNS and two TCP servers. To decide before M2 closes. +- **Heap, Debug Build, GNSS on:** with IRC on TLS, first 33 KB free and a 12.6 KB low (floor 40 KB; v0.2.1 had a 79 KB low). Not a GNSS cost: the OTA and Debug Build work added task stacks, a console ring, serial buffers, mDNS and two TCP servers. **Trimmed by measurement:** each task's peak stack was measured through its worst case (an ECDSA-checked install over Wi-Fi and from SD, get/put, a core dump fetch, an IRC TLS handshake), then stacks were set to peak plus about 2 KB: loop 8→6 KB, update 8→5, storage 10→6, irc 8→6; the console ring 6→4 KB, serial TX 2→1 KB, GNSS UART 1 KB→512 B. **After:** 46 KB free with IRC connected, an 18 KB low. The steady state clears the floor; the TLS handshake peak doesn't yet. Next candidates: mbedTLS dynamic buffers (custom sdkconfig), one network task for the update and debug listeners, mDNS on demand. **Goal:** the device knows where it is and what time it is without a network: a GNSS Service in the background, a GNSS App with the position and a sky view of the satellites, the clock set from satellites when there's no NTP, and Tracks recorded to the SD card. diff --git a/src/main.cpp b/src/main.cpp index 3c56d0c..0143b9f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -120,6 +120,9 @@ static StatusInfo currentStatus() { // (UpdateService::tick) decides instead, and an unconfirmed image stays PENDING_VERIFY. extern "C" bool verifyRollbackLater() { return true; } // C linkage, or the weak default wins +// The main loop's stack: Arduino's default is 8 KB; the measured peak is 3 KB (M2). +size_t getArduinoLoopTaskStackSize() { return 6144; } + static void setupSafeMode(int crashes); void setup() { @@ -128,7 +131,7 @@ void setup() { int crashes = crash_report::noteBoot(nvs); safeMode = SafeMode::active(crashes); Serial.setRxBufferSize(2 * FileReceiver::kChunk); // before the port opens; sd put sends 1 chunk at a time - Serial.setTxBufferSize(2048); // the console skips Serial when it's full: room for a burst like `tasks` + Serial.setTxBufferSize(1024); // the console skips Serial when it's full: room for a burst like `tasks` auto cfg = M5.config(); M5Cardputer.begin(cfg, true); diff --git a/src/platform/console.h b/src/platform/console.h index 49d097e..214b395 100644 --- a/src/platform/console.h +++ b/src/platform/console.h @@ -17,7 +17,7 @@ class Console : public Print { using Print::write; #ifdef RORO_DEBUG - static constexpr size_t kRingBytes = 6144; + static constexpr size_t kRingBytes = 4096; // Also copies ESP-IDF's own log lines into the ring (they still reach the serial port). void captureEspLogs(); diff --git a/src/services/gnss_service.cpp b/src/services/gnss_service.cpp index 82c9775..a769c11 100644 --- a/src/services/gnss_service.cpp +++ b/src/services/gnss_service.cpp @@ -32,7 +32,7 @@ void GnssService::start() { void GnssService::stop() { close(); } void GnssService::open(uint32_t nowMs) { - Serial1.setRxBufferSize(1024); // before begin(): over 2 s of NMEA + Serial1.setRxBufferSize(512); // before begin(): over a second of NMEA, read every 50 ms Serial1.begin(kBaud, SERIAL_8N1, kRxPin, kTxPin); open_ = true; active_ = false; // tick() wakes it or puts it to sleep, per the setting diff --git a/src/services/gnss_service.h b/src/services/gnss_service.h index 50ba80a..8051a5a 100644 --- a/src/services/gnss_service.h +++ b/src/services/gnss_service.h @@ -15,7 +15,7 @@ namespace roro { // The GNSS receiver on the Cap LoRa-1262 (see CONTEXT.md and docs/milestones/M2.md): reads its -// NMEA from the main loop's tick (about 450 bytes/s, so a 1 KB UART buffer covers any stall), +// NMEA from the main loop's tick (about 450 bytes/s, so a 512-byte UART buffer covers a second), // holds the current Fix, and sets the clock from it. Settings → GNSS switches it on and off. class GnssService : public Service { public: diff --git a/src/services/irc_service.cpp b/src/services/irc_service.cpp index 64e87e1..b0dfcda 100644 --- a/src/services/irc_service.cpp +++ b/src/services/irc_service.cpp @@ -33,7 +33,7 @@ IrcService::IrcService(KeyValueStore& store, const std::string& defaultNick, Wif void IrcService::start() { if (task_) return; lock_ = xSemaphoreCreateMutex(); - xTaskCreate(taskEntry, "irc", 8192, this, 1, &task_); + xTaskCreate(taskEntry, "irc", 6144, this, 1, &task_); // peak 4.0 KB (TLS handshake, M2) } void IrcService::connect() { diff --git a/src/services/storage_service.cpp b/src/services/storage_service.cpp index 012f099..269cb80 100644 --- a/src/services/storage_service.cpp +++ b/src/services/storage_service.cpp @@ -14,7 +14,7 @@ namespace roro { void StorageService::start() { if (task_) return; - xTaskCreate(taskEntry, "storage", 10240, this, 1, &task_); // room for a signature check + xTaskCreate(taskEntry, "storage", 6144, this, 1, &task_); // peak 3.9 KB (SD install with its signature check, M2) } void StorageService::stop() { diff --git a/src/services/update_service.cpp b/src/services/update_service.cpp index 9967fa0..1b3e9ee 100644 --- a/src/services/update_service.cpp +++ b/src/services/update_service.cpp @@ -90,7 +90,7 @@ void UpdateService::start() { store_.putString("ota_pending", ""); } - if (!task_) xTaskCreate(taskEntry, "update", 8192, this, 1, &task_); + if (!task_) xTaskCreate(taskEntry, "update", 5120, this, 1, &task_); // peak 3.4 KB (ECDSA check, M2) } void UpdateService::bootGuard(KeyValueStore& store) {