Skip to content
Open
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
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
51 changes: 38 additions & 13 deletions examples/kiss_modem/KissModem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -45,22 +79,13 @@ void KissModem::writeByte(uint8_t 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ViezeVingertjes any additional thoughts on this? Happy to make revisions following Scott's suggestion, but I wanted your feedback because the KISS firmware is your baby.

I can confirm that this change, plus a couple changes on the pyMC side have completely eliminated the lockup and unresponsive frame issues that we were facing (which is super exciting).

Copy link
Copy Markdown
Contributor

@ViezeVingertjes ViezeVingertjes Jun 2, 2026

Choose a reason for hiding this comment

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

@ViezeVingertjes any additional thoughts on this? Happy to make revisions following Scott's suggestion, but I wanted your feedback because the KISS firmware is your baby.

I think to remember availableForWrite() is like only 64 or 128 bytes max depending on HWCDC/USBCDC, so now i wonder why it works haha! Does it drop all frames above it now making it look like it does because it does send smaller frames through?

Sorry have been busy, will prioritize looking at it further today. 💯 Most important is that we dont drop/lose frames without the proper feedback to the client, otherwise it wouldnt know to retry etc. (Although no response is also an indicator i guess)

_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) {
Expand Down
10 changes: 10 additions & 0 deletions examples/kiss_modem/KissModem.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
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 @@ -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);
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