diff --git a/build.sh b/build.sh index 313c4c47a0..23e31ab25c 100755 --- a/build.sh +++ b/build.sh @@ -93,7 +93,7 @@ get_pio_envs_ending_with_string() { # $1 should be the environment name get_platform_for_env() { local env_name=$1 - echo "$PIO_CONFIG_JSON" | python3 -c " + printf '%s' "$PIO_CONFIG_JSON" | python3 -c " import sys, json, re data = json.load(sys.stdin) for section, options in data: diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index eeab1501d5..6aef46e2c1 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -32,6 +32,40 @@ void KissModem::begin() { _tx_state = TX_IDLE; } +size_t KissModem::escapedLength(uint8_t b) const { + return (b == KISS_FEND || b == KISS_FESC) ? 2 : 1; +} + +size_t KissModem::escapedLength(const uint8_t* data, size_t len) const { + size_t total = 0; + for (size_t i = 0; i < len; i++) { + total += escapedLength(data[i]); + } + return total; +} + +bool KissModem::canWriteFrame(size_t total_len) const { + int available = _serial.availableForWrite(); + return available > 0 && (size_t)available >= total_len; +} + +void KissModem::writeEscapedFrame(const uint8_t* prefix, size_t prefix_len, const uint8_t* data, uint16_t len) { + // All-or-nothing: only write if the whole escaped frame fits, so loop() never blocks and frames are never truncated + size_t total_len = 2; // frame delimiters + total_len += escapedLength(prefix, prefix_len); + total_len += escapedLength(data, len); + if (!canWriteFrame(total_len)) return; + + _serial.write(KISS_FEND); + for (size_t i = 0; i < prefix_len; i++) { + writeByte(prefix[i]); + } + for (uint16_t i = 0; i < len; i++) { + writeByte(data[i]); + } + _serial.write(KISS_FEND); +} + void KissModem::writeByte(uint8_t b) { if (b == KISS_FEND) { _serial.write(KISS_FESC); @@ -45,22 +79,13 @@ void KissModem::writeByte(uint8_t b) { } void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(type); - for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); - } - _serial.write(KISS_FEND); + uint8_t prefix[] = { type }; + writeEscapedFrame(prefix, sizeof(prefix), data, len); } void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(KISS_CMD_SETHARDWARE); - writeByte(sub_cmd); - for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); - } - _serial.write(KISS_FEND); + uint8_t prefix[] = { KISS_CMD_SETHARDWARE, sub_cmd }; + writeEscapedFrame(prefix, sizeof(prefix), data, len); } void KissModem::writeHardwareError(uint8_t error_code) { diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bbe99d6de4..8f19c213ed 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -28,6 +28,12 @@ #define KISS_DEFAULT_SLOTTIME 10 #define KISS_TX_TIMEOUT_FACTOR 3/2 // 1.5x estimated airtime +// Max ms a USB-CDC write may block on a stalled host, so loop() never freezes (UART drains via FIFO, never hits this) +#define KISS_WRITE_TIMEOUT_MS 50 + +// Must fit a full escaped DATA frame (~514B) for all-or-nothing writes; 256B default drops max-size RX frames +#define KISS_TX_BUFFER_SIZE 1024 + #define HW_CMD_GET_IDENTITY 0x01 #define HW_CMD_GET_RANDOM 0x02 #define HW_CMD_VERIFY_SIGNATURE 0x03 @@ -131,6 +137,10 @@ class KissModem { RadioConfig _config; bool _signal_report_enabled; + size_t escapedLength(uint8_t b) const; + size_t escapedLength(const uint8_t* data, size_t len) const; + bool canWriteFrame(size_t total_len) const; // true only if the whole frame fits the TX buffer now + void writeEscapedFrame(const uint8_t* prefix, size_t prefix_len, const uint8_t* data, uint16_t len); 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..1f4d4b2b6f 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -107,7 +107,13 @@ void setup() { #endif modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors); #else +#if defined(ESP32) && (ARDUINO_USB_MODE == 1) + Serial.setTxBufferSize(KISS_TX_BUFFER_SIZE); // HWCDC ring must fit a whole KISS frame; set before begin() +#endif Serial.begin(115200); +#if defined(ESP32) + 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