From 3819f3c5816d6155652e4790b4956b85c260d8d9 Mon Sep 17 00:00:00 2001 From: Elliot Matson Date: Sun, 4 Oct 2026 16:53:58 -0500 Subject: [PATCH] Wait for data when parsing JSON from a TLS stream NetworkClientSecure::read(buf, n) returns -1 whenever no decrypted data is ready yet, and NetworkClient::readBytes() treats that as an error and stops. ArduinoJson reads through readBytes(), so the GitHub release list (52 KB on the development channel) ended at the first pause and every check failed with IncompleteInput -- at boot, with telemetry's TLS connection opening alongside, on every boot. A cube on v0.6.0-b2's development channel could never see a newer release. WaitingStream exposes only the single-byte read(), so Stream's own readBytes(), which retries until the timeout, is used. The Weather pattern parsed the same way and gets the same wrapper. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 6 ++++ lib/cube/updates.cpp | 4 ++- lib/cube_utils/waiting_stream.h | 41 ++++++++++++++++++++++++ lib/patterns/weather/weather_pattern.cpp | 4 ++- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 lib/cube_utils/waiting_stream.h 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) {