From a86a644adcc270e7b161b9d16eae2cf0388654d9 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Tue, 14 Jul 2026 14:53:41 -0400 Subject: [PATCH] Reduce TLS memory requirements for KOReader sync Split heap gate into separate free-memory (50KB) and largest-block (20KB) thresholds based on field measurements. Enable wolfSSL single-precision ECC to use fixed 256-bit arrays instead of heap-allocated fast-math bignums, reducing TLS handshake memory footprint. Reclaim ~7KB by right-sizing ESP timer task stacks and move WiFi code out of IRAM to free ~25-30KB for heap. Add memory audit landmarks for font and EPUB allocations. --- lib/EpdFont/FontDecompressor.cpp | 5 ++ lib/EpdFont/SdCardFont.h | 1 + lib/EpdFont/SdCardFontManager.h | 1 + lib/Epub/Epub.h | 1 + lib/Epub/Epub/Section.h | 1 + lib/Epub/Epub/css/CssParser.h | 1 + lib/KOReaderSync/KOReaderSyncClient.cpp | 22 ++++- platformio.ini | 24 +++++- scripts/patch_wolfssl.py | 6 +- src/HeapMap.cpp | 85 ++++++++++++++++++++ src/HeapMap.h | 10 +++ src/SdCardFontSystem.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 75 ++--------------- src/main.cpp | 24 ++++++ src/network/CrossPointWebServer.cpp | 17 ++++ src/network/CrossPointWebServer.h | 1 + 16 files changed, 200 insertions(+), 75 deletions(-) create mode 100644 src/HeapMap.cpp create mode 100644 src/HeapMap.h diff --git a/lib/EpdFont/FontDecompressor.cpp b/lib/EpdFont/FontDecompressor.cpp index 786f7d3e..8b87814f 100644 --- a/lib/EpdFont/FontDecompressor.cpp +++ b/lib/EpdFont/FontDecompressor.cpp @@ -369,6 +369,11 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8 } stats.pageBufferBytes += totalBytes; stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry); + // MEMFIX-PORT: page-slot address landmark for the heap map; portable + // Landmark for the heap block map: page slots are the largest flash-font + // allocations and otherwise show up as anonymous ~4-20 KB used blocks. + LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast(slot.buffer), (unsigned)totalBytes, + (unsigned)glyphCount); slot.fontData = fontData; slot.glyphCount = glyphCount; diff --git a/lib/EpdFont/SdCardFont.h b/lib/EpdFont/SdCardFont.h index 68d2d960..c43ec3b6 100644 --- a/lib/EpdFont/SdCardFont.h +++ b/lib/EpdFont/SdCardFont.h @@ -104,6 +104,7 @@ class SdCardFont { uint32_t uniqueGlyphs = 0; uint32_t bitmapBytes = 0; }; + // MEMFIX-PORT: SD font resident-bytes audit; portable // Log per-style resident heap (full tables + kept-if-fits mini arenas + // advance tables + overflow bitmaps) and return the total in bytes. Pure // accounting — no allocation, no state change. diff --git a/lib/EpdFont/SdCardFontManager.h b/lib/EpdFont/SdCardFontManager.h index ce186efe..9ccde150 100644 --- a/lib/EpdFont/SdCardFontManager.h +++ b/lib/EpdFont/SdCardFontManager.h @@ -32,6 +32,7 @@ class SdCardFontManager { // Get name of currently loaded family (empty if none). const std::string& currentFamilyName() const { return loadedFamilyName_; }; + // MEMFIX-PORT: font manager audit passthrough; portable // Sum of loaded fonts' resident heap (see SdCardFont::reportMemory). size_t reportMemory() const; diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index b3621c09..abce956e 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -44,6 +44,7 @@ class Epub { } ~Epub() = default; std::string& getBasePath() { return contentBasePath; } + // MEMFIX-PORT: epub resident-bytes audit accessor; portable // Approximate resident heap of the open book (audit): path strings, the CSS // file list, and the parsed stylesheet. BookMetadataCache is file-backed // (counts + HalFile handles) and contributes little. diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 77ee12e1..5151726a 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -132,6 +132,7 @@ class Section { // (covers finalized sections and partials from a previous session). std::optional findAnchor(const std::string& anchor) const; + // MEMFIX-PORT: section resident-bytes audit accessor; portable // Approximate resident heap for the audit log. Steady state (no build) a // Section holds little beyond itself; during a build the page LUT and path // strings dominate (the parser's internal footprint is not walked here). diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index 3fe74f04..1a77ca0c 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -67,6 +67,7 @@ class CssParser { */ [[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue); + // MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable // Approximate resident heap of the parsed stylesheet, for the audit log. // unordered_map cost model: bucket array + one node per rule (libstdc++ node // overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO. diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index d8395366..2c20054e 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -22,7 +22,21 @@ constexpr char DEVICE_ID[] = "crosspoint-reader"; // footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative // floor. Check both total free heap and largest contiguous block so fragmented // heap does not fall through into a failed TLS allocation path. -constexpr uint32_t MIN_HEAP_FOR_TLS = 55000; +// MEMFIX-PORT: TLS heap gate; portable +// Field data (July 2026): launching sync from a reader session lands at +// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles +// allocation failure by returning MEMORY_E (no abort under -fno-exceptions), +// so an optimistic attempt degrades to the same clean "sync failed" as the +// gate — the gate only needs to keep out states where a doomed handshake +// would waste tens of seconds, not guarantee success. +// +// Free and largest-block have separate requirements: with SP ECC +// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so +// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not +// a run of fast-math bignums. A handshake was measured succeeding inside a +// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit. +constexpr uint32_t MIN_FREE_FOR_TLS = 50000; +constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000; // Apply the shared KOSync auth headers after begin(). x-auth-* is the native // KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility. @@ -39,9 +53,9 @@ void applyAuthHeaders(freeink::SecureHttpClient& http) { bool insufficientHeap() { const uint32_t freeHeap = ESP.getFreeHeap(); const uint32_t maxAllocHeap = ESP.getMaxAllocHeap(); - if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) { - LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap, - maxAllocHeap, MIN_HEAP_FOR_TLS); + if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) { + LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap, + MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS); return true; } return false; diff --git a/platformio.ini b/platformio.ini index 89c7ed86..55f89d6e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,14 @@ build_flags = -DWOLFSSL_OPTIONS_H -DWOLFSSL_CLIENT_EXAMPLE -DWOLFSSL_TLS13 - -DWOLFSSL_SP_RISCV32 +# MEMFIX-PORT: single-precision ECC (sp_c32.c). Without it every P-256 operation +# (TLS 1.3 key_share keygen, ECDHE, ECDSA cert verify) runs on fast-math bignums +# that WOLFSSL_SMALL_STACK heap-allocates at FP_MAX_BITS size -- tens of KB of +# temporaries, which OOMs (MP_MEM) at the ~50KB free heap a reading session +# leaves. SP uses fixed 256-bit arrays: a few KB, and several times faster. +# SP_SMALL trades the large precomputed point tables for smaller flash. + -DWOLFSSL_HAVE_SP_ECC + -DWOLFSSL_SP_SMALL -DHAVE_TLS_EXTENSIONS -DHAVE_SUPPORTED_CURVES -DHAVE_HKDF @@ -99,6 +106,21 @@ custom_sdkconfig = CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30 + ; MEMFIX-PORT: task stack right-sizing (~7 KB); NOTE develop has no + ; custom_sdkconfig block — port via sdkconfig.defaults or equivalent. + ; Task stack right-sizing from measured high-water marks (heap block map + + ; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across + ; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB + ; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap. + CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096 + CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560 + ; Move the WiFi stack's non-critical hot paths out of IRAM into flash. + ; On the C3, IRAM and DRAM share one SRAM pool, so the ~25-30 KB this + ; frees lands directly in the heap — paid for with lower WiFi throughput + ; during transfers (occasional sync/OTA use, not streaming: acceptable). + ; IRAM cost is static, so the heap gain applies even with WiFi off. + CONFIG_ESP_WIFI_IRAM_OPT=n + CONFIG_ESP_WIFI_RX_IRAM_OPT=n ; Keep the Arduino wrappers for the removed cloud components (below) out of ; the core source list; all other bundled libraries default to enabled. CONFIG_ARDUINO_SELECTIVE_COMPILATION=y diff --git a/scripts/patch_wolfssl.py b/scripts/patch_wolfssl.py index 95b86964..b905e879 100644 --- a/scripts/patch_wolfssl.py +++ b/scripts/patch_wolfssl.py @@ -12,8 +12,12 @@ OVERRIDES = f""" #ifndef HAVE_FFDHE_2048 #define HAVE_FFDHE_2048 #endif +/* MEMFIX-PORT: 8192 handles up to RSA-4096 keys (the public-CA maximum, + ISRG Root X1 included) with half the per-bignum heap of 16384: with + WOLFSSL_SMALL_STACK each fast-math temp is FP_MAX_BITS/8 * 2 bytes on the + heap, and TLS cert verification allocates dozens at once. */ #undef FP_MAX_BITS -#define FP_MAX_BITS 16384 +#define FP_MAX_BITS 8192 """ diff --git a/src/HeapMap.cpp b/src/HeapMap.cpp new file mode 100644 index 00000000..d89dbf42 --- /dev/null +++ b/src/HeapMap.cpp @@ -0,0 +1,85 @@ +#include "HeapMap.h" + +#include +#include +#include +#include + +#include + +#include "rom/ets_sys.h" + +// heap_caps_dump walks each heap inside a critical section (interrupts +// masked), so its output can neither go through the UART-0 ROM path we can't +// see nor be flow-controlled toward the CDC (the CDC ring drains in an +// interrupt handler — waiting deadlocks into the interrupt WDT, +// field-verified). Instead the ROM putc parses each line into a compact +// record; the captured table is logged after the dump with interrupts live. +namespace heapmap { +namespace { +struct BlockRec { + uint32_t addr; + uint32_t size; + bool free; +}; +constexpr uint16_t kMaxRecs = 1400; +BlockRec* g_recs = nullptr; // borrowed buffer, valid only during capture +uint16_t g_recCount = 0; +bool g_overflowed = false; +char g_line[96]; +uint8_t g_lineLen = 0; + +void captInterpolatePutc(char c) { + if (c != '\n') { + if (g_lineLen < sizeof(g_line) - 1) g_line[g_lineLen++] = c; + return; + } + g_line[g_lineLen] = '\0'; + g_lineLen = 0; + // e.g. "Block 0x3fcc69bc data, size: 89424 bytes, Free: Yes" + unsigned addr = 0, size = 0; + char freeWord[4] = {0}; + if (sscanf(g_line, "Block 0x%x data, size: %u bytes, Free: %3s", &addr, &size, freeWord) == 3) { + if (g_recs && g_recCount < kMaxRecs) { + g_recs[g_recCount++] = {addr, size, freeWord[0] == 'Y'}; + } else { + g_overflowed = true; + } + } +} +} // namespace + +void dump() { + auto recBuf = makeUniqueNoThrow(kMaxRecs); + if (!recBuf) { + LOG_ERR("MEM", "heap map skipped: no room for capture buffer"); + return; + } + g_recs = recBuf.get(); + g_recCount = 0; + g_overflowed = false; + g_lineLen = 0; + // Capture (interrupts masked inside the dump): parse into records, never + // wait. Log the table afterward with the system live. NOTE: the capture + // buffer itself appears in the map as a used block of ~17.4 KB — it frees + // on return (observer effect, do not chase it as a leak/splitter). + ets_install_putc1(&captInterpolatePutc); + heap_caps_dump(MALLOC_CAP_8BIT); + ets_install_uart_printf(); + g_recs = nullptr; + + LOG_DBG("MEM", "---- heap block map: %u blocks%s ----", g_recCount, g_overflowed ? " (TRUNCATED)" : ""); + uint32_t dustCount = 0, dustBytes = 0; + for (uint16_t i = 0; i < g_recCount; ++i) { + const auto& r = recBuf[i]; + if (r.free || r.size >= 256) { + LOG_DBG("MEM", "%s 0x%08x %u", r.free ? "FREE" : "used", r.addr, r.size); + } else { + dustCount++; + dustBytes += r.size; + } + } + LOG_DBG("MEM", "dust: %u used blocks < 256B totaling %u bytes", dustCount, dustBytes); + LOG_DBG("MEM", "---- end heap block map ----"); +} +} // namespace heapmap diff --git a/src/HeapMap.h b/src/HeapMap.h new file mode 100644 index 00000000..3e41b617 --- /dev/null +++ b/src/HeapMap.h @@ -0,0 +1,10 @@ +#pragma once + +// MEMFIX-PORT: heap block map (on-demand via CMD:MEMMAP + reader one-shot); portable, no BLE dependency +namespace heapmap { +// Capture-and-log the DRAM heap block map (address/size/free per block, +// sub-256B used blocks rolled up as "dust"). Safe to call from the main loop; +// ~60-100 LOG_DBG lines. See HeapMap.cpp for why capture-then-log is the only +// shape that works (heap_caps_dump runs with interrupts masked). +void dump(); +} // namespace heapmap diff --git a/src/SdCardFontSystem.h b/src/SdCardFontSystem.h index 59acbefd..d96d1752 100644 --- a/src/SdCardFontSystem.h +++ b/src/SdCardFontSystem.h @@ -32,6 +32,7 @@ class SdCardFontSystem { /// Non-const access to the registry (for FontInstaller). SdCardFontRegistry& registry() { return registry_; } + // MEMFIX-PORT: font system audit passthrough; portable /// Resident heap held by loaded SD fonts (audit; see SdCardFont::reportMemory). size_t reportFontMemory() const { return manager_.reportMemory(); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 68b24165..511c3155 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -41,6 +41,7 @@ #include "SdCardFontSystem.h" #include #include "rom/ets_sys.h" +#include "HeapMap.h" namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() @@ -70,45 +71,6 @@ bool isInReadFolder(const std::string& path) { return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/'; } -// Heap block map capture. heap_caps_dump walks each heap inside a critical -// section (interrupts masked), so its output can neither go through the -// UART-0 ROM path we can't see nor be flow-controlled toward the CDC (the CDC -// ring drains in an interrupt handler — waiting deadlocks into the interrupt -// WDT, field-verified). Instead the ROM putc parses each line into a compact -// record; the captured table is logged after the dump with interrupts live. -namespace heapmap { -struct BlockRec { - uint32_t addr; - uint32_t size; - bool free; -}; -constexpr uint16_t kMaxRecs = 1400; -BlockRec* recs = nullptr; // borrowed buffer, valid only during capture -uint16_t recCount = 0; -bool overflowed = false; -char line[96]; -uint8_t lineLen = 0; - -void putc(char c) { - if (c != '\n') { - if (lineLen < sizeof(line) - 1) line[lineLen++] = c; - return; - } - line[lineLen] = '\0'; - lineLen = 0; - // e.g. "Block 0x3fcc69bc data, size: 89424 bytes, Free: Yes" - unsigned addr = 0, size = 0; - char freeWord[4] = {0}; - if (sscanf(line, "Block 0x%x data, size: %u bytes, Free: %3s", &addr, &size, freeWord) == 3) { - if (recs && recCount < kMaxRecs) { - recs[recCount++] = {addr, size, freeWord[0] == 'Y'}; - } else { - overflowed = true; - } - } -} -} // namespace heapmap - class FrameBufferBuildLoan { public: explicit FrameBufferBuildLoan(GfxRenderer& renderer) : renderer_(renderer) {} @@ -1545,6 +1507,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { // preceding lines show (mini rebuilds, kern reloads, BLE churn). LOG_DBG("MEM", "post-render: free=%u maxAlloc=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap()); { + // MEMFIX-PORT: per-render heap owner audit + one-shot block map; portable // Heap audit: attribute resident heap to its owners so margin work is // measurement-driven. `other` = IDF/Arduino baseline + SdFat + epub // metadata + BLE (when on) + anything not yet instrumented. @@ -1568,36 +1531,10 @@ void EpubReaderActivity::render(RenderLock&& lock) { static bool heapMapDumped = false; if (!heapMapDumped && !(section && section->isBuilding())) { heapMapDumped = true; - auto recBuf = makeUniqueNoThrow(heapmap::kMaxRecs); - if (recBuf) { - heapmap::recs = recBuf.get(); - heapmap::recCount = 0; - heapmap::overflowed = false; - heapmap::lineLen = 0; - // Capture (interrupts masked inside the dump): parse into records, - // never wait. Log the table afterward with the system live. - ets_install_putc1(&heapmap::putc); - heap_caps_dump(MALLOC_CAP_8BIT); - ets_install_uart_printf(); - heapmap::recs = nullptr; - - LOG_DBG("MEM", "---- heap block map: %u blocks%s ----", heapmap::recCount, - heapmap::overflowed ? " (TRUNCATED)" : ""); - uint32_t dustCount = 0, dustBytes = 0; - for (uint16_t i = 0; i < heapmap::recCount; ++i) { - const auto& r = recBuf[i]; - if (r.free || r.size >= 256) { - LOG_DBG("MEM", "%s 0x%08x %u", r.free ? "FREE" : "used", r.addr, r.size); - } else { - dustCount++; - dustBytes += r.size; - } - } - LOG_DBG("MEM", "dust: %u used blocks < 256B totaling %u bytes", dustCount, dustBytes); - LOG_DBG("MEM", "---- end heap block map ----"); - } else { - LOG_ERR("MEM", "heap map skipped: no room for capture buffer"); - } + // Landmarks: known owners' addresses, so map blocks self-identify. + LOG_DBG("MEM", "landmark framebuffer=%p section=%p epub=%p activity=%p", renderer.getFrameBuffer(), + static_cast(section.get()), static_cast(epub.get()), static_cast(this)); + heapmap::dump(); } } } diff --git a/src/main.cpp b/src/main.cpp index 2ff9ac2f..09a60c35 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,8 @@ #include "images/LoadingIcon.h" #include "util/ButtonNavigator.h" #include "util/ScreenshotUtil.h" +#include +#include "HeapMap.h" GfxRenderer renderer(display); MappedInputManager mappedInputManager(gpio, renderer); @@ -596,6 +598,23 @@ void loop() { LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); lastMemPrint = millis(); + // MEMFIX-PORT: per-task stack high-water audit; portable + // Task stack audit (~once/min): name, stack base (matches blocks in the + // heap map), and high-water free bytes — the margin available for + // right-sizing each stack. TRACE_FACILITY is on in this sdkconfig. + static uint8_t memPrintCount = 0; + if (++memPrintCount >= 6) { + memPrintCount = 0; + const UBaseType_t taskCount = uxTaskGetNumberOfTasks(); + auto taskStatus = makeUniqueNoThrow(taskCount + 2); + if (taskStatus) { + const UBaseType_t got = uxTaskGetSystemState(taskStatus.get(), taskCount + 2, nullptr); + for (UBaseType_t i = 0; i < got; ++i) { + LOG_DBG("MEM", "task %-20s stackBase=%p highWaterFree=%u", taskStatus[i].pcTaskName, + taskStatus[i].pxStackBase, (unsigned)taskStatus[i].usStackHighWaterMark); + } + } + } } // Handle incoming serial commands, @@ -611,6 +630,11 @@ void loop() { uint8_t* buf = display.getFrameBuffer(); logSerial.write(buf, bufferSize); logSerial.printf("SCREENSHOT_END\n"); + // MEMFIX-PORT: on-demand heap map serial command; portable + } else if (cmd == "MEMMAP") { + // On-demand heap block map: capture the heap exactly when it looks + // interesting (e.g. maxAlloc degraded mid-session) without a reboot. + heapmap::dump(); } } } diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 2433a520..6dcbb1d0 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -201,6 +201,14 @@ void CrossPointWebServer::begin() { udpActive = udp.begin(LOCAL_UDP_PORT); LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT); + // All request handlers run on the task that calls handleClient(). Register + // that task before any handler can call esp_task_wdt_reset(). + const esp_err_t watchdogResult = esp_task_wdt_add(nullptr); + watchdogTaskRegistered = watchdogResult == ESP_OK; + if (!watchdogTaskRegistered) { + LOG_ERR("WEB", "Failed to register web server task with watchdog: %s", esp_err_to_name(watchdogResult)); + } + running = true; LOG_DBG("WEB", "Web server started on port %d", port); @@ -230,6 +238,10 @@ void CrossPointWebServer::abortWsUpload(const char* tag) { void CrossPointWebServer::stop() { if (!running || !server) { LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get()); + if (watchdogTaskRegistered) { + esp_task_wdt_delete(nullptr); + watchdogTaskRegistered = false; + } return; } @@ -270,6 +282,11 @@ void CrossPointWebServer::stop() { LOG_DBG("WEB", "Web server stopped and deleted"); LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap()); + if (watchdogTaskRegistered) { + esp_task_wdt_delete(nullptr); + watchdogTaskRegistered = false; + } + // Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared // later in the file and will be cleared when they go out of scope or on next upload LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap()); diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index fc1aaf6e..5d5e0048 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -72,6 +72,7 @@ class CrossPointWebServer { std::unique_ptr server = nullptr; std::unique_ptr wsServer = nullptr; bool running = false; + bool watchdogTaskRegistered = false; bool apMode = false; // true when running in AP mode, false for STA mode uint16_t port = 80; uint16_t wsPort = 81; // WebSocket port