Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions examples/kiss_modem/KissModem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering if a better approach is to pre-calc the total frame length, then introduce a new method like:
bool canWriteFrame(size_t len);

And for logic to be:
writeFrame( ... ) {
size_t total_len = ... ;
if (!canWriteFrame(total_len)) return; // bail, all or nothing
_serial.write( ... ); ...
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about the same, but looks like those buffers are quite small (like 64 bytes in some cases). Need some sleep first, but its an interesting one to look into.

_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) {
Expand Down
9 changes: 9 additions & 0 deletions examples/kiss_modem/KissModem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions examples/kiss_modem/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions variants/heltec_v4/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions variants/station_g2/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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