Fix ESP32-S3 USB CDC: post-disconnect task-WDT reboot from blocking console log writes (#10956)
* Fix ESP32-S3 USB CDC: post-disconnect task-WDT reboot from blocking console log writes When a serial API client disconnects (USB cable still attached), the host stops draining the USB-Serial/JTAG CDC buffer. The next raw-text debug log write can then block the main loop task indefinitely (measured: a single write blocked 52.4 s). The loop task stops feeding the app task watchdog (APP_WATCHDOG_SECS = 90 s, trigger_panic), so the device reboots with esp_reset_reason = ESP_RST_TASK_WDT ~97 s after every serial disconnect. On 2.7.x (arduino-esp32 2.x / IDF 4.4) the reboot also re-enumerated USB; since the Arduino 3.x migration the reboot is silent (USB stays enumerated), making it look like a random reboot ~90 s after using the CLI. Fix: on USB CDC targets, keep console TX in non-blocking mode (txTimeout 0, drop-oldest) whenever no API client is provably alive, and restore the normal bounded timeout while a client is connected so protobuf API frames are never truncated. Toggle points: boot, handleToRadio (host sent bytes), and onConnectionChanged (set non-blocking before disconnect handling emits more log lines to a dead port). Repro/validation on HELTEC_WIRELESS_TRACKER_V2 (macOS + Linux hosts): open+close any meshtastic-python session, wait 150 s: reboot_count +1 every time on unpatched builds; flat with this fix. Max observed log write stall drops from 52405 ms to 30 ms. * Condense comments per review feedback --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
@@ -65,6 +65,8 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
|
|||||||
Port.setRX(SERIAL2_RX);
|
Port.setRX(SERIAL2_RX);
|
||||||
#endif
|
#endif
|
||||||
Port.begin(SERIAL_BAUD);
|
Port.begin(SERIAL_BAUD);
|
||||||
|
// Boot with console TX in non-blocking mode: no host is provably listening yet.
|
||||||
|
setHostDraining(false);
|
||||||
time_t timeout = millis();
|
time_t timeout = millis();
|
||||||
while (!Port) {
|
while (!Port) {
|
||||||
if (Throttle::isWithinTimespanMs(timeout, FIVE_SECONDS_MS)) {
|
if (Throttle::isWithinTimespanMs(timeout, FIVE_SECONDS_MS)) {
|
||||||
@@ -121,6 +123,28 @@ bool SerialConsole::checkIsConnected()
|
|||||||
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
|
return Throttle::isWithinTimespanMs(lastContactMsec, SERIAL_CONNECTION_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SerialConsole::setHostDraining(bool draining)
|
||||||
|
{
|
||||||
|
#ifdef IS_USB_SERIAL
|
||||||
|
// Timeout 0 makes HWCDC writes drop instead of block when the host stops draining;
|
||||||
|
// bounded blocking is restored while an API client is connected so frames aren't truncated.
|
||||||
|
Port.setTxTimeoutMs(draining ? 100 : 0);
|
||||||
|
#else
|
||||||
|
(void)draining;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SerialConsole::onConnectionChanged(bool connected)
|
||||||
|
{
|
||||||
|
// Order matters on disconnect: make console TX non-blocking *before* the
|
||||||
|
// PowerFSM/close handling below emits more log lines to a dead port.
|
||||||
|
if (!connected)
|
||||||
|
setHostDraining(false);
|
||||||
|
StreamAPI::onConnectionChanged(connected);
|
||||||
|
if (connected)
|
||||||
|
setHostDraining(true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* we override this to notice when we've received a protobuf over the serial
|
* we override this to notice when we've received a protobuf over the serial
|
||||||
* stream. Then we shut off debug serial output.
|
* stream. Then we shut off debug serial output.
|
||||||
@@ -129,6 +153,10 @@ bool SerialConsole::handleToRadio(const uint8_t *buf, size_t len)
|
|||||||
{
|
{
|
||||||
// only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled.
|
// only talk to the API once the configuration has been loaded and we're sure the serial port is not disabled.
|
||||||
if (config.has_lora && config.security.serial_enabled) {
|
if (config.has_lora && config.security.serial_enabled) {
|
||||||
|
// The host just sent us bytes, so it is alive and draining the port:
|
||||||
|
// restore normal bounded-blocking TX before any API response is written.
|
||||||
|
setHostDraining(true);
|
||||||
|
|
||||||
// Switch to protobufs for log messages
|
// Switch to protobufs for log messages
|
||||||
usingProtobufs = true;
|
usingProtobufs = true;
|
||||||
canWrite = true;
|
canWrite = true;
|
||||||
|
|||||||
@@ -40,8 +40,17 @@ class SerialConsole : public StreamAPI, public RedirectablePrint, private concur
|
|||||||
|
|
||||||
virtual void onNowHasData(uint32_t fromRadioNum) override;
|
virtual void onNowHasData(uint32_t fromRadioNum) override;
|
||||||
|
|
||||||
|
/// Track serial API connect/disconnect so we can make console writes
|
||||||
|
/// non-blocking while no host is listening (see setHostDraining()).
|
||||||
|
virtual void onConnectionChanged(bool connected) override;
|
||||||
|
|
||||||
/// Possibly switch to protobufs if we see a valid protobuf message
|
/// Possibly switch to protobufs if we see a valid protobuf message
|
||||||
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
|
virtual void log_to_serial(const char *logLevel, const char *format, va_list arg);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// On USB CDC targets, keep console TX non-blocking unless a host is draining the
|
||||||
|
/// port, so a dead host can't stall the main loop and trip the task watchdog.
|
||||||
|
void setHostDraining(bool draining);
|
||||||
};
|
};
|
||||||
|
|
||||||
// A simple wrapper to allow non class aware code write to the console
|
// A simple wrapper to allow non class aware code write to the console
|
||||||
|
|||||||
Reference in New Issue
Block a user