From fbcd524a65fc9872a8a788ca39ee49b09b18ecbe Mon Sep 17 00:00:00 2001 From: Kaj Schittecat Date: Tue, 4 Aug 2026 19:17:43 +0200 Subject: [PATCH] v4: reserve the net worker's stack in begin() + boot memory probes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The worker's 8 KB internal stack was only ever claimed lazily, the first time the map or store asked for it — by which point the Wi-Fi driver has taken the internal heap. Its own comment always said 'allocated at UITask::begin() time' and explained why (grab it before Wi-Fi associates, then it stays ours); that call site never existed. Measured on the V4: 68 KB free / 59 KB largest block at end of boot, versus 664 bytes free and a 4-byte largest block once Wi-Fi was up — so tiles, the update check and the app catalog were all dead there, not just the store. Co-Authored-By: Claude Opus 4.8 --- src/main.cpp | 21 ++++++++++ src/ui-touch/UITask.cpp | 87 +++++++++++++++++++++++++++++++++++------ 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 7f17415..531208f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -275,6 +275,16 @@ void meshcomodClearSdMigLatch() { } #endif + +// Boot memory trace: 175 KB of internal RAM is gone before the UI starts, and +// nothing said where. Print free/largest at each init step so the consumer +// names itself instead of being guessed at. +#define BOOTMEM(tag) do { \ + multi_heap_info_t _h{}; heap_caps_get_info(&_h, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); \ + Serial.printf("[BOOTMEM] %-12s free=%6u largest=%6u psram=%7u\n", tag, \ + (unsigned)_h.total_free_bytes, (unsigned)_h.largest_free_block, \ + (unsigned)ESP.getFreePsram()); Serial.flush(); } while (0) + void setup() { Serial.begin(115200); #if defined(HAS_RAK_TAP_V2) @@ -283,6 +293,7 @@ void setup() { delay(200); #endif Serial.println("[BOOT] setup start"); + BOOTMEM("setup-start"); // Widen the task-watchdog grace period. The ~5 s default trips during a legitimate-but-slow flash // burst — a SPIFFS garbage-collect, or a bulk save (DataStore issues ~12 flash ops per contact, @@ -325,6 +336,7 @@ void setup() { board.begin(); Serial.println("[BOOT] board ok"); + BOOTMEM("post-board"); #if defined(HAS_RAK_TAP_V2) // Quick PSRAM sanity check — silent crash before SPIFFS could be bad PSRAM config @@ -372,7 +384,9 @@ void setup() { } #endif + BOOTMEM("pre-radio"); if (!radio_init()) { halt(); } + BOOTMEM("post-radio"); Serial.println("[BOOT] radio ok"); fast_rng.begin(radio_driver.getRngSeed()); @@ -405,6 +419,7 @@ void setup() { #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + BOOTMEM("pre-fs"); InternalFS.begin(); #if defined(QSPIFLASH) if (!QSPIFlash.begin()) { @@ -418,7 +433,9 @@ void setup() { ExtraFS.begin(); #endif #endif + BOOTMEM("pre-store"); store.begin(); + BOOTMEM("pre-mesh"); the_mesh.begin( #ifdef DISPLAY_CLASS disp != NULL @@ -432,9 +449,13 @@ void setup() { #else serial_interface.begin(Serial); #endif + BOOTMEM("pre-iface"); the_mesh.startInterface(serial_interface); + BOOTMEM("post-iface"); #elif defined(RP2040_PLATFORM) + BOOTMEM("pre-littlefs"); LittleFS.begin(); + BOOTMEM("post-littlefs"); store.begin(); the_mesh.begin( #ifdef DISPLAY_CLASS diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index b515030..04322ea 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -1328,6 +1328,14 @@ static void statusBarSetTall(bool tall) { // NOT be called from here — it calls back into statusBarSetTall = recursion.) } +void reserveTileFetchStack(); // fwd: claim the worker stack before Wi-Fi eats the heap + +#define MEMPROBE(tag) do { \ + multi_heap_info_t _hi{}; heap_caps_get_info(&_hi, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); \ + Serial.printf("[MEM] %-14s free=%6u largest=%6u psram=%7u\n", tag, \ + (unsigned)_hi.total_free_bytes, (unsigned)_hi.largest_free_block, \ + (unsigned)ESP.getFreePsram()); } while (0) + // ---- AppPage: the shared full-screen app-page chrome (see AppPage.h) -------------- // Thin wrappers over the machinery just above, exported so the self-contained app // modules (SnakeGame) build the same page as the in-file tool @@ -24340,8 +24348,9 @@ static void luaStoreFetchCatalogWorker(WiFiClient& client, HTTPClient& http) { s_luacat_buf = (char*)heap_caps_malloc(kLuaCatBufMax, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!s_luacat_buf) { return; } s_luacat_buf[0] = 0; - luaStoreHttpGet(client, http, "http://firmware.wadamesh.com/apps/apps.json", - s_luacat_buf, kLuaCatBufMax); + int cn = luaStoreHttpGet(client, http, "http://firmware.wadamesh.com/apps/apps.json", + s_luacat_buf, kLuaCatBufMax); + Serial.printf("[APPCAT] n=%d wifi=%d\n", cn, (int)WiFi.status()); } // Download .lua + .json (immutable per-version path) into /apps/. @@ -24400,8 +24409,9 @@ static void luaStoreFetchLangCatalogWorker(WiFiClient& client, HTTPClient& http) s_langcat_buf = (char*)heap_caps_malloc(kCap, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!s_langcat_buf) return; s_langcat_buf[0] = 0; - luaStoreHttpGet(client, http, "http://firmware.wadamesh.com/apps/langs.json", - s_langcat_buf, kCap); + int ln = luaStoreHttpGet(client, http, "http://firmware.wadamesh.com/apps/langs.json", + s_langcat_buf, kCap); + Serial.printf("[LANGCAT] n=%d\n", ln); } // Chunked writer through an internal bounce buffer, tolerant of transient @@ -24886,6 +24896,7 @@ static void tileFetchTaskFn(void* arg) { // Lua app store: catalog fetch + app install (LUA_APPS.md Phase 2). if (s_luacat_request) { s_luacat_request = false; + Serial.println("[APPCAT] worker picked up the request"); luaStoreFetchCatalogWorker(client, http); s_luacat_done = true; continue; @@ -37588,7 +37599,9 @@ static void luaStoreRebuildList() { if (s_lua_cat_n <= 0) { lv_obj_t* card = lv_obj_create(s_luastore_list); lv_obj_remove_style_all(card); - lv_obj_set_size(card, W - 8, lh14 + lh12 + 22); + // Height from the wrapped hint, not a one-line guess — on a 240 px panel + // the second/third line used to be cut off by the card's own bottom edge. + lv_obj_set_size(card, W - 8, lh14 + lh12 * (narrow ? 3 : 2) + 22); lv_obj_set_style_bg_color(card, lv_color_hex(COLOR_PANEL), LV_PART_MAIN); lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_radius(card, 10, LV_PART_MAIN); @@ -37600,9 +37613,22 @@ static void luaStoreRebuildList() { lv_obj_set_style_text_color(t, lv_color_hex(s_lua_cat_n < 0 ? 0xE08080 : COLOR_TEXT), LV_PART_MAIN); lv_obj_set_pos(t, 0, 0); lv_obj_t* h = lv_label_create(card); - lv_label_set_text(h, s_lua_cat_n < 0 - ? TR("Turn Wi-Fi on, then tap refresh. Apps already installed still work.") - : TR("Fetching the app list from firmware.wadamesh.com")); + if (s_lua_cat_n < 0 && s_tile_fetch_task == nullptr) { + // The network worker could not start: report the numbers that decide it + // (it needs one contiguous 8 KB internal block) right on screen, so this + // is diagnosable from the device instead of a serial cable. + char mem[128]; + snprintf(mem, sizeof mem, + "%s\nfree %u KB, largest block %u KB (needs 8 KB)", + TR("Not enough free memory to fetch it. Reboot and try again."), + (unsigned)(ESP.getFreeHeap() / 1024), + (unsigned)(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024)); + lv_label_set_text(h, mem); + } else { + lv_label_set_text(h, s_lua_cat_n < 0 + ? TR("Turn Wi-Fi on, then tap refresh. Apps already installed still work.") + : TR("Fetching the app list from firmware.wadamesh.com")); + } lv_obj_set_style_text_font(h, &g_font_12, LV_PART_MAIN); lv_obj_set_style_text_color(h, lv_color_hex(COLOR_SUB), LV_PART_MAIN); lv_obj_set_width(h, W - 26); @@ -37781,7 +37807,7 @@ static void openLuaStorePage() { lv_obj_set_flex_flow(s_luastore_list, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(s_luastore_list, 6, LV_PART_MAIN); - appPageBeginSlim(TR("Lua Store"), &closeLuaStorePage); // one-line bar; the tab bar is the page header + appPageBeginSlim(TR("Store"), &closeLuaStorePage); // one-line bar; the tab bar is the page header // Open instantly from the cached install list; the card re-listing (sideload // pickup) runs on the worker and the poll timer repaints when it lands. @@ -37791,7 +37817,22 @@ static void openLuaStorePage() { s_luacat_done = false; s_luacat_request = true; } - ensureTileFetchTaskRunning(); + MEMPROBE("store-open"); + const bool worker_up = ensureTileFetchTaskRunning(); + Serial.printf("[STORE] open: worker=%d spawn_ok=%d wifi=%d free_int=%u largest=%u free_psram=%u\n", + (int)worker_up, (int)s_tile_fetch_spawn_ok, (int)WiFi.status(), + (unsigned)ESP.getFreeHeap(), + (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT), + (unsigned)ESP.getFreePsram()); + // The net worker needs an 8 KB contiguous internal block. When memory is too + // tight to start it, nothing will ever service the request — so fail visibly + // instead of spinning on "Loading catalog..." forever. + if (!worker_up) { + s_lua_cat_n = -1; + s_luacat_request = false; + s_langcat_n = -1; + s_langcat_request = false; + } if (s_luastore_tab == 2) { // reopened straight onto Languages luaStoreScanLangs(); if (s_langcat_n <= 0) { @@ -38507,7 +38548,7 @@ static void openAppDrawer() { #if !CAP_LUA_APPS { nullptr, "Snake", APPACT_SNAKE, 0, 0x53C06B }, // native snake (Lua boards install it from the Store) #else - { LV_SYMBOL_DOWNLOAD, "Lua Store", APPACT_STORE, 0, 0x15B6A6 }, // Lua app store (brand teal) + { LV_SYMBOL_DOWNLOAD, "WADA Store", APPACT_STORE, 0, 0x15B6A6 }, // Lua app store (brand teal) #endif { LV_SYMBOL_POWER, "Power", APPACT_POWER, 0, 0xE05544 }, // power red }; @@ -41999,7 +42040,9 @@ static void buildUiTree() { makeChatDetail(g_lv.dm); makeChatDetail(g_lv.ch); + MEMPROBE("pre-settings"); makeSettings(tab_settings); + MEMPROBE("post-UI-build"); #if defined(HAS_TANMATSU) navBuildTabKeyHints(); // coloured △□○♣◇ shapes on the tab bar = the physical F-keys @@ -45846,6 +45889,16 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #if defined(HAS_TOUCH_UI) g_lv.task = this; if (!g_lv.ready) { + // Claim the net worker's 8 KB internal stack NOW, while the heap is still + // whole. Its own comment always said "allocated at UITask::begin() time", + // but the only caller was the fallback inside ensureTileFetchTaskRunning() + // — which runs when the user first opens the map/store, by which point the + // Wi-Fi driver has taken the internal heap. On the V4 that left a 4-byte + // largest block, so the worker could never start and tiles, the update + // check and the app catalog were all silently dead. + reserveTileFetchStack(); + MEMPROBE("post-reserve"); + MEMPROBE("pre-lv_init"); lv_init(); initTouchFontFallbacks(); #if CAP_ROUND_CORNERS @@ -46015,6 +46068,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif // Accent-popup picker (both boards: on-screen + physical keyboard). Default on. s_accent_popups = touchPrefsGetAccentPopups(); + { + multi_heap_info_t hi{}; + heap_caps_get_info(&hi, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + Serial.printf("[MEM] boot int: total=%u free=%u largest=%u minfree=%u | psram free=%u\n", + (unsigned)(hi.total_free_bytes + hi.total_allocated_bytes), + (unsigned)hi.total_free_bytes, (unsigned)hi.largest_free_block, + (unsigned)hi.minimum_free_bytes, (unsigned)ESP.getFreePsram()); + } i18nSetLang(touchPrefsGetUiLang()); // active UI translation language (before the UI builds) #if CAP_LUA_APPS uiLangFileBootLoad(); // overlay the active .lang file (mounts data storage a bit earlier) @@ -46075,7 +46136,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // when the base orientation is portrait. In hardware landscape the LVGL // rotation is left at NONE so it never software-rotates on top of the panel. g_lv.disp_drv.sw_rotate = 1; - lv_disp_drv_register(&g_lv.disp_drv); + MEMPROBE("pre-disp_reg"); + lv_disp_drv_register(&g_lv.disp_drv); + MEMPROBE("post-disp_reg"); #if defined(HAS_TANMATSU) // Apply the 270° software rotation now that the driver is registered: logical surface // becomes 800x480 and lv_disp_get_hor/ver_res report landscape for every layout query.