diff --git a/CLAUDE.md b/CLAUDE.md index 756ec34..9724b0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,6 +179,12 @@ scripts/capture_patterns.py --host cube.local ` (frames come from repository secrets), but anything compiled in can be read out of a release. The broker's ACL is what keeps the shared login to a cube's own topics; a new setting reachable over MQTT goes in `lib/remote` and `Cube::applyRemote`. +- **Parse JSON from the network through `WaitingStream`.** + `NetworkClientSecure` returns -1 from `read(buf, n)` whenever no data has + arrived yet, and `NetworkClient::readBytes()` takes that as the end, so + `deserializeJson(doc, http.getStream())` fails with `IncompleteInput` at + the first pause. That broke every development-channel update check in + v0.6.0-b2. Wrap the client: `WaitingStream body(http.getStream())`. - **Updates only go forwards.** The GitHub updater installs a release only if `firmware_image::isNewer()` says its tag is newer than `FW_VERSION`. GitHub's "latest" is the newest *stable* release, older than any beta. diff --git a/lib/cube/updates.cpp b/lib/cube/updates.cpp index 1954d96..fc102c7 100644 --- a/lib/cube/updates.cpp +++ b/lib/cube/updates.cpp @@ -4,6 +4,7 @@ #include #include #include "fonts.h" +#include "waiting_stream.h" // The cube's custom app descriptor, placed right after the standard one in @@ -780,7 +781,8 @@ bool Updates::findFirmwareRelease(String &tag, String &firmwareUrl) releaseFilter["assets"][0]["browser_download_url"] = true; JsonDocument doc(spiRamAllocator()); - DeserializationError err = deserializeJson(doc, http.getStream(), DeserializationOption::Filter(filter)); + WaitingStream body(http.getStream()); + DeserializationError err = deserializeJson(doc, body, DeserializationOption::Filter(filter)); http.end(); if (err) { diff --git a/lib/cube_utils/waiting_stream.h b/lib/cube_utils/waiting_stream.h new file mode 100644 index 0000000..d0e0f85 --- /dev/null +++ b/lib/cube_utils/waiting_stream.h @@ -0,0 +1,41 @@ +/** + * @file waiting_stream.h + * @brief A Stream view of a network client that waits for data instead of + * reporting the end of it. + */ +#ifndef WAITING_STREAM_H +#define WAITING_STREAM_H + +#include + +/** + * @brief Wraps a network client so a parser reading from it waits through + * gaps in the data. + * + * NetworkClientSecure::read(buf, size) returns -1 whenever no decrypted data + * is ready yet, and NetworkClient::readBytes() treats -1 as an error and + * stops. ArduinoJson reads through readBytes(), so the first pause in a + * download -- every time, on a busy network -- ended the input and the parse + * failed with IncompleteInput. This class overrides only the single-byte + * read(), so Stream's own readBytes() is used: it retries until the client's + * timeout passes with nothing arriving. + */ +class WaitingStream : public Stream +{ +public: + /** @param source the client to read from; its timeout is used. */ + explicit WaitingStream(Stream &source) : source(source) + { + setTimeout(source.getTimeout()); + } + + int available() override { return source.available(); } + int read() override { return source.read(); } + int peek() override { return source.peek(); } + size_t write(uint8_t) override { return 0; } + +private: + Stream &source; +}; + +#endif diff --git a/lib/patterns/weather/weather_pattern.cpp b/lib/patterns/weather/weather_pattern.cpp index 3544db4..23356c8 100644 --- a/lib/patterns/weather/weather_pattern.cpp +++ b/lib/patterns/weather/weather_pattern.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "waiting_stream.h" #include #include @@ -84,7 +85,8 @@ namespace http.end(); return false; } - const DeserializationError err = deserializeJson(doc, http.getStream(), DeserializationOption::Filter(filter)); + WaitingStream body(http.getStream()); + const DeserializationError err = deserializeJson(doc, body, DeserializationOption::Filter(filter)); http.end(); if (err) {