Summary
write_serial() (main/serial.c:150-174) loops until all bytes are written, but only treats a return of -1 as an error:
int written = 0;
while (written != length) {
const int wrote = tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, msg + written, length - written);
// or on ESP32: uart_write_bytes(UART_NUM_0, msg + written, length - written);
if (wrote == -1) {
return false;
}
written += wrote;
}
If the transport returns 0 repeatedly — e.g. tinyusb_cdcacm_write_queue() when the USB-CDC TX buffer is full and no host is reading (cable unplugged, or host app killed mid-transfer), or uart_write_bytes() with a full TX ring buffer — the loop spins forever with no delay and no timeout, causing the task watchdog to fire (device reboot) or a permanent hang. Note the fwrite() path (USB-JTAG serial on ESP32-S3) never returns -1, so any short write/error there already spins forever today.
write_ble() (main/ble/ble.c:308-334) has the correct pattern: bounded retries (~2s) then return false. And jade_process_get_out_message() (main/process.c:467-469) documents that a failed write drops the message (the host times out and retries), so returning false is safe and recoverable.
Suggested fix
Mirror the BLE approach in write_serial(): retry with a bounded timeout (with a small delay between attempts), then return false after e.g. ~2s of consecutive zero-writes. The success path (wrote > 0) is unchanged; the only behavior change is on a stuck link, where dropping the message (host retry) replaces a watchdog reboot.
Verification
Confirmed still present on current master (as of 2026-08-04): main/serial.c:156 unchanged. The exact trigger needs hardware confirmation of the driver return value (0 vs -1) when the device is disconnected mid-write.
Summary
write_serial()(main/serial.c:150-174) loops until all bytes are written, but only treats a return of-1as an error:If the transport returns
0repeatedly — e.g.tinyusb_cdcacm_write_queue()when the USB-CDC TX buffer is full and no host is reading (cable unplugged, or host app killed mid-transfer), oruart_write_bytes()with a full TX ring buffer — the loop spins forever with no delay and no timeout, causing the task watchdog to fire (device reboot) or a permanent hang. Note thefwrite()path (USB-JTAG serial on ESP32-S3) never returns-1, so any short write/error there already spins forever today.write_ble()(main/ble/ble.c:308-334) has the correct pattern: bounded retries (~2s) then returnfalse. Andjade_process_get_out_message()(main/process.c:467-469) documents that a failed write drops the message (the host times out and retries), so returningfalseis safe and recoverable.Suggested fix
Mirror the BLE approach in
write_serial(): retry with a bounded timeout (with a small delay between attempts), then returnfalseafter e.g. ~2s of consecutive zero-writes. The success path (wrote > 0) is unchanged; the only behavior change is on a stuck link, where dropping the message (host retry) replaces a watchdog reboot.Verification
Confirmed still present on current
master(as of 2026-08-04):main/serial.c:156unchanged. The exact trigger needs hardware confirmation of the driver return value (0 vs -1) when the device is disconnected mid-write.