From 0fb570338f57b6b2fdb5426f92e6e61c58e9db38 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 3 Feb 2026 20:58:37 -0800 Subject: [PATCH 1/2] fix(kiss): periodic noise floor calibration and AGC reset - Trigger noise floor calibration every 2s and AGC reset every 30s in main loop. - Reorder loop to match Dispatcher: calibrate + radio.loop() before AGC reset and recvRaw() so RSSI is never sampled right after startReceive(). - Update protocol doc with calibration intervals and typical noise floor range. - Variant platformio.ini updates (heltec_v3, rak4631). --- docs/kiss_modem_protocol.md | 20 ++++++++++++++++---- examples/kiss_modem/main.cpp | 23 +++++++++++++++++++---- variants/heltec_v3/platformio.ini | 9 +++++++++ variants/rak4631/platformio.ini | 11 ++++++++++- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index e80c3b29..067e1539 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -28,6 +28,8 @@ Maximum unescaped frame size: 512 bytes. ## Commands +Command and response codes below are taken from `examples/kiss_modem/KissModem.h` and the switch in `KissModem::processFrame()`. + ### Request Commands (Host → Modem) | Command | Value | Data | @@ -43,10 +45,10 @@ Maximum unescaped frame size: 512 bytes. | `CMD_HASH` | `0x08` | Data to hash | | `CMD_SET_RADIO` | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) | | `CMD_SET_TX_POWER` | `0x0A` | Power dBm (1) | -| `CMD_SET_SYNC_WORD` | `0x0B` | Sync word (1) | +| *reserved* | `0x0B` | *(not implemented)* | | `CMD_GET_RADIO` | `0x0C` | - | | `CMD_GET_TX_POWER` | `0x0D` | - | -| `CMD_GET_SYNC_WORD` | `0x0E` | - | +| *reserved* | `0x0E` | *(not implemented)* | | `CMD_GET_VERSION` | `0x0F` | - | | `CMD_GET_CURRENT_RSSI` | `0x10` | - | | `CMD_IS_CHANNEL_BUSY` | `0x11` | - | @@ -73,7 +75,7 @@ Maximum unescaped frame size: 512 bytes. | `RESP_OK` | `0x29` | - | | `RESP_RADIO` | `0x2A` | Freq (4) + BW (4) + SF (1) + CR (1) | | `RESP_TX_POWER` | `0x2B` | Power dBm (1) | -| `RESP_SYNC_WORD` | `0x2C` | Sync word (1) | +| *reserved* | `0x2C` | *(not implemented)* | | `RESP_VERSION` | `0x2D` | Version (1) + Reserved (1) | | `RESP_ERROR` | `0x2E` | Error code (1) | | `RESP_TX_DONE` | `0x2F` | Result (1): 0x00=failed, 0x01=success | @@ -119,9 +121,19 @@ All values little-endian. | RSSI | 1 byte | Signal strength dBm, signed | | Packet | variable | Raw MeshCore packet | +### Noise Floor (RESP_NOISE_FLOOR) + +Response to `CMD_GET_NOISE_FLOOR` (0x13). Little-endian. + +| Field | Size | Description | +|--------------|------|--------------------------------| +| Noise floor | 2 | int16_t, dBm (signed), e.g. -120 | + +The modem recalibrates the noise floor periodically (every 2 s) from RX samples when idle. The receiver AGC is also reset periodically (every 30 s) so RSSI and noise floor do not drift to the minimum (-120). Typical range after calibration is about -120 to -90 dBm. Values may be 0 or briefly stale until the radio has been in receive mode long enough to collect 64 samples. + ### Stats (RESP_STATS) -All values little-endian. +Response to `CMD_GET_STATS` (0x14). All values little-endian. | Field | Size | Description | |-------|------|-------------| diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 959222b9..13855309 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -12,9 +12,14 @@ #include #endif +#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 // match Dispatcher default +#define AGC_RESET_INTERVAL_MS 30000 // periodic RX restart so AGC doesn't drift (repeater uses same via prefs) + StdRNG rng; mesh::LocalIdentity identity; KissModem* modem; +static uint32_t next_noise_floor_calib_ms = 0; +static uint32_t next_agc_reset_ms = 0; void halt() { while (1) ; @@ -94,7 +99,16 @@ void loop() { uint8_t packet[KISS_MAX_PACKET_SIZE]; uint16_t len; - + + // Match Dispatcher order: noise floor calib + loop() first, so we never sample RSSI in the same + // iteration as startReceive() (AGC reset -> recvRaw below). Sampling right after startReceive() + // can yield settling/cold RSSI and drive the floor toward -120. + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { + radio_driver.triggerNoiseFloorCalibrate(0); // 0 = no interference threshold (KISS has no prefs) + next_noise_floor_calib_ms = millis(); + } + radio_driver.loop(); + if (modem->getPacketToSend(packet, &len)) { radio_driver.startSendRaw(packet, len); while (!radio_driver.isSendComplete()) { @@ -104,14 +118,15 @@ void loop() { modem->onTxComplete(true); } + if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { + radio_driver.resetAGC(); // next recvRaw() will startReceive() and reset AGC so RSSI/noise floor don't stick at -120 + next_agc_reset_ms = millis(); + } uint8_t rx_buf[256]; int rx_len = radio_driver.recvRaw(rx_buf, sizeof(rx_buf)); - if (rx_len > 0) { int8_t snr = (int8_t)(radio_driver.getLastSNR() * 4); int8_t rssi = (int8_t)radio_driver.getLastRSSI(); modem->onPacketReceived(snr, rssi, rx_buf, rx_len); } - - radio_driver.loop(); } diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 6b61eff5..4d299104 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -367,3 +367,12 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} + +[env:Heltec_v3_kiss_modem] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/kiss_modem/> +lib_deps = + ${Heltec_lora32_v3.lib_deps} \ No newline at end of file diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 9a9ab2dd..737ef565 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -183,4 +183,13 @@ build_flags = -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + - +<../examples/simple_sensor> \ No newline at end of file + +<../examples/simple_sensor> + +[env:RAK_4631_kiss_modem] +extends = rak4631 +build_flags = + ${rak4631.build_flags} +build_src_filter = ${rak4631.build_src_filter} + +<../examples/kiss_modem/> +lib_deps = + ${rak4631.lib_deps} \ No newline at end of file From c0b81b9ad867dd0b018c4a513b0424995226fb97 Mon Sep 17 00:00:00 2001 From: Adam Gessaman Date: Thu, 5 Feb 2026 09:46:30 -0800 Subject: [PATCH 2/2] Clean up comments on kiss noise floor changes. --- docs/kiss_modem_protocol.md | 4 +--- examples/kiss_modem/main.cpp | 12 +++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 067e1539..00b0bf90 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -28,8 +28,6 @@ Maximum unescaped frame size: 512 bytes. ## Commands -Command and response codes below are taken from `examples/kiss_modem/KissModem.h` and the switch in `KissModem::processFrame()`. - ### Request Commands (Host → Modem) | Command | Value | Data | @@ -129,7 +127,7 @@ Response to `CMD_GET_NOISE_FLOOR` (0x13). Little-endian. |--------------|------|--------------------------------| | Noise floor | 2 | int16_t, dBm (signed), e.g. -120 | -The modem recalibrates the noise floor periodically (every 2 s) from RX samples when idle. The receiver AGC is also reset periodically (every 30 s) so RSSI and noise floor do not drift to the minimum (-120). Typical range after calibration is about -120 to -90 dBm. Values may be 0 or briefly stale until the radio has been in receive mode long enough to collect 64 samples. +The modem recalibrates the noise floor every two seconds with an AGC reset every 30 seconds. ### Stats (RESP_STATS) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 13855309..3a610460 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -12,8 +12,8 @@ #include #endif -#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 // match Dispatcher default -#define AGC_RESET_INTERVAL_MS 30000 // periodic RX restart so AGC doesn't drift (repeater uses same via prefs) +#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 +#define AGC_RESET_INTERVAL_MS 30000 StdRNG rng; mesh::LocalIdentity identity; @@ -100,11 +100,9 @@ void loop() { uint8_t packet[KISS_MAX_PACKET_SIZE]; uint16_t len; - // Match Dispatcher order: noise floor calib + loop() first, so we never sample RSSI in the same - // iteration as startReceive() (AGC reset -> recvRaw below). Sampling right after startReceive() - // can yield settling/cold RSSI and drive the floor toward -120. + // trigger noise floor calibration if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { - radio_driver.triggerNoiseFloorCalibrate(0); // 0 = no interference threshold (KISS has no prefs) + radio_driver.triggerNoiseFloorCalibrate(0); next_noise_floor_calib_ms = millis(); } radio_driver.loop(); @@ -119,7 +117,7 @@ void loop() { } if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { - radio_driver.resetAGC(); // next recvRaw() will startReceive() and reset AGC so RSSI/noise floor don't stick at -120 + radio_driver.resetAGC(); next_agc_reset_ms = millis(); } uint8_t rx_buf[256];