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.
This commit is contained in:
Carlos Valdes
2026-08-04 14:14:08 +00:00
committed by GitHub
parent 7302db1672
commit d89f3eb8fc
2 files changed
+33 -23

No files matched your search

+26 -19
View File
@@ -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.
+7 -4
View File
@@ -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:
/**