fix(serial): don't sleep forever with pending PhoneAPI output on UART consoles (#11500)

* fix(serial): don't sleep forever with pending PhoneAPI output on UART consoles

Since #11164 bounded the stream drain, a config dump can end a dispatch
with output still queued. On UART-console ESP32 boards runOnce() then
returns INT32_MAX with no RX pending, and neither rxInt() nor
onNowHasData() fires for the remaining output, so the download wedges
mid nodeinfo stream until the client happens to send a byte.

Add StreamAPI::hasPendingOutput() (transport-retained frame or queued
PhoneAPI data) and have SerialConsole::runOnce() short-poll (<=25ms)
while it holds instead of sleeping INT32_MAX. The #11164 write budget
is unchanged; idle sleep behavior with a drained queue is unchanged.
The retained-frame probe also covers the ESP32-S2 USB-CDC branch,
which takes the same INT32_MAX path.

* test(serial): restore scratch NodeDB via tearDown, trim comments to house style

A failed TEST_ASSERT longjmps out of a Unity test without running
destructors, so RAII cannot restore the swapped nodeDB pointer; install
the scratch NodeDB explicitly and restore/delete it in tearDown(),
which runs after every test outcome. Also shorten the new comments to
the two-line house limit.
This commit is contained in:
Ben Meadors
2026-08-14 10:41:44 -05:00
committed by GitHub
parent fb6a212b44
commit 905482ccce
5 changed files with 112 additions and 3 deletions
+15
View File
@@ -125,6 +125,10 @@ int32_t SerialConsole::runOnce()
int32_t delay = runOncePart();
#if defined(SERIAL_HAS_ON_RECEIVE) || defined(CONFIG_IDF_TARGET_ESP32S2)
// Nothing wakes the idle sleep for "TX space freed" or a bounded-drain remainder
// (#11164), so keep polling while the API holds undelivered output.
if (hasPendingOutput())
return delay < 25 ? delay : 25; // 0 continues a budget slice; else short-poll TX drain
return Port.available() ? delay : INT32_MAX;
#elif defined(IS_USB_SERIAL)
return HWCDC::isPlugged() ? delay : (1000 * 20);
@@ -212,6 +216,17 @@ bool SerialConsole::finishPendingFrame()
#endif
}
/// Report a retained USB CDC frame awaiting TX space.
bool SerialConsole::hasRetainedFrame()
{
#ifdef IS_USB_SERIAL
concurrency::LockGuard guard(&streamLock);
return !frameWriter.isIdle();
#else
return false;
#endif
}
/// Protect the retained log buffer from being overwritten.
bool SerialConsole::canEncodeLogRecord()
{
+2
View File
@@ -51,6 +51,8 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
/// Continue retained USB CDC output before PhoneAPI advances.
virtual bool finishPendingFrame() override;
/// Report a retained USB CDC frame awaiting TX space.
virtual bool hasRetainedFrame() override;
/// Return whether the dedicated log buffer can be safely overwritten.
virtual bool canEncodeLogRecord() override;
/// Write or retain one framed USB CDC message.
+6
View File
@@ -33,6 +33,12 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
return result;
}
/// Report undelivered output so idle-sleep decisions keep the drain alive.
bool StreamAPI::hasPendingOutput()
{
return canWrite && (hasRetainedFrame() || available());
}
/**
* Read any rx chars from the link and call handleRecStream
*/
+6
View File
@@ -57,6 +57,10 @@ class StreamAPI : public PhoneAPI
virtual int32_t runOncePart();
virtual int32_t runOncePart(char *buf, uint16_t bufLen);
/// True while undelivered output remains (retained frame or queued PhoneAPI data); callers
/// woken only by RX activity must keep polling while set, as drains stop mid-dump (#11164).
bool hasPendingOutput();
/// Check the current underlying physical link to see if the client is currently connected
virtual bool checkIsConnected() override = 0;
@@ -104,6 +108,8 @@ class StreamAPI : public PhoneAPI
/// Complete retained transport output before dequeuing another PhoneAPI packet.
virtual bool finishPendingFrame() { return true; }
/// Return whether the transport retains an incomplete frame awaiting TX space.
virtual bool hasRetainedFrame() { return false; }
/// Return whether the dedicated log buffer is available for encoding.
virtual bool canEncodeLogRecord() { return true; }
/// Frame and write a payload, optionally using best-effort admission.