From 34c499bd496e52a7fd66a9ea4268df1822d6d45d Mon Sep 17 00:00:00 2001 From: Stefan Ostermann Date: Sat, 8 Nov 2025 23:08:49 +0100 Subject: [PATCH] [ai] tcp optimizations --- platformio.ini | 6 +- src/main.cpp | 206 ++++++++++++++++--------------------------------- 2 files changed, 69 insertions(+), 143 deletions(-) diff --git a/platformio.ini b/platformio.ini index 5273a75..e806882 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,14 +13,14 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ board = wemos_d1_mini32 framework = arduino lib_deps = - ESP32Async/ESPAsyncWebServer@3.7.10 + ESP32Async/ESPAsyncWebServer@3.8.1 alanswx/ESPAsyncWiFiManager@0.31 miguelbalboa/MFRC522@^1.4.12 monitor_speed = 115200 build_flags = -Os ; Optimize for size ; -DDEBUG ; Hannabox Debugging -; -DCORE_DEBUG_LEVEL=0 ; Disable all debug output + -DCORE_DEBUG_LEVEL=0 ; Disable all debug output ; -DARDUINO_LOOP_STACK_SIZE=4096 ; Balanced to avoid stack canary without starving heap ; -DWIFI_TASK_STACK_SIZE=3072 ; Reduce WiFi task stack ; -DARDUINO_EVENT_TASK_STACK_SIZE=2048 ; Reduce event task stack @@ -29,7 +29,7 @@ build_flags = ; -DCONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 ; -DCONFIG_ASYNC_TCP_PRIORITY=10 ; (keep default) ; -DCONFIG_ASYNC_TCP_QUEUE_SIZE=64 ; (keep default) -; -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 ; force async_tcp task to be on same core as Arduino app (default is any core) + -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 ; force async_tcp task to be on same core as Arduino app (default is any core) -DCONFIG_ASYNC_TCP_STACK_SIZE=4096 ; reduce AsyncTCP task stack (default can be large) monitor_filters = esp32_exception_decoder board_build.partitions = huge_app.csv diff --git a/src/main.cpp b/src/main.cpp index fdb972a..8ab8a60 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -958,6 +958,29 @@ static void streamStateJSON(Print &out) out.print(F("}")); } +struct ChunkedSkipBufferPrint : public Print { + uint8_t* out; + size_t maxLen; + size_t pos; + size_t skip; + size_t seen; + ChunkedSkipBufferPrint(uint8_t* o, size_t m, size_t s) : out(o), maxLen(m), pos(0), skip(s), seen(0) {} + virtual size_t write(uint8_t c) { + seen++; + if (skip > 0) { skip--; return 1; } + if (pos < maxLen) { out[pos++] = c; return 1; } + // buffer full - keep counting to know total size + return 1; + } + virtual size_t write(const uint8_t* buffer, size_t size) { + size_t n = 0; + while (n < size) { if (write(buffer[n]) != 1) break; n++; } + return n; + } + size_t bytesWritten() const { return pos; } + size_t totalProduced() const { return seen; } +}; + void stop() { if (audio.isRunning()) @@ -1257,13 +1280,14 @@ static void serveStaticFile(AsyncWebServerRequest *request, } } + + + void init_webserver() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - serveStaticFile(request, PATH_INDEX, PATH_INDEX_GZ, txt_html_charset, hdr_cache_control_val, F("ERROR: /system/index.html(.gz) not found!"), true); - - + serveStaticFile(request, PATH_INDEX, PATH_INDEX_GZ, txt_html_charset, hdr_cache_control_val, F("ERROR: /system/index.html(.gz) not found!"), true); }); server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) @@ -1276,158 +1300,60 @@ void init_webserver() { serveStaticFile(request, PATH_SCRIPT, PATH_SCRIPT_GZ, "application/javascript", "public, max-age=300", F("ERROR: /system/script.js(.gz) not found!"), true); }); + + // Dynamic endpoints to avoid template processing heap spikes server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request) { webreq_enter(); - // Backpressure-safe, chunked HTML streaming to avoid cbuf growth/OOM - struct DirectoryHtmlStreamState { - struct Frame { - const DirectoryNode* node; - size_t fileIdx; - size_t childIdx; - bool headerDone; - }; - - Frame stack[MAX_DEPTH]; - int top; - bool openedUL; - bool closedUL; - explicit DirectoryHtmlStreamState(const DirectoryNode* root) - : top(-1), openedUL(false), closedUL(false) { - push(root); - } - inline void push(const DirectoryNode* n) { - if (top + 1 < (int)MAX_DEPTH) { - ++top; - stack[top] = { n, 0, 0, false }; - } else { - // Depth exceeded: stop descending further. Listing will be truncated but safe. - } - } - inline void pop() { if (top >= 0) --top; } - inline Frame& cur() { return stack[top]; } - - size_t next(uint8_t* out, size_t maxLen) { - char* p = (char*)out; - size_t remaining = maxLen; - - auto putLiteral = [&](const char* s) { - for (const char* q = s; *q && remaining; ++q) { *p++ = *q; --remaining; } - return remaining != 0; - }; - - auto putNumberLiOpen = [&](unsigned id) { - int n = snprintf(p, remaining, "
  • ", id); - if (n <= 0) return false; - if ((size_t)n > remaining) { p += remaining; remaining = 0; return false; } - p += n; remaining -= (size_t)n; return remaining != 0; - }; - - auto putNumberDirHeaderOpen = [&](unsigned id) { - int n = snprintf(p, remaining, "
  • ", id); - if (n <= 0) return false; - if ((size_t)n > remaining) { p += remaining; remaining = 0; return false; } - p += n; remaining -= (size_t)n; return remaining != 0; - }; - - auto putStrUnsafe = [&](const String& s) { - // Follow existing behavior: raw text (no escaping) - for (size_t i = 0; i < s.length() && remaining; ++i) { *p++ = s[i]; --remaining; } - return remaining != 0; - }; - - if (!openedUL) { - putLiteral("\n"); - closedUL = true; - } - - return maxLen - remaining; - } - }; - - struct StreamCtx { DirectoryHtmlStreamState* state; }; - auto* ctx = new StreamCtx{ new DirectoryHtmlStreamState(&rootNode) }; - auto resp = request->beginChunkedResponse( + request->onDisconnect([](){ webreq_exit(); }); +#ifdef DEBUG + Serial.printf("Serving /directory heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles()); +#endif + // True chunked response: re-generate output deterministically and skip 'index' bytes each call + AsyncWebServerResponse *response = request->beginChunkedResponse( txt_html_charset, - [ctx](uint8_t* buffer, size_t maxLen, size_t /*index*/) -> size_t { - // Generate next chunk; return 0 when done, and free state - size_t n = ctx->state ? ctx->state->next(buffer, maxLen) : 0; - if (n == 0 && ctx->state) { delete ctx->state; ctx->state = nullptr; } - return n; + [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + ChunkedSkipBufferPrint sink(buffer, maxLen, index); + // Generate HTML directly into the sink (no large intermediate buffers) + rootNode.streamDirectoryHTML(sink); + // finished? + if (index >= sink.totalProduced()) { + return 0; + } + return sink.bytesWritten(); } ); -#ifdef DEBUG - Serial.printf("Serving /directory (chunked) heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles()); -#endif - resp->addHeader(hdr_cache_control_key, hdr_cache_control_val); - resp->addHeader(hdr_connection_key, hdr_connection_val); - // Ensure cleanup after transfer completes or client aborts - request->onDisconnect([ctx](){ - if (ctx->state) { delete ctx->state; } - delete ctx; - webreq_exit(); - }); - request->send(resp); + // Optional headers: + response->addHeader(hdr_cache_control_key, hdr_cache_control_val); + response->addHeader(hdr_connection_key, hdr_connection_val); + request->send(response); }); server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request) { webreq_enter(); request->onDisconnect([](){ webreq_exit();}); - // Stream mapping to avoid building a large HTML String - AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset, buffer_size); #ifdef DEBUG Serial.printf("Serving /mapping heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); -#endif - stream->addHeader(hdr_cache_control_key, hdr_cache_control_val); - stream->addHeader(hdr_connection_key, hdr_connection_val); - streamMappingHTML(*stream); - request->send(stream); +#endif + // True chunked response using a deterministic generator with byte skipping based on 'index' + AsyncWebServerResponse *response = request->beginChunkedResponse( + txt_html_charset, + [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + ChunkedSkipBufferPrint sink(buffer, maxLen, index); + streamMappingHTML(sink); + // finished? + if (index >= sink.totalProduced()) { + return 0; + } + return sink.bytesWritten(); + } + ); + // Optional headers: + response->addHeader(hdr_cache_control_key, hdr_cache_control_val); + response->addHeader(hdr_connection_key, hdr_connection_val); + request->send(response); });