From d89f3eb8fc70ddb3ce63207d9a89097cef6edf16 Mon Sep 17 00:00:00 2001 From: Carlos Valdes Date: Tue, 4 Aug 2026 16:14:08 +0200 Subject: [PATCH] fix(api): bound the stream drain so a full config dump can't starve the watchdog (#11164) writeStream() drained the whole queue in one call - "send every packet we can". A client asking for the full config gets the node database, then the file manifest, then the packet backlog and the position replay, and none of that returns to loop(). On a full node database (120 entries) the dump runs past eight seconds, so on RP2350, where rp2040Loop() arms an 8s hardware watchdog and is the only thing that calls watchdog_update(), the board resets in the middle of the manifest. Reproducible on every connection; with a small node database the dump finished under the timeout and nothing looked wrong. Measured on a pico2_w5500_e22: last loop iteration at millis=27242, ServerAPI kept logging until uptime 35s, reset at 35.2s = 27.242 + 8.0. Take a slice instead. The PhoneAPI state machine is resumable, so writeStream() stops after STREAM_WRITE_BUDGET_MSEC and reports whether anything is left; runOncePart() then asks to be re-run immediately rather than sleeping out readStream's idle delay, so the dump keeps its throughput while loop() gets to feed the watchdog between slices. Backpressure on a retained frame still returns the normal delay - re-running at once would just spin on a full transport. Verified on hardware: 10/10 full config dumps against a node with 120 entries, no resets, dump still completes in ~8s. --- src/mesh/StreamAPI.cpp | 45 ++++++++++++++++++++++++------------------ src/mesh/StreamAPI.h | 11 +++++++---- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 5dd1ef99b..f0bb1c455 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -13,7 +13,9 @@ int32_t StreamAPI::runOncePart() { auto result = readStream(); - writeStream(); + // More to send: come straight back instead of sleeping out readStream's idle delay. + if (writeStream()) + result = 0; checkConnectionTimeout(); return result; } @@ -22,7 +24,8 @@ int32_t StreamAPI::runOncePart() int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) { auto result = readStream(buf, bufLen); - writeStream(); + if (writeStream()) + result = 0; checkConnectionTimeout(); return result; } @@ -44,25 +47,29 @@ int32_t StreamAPI::readStream(const char *buf, uint16_t bufLen) } } -/** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ -void StreamAPI::writeStream() +/// Emit a slice of pending output. True means "more to send, come straight back"; false covers +/// both a drained queue and backpressure, where retrying at once would only spin. +bool StreamAPI::writeStream() { - if (canWrite) { - // A transport that retained a short frame must complete it before - // getFromRadio() advances the PhoneAPI state to the next packet. - if (!finishPendingFrame()) - return; + if (!canWrite) + return false; - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - if (len != 0 && !emitTxBuffer(len)) - break; - } while (len); - } + // A retained short frame must complete before getFromRadio() advances the PhoneAPI state. + if (!finishPendingFrame()) + return false; + + // Draining a full dump in one call never returns to loop(), so the 8s hardware watchdog + // fires mid-dump. PhoneAPI is resumable, so stop at the budget and continue next dispatch. + uint32_t len; + uint32_t started = millis(); + do { + // Send every packet we can, up to this slice's budget + len = getFromRadio(txBuf + HEADER_LEN); + if (len != 0 && !emitTxBuffer(len)) + return false; + } while (len && Throttle::isWithinTimespanMs(started, STREAM_WRITE_BUDGET_MSEC)); + + return len != 0; } /// Parse supplied bytes through the framed ToRadio receive state machine. diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 705460532..c91da4d02 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -9,6 +9,10 @@ // A To/FromRadio packet + our 32 bit header #define MAX_STREAM_BUF_SIZE (MAX_TO_FROM_RADIO_SIZE + sizeof(uint32_t)) +// Cap on one writeStream() slice: an uncapped dump never reaches loop(), so a board with a +// hardware watchdog (RP2350 arms 8s) resets mid-dump. +#define STREAM_WRITE_BUDGET_MSEC 100 + /** * A version of our 'phone' API that talks over a Stream. So therefore well suited to use with serial links * or TCP connections. @@ -64,10 +68,9 @@ class StreamAPI : public PhoneAPI int32_t readStream(const char *buf, uint16_t bufLen); int32_t handleRecStream(const char *buf, uint16_t bufLen); - /** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ - void writeStream(); + /// Emit a slice of pending output. True asks the caller to come straight back; false covers + /// both a drained queue and backpressure, so it does not mean the queue is empty. + bool writeStream(); protected: /**