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
/rfid endpoint - Serve with chunked response #280
Conversation
src/Web.cpp
Outdated
} | ||
|
||
static String tagIdToJsonStr(const char *key) { | ||
StaticJsonDocument<512> doc; |
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.
Isn't 512 bytes a bit large for a single tag id?
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.
A single tag id looks like:
{ "id": "003108198106", "fileOrUrl": "/Die Schönsten Lieder Zum Einschlafen", "playMode": 3, "lastPlayPos": 1842837, "trackLastPlayed": 0 }
maxLen of "fileOrUrl" is MAX_FILEPATH_LENTGH = 256, size calculation with https://arduinojson.org/v6/assistant gives about 350 Bytes needed. Recommended is power of 2, so 512 Bytes is best size.
src/Web.cpp
Outdated
} | ||
static std::vector<String> nvsKeys {}; |
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.
This vector will never shrink, just something to be aware of. Though it's good to avoid frequent allocations...
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.
nvsKeys is declared as local variable and i assume it gets out of scope after function handleGetRFIDRequest() finished.
Am i wrong here?
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.
It's declared as static, meaning it lives as long as the entire program.
If you didn't add nvsKeys.clear() directly afterwards, I it should continue to grow with each call.
The upside is that push_back does not need to reserve more memory every time and instead can re-use the same heap space. The downside is that the heap space that is the capacity of the vector is never reduced or freed.
src/Web.cpp
Outdated
if (nvsIndex == 0) { | ||
// start, write first tag | ||
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str()); | ||
len += sprintf(((char *) buffer), "[%s", json.c_str()); |
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.
What if maxlen < json len? Can we handle that case? We should make sure to never write more bytes to buffer than allowed.
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.
Note: put the first tag also in the loop, then you won't forget the size check too
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.
Good catch!
I never saw a buffer smaller than 1500 Bytes so JSON length never exceeds.
But i will do an additional check here for code safety.
nvsKeys.clear(); | ||
// Dumps all RFID-keys from NVS into key array | ||
listNVSKeys("rfidTags", &nvsKeys, DumpNvsToArrayCallback); | ||
if (nvsKeys.size() == 0) { |
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.
Would not need this special handling for size == 0 if in the chunked response there was no special handling for the first tag.
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.
Writing the first tag there is no comma in JSON, it is starting with the second entry.
That's is the reason to handle the special case size==0 here and not even start a chunked response.
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.
Not starting the chunked response in the first place is probably a good idea.
src/Web.cpp
Outdated
} | ||
if (nvsIndex == nvsKeys.size()) { | ||
// finish | ||
len += sprintf(((char *) buffer + len), "]"); |
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.
Edge case: that single byte for ']' or ',' could be one byte more than maxLen.
Why not write '[' and ']' by themselves without sprintf?
src/Web.cpp
Outdated
@@ -457,6 +457,7 @@ void webserverStart(void) { | |||
|
|||
// RFID | |||
wServer.on("/rfid", HTTP_GET, handleGetRFIDRequest); | |||
wServer.addRewrite(new OneParamRewrite("/rfid/details", "/rfid?details=true")); |
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.
OneParamRewrite is usually for path parameters in the form of /path/{some_param} -> /path?some_param={some_param} because AsyncWebServer cannot handle path params natively.
Does this even work and do something? Even if it does, using the "normal" AsyncWebRewrite makes more sense: https://github.com/me-no-dev/ESPAsyncWebServer#param-rewrite-with-matching
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.
Yes it works fine, do you have a concrete improved code here?
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.
Didn't test it, but just replacing 'OneParamRewrite' with 'AsyncWebRewrite' should do the same thing.
src/Web.cpp
Outdated
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str()); | ||
len += sprintf(((char *) buffer), "[%s", json.c_str()); | ||
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str(), withDetails); | ||
if (json.length() > maxLen) { |
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.
You check that, but 4 lines later you're writing json.length() + 1 bytes, so you should also check for json.length() + 1 available space
src/Web.cpp
Outdated
nvsIndex++; | ||
} | ||
while (nvsIndex < nvsKeys.size()) { | ||
// write tags as long we have enough room | ||
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str()); | ||
json = tagIdToJsonStr(nvsKeys[nvsIndex].c_str(), withDetails); | ||
if ((len + json.length()) > maxLen) { |
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.
Same issue, check is for 1 byte too few because of the comma
src/Web.cpp
Outdated
nvsIndex++; | ||
} | ||
if (nvsIndex == nvsKeys.size()) { | ||
// finish | ||
len += sprintf(((char *) buffer + len), "]"); | ||
len += snprintf(((char *) buffer + len), maxLen, "]"); |
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.
That byte is written without a length check at all.
Regarding the buffer copying, it's still calling for memory issues IMHO.
Needs to be converted to
that would already prevent any invalid memory access when writing the buffer. If the string doesn't fit, the json would still be invalid though. |
endpoint /rfid/ids returns only tag ids buffer check
/rfid endpoint:
A static buffer of 8KB is currently used to list the RFID tags. If this buffer is full, the returned JSON is truncated and not all entries are displayed in the web interface. . This happens with me from approx. 60 entries (depends on path length). It could also lead to a low memory situation and, in the worst case, to a crash.
With this PR, a list is first created holding the keys only and later the details such as path and game mode are sent as a chunked response with a smaller buffer.
No changes in the delivered JSON except that it is always complete