diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index eeab1501d5..19be5d412f 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -22,6 +22,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _getStatsCallback = nullptr; _config = {0, 0, 0, 0, 0}; _signal_report_enabled = true; + _tx_write_aborted = false; } void KissModem::begin() { @@ -32,35 +33,58 @@ void KissModem::begin() { _tx_state = TX_IDLE; } +void KissModem::beginFrameWrite() { + _tx_write_aborted = false; +} + +void KissModem::rawWrite(uint8_t b) { + /* A frame is sent all-or-nothing; once we start dropping, swallow the rest so + we never emit a truncated KISS frame. */ + if (_tx_write_aborted) return; + + /* Non-blocking: if the TX buffer is full, drop this frame instead of waiting. + loop() is single-threaded and also services RX and the radio, so it must not + stall on a host that has stopped reading. Writing only when space is free + keeps the underlying write() from blocking; a dropped reply is harmless as + the host retries. */ + if (_serial.availableForWrite() <= 0) { + _tx_write_aborted = true; + return; + } + _serial.write(b); +} + void KissModem::writeByte(uint8_t b) { if (b == KISS_FEND) { - _serial.write(KISS_FESC); - _serial.write(KISS_TFEND); + rawWrite(KISS_FESC); + rawWrite(KISS_TFEND); } else if (b == KISS_FESC) { - _serial.write(KISS_FESC); - _serial.write(KISS_TFESC); + rawWrite(KISS_FESC); + rawWrite(KISS_TFESC); } else { - _serial.write(b); + rawWrite(b); } } void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); + beginFrameWrite(); + rawWrite(KISS_FEND); writeByte(type); for (uint16_t i = 0; i < len; i++) { writeByte(data[i]); } - _serial.write(KISS_FEND); + rawWrite(KISS_FEND); } void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); + beginFrameWrite(); + rawWrite(KISS_FEND); writeByte(KISS_CMD_SETHARDWARE); writeByte(sub_cmd); for (uint16_t i = 0; i < len; i++) { writeByte(data[i]); } - _serial.write(KISS_FEND); + rawWrite(KISS_FEND); } void KissModem::writeHardwareError(uint8_t error_code) { diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bbe99d6de4..dedd51b73e 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -28,6 +28,11 @@ #define KISS_DEFAULT_SLOTTIME 10 #define KISS_TX_TIMEOUT_FACTOR 3/2 // 1.5x estimated airtime +/* Upper bound (ms) on how long a serial write may wait for the host to drain the + TX buffer. Keeps a stalled USB-CDC host from freezing the single-threaded loop(); + UART transports drain via FIFO and never reach this. */ +#define KISS_WRITE_TIMEOUT_MS 50 + #define HW_CMD_GET_IDENTITY 0x01 #define HW_CMD_GET_RANDOM 0x02 #define HW_CMD_VERIFY_SIGNATURE 0x03 @@ -131,6 +136,10 @@ class KissModem { RadioConfig _config; bool _signal_report_enabled; + bool _tx_write_aborted; // set when the current frame is dropped (no TX buffer space) + + void beginFrameWrite(); // reset per-frame abort state + void rawWrite(uint8_t b); // non-blocking write: drops the frame rather than stalling loop() void writeByte(uint8_t b); void writeFrame(uint8_t type, const uint8_t* data, uint16_t len); void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 7fbcaed127..eca2f42ef3 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -108,6 +108,12 @@ void setup() { modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors); #else Serial.begin(115200); +#if defined(ESP32) + /* Cap how long a USB-CDC write blocks waiting for the host to drain the TX + buffer. loop() is single-threaded, so an unbounded wait when the host stalls + would also freeze RX and radio servicing. */ + Serial.setTxTimeoutMs(KISS_WRITE_TIMEOUT_MS); +#endif uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index fabf38272d..4b6185162b 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -434,3 +434,13 @@ lib_deps = extends = Heltec_lora32_v4 build_src_filter = ${Heltec_lora32_v4.build_src_filter} +<../examples/kiss_modem/> +; Use the USB-Serial-JTAG peripheral (HWCDC) instead of TinyUSB CDC. The TinyUSB +; USBCDC path wedges permanently under TX backpressure on ESP32-S3 (write() busy- +; spins while "connected", RX events post to a 5-deep queue with portMAX_DELAY), +; leaving the modem unresponsive across host restarts. HWCDC bounds its writes and +; posts RX from ISR, so it does not hang. build_unflags strips the board default +; (=0); a bare -U is unreliable here because SCons reorders it after the -D defines. +build_unflags = -DARDUINO_USB_MODE=0 +build_flags = + ${Heltec_lora32_v4.build_flags} + -DARDUINO_USB_MODE=1 diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 6432b52386..c10e19337b 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -243,3 +243,13 @@ lib_deps = extends = Station_G2 build_src_filter = ${Station_G2.build_src_filter} +<../examples/kiss_modem/> +; Use the USB-Serial-JTAG peripheral (HWCDC) instead of TinyUSB CDC. The TinyUSB +; USBCDC path wedges permanently under TX backpressure on ESP32-S3 (write() busy- +; spins while "connected", RX events post to a 5-deep queue with portMAX_DELAY), +; leaving the modem unresponsive across host restarts. HWCDC bounds its writes and +; posts RX from ISR, so it does not hang. build_unflags strips the board default +; (=0); a bare -U is unreliable here because SCons reorders it after the -D defines. +build_unflags = -DARDUINO_USB_MODE=0 +build_flags = + ${Station_G2.build_flags} + -DARDUINO_USB_MODE=1