-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Stream /json/effects via respondModeData(namesOnly=true), eliminating JSON buffer lock
#5519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5497f7e
b7c863a
ca7f8d4
028902e
c8087e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1213,23 +1213,32 @@ 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; 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 '@') | ||
| 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 = '['; | ||
|
Comment on lines
1227
to
1243
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Track the first emitted element, not Line 1229 intentionally skips empty mode descriptors, but Line 1243 only replaces the leading comma with 🐛 Proposed fix void respondModeData(AsyncWebServerRequest* request, bool namesOnly = false) {
size_t fx_index = 0;
+ bool arrayStarted = false;
+ bool wroteElement = false;
request->sendChunked(FPSTR(CONTENT_TYPE_JSON),
- [fx_index, namesOnly](uint8_t* data, size_t len, size_t) mutable {
+ [fx_index, namesOnly, arrayStarted, wroteElement](uint8_t* data, size_t len, size_t) mutable {
size_t bytes_written = 0;
char lineBuffer[256];
+
+ if (!arrayStarted) {
+ if (len == 0) return 0;
+ *data++ = '[';
+ --len;
+ ++bytes_written;
+ arrayStarted = true;
+ }
+
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)
char* dataPtr = strchr(lineBuffer,'@'); // Find '@', if there is one; non-const so namesOnly mode can truncate lineBuffer here
@@
} else {
value = dataPtr ? dataPtr + 1 : ""; // everything after '@' is the fx data
}
- size_t mode_bytes = writeJSONStringElement(data, len, value);
+ size_t mode_bytes = wroteElement ? writeJSONStringElement(data, len, value) : writeJSONString(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;
len -= mode_bytes;
bytes_written += mode_bytes;
+ wroteElement = true;
}
++fx_index;
}🤖 Prompt for AI Agents |
||
| data += mode_bytes; | ||
|
|
@@ -1276,7 +1285,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 +1294,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 +1321,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 +1335,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: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a larger token buffer to avoid corrupting valid whitespace-heavy JSON.
Line 93 only allows 7 bytes before the delimiter.
readBytesUntil()will split a longer JSON token, so indentation/whitespace before-1,0, or1can be parsed as an extra0entry and shift the rest of the gap table.🐛 Proposed fix
🤖 Prompt for AI Agents