fix(ui-lvgl): 16 KB UI stack (screenshots reset the device); USB popup after the splash

The UI runs on Arduino's loop task, whose stock 8 KB stack had 340 B left
at its deepest point; a screenshot's file write went past it and reset the
device. The loop stack is now 16 KB (measured peak ~7.8 KB) and the
screenshot's line buffer is static. Diagnostics names the row
"UI stack free (lowest)".

With a cable in at boot, the USB popup waited for nothing and showed over
the splash; it now comes once the splash has faded.

Adds docs/development/plan-next.md, the plan up to the merge into dev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-28 21:30:46 +02:00
co-authored by Claude Opus 5.5
parent 7a21e58c54
commit a11df8431b
5 changed files with 120 additions and 2 deletions
+109
View File
@@ -0,0 +1,109 @@
# Plan: from here to the merge into dev (2026-09-28)
Work in order; each item is implemented, checked in the sim, flashed, and
committed only after it's accepted. Tick items off as they land. Working
file: goes in the repo clean-up (stage I).
## A. Bugs
- [x] **A1. Restart on a screenshot + "340 B stack free".** Likely one cause.
Diagnostics shows the UI loop task's lowest-ever stack headroom
(`uxTaskGetStackHighWaterMark`); the Arduino loop stack is 8 KB, so
340 B left is nearly an overflow. `takeScreenshot()` keeps a 960 B line
buffer on that stack, plus the FAT write.
1. Confirm with Diagnostics > Last crash after a screenshot restart.
2. Move the line buffer off the stack.
3. Raise the loop stack (internal RAM).
4. Measure the headroom on heavy screens (map, history, OTA).
5. Label the Diagnostics row with the task it measures.
Done 2026-09-28: loop stack 16 KB (SET_LOOP_TASK_STACK_SIZE), line
buffer static, row "UI stack free (lowest)". Measured 8532 B free
after map, history and screenshots, so the UI peaks at ~7.8 KB --
the old 8 KB left 340 B. Could go down to 12 KB if RAM gets tight.
- [x] **A2. USB popup before the splash screen is gone.** `usbPoll()` waits
for the splash to finish; a cable plugged in at boot shows the popup
right after it.
## B. Map
- [x] **B1. Map menu order.** List the current entries, propose groups and
an order (view, overlays, areas & downloads, trails, test), agree on
it before coding. Done: TRAIL / LIVE SHARE / ARRIVAL ALERT (each with
its own options page, one section of PG_NAV), OFFLINE MAPS (Map areas,
Live tiles; "Download an area" dropped, a running / unfinished
download shows at the top of Map areas), LAYERS.
- [x] **B2. Downloaded areas.** Selecting an area previews it at once (no
Show button); leaving the menu hides it and restores the previous map
view. Fixes the area staying on the map after Show.
## C. Diagnostics: the noise tests (to think over)
Diagnostics stays as it is. Only the Noise tab is in question: the tests that
found the interference source (noise floor with the board's parts off one by
one, the 850-930 MHz and mesh-channel sweeps, the spike hunt, the 15 s states
for a second radio). They are L2-specific and block the loop for ~90 s.
- [ ] Decide: remove them, or keep one universal tool (for example a noise
floor sweep round the mesh frequency that works on any board) and drop
the L2 part toggling and spike hunt.
## D. Quiet hours in the core
- [ ] A start and end time when the device is silent; in ui-core, so L1, L2
and other boards get it. New NodePrefs fields go at the end of the
stored layout, so an L1 keeps its settings after the update.
To decide: the Clock alarm rings anyway (proposed yes); whether a
message wakes the screen during quiet hours; with the clock not set,
quiet hours are off (proposed).
## E. OTA end-to-end test
- [ ] After the feature changes: a real release with an L2 asset (the v3 env
must be the `*_solo_lvgl` one before tagging), install over WiFi.
Publishing the release needs confirmation first.
## F. The big review
- [ ] Dead code; similar elements written several times, merged into one;
places to speed things up or save RAM, flash and battery.
First a list of findings to accept, then fixes in batches, each checked
in the sim and on the device.
## G. Core parity with L1 SOLO
- [ ] A feature table: L1 SOLO / ui-core / L2 (ui-lvgl). What's missing goes
into ui-core, not into each UI separately.
## H. Before the merge into dev
- [ ] The pull request about the lock screen.
- [ ] The new issues.
- [ ] Merge into dev.
- [ ] A last review.
## I. Repo, documentation, website, firmware tiers (its own detailed plan)
- [ ] Repo: remove working files (plans etc.), keep only the project's code;
README down to the minimum; add Buy Me a Coffee.
- [ ] Documentation written from scratch: short, describing the firmware's
features, easy to browse; sections marking where devices differ; no
screenshots of every screen and device. It stays in the repo, its
official entry is the simulator website.
- [ ] Website rebuilt and polished; the simulator lets you pick which
firmware to simulate.
- [ ] Firmware in three tiers, one shared core, extras (map tiles etc.) by
what the hardware can do, making full use of each device:
- minimal -- nRF52, a limited UI, small e-ink and OLED;
- standard -- mostly ESP32, large e-ink and LCD, higher resolutions,
a somewhat richer UI;
- color -- colour touch screens.
Proposed: write a short tier spec (which boards, which features, what's in
the core) as a document before stage F, so the review consolidates towards
it; implement the tiers, docs and website after the merge.
## Open decisions
- C: remove the noise tests or keep a universal version.
- D: alarm during quiet hours; messages waking the screen.
- I: tier spec before stage F, or after the merge.
@@ -66,7 +66,7 @@ static const Name NAMES[] = {
{ "Noise floor", SEC_RADIO, "Noise floor", false }, { "RSSI/SNR", SEC_RADIO, "Last packet", false },
{ "Queue", SEC_RADIO, "Send queue", false }, { "Errors", SEC_RADIO, "Errors", false },
{ "RXPS wd s/h", SEC_RADIO, "RX watchdog soft / hard", false },
{ "Heap free", SEC_MEMORY, "Heap free", false }, { "Stack free", SEC_MEMORY, "Stack free", false },
{ "Heap free", SEC_MEMORY, "Heap free", false }, { "Stack free", SEC_MEMORY, "UI stack free (lowest)", false },
{ "Pool free", SEC_MEMORY, "Packet pool free", false },
};
static const Name* nameOf(const char* core) {
@@ -92,6 +92,7 @@ static void dismiss() {
anim::run(s_root, anim::setOpa, LV_OPA_COVER, LV_OPA_TRANSP, 300, freeAll);
}
static void onTimer(lv_timer_t* t) { (void)t; dismiss(); }
static bool up() { return s_root != nullptr; } // until its fade-out has finished
static void onTap(lv_event_t* e) { (void)e; dismiss(); }
// One character of the lettering: its width, and whether (x, y) is inked.
@@ -504,10 +504,13 @@ static void usbPanel(const char* title, const char* text, bool buttons) {
}
}
namespace splash { static bool up(); } // Splash.h
void UITask::usbPoll() {
using namespace usbview;
static uint32_t next = 0;
if ((int32_t)(millis() - next) < 0) return;
if (splash::up()) return; // a cable in at boot: the popup comes once the splash is gone
next = millis() + 250;
if (lvport::usbDriveOn()) {
if (!lvport::usbDriveDone()) return;
+6 -1
View File
@@ -4,6 +4,11 @@
#include "target.h"
#if defined(ESP32)
#include <esp_heap_caps.h>
// The UI runs on Arduino's loop task: LVGL's renderer, FAT on the card and
// the map's decoders all nest on its stack, and the stock 8 KB left a few
// hundred bytes at the deepest point (Diagnostics > Stack free, the lowest
// ever) -- a screenshot's file write went past it and reset the device.
SET_LOOP_TASK_STACK_SIZE(16 * 1024);
#endif
#include <new>
#include <stdarg.h>
@@ -1496,7 +1501,7 @@ void UITask::takeScreenshot() {
put32(2, size); put32(10, 54); put32(14, 40); put32(18, W); put32(22, H);
hdr[26] = 1; hdr[28] = 24; put32(34, row * H);
ok = fwrite(hdr, 1, 54, f) == 54;
uint8_t line[320 * 3];
static uint8_t line[320 * 3]; // not on the loop task's stack
for (int32_t y = H - 1; ok && y >= 0; y--) { // bottom-up, BGR
const uint16_t* s = px + y * W;
for (int32_t x = 0; x < W && x < 320; x++) {