From 5b5e86c197ce53ee706fca50a8a3b0c2e60f1b0f Mon Sep 17 00:00:00 2001 From: Kaj Schittecat Date: Sat, 22 Aug 2026 19:53:14 +0200 Subject: [PATCH] console mode: split CLI output on newlines, and self-heal after a panic The overlap: the node CLI hands its whole reply to the terminal sink as ONE buffer containing '\n' (its help text is a dozen lines in a single string). consoleWriteLine stored that as one ring entry, so the text renderer drew the breaks itself while our y-cursor still advanced by a single row, and every later line landed on top of the one before it. Worsening down the screen, which is what the photo shows. Now split on newlines first, then wrap each piece to the panel width, so no stored line can be wider than the panel and the renderer never wraps one on its own. The bootloop: the device panicked, rebooted into console mode, and panicked again, with no way back to the UI from the device. Console mode now checks esp_reset_reason() and, if the previous boot panicked, comes up graphical and clears the pref. That is the third way out the plan called for, and the only one that needs the user to know nothing. Honest about attribution: it boots clean now, but I cannot prove the newline bug was the panic. The serial cut off right after 'serial_interface ok' because those prints were not flushed, so the fault may have been later than it looked. Boot breadcrumbs added (flushed) so the next one localises itself. Co-Authored-By: Claude Opus 5 --- src/ui-touch/ConsoleUI.cpp | 38 ++++++++++++++++++++++++++++++-------- src/ui-touch/UITask.cpp | 14 ++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/ui-touch/ConsoleUI.cpp b/src/ui-touch/ConsoleUI.cpp index 8afcd54..d48e399 100644 --- a/src/ui-touch/ConsoleUI.cpp +++ b/src/ui-touch/ConsoleUI.cpp @@ -440,15 +440,37 @@ bool consoleActive() { return s_active; } void consoleWriteLine(const char* line) { if (!s_ring) return; if (!line) { ringPush("", 0); return; } - // Wrap to the panel width rather than truncating: a command reply that runs - // off the edge is the same as no reply at all on a screen this size. const int width = s_cols > 0 ? s_cols : 40; - const int len = (int)strlen(line); - if (len == 0) { ringPush("", 0); return; } - for (int off = 0; off < len; off += width) { - int n = len - off; - if (n > width) n = width; - ringPush(line + off, n); + + // Split on embedded newlines FIRST. The node CLI hands its reply to the sink + // as one buffer containing '\n' (its help text is a dozen lines in a single + // string), and storing that as one ring entry meant print() rendered the + // breaks itself while our y-cursor still advanced by one row. Every later + // line then landed on top of the one before it, which is the overlap in the + // report, worsening down the screen as the error accumulated. + // + // Then wrap each piece to the panel width, rather than truncating: a reply + // that runs off the edge is the same as no reply on a screen this size. This + // also guarantees no stored line can be wider than the panel, so the text + // renderer never wraps one on its own and desynchronises the cursor again. + int pushed = 0; + const char* seg = line; + while (seg && pushed < 512) { // bound: a runaway reply cannot spin here + const char* nl = strchr(seg, '\n'); + int seg_len = nl ? (int)(nl - seg) : (int)strlen(seg); + // Strip a trailing CR so CRLF output does not leave a stray glyph. + if (seg_len > 0 && seg[seg_len - 1] == '\r') seg_len--; + if (seg_len == 0) { + ringPush("", 0); pushed++; + } else { + for (int off = 0; off < seg_len && pushed < 512; off += width) { + int n = seg_len - off; + if (n > width) n = width; + ringPush(seg + off, n); pushed++; + } + } + if (!nl) break; + seg = nl + 1; } } diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index ab902cb..6846aa5 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -49283,14 +49283,28 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #if CAP_CONSOLE && defined(ESP32) s_console_mode = touchPrefsGetConsoleMode(); + // SELF-HEAL. If the previous boot panicked while console mode was on, come up + // in the graphical UI and turn console mode off. Without this a crash on the + // console path is unrecoverable from the device: it panics, reboots into the + // console, panics again. The plan called for three ways out and this is the + // one that does not need the user to know anything. + if (s_console_mode && esp_reset_reason() == ESP_RST_PANIC) { + s_console_mode = false; + touchPrefsSetConsoleMode(false); + Serial.println("[BOOT] console mode disabled after a panic — starting the UI"); + } if (s_console_mode) { // Nothing below this point runs: no lv_init(), no draw buffer, no widget // tree. The console owns the panel from here. + Serial.println("[BOOT] console: begin"); Serial.flush(); consoleBegin(_display); + Serial.println("[BOOT] console: panel up"); Serial.flush(); consoleWriteLine("wadamesh console"); consoleWriteLine("type 'help', or 'ui' to go back to the graphical UI"); consoleWriteLine(""); + Serial.println("[BOOT] console: text ok"); Serial.flush(); the_mesh.setTerminalSink(&consoleWriteLine); + Serial.println("[BOOT] console: sink set"); Serial.flush(); return; } #endif