From 5497f7e7e99253d0b0de884cf7723a21861535ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:00:48 +0000 Subject: [PATCH 1/5] Stream /json/effects via respondModeData(namesOnly=true), eliminating JSON buffer lock Agent-Logs-Url: https://github.com/wled/WLED/sessions/88471964-aa31-4148-9f41-fa2fbdc8da34 Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com> --- wled00/json.cpp | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 366c74e6d4..9add06095f 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1213,23 +1213,30 @@ static size_t writeJSONStringElement(uint8_t* dest, size_t maxLen, const char* s return 1 + n; } -// Generate a streamed JSON response for the mode data -// This uses sendChunked to send the reply in blocks based on how much fit in the outbound -// packet buffer, minimizing the required state (ie. just the next index to send). This -// allows us to send an arbitrarily large response without using any significant amount of -// memory (so no worries about buffer limits). -void respondModeData(AsyncWebServerRequest* request) { +// Generate a streamed JSON response for the mode data (namesOnly=false) or mode names +// (namesOnly=true). This uses sendChunked to send the reply in blocks based on how much +// fit in the outbound packet buffer, minimizing the required state (ie. just the next index +// to send). This allows us to send an arbitrarily large response without using any +// significant amount of memory (so no worries about buffer limits). +void respondModeData(AsyncWebServerRequest* request, bool namesOnly = false) { size_t fx_index = 0; request->sendChunked(FPSTR(CONTENT_TYPE_JSON), - [fx_index](uint8_t* data, size_t len, size_t) mutable { + [fx_index, namesOnly](uint8_t* data, size_t len, size_t) mutable { size_t bytes_written = 0; char lineBuffer[256]; while (fx_index < strip.getModeCount()) { strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)-1); // Copy to stack buffer for strchr if (lineBuffer[0] != 0) { lineBuffer[sizeof(lineBuffer)-1] = '\0'; // terminate string (only needed if strncpy filled the buffer) - const char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one - size_t mode_bytes = writeJSONStringElement(data, len, dataPtr ? dataPtr + 1 : ""); + char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one + const char* value; + if (namesOnly) { + if (dataPtr) *dataPtr = '\0'; // truncate at '@' to get name only + value = lineBuffer; + } else { + value = dataPtr ? dataPtr + 1 : ""; // everything after '@' is the fx data + } + size_t mode_bytes = writeJSONStringElement(data, len, value); if (mode_bytes == 0) break; // didn't fit; break loop and try again next packet if (fx_index == 0) *data = '['; data += mode_bytes; @@ -1276,7 +1283,7 @@ class LockedJsonResponse: public AsyncJsonResponse { void serveJson(AsyncWebServerRequest* request) { enum class json_target { - all, state, info, state_info, nodes, effects, palettes, networks, config, pins + all, state, info, state_info, nodes, palettes, networks, config, pins }; json_target subJson = json_target::all; @@ -1285,7 +1292,7 @@ void serveJson(AsyncWebServerRequest* request) else if (url.indexOf("info") > 0) subJson = json_target::info; else if (url.indexOf("si") > 0) subJson = json_target::state_info; else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes; - else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects; + else if (url.indexOf(F("eff")) > 0) { respondModeData(request, true); return; } else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; else if (url.indexOf(F("fxda")) > 0) { respondModeData(request); return; } else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; @@ -1312,7 +1319,7 @@ void serveJson(AsyncWebServerRequest* request) } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) // make sure you delete "response" if no "request->send(response);" is made - LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary + LockedJsonResponse *response = new LockedJsonResponse(pDoc, false); // will clear JsonDocument JsonVariant lDoc = response->getRoot(); @@ -1326,8 +1333,6 @@ void serveJson(AsyncWebServerRequest* request) serializeNodes(lDoc); break; case json_target::palettes: serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; - case json_target::effects: - serializeModeNames(lDoc); break; case json_target::networks: serializeNetworks(lDoc); break; case json_target::config: From b7c863a3c0cb52e029fdde620ad7e6f59496c85a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:04:05 +0000 Subject: [PATCH 2/5] Add clarifying comments per code review feedback Agent-Logs-Url: https://github.com/wled/WLED/sessions/88471964-aa31-4148-9f41-fa2fbdc8da34 Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com> --- wled00/json.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 9add06095f..2119dc32da 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1228,8 +1228,10 @@ void respondModeData(AsyncWebServerRequest* request, bool namesOnly = false) { strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)-1); // Copy to stack buffer for strchr if (lineBuffer[0] != 0) { lineBuffer[sizeof(lineBuffer)-1] = '\0'; // terminate string (only needed if strncpy filled the buffer) - char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one + char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one; non-const to allow truncation const char* value; + // namesOnly=true → emit the display name (everything before '@') + // namesOnly=false → emit the fx-data string (everything after '@') if (namesOnly) { if (dataPtr) *dataPtr = '\0'; // truncate at '@' to get name only value = lineBuffer; From ca7f8d463a9224e8a826f9860306a98cf6817ea1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:15:06 +0000 Subject: [PATCH 3/5] Chunk-process 2d-gaps.json in setUpMatrix(), eliminating JSON buffer lock Read gap values directly from file one number at a time (like deserializeMap() already does for ledmap.json), removing the need for requestJSONBufferLock() / pDoc / readObjectFromFile(). Saves ~132B flash and frees the JSON buffer for concurrent use during matrix setup. Agent-Logs-Url: https://github.com/wled/WLED/sessions/fb4e360b-89a2-4297-83dc-7a0da2f9fc96 Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com> --- wled00/FX_2Dfcn.cpp | 37 ++++++++++++++++++++++--------------- wled00/const.h | 2 +- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index a0cc3c4461..3403eaba60 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -71,29 +71,36 @@ void WS2812FX::setUpMatrix() { // allowed values are: -1 (missing pixel/no LED attached), 0 (inactive/unused pixel), 1 (active/used pixel) char fileName[32]; strcpy_P(fileName, PSTR("/2d-gaps.json")); bool isFile = WLED_FS.exists(fileName); - size_t gapSize = 0; int8_t *gapTable = nullptr; - if (isFile && requestJSONBufferLock(JSON_LOCK_LEDGAP)) { + if (isFile) { DEBUG_PRINT(F("Reading LED gap from ")); DEBUG_PRINTLN(fileName); - // read the array into global JSON buffer - if (readObjectFromFile(fileName, nullptr, pDoc)) { - // the array is similar to ledmap, except it has only 3 values: - // -1 ... missing pixel (do not increase pixel count) - // 0 ... inactive pixel (it does count, but should be mapped out (-1)) - // 1 ... active pixel (it will count and will be mapped) - JsonArray map = pDoc->as(); - gapSize = map.size(); - if (!map.isNull() && gapSize >= matrixSize) { // not an empty map - gapTable = static_cast(p_malloc(gapSize)); - if (gapTable) for (size_t i = 0; i < gapSize; i++) { - gapTable[i] = constrain(map[i], -1, 1); + gapTable = static_cast(p_malloc(matrixSize)); + if (gapTable) { + // read entries directly from the file, one number at a time + // (no JSON buffer / pDoc needed — the file is a plain JSON array) + size_t gapIdx = 0; + File f = WLED_FS.open(fileName, "r"); + if (f) { + f.find('['); // skip to start of array + while (f.available() && gapIdx < matrixSize) { + char number[8]; + size_t numRead = f.readBytesUntil(',', number, sizeof(number) - 1); + number[numRead] = 0; + if (numRead > 0) { + if (strchr(number, ']') && !strchr(number, '-') && !isdigit((unsigned char)number[0])) break; // end of array without a number + gapTable[gapIdx++] = constrain(atoi(number), -1, 1); + } else break; } + f.close(); + } + if (gapIdx < matrixSize) { // file was too short or could not be read — discard + p_free(gapTable); + gapTable = nullptr; } } DEBUG_PRINTLN(F("Gaps loaded.")); - releaseJSONBufferLock(); } unsigned x, y, pix=0; //pixel diff --git a/wled00/const.h b/wled00/const.h index e49dd2900a..e6601547ab 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -478,7 +478,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define JSON_LOCK_SERVEJSON 17 #define JSON_LOCK_NOTIFY 18 #define JSON_LOCK_PRESET_NAME 19 -#define JSON_LOCK_LEDGAP 20 +// JSON_LOCK 20 formerly used for LEDGAP (now parsed without JSON buffer) #define JSON_LOCK_LEDMAP_ENUM 21 #define JSON_LOCK_REMOTE 22 From 028902e596f4c81157afb032b46fc7a8f5931380 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:19:18 +0000 Subject: [PATCH 4/5] Improve 2d-gaps.json end-of-array handling and align with deserializeMap() pattern Agent-Logs-Url: https://github.com/wled/WLED/sessions/fb4e360b-89a2-4297-83dc-7a0da2f9fc96 Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com> --- wled00/FX_2Dfcn.cpp | 14 ++++++++++++-- wled00/json.cpp | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 3403eaba60..a865626964 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -80,16 +80,26 @@ void WS2812FX::setUpMatrix() { if (gapTable) { // read entries directly from the file, one number at a time // (no JSON buffer / pDoc needed — the file is a plain JSON array) + // follows the same parsing pattern used by deserializeMap() for ledmap.json size_t gapIdx = 0; File f = WLED_FS.open(fileName, "r"); if (f) { f.find('['); // skip to start of array while (f.available() && gapIdx < matrixSize) { char number[8]; - size_t numRead = f.readBytesUntil(',', number, sizeof(number) - 1); + size_t numRead = f.readBytesUntil(',', number, sizeof(number) - 1); // last entry reads up to ']' (no trailing comma) number[numRead] = 0; if (numRead > 0) { - if (strchr(number, ']') && !strchr(number, '-') && !isdigit((unsigned char)number[0])) break; // end of array without a number + char *end = strchr(number, ']'); // check for end-of-array marker + bool foundDigit = (end == nullptr); // no ']' means the whole token is a number + if (end != nullptr) { + // ']' present — only accept if a digit (or '-') appears before it + for (int k = 0; &number[k] != end; k++) { + if (number[k] >= '0' && number[k] <= '9') { foundDigit = true; break; } + if (number[k] == '-') { foundDigit = true; break; } + } + } + if (!foundDigit) break; // ']' with no number — array ended gapTable[gapIdx++] = constrain(atoi(number), -1, 1); } else break; } diff --git a/wled00/json.cpp b/wled00/json.cpp index 2119dc32da..5b97c729f1 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1228,7 +1228,7 @@ void respondModeData(AsyncWebServerRequest* request, bool namesOnly = false) { strncpy_P(lineBuffer, strip.getModeData(fx_index), sizeof(lineBuffer)-1); // Copy to stack buffer for strchr if (lineBuffer[0] != 0) { lineBuffer[sizeof(lineBuffer)-1] = '\0'; // terminate string (only needed if strncpy filled the buffer) - char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one; non-const to allow truncation + char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one; non-const so namesOnly mode can truncate lineBuffer here const char* value; // namesOnly=true → emit the display name (everything before '@') // namesOnly=false → emit the fx-data string (everything after '@') From c8087e0fbb961a5149f74c04e6c280716b6e1fe6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:24:57 +0000 Subject: [PATCH 5/5] Restore gap-value format description comment (lines 81-84) Agent-Logs-Url: https://github.com/wled/WLED/sessions/5bf52c85-712d-468e-bc45-42f46ff2f14e Co-authored-by: softhack007 <91616163+softhack007@users.noreply.github.com> --- wled00/FX_2Dfcn.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index a865626964..34b9436710 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -78,6 +78,10 @@ void WS2812FX::setUpMatrix() { DEBUG_PRINTLN(fileName); gapTable = static_cast(p_malloc(matrixSize)); if (gapTable) { + // the array is similar to ledmap, except it has only 3 values: + // -1 ... missing pixel (do not increase pixel count) + // 0 ... inactive pixel (it does count, but should be mapped out (-1)) + // 1 ... active pixel (it will count and will be mapped) // read entries directly from the file, one number at a time // (no JSON buffer / pDoc needed — the file is a plain JSON array) // follows the same parsing pattern used by deserializeMap() for ledmap.json