Public Access
Trim task stacks and buffers by measurement: +15 KB with IRC up
Peak stack use was measured through each task's worst case (an ECDSA-checked install over Wi-Fi and from SD, get/put, a core dump fetch, an IRC TLS handshake); stacks are now peak plus about 2 KB: loop 8 -> 6 KB, update 8 -> 5, storage 10 -> 6, irc 8 -> 6. The Debug Build's console ring goes 6 -> 4 KB, serial TX 2 -> 1 KB, the GNSS UART buffer 1 KB -> 512 B. On the device, with IRC on TLS: 46 KB free (was 31), an 18 KB low (was 9.4). The re-run of every worst case left at least 1.6 KB of stack free in each task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# A Debug Console over Wi-Fi, in Debug Builds only
|
# 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.
|
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.
|
- **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.
|
- **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.
|
- **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.
|
- **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
|
## Keep a Debug Build in the fallback slot
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# M2 — GNSS
|
# 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
|
## 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).
|
- **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.
|
- 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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -120,6 +120,9 @@ static StatusInfo currentStatus() {
|
|||||||
// (UpdateService::tick) decides instead, and an unconfirmed image stays PENDING_VERIFY.
|
// (UpdateService::tick) decides instead, and an unconfirmed image stays PENDING_VERIFY.
|
||||||
extern "C" bool verifyRollbackLater() { return true; } // C linkage, or the weak default wins
|
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);
|
static void setupSafeMode(int crashes);
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
@@ -128,7 +131,7 @@ void setup() {
|
|||||||
int crashes = crash_report::noteBoot(nvs);
|
int crashes = crash_report::noteBoot(nvs);
|
||||||
safeMode = SafeMode::active(crashes);
|
safeMode = SafeMode::active(crashes);
|
||||||
Serial.setRxBufferSize(2 * FileReceiver::kChunk); // before the port opens; sd put sends 1 chunk at a time
|
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();
|
auto cfg = M5.config();
|
||||||
M5Cardputer.begin(cfg, true);
|
M5Cardputer.begin(cfg, true);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Console : public Print {
|
|||||||
using Print::write;
|
using Print::write;
|
||||||
|
|
||||||
#ifdef RORO_DEBUG
|
#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).
|
// Also copies ESP-IDF's own log lines into the ring (they still reach the serial port).
|
||||||
void captureEspLogs();
|
void captureEspLogs();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ void GnssService::start() {
|
|||||||
void GnssService::stop() { close(); }
|
void GnssService::stop() { close(); }
|
||||||
|
|
||||||
void GnssService::open(uint32_t nowMs) {
|
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);
|
Serial1.begin(kBaud, SERIAL_8N1, kRxPin, kTxPin);
|
||||||
open_ = true;
|
open_ = true;
|
||||||
active_ = false; // tick() wakes it or puts it to sleep, per the setting
|
active_ = false; // tick() wakes it or puts it to sleep, per the setting
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
namespace roro {
|
namespace roro {
|
||||||
|
|
||||||
// The GNSS receiver on the Cap LoRa-1262 (see CONTEXT.md and docs/milestones/M2.md): reads its
|
// 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.
|
// holds the current Fix, and sets the clock from it. Settings → GNSS switches it on and off.
|
||||||
class GnssService : public Service {
|
class GnssService : public Service {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ IrcService::IrcService(KeyValueStore& store, const std::string& defaultNick, Wif
|
|||||||
void IrcService::start() {
|
void IrcService::start() {
|
||||||
if (task_) return;
|
if (task_) return;
|
||||||
lock_ = xSemaphoreCreateMutex();
|
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() {
|
void IrcService::connect() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace roro {
|
|||||||
|
|
||||||
void StorageService::start() {
|
void StorageService::start() {
|
||||||
if (task_) return;
|
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() {
|
void StorageService::stop() {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ void UpdateService::start() {
|
|||||||
store_.putString("ota_pending", "");
|
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) {
|
void UpdateService::bootGuard(KeyValueStore& store) {
|
||||||
|
|||||||
Reference in New Issue
Block a user