From 5dcc377b775dd2bf192521c97bcf04f05f4bbe04 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 6 Feb 2026 11:07:10 +0100 Subject: [PATCH 01/13] Rewrite KISS modem to be fully spec-compliant --- docs/kiss_modem_protocol.md | 239 ++++++++++-------- examples/kiss_modem/KissModem.cpp | 375 ++++++++++++++++++----------- examples/kiss_modem/KissModem.h | 170 ++++++++----- examples/kiss_modem/main.cpp | 54 ++--- variants/xiao_nrf52/platformio.ini | 11 +- 5 files changed, 515 insertions(+), 334 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 00b0bf90f..e042053c6 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -1,6 +1,6 @@ # MeshCore KISS Modem Protocol -Serial protocol for the KISS modem firmware. Enables sending/receiving MeshCore packets over LoRa and cryptographic operations using the modem's identity. +Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command. ## Serial Configuration @@ -8,7 +8,7 @@ Serial protocol for the KISS modem firmware. Enables sending/receiving MeshCore ## Frame Format -Standard KISS framing with byte stuffing. +Standard KISS framing per the KA9Q/K3MC specification. | Byte | Name | Description | |------|------|-------------| @@ -18,89 +18,146 @@ Standard KISS framing with byte stuffing. | `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) | ``` -┌──────┬─────────┬──────────────┬──────┐ -│ FEND │ Command │ Data (escaped)│ FEND │ -│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ -└──────┴─────────┴──────────────┴──────┘ +┌──────┬───────────┬──────────────┬──────┐ +│ FEND │ Type Byte │ Data (escaped)│ FEND │ +│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ +└──────┴───────────┴──────────────┴──────┘ ``` +### Type Byte + +The type byte is split into two nibbles: + +| Bits | Field | Description | +|------|-------|-------------| +| 7-4 | Port | Port number (0 for single-port TNC) | +| 3-0 | Command | Command number | + Maximum unescaped frame size: 512 bytes. -## Commands - -### Request Commands (Host → Modem) - -| Command | Value | Data | -|---------|-------|------| -| `CMD_DATA` | `0x00` | Packet (2-255 bytes) | -| `CMD_GET_IDENTITY` | `0x01` | - | -| `CMD_GET_RANDOM` | `0x02` | Length (1 byte, 1-64) | -| `CMD_VERIFY_SIGNATURE` | `0x03` | PubKey (32) + Signature (64) + Data | -| `CMD_SIGN_DATA` | `0x04` | Data to sign | -| `CMD_ENCRYPT_DATA` | `0x05` | Key (32) + Plaintext | -| `CMD_DECRYPT_DATA` | `0x06` | Key (32) + MAC (2) + Ciphertext | -| `CMD_KEY_EXCHANGE` | `0x07` | Remote PubKey (32) | -| `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) | -| *reserved* | `0x0B` | *(not implemented)* | -| `CMD_GET_RADIO` | `0x0C` | - | -| `CMD_GET_TX_POWER` | `0x0D` | - | -| *reserved* | `0x0E` | *(not implemented)* | -| `CMD_GET_VERSION` | `0x0F` | - | -| `CMD_GET_CURRENT_RSSI` | `0x10` | - | -| `CMD_IS_CHANNEL_BUSY` | `0x11` | - | -| `CMD_GET_AIRTIME` | `0x12` | Packet length (1) | -| `CMD_GET_NOISE_FLOOR` | `0x13` | - | -| `CMD_GET_STATS` | `0x14` | - | -| `CMD_GET_BATTERY` | `0x15` | - | -| `CMD_PING` | `0x16` | - | -| `CMD_GET_SENSORS` | `0x17` | Permissions (1) | - -### Response Commands (Modem → Host) - -| Command | Value | Data | -|---------|-------|------| -| `CMD_DATA` | `0x00` | SNR (1) + RSSI (1) + Packet | -| `RESP_IDENTITY` | `0x21` | PubKey (32) | -| `RESP_RANDOM` | `0x22` | Random bytes (1-64) | -| `RESP_VERIFY` | `0x23` | Result (1): 0x00=invalid, 0x01=valid | -| `RESP_SIGNATURE` | `0x24` | Signature (64) | -| `RESP_ENCRYPTED` | `0x25` | MAC (2) + Ciphertext | -| `RESP_DECRYPTED` | `0x26` | Plaintext | -| `RESP_SHARED_SECRET` | `0x27` | Shared secret (32) | -| `RESP_HASH` | `0x28` | SHA-256 hash (32) | -| `RESP_OK` | `0x29` | - | -| `RESP_RADIO` | `0x2A` | Freq (4) + BW (4) + SF (1) + CR (1) | -| `RESP_TX_POWER` | `0x2B` | Power dBm (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 | -| `RESP_CURRENT_RSSI` | `0x30` | RSSI dBm (1, signed) | -| `RESP_CHANNEL_BUSY` | `0x31` | Result (1): 0x00=clear, 0x01=busy | -| `RESP_AIRTIME` | `0x32` | Milliseconds (4) | -| `RESP_NOISE_FLOOR` | `0x33` | dBm (2, signed) | -| `RESP_STATS` | `0x34` | RX (4) + TX (4) + Errors (4) | -| `RESP_BATTERY` | `0x35` | Millivolts (2) | -| `RESP_PONG` | `0x36` | - | -| `RESP_SENSORS` | `0x37` | CayenneLPP payload | - -## Error Codes +## Standard KISS Commands + +### Host to TNC + +| Command | Value | Data | Description | +|---------|-------|------|-------------| +| Data | `0x00` | Raw packet | Queue packet for transmission | +| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) | +| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) | +| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) | +| TXtail | `0x04` | Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) | +| FullDuplex | `0x05` | Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) | +| SetHardware | `0x06` | Sub-command + data | MeshCore extensions (see below) | +| Return | `0xFF` | - | Exit KISS mode (no-op) | + +### TNC to Host + +| Type | Value | Data | Description | +|------|-------|------|-------------| +| Data | `0x00` | Raw packet | Received packet from radio | + +Data frames carry raw packet data only, with no metadata prepended. + +### CSMA Behavior + +The TNC implements p-persistent CSMA for half-duplex operation: + +1. When a packet is queued, monitor carrier detect +2. When the channel clears, generate a random value 0-255 +3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit +4. Otherwise, wait SlotTime and repeat from step 1 + +In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY. + +## SetHardware Extensions (0x06) + +MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames. + +### Frame Format + +``` +┌──────┬──────┬─────────────┬──────────────┬──────┐ +│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │ +│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │ +└──────┴──────┴─────────────┴──────────────┴──────┘ +``` + +### Request Sub-commands (Host to TNC) + +| Sub-command | Value | Data | +|-------------|-------|------| +| GetIdentity | `0x01` | - | +| GetRandom | `0x02` | Length (1 byte, 1-64) | +| VerifySignature | `0x03` | PubKey (32) + Signature (64) + Data | +| SignData | `0x04` | Data to sign | +| EncryptData | `0x05` | Key (32) + Plaintext | +| DecryptData | `0x06` | Key (32) + MAC (2) + Ciphertext | +| KeyExchange | `0x07` | Remote PubKey (32) | +| Hash | `0x08` | Data to hash | +| SetRadio | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) | +| SetTxPower | `0x0A` | Power dBm (1) | +| GetRadio | `0x0C` | - | +| GetTxPower | `0x0D` | - | +| GetVersion | `0x0F` | - | +| GetCurrentRssi | `0x10` | - | +| IsChannelBusy | `0x11` | - | +| GetAirtime | `0x12` | Packet length (1) | +| GetNoiseFloor | `0x13` | - | +| GetStats | `0x14` | - | +| GetBattery | `0x15` | - | +| Ping | `0x16` | - | +| GetSensors | `0x17` | Permissions (1) | + +### Response Sub-commands (TNC to Host) + +| Sub-command | Value | Data | +|-------------|-------|------| +| Identity | `0x21` | PubKey (32) | +| Random | `0x22` | Random bytes (1-64) | +| Verify | `0x23` | Result (1): 0x00=invalid, 0x01=valid | +| Signature | `0x24` | Signature (64) | +| Encrypted | `0x25` | MAC (2) + Ciphertext | +| Decrypted | `0x26` | Plaintext | +| SharedSecret | `0x27` | Shared secret (32) | +| Hash | `0x28` | SHA-256 hash (32) | +| OK | `0x29` | - | +| Radio | `0x2A` | Freq (4) + BW (4) + SF (1) + CR (1) | +| TxPower | `0x2B` | Power dBm (1) | +| Version | `0x2D` | Version (1) + Reserved (1) | +| Error | `0x2E` | Error code (1) | +| TxDone | `0x2F` | Result (1): 0x00=failed, 0x01=success | +| CurrentRssi | `0x30` | RSSI dBm (1, signed) | +| ChannelBusy | `0x31` | Result (1): 0x00=clear, 0x01=busy | +| Airtime | `0x32` | Milliseconds (4) | +| NoiseFloor | `0x33` | dBm (2, signed) | +| Stats | `0x34` | RX (4) + TX (4) + Errors (4) | +| Battery | `0x35` | Millivolts (2) | +| Pong | `0x36` | - | +| Sensors | `0x37` | CayenneLPP payload | +| RxMeta | `0x38` | SNR (1) + RSSI (1) | + +### Error Codes | Code | Value | Description | |------|-------|-------------| -| `ERR_INVALID_LENGTH` | `0x01` | Request data too short | -| `ERR_INVALID_PARAM` | `0x02` | Invalid parameter value | -| `ERR_NO_CALLBACK` | `0x03` | Feature not available | -| `ERR_MAC_FAILED` | `0x04` | MAC verification failed | -| `ERR_UNKNOWN_CMD` | `0x05` | Unknown command | -| `ERR_ENCRYPT_FAILED` | `0x06` | Encryption failed | -| `ERR_TX_PENDING` | `0x07` | TX already pending | +| InvalidLength | `0x01` | Request data too short | +| InvalidParam | `0x02` | Invalid parameter value | +| NoCallback | `0x03` | Feature not available | +| MacFailed | `0x04` | MAC verification failed | +| UnknownCmd | `0x05` | Unknown sub-command | +| EncryptFailed | `0x06` | Encryption failed | + +### Unsolicited Events + +The TNC sends these SetHardware frames without a preceding request: + +**TxDone (0x2F)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. + +**RxMeta (0x38)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Standard KISS clients ignore this frame. ## Data Formats -### Radio Parameters (CMD_SET_RADIO / RESP_RADIO) +### Radio Parameters (SetRadio / Radio response) All values little-endian. @@ -111,27 +168,9 @@ All values little-endian. | SF | 1 byte | Spreading factor (5-12) | | CR | 1 byte | Coding rate (5-8) | -### Received Packet (CMD_DATA response) - -| Field | Size | Description | -|-------|------|-------------| -| SNR | 1 byte | Signal-to-noise × 4, signed | -| 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. +### Stats (Stats response) -| Field | Size | Description | -|--------------|------|--------------------------------| -| Noise floor | 2 | int16_t, dBm (signed), e.g. -120 | - -The modem recalibrates the noise floor every two seconds with an AGC reset every 30 seconds. - -### Stats (RESP_STATS) - -Response to `CMD_GET_STATS` (0x14). All values little-endian. +All values little-endian. | Field | Size | Description | |-------|------|-------------| @@ -139,7 +178,7 @@ Response to `CMD_GET_STATS` (0x14). All values little-endian. | TX | 4 bytes | Packets transmitted | | Errors | 4 bytes | Receive errors | -### Sensor Permissions (CMD_GET_SENSORS) +### Sensor Permissions (GetSensors) | Bit | Value | Description | |-----|-------|-------------| @@ -149,14 +188,14 @@ Response to `CMD_GET_STATS` (0x14). All values little-endian. Use `0x07` for all permissions. -### Sensor Data (RESP_SENSORS) +### Sensor Data (Sensors response) Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing. ## Notes - Modem generates identity on first boot (stored in flash) -- SNR values multiplied by 4 for 0.25 dB precision -- Wait for `RESP_TX_DONE` before sending next packet -- Sending `CMD_DATA` while TX is pending returns `ERR_TX_PENDING` +- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision +- TxDone is sent as a SetHardware event after each transmission +- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames - See [packet_structure.md](./packet_structure.md) for packet format diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index d9c71bf85..9915ec5ec 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -9,10 +9,20 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _rx_active = false; _has_pending_tx = false; _pending_tx_len = 0; + _txdelay = KISS_DEFAULT_TXDELAY; + _persistence = KISS_DEFAULT_PERSISTENCE; + _slottime = KISS_DEFAULT_SLOTTIME; + _txtail = 0; + _fullduplex = 0; + _tx_state = TX_IDLE; + _tx_timer = 0; _setRadioCallback = nullptr; _setTxPowerCallback = nullptr; _getCurrentRssiCallback = nullptr; _getStatsCallback = nullptr; + _sendPacketCallback = nullptr; + _isSendCompleteCallback = nullptr; + _onSendFinishedCallback = nullptr; _config = {0, 0, 0, 0, 0}; } @@ -21,6 +31,7 @@ void KissModem::begin() { _rx_escaped = false; _rx_active = false; _has_pending_tx = false; + _tx_state = TX_IDLE; } void KissModem::writeByte(uint8_t b) { @@ -35,23 +46,33 @@ void KissModem::writeByte(uint8_t b) { } } -void KissModem::writeFrame(uint8_t cmd, const uint8_t* data, uint16_t len) { +void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { _serial.write(KISS_FEND); - writeByte(cmd); + writeByte(type); for (uint16_t i = 0; i < len; i++) { writeByte(data[i]); } _serial.write(KISS_FEND); } -void KissModem::writeErrorFrame(uint8_t error_code) { - writeFrame(RESP_ERROR, &error_code, 1); +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); +} + +void KissModem::writeHardwareError(uint8_t error_code) { + writeHardwareFrame(HW_RESP_ERROR, &error_code, 1); } void KissModem::loop() { while (_serial.available()) { uint8_t b = _serial.read(); - + if (b == KISS_FEND) { if (_rx_active && _rx_len > 0) { processFrame(); @@ -61,283 +82,368 @@ void KissModem::loop() { _rx_active = true; continue; } - + if (!_rx_active) continue; - + if (b == KISS_FESC) { _rx_escaped = true; continue; } - + if (_rx_escaped) { _rx_escaped = false; if (b == KISS_TFEND) b = KISS_FEND; else if (b == KISS_TFESC) b = KISS_FESC; + else continue; } - + if (_rx_len < KISS_MAX_FRAME_SIZE) { _rx_buf[_rx_len++] = b; } } + + processTx(); } void KissModem::processFrame() { if (_rx_len < 1) return; - - uint8_t cmd = _rx_buf[0]; + + uint8_t type_byte = _rx_buf[0]; + + if (type_byte == KISS_CMD_RETURN) return; + + uint8_t port = (type_byte >> 4) & 0x0F; + uint8_t cmd = type_byte & 0x0F; + + if (port != 0) return; + const uint8_t* data = &_rx_buf[1]; uint16_t data_len = _rx_len - 1; - + switch (cmd) { - case CMD_DATA: - if (data_len < 2) { - writeErrorFrame(ERR_INVALID_LENGTH); - } else if (data_len > KISS_MAX_PACKET_SIZE) { - writeErrorFrame(ERR_INVALID_LENGTH); - } else if (_has_pending_tx) { - writeErrorFrame(ERR_TX_PENDING); - } else { + case KISS_CMD_DATA: + if (data_len > 0 && data_len <= KISS_MAX_PACKET_SIZE && !_has_pending_tx) { memcpy(_pending_tx, data, data_len); _pending_tx_len = data_len; _has_pending_tx = true; } break; - case CMD_GET_IDENTITY: + + case KISS_CMD_TXDELAY: + if (data_len >= 1) _txdelay = data[0]; + break; + + case KISS_CMD_PERSISTENCE: + if (data_len >= 1) _persistence = data[0]; + break; + + case KISS_CMD_SLOTTIME: + if (data_len >= 1) _slottime = data[0]; + break; + + case KISS_CMD_TXTAIL: + if (data_len >= 1) _txtail = data[0]; + break; + + case KISS_CMD_FULLDUPLEX: + if (data_len >= 1) _fullduplex = data[0]; + break; + + case KISS_CMD_SETHARDWARE: + if (data_len >= 1) { + handleHardwareCommand(data[0], data + 1, data_len - 1); + } + break; + + default: + break; + } +} + +void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { + switch (sub_cmd) { + case HW_CMD_GET_IDENTITY: handleGetIdentity(); break; - case CMD_GET_RANDOM: - handleGetRandom(data, data_len); + case HW_CMD_GET_RANDOM: + handleGetRandom(data, len); break; - case CMD_VERIFY_SIGNATURE: - handleVerifySignature(data, data_len); + case HW_CMD_VERIFY_SIGNATURE: + handleVerifySignature(data, len); break; - case CMD_SIGN_DATA: - handleSignData(data, data_len); + case HW_CMD_SIGN_DATA: + handleSignData(data, len); break; - case CMD_ENCRYPT_DATA: - handleEncryptData(data, data_len); + case HW_CMD_ENCRYPT_DATA: + handleEncryptData(data, len); break; - case CMD_DECRYPT_DATA: - handleDecryptData(data, data_len); + case HW_CMD_DECRYPT_DATA: + handleDecryptData(data, len); break; - case CMD_KEY_EXCHANGE: - handleKeyExchange(data, data_len); + case HW_CMD_KEY_EXCHANGE: + handleKeyExchange(data, len); break; - case CMD_HASH: - handleHash(data, data_len); + case HW_CMD_HASH: + handleHash(data, len); break; - case CMD_SET_RADIO: - handleSetRadio(data, data_len); + case HW_CMD_SET_RADIO: + handleSetRadio(data, len); break; - case CMD_SET_TX_POWER: - handleSetTxPower(data, data_len); + case HW_CMD_SET_TX_POWER: + handleSetTxPower(data, len); break; - case CMD_GET_RADIO: + case HW_CMD_GET_RADIO: handleGetRadio(); break; - case CMD_GET_TX_POWER: + case HW_CMD_GET_TX_POWER: handleGetTxPower(); break; - case CMD_GET_VERSION: + case HW_CMD_GET_VERSION: handleGetVersion(); break; - case CMD_GET_CURRENT_RSSI: + case HW_CMD_GET_CURRENT_RSSI: handleGetCurrentRssi(); break; - case CMD_IS_CHANNEL_BUSY: + case HW_CMD_IS_CHANNEL_BUSY: handleIsChannelBusy(); break; - case CMD_GET_AIRTIME: - handleGetAirtime(data, data_len); + case HW_CMD_GET_AIRTIME: + handleGetAirtime(data, len); break; - case CMD_GET_NOISE_FLOOR: + case HW_CMD_GET_NOISE_FLOOR: handleGetNoiseFloor(); break; - case CMD_GET_STATS: + case HW_CMD_GET_STATS: handleGetStats(); break; - case CMD_GET_BATTERY: + case HW_CMD_GET_BATTERY: handleGetBattery(); break; - case CMD_PING: + case HW_CMD_PING: handlePing(); break; - case CMD_GET_SENSORS: - handleGetSensors(data, data_len); + case HW_CMD_GET_SENSORS: + handleGetSensors(data, len); break; default: - writeErrorFrame(ERR_UNKNOWN_CMD); + writeHardwareError(HW_ERR_UNKNOWN_CMD); + break; + } +} + +void KissModem::processTx() { + switch (_tx_state) { + case TX_IDLE: + if (_has_pending_tx) { + if (_fullduplex) { + _tx_timer = millis(); + _tx_state = TX_DELAY; + } else { + _tx_state = TX_WAIT_CLEAR; + } + } + break; + + case TX_WAIT_CLEAR: + if (!_radio.isReceiving()) { + uint8_t rand_val; + _rng.random(&rand_val, 1); + if (rand_val <= _persistence) { + _tx_timer = millis(); + _tx_state = TX_DELAY; + } else { + _tx_timer = millis(); + _tx_state = TX_SLOT_WAIT; + } + } + break; + + case TX_SLOT_WAIT: + if (millis() - _tx_timer >= (uint32_t)_slottime * 10) { + _tx_state = TX_WAIT_CLEAR; + } + break; + + case TX_DELAY: + if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) { + if (_sendPacketCallback) { + _sendPacketCallback(_pending_tx, _pending_tx_len); + _tx_state = TX_SENDING; + } else { + _has_pending_tx = false; + _tx_state = TX_IDLE; + } + } + break; + + case TX_SENDING: + if (_isSendCompleteCallback && _isSendCompleteCallback()) { + if (_onSendFinishedCallback) _onSendFinishedCallback(); + uint8_t result = 0x01; + writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); + _has_pending_tx = false; + _tx_state = TX_IDLE; + } break; } } +void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { + writeFrame(KISS_CMD_DATA, packet, len); + uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; + writeHardwareFrame(HW_RESP_RX_META, meta, 2); +} + void KissModem::handleGetIdentity() { - writeFrame(RESP_IDENTITY, _identity.pub_key, PUB_KEY_SIZE); + writeHardwareFrame(HW_RESP_IDENTITY, _identity.pub_key, PUB_KEY_SIZE); } void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) { if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + uint8_t requested = data[0]; if (requested < 1 || requested > 64) { - writeErrorFrame(ERR_INVALID_PARAM); + writeHardwareError(HW_ERR_INVALID_PARAM); return; } - + uint8_t buf[64]; _rng.random(buf, requested); - writeFrame(RESP_RANDOM, buf, requested); + writeHardwareFrame(HW_RESP_RANDOM, buf, requested); } void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) { if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + mesh::Identity signer(data); const uint8_t* signature = data + PUB_KEY_SIZE; const uint8_t* msg = data + PUB_KEY_SIZE + SIGNATURE_SIZE; uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE; - + uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00; - writeFrame(RESP_VERIFY, &result, 1); + writeHardwareFrame(HW_RESP_VERIFY, &result, 1); } void KissModem::handleSignData(const uint8_t* data, uint16_t len) { if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + uint8_t signature[SIGNATURE_SIZE]; _identity.sign(signature, data, len); - writeFrame(RESP_SIGNATURE, signature, SIGNATURE_SIZE); + writeHardwareFrame(HW_RESP_SIGNATURE, signature, SIGNATURE_SIZE); } void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) { if (len < PUB_KEY_SIZE + 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + const uint8_t* key = data; const uint8_t* plaintext = data + PUB_KEY_SIZE; uint16_t plaintext_len = len - PUB_KEY_SIZE; - + uint8_t buf[KISS_MAX_FRAME_SIZE]; int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len); - + if (encrypted_len > 0) { - writeFrame(RESP_ENCRYPTED, buf, encrypted_len); + writeHardwareFrame(HW_RESP_ENCRYPTED, buf, encrypted_len); } else { - writeErrorFrame(ERR_ENCRYPT_FAILED); + writeHardwareError(HW_ERR_ENCRYPT_FAILED); } } void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) { if (len < PUB_KEY_SIZE + CIPHER_MAC_SIZE + 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + const uint8_t* key = data; const uint8_t* ciphertext = data + PUB_KEY_SIZE; uint16_t ciphertext_len = len - PUB_KEY_SIZE; - + uint8_t buf[KISS_MAX_FRAME_SIZE]; int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len); - + if (decrypted_len > 0) { - writeFrame(RESP_DECRYPTED, buf, decrypted_len); + writeHardwareFrame(HW_RESP_DECRYPTED, buf, decrypted_len); } else { - writeErrorFrame(ERR_MAC_FAILED); + writeHardwareError(HW_ERR_MAC_FAILED); } } void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) { if (len < PUB_KEY_SIZE) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + uint8_t shared_secret[PUB_KEY_SIZE]; _identity.calcSharedSecret(shared_secret, data); - writeFrame(RESP_SHARED_SECRET, shared_secret, PUB_KEY_SIZE); + writeHardwareFrame(HW_RESP_SHARED_SECRET, shared_secret, PUB_KEY_SIZE); } void KissModem::handleHash(const uint8_t* data, uint16_t len) { if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + uint8_t hash[32]; mesh::Utils::sha256(hash, 32, data, len); - writeFrame(RESP_HASH, hash, 32); -} - -bool KissModem::getPacketToSend(uint8_t* packet, uint16_t* len) { - if (!_has_pending_tx) return false; - - memcpy(packet, _pending_tx, _pending_tx_len); - *len = _pending_tx_len; - _has_pending_tx = false; - return true; -} - -void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { - uint8_t buf[2 + KISS_MAX_PACKET_SIZE]; - buf[0] = (uint8_t)snr; - buf[1] = (uint8_t)rssi; - memcpy(&buf[2], packet, len); - writeFrame(CMD_DATA, buf, 2 + len); + writeHardwareFrame(HW_RESP_HASH, hash, 32); } void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) { if (len < 10) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } if (!_setRadioCallback) { - writeErrorFrame(ERR_NO_CALLBACK); + writeHardwareError(HW_ERR_NO_CALLBACK); return; } - + uint32_t freq_hz, bw_hz; memcpy(&freq_hz, data, 4); memcpy(&bw_hz, data + 4, 4); uint8_t sf = data[8]; uint8_t cr = data[9]; - + _config.freq_hz = freq_hz; _config.bw_hz = bw_hz; _config.sf = sf; _config.cr = cr; - + float freq = freq_hz / 1000000.0f; float bw = bw_hz / 1000.0f; - + _setRadioCallback(freq, bw, sf, cr); - writeFrame(RESP_OK, nullptr, 0); + writeHardwareFrame(HW_RESP_OK, nullptr, 0); } void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) { if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } if (!_setTxPowerCallback) { - writeErrorFrame(ERR_NO_CALLBACK); + writeHardwareError(HW_ERR_NO_CALLBACK); return; } - + _config.tx_power = data[0]; _setTxPowerCallback(data[0]); - writeFrame(RESP_OK, nullptr, 0); + writeHardwareFrame(HW_RESP_OK, nullptr, 0); } void KissModem::handleGetRadio() { @@ -346,92 +452,87 @@ void KissModem::handleGetRadio() { memcpy(buf + 4, &_config.bw_hz, 4); buf[8] = _config.sf; buf[9] = _config.cr; - writeFrame(RESP_RADIO, buf, 10); + writeHardwareFrame(HW_RESP_RADIO, buf, 10); } void KissModem::handleGetTxPower() { - writeFrame(RESP_TX_POWER, &_config.tx_power, 1); + writeHardwareFrame(HW_RESP_TX_POWER, &_config.tx_power, 1); } void KissModem::handleGetVersion() { uint8_t buf[2]; buf[0] = KISS_FIRMWARE_VERSION; buf[1] = 0; - writeFrame(RESP_VERSION, buf, 2); -} - -void KissModem::onTxComplete(bool success) { - uint8_t result = success ? 0x01 : 0x00; - writeFrame(RESP_TX_DONE, &result, 1); + writeHardwareFrame(HW_RESP_VERSION, buf, 2); } void KissModem::handleGetCurrentRssi() { if (!_getCurrentRssiCallback) { - writeErrorFrame(ERR_NO_CALLBACK); + writeHardwareError(HW_ERR_NO_CALLBACK); return; } - + float rssi = _getCurrentRssiCallback(); int8_t rssi_byte = (int8_t)rssi; - writeFrame(RESP_CURRENT_RSSI, (uint8_t*)&rssi_byte, 1); + writeHardwareFrame(HW_RESP_CURRENT_RSSI, (uint8_t*)&rssi_byte, 1); } void KissModem::handleIsChannelBusy() { uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00; - writeFrame(RESP_CHANNEL_BUSY, &busy, 1); + writeHardwareFrame(HW_RESP_CHANNEL_BUSY, &busy, 1); } void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) { if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + uint8_t packet_len = data[0]; uint32_t airtime = _radio.getEstAirtimeFor(packet_len); - writeFrame(RESP_AIRTIME, (uint8_t*)&airtime, 4); + writeHardwareFrame(HW_RESP_AIRTIME, (uint8_t*)&airtime, 4); } void KissModem::handleGetNoiseFloor() { int16_t noise_floor = _radio.getNoiseFloor(); - writeFrame(RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2); + writeHardwareFrame(HW_RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2); } void KissModem::handleGetStats() { if (!_getStatsCallback) { - writeErrorFrame(ERR_NO_CALLBACK); + writeHardwareError(HW_ERR_NO_CALLBACK); return; } - + uint32_t rx, tx, errors; _getStatsCallback(&rx, &tx, &errors); uint8_t buf[12]; memcpy(buf, &rx, 4); memcpy(buf + 4, &tx, 4); memcpy(buf + 8, &errors, 4); - writeFrame(RESP_STATS, buf, 12); + writeHardwareFrame(HW_RESP_STATS, buf, 12); } void KissModem::handleGetBattery() { uint16_t mv = _board.getBattMilliVolts(); - writeFrame(RESP_BATTERY, (uint8_t*)&mv, 2); + writeHardwareFrame(HW_RESP_BATTERY, (uint8_t*)&mv, 2); } void KissModem::handlePing() { - writeFrame(RESP_PONG, nullptr, 0); + writeHardwareFrame(HW_RESP_PONG, nullptr, 0); } void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); + writeHardwareError(HW_ERR_INVALID_LENGTH); return; } - + uint8_t permissions = data[0]; CayenneLPP telemetry(255); if (_sensors.querySensors(permissions, telemetry)) { - writeFrame(RESP_SENSORS, telemetry.getBuffer(), telemetry.getSize()); + writeHardwareFrame(HW_RESP_SENSORS, telemetry.getBuffer(), telemetry.getSize()); } else { - writeFrame(RESP_SENSORS, nullptr, 0); + writeHardwareFrame(HW_RESP_SENSORS, nullptr, 0); } } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 170bb0c2a..98f3c3003 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -11,62 +11,74 @@ #define KISS_TFEND 0xDC #define KISS_TFESC 0xDD -#define KISS_MAX_FRAME_SIZE 512 +#define KISS_MAX_FRAME_SIZE 512 #define KISS_MAX_PACKET_SIZE 255 -#define CMD_DATA 0x00 -#define CMD_GET_IDENTITY 0x01 -#define CMD_GET_RANDOM 0x02 -#define CMD_VERIFY_SIGNATURE 0x03 -#define CMD_SIGN_DATA 0x04 -#define CMD_ENCRYPT_DATA 0x05 -#define CMD_DECRYPT_DATA 0x06 -#define CMD_KEY_EXCHANGE 0x07 -#define CMD_HASH 0x08 -#define CMD_SET_RADIO 0x09 -#define CMD_SET_TX_POWER 0x0A -#define CMD_GET_RADIO 0x0C -#define CMD_GET_TX_POWER 0x0D -#define CMD_GET_VERSION 0x0F -#define CMD_GET_CURRENT_RSSI 0x10 -#define CMD_IS_CHANNEL_BUSY 0x11 -#define CMD_GET_AIRTIME 0x12 -#define CMD_GET_NOISE_FLOOR 0x13 -#define CMD_GET_STATS 0x14 -#define CMD_GET_BATTERY 0x15 -#define CMD_PING 0x16 -#define CMD_GET_SENSORS 0x17 - -#define RESP_IDENTITY 0x21 -#define RESP_RANDOM 0x22 -#define RESP_VERIFY 0x23 -#define RESP_SIGNATURE 0x24 -#define RESP_ENCRYPTED 0x25 -#define RESP_DECRYPTED 0x26 -#define RESP_SHARED_SECRET 0x27 -#define RESP_HASH 0x28 -#define RESP_OK 0x29 -#define RESP_RADIO 0x2A -#define RESP_TX_POWER 0x2B -#define RESP_VERSION 0x2D -#define RESP_ERROR 0x2E -#define RESP_TX_DONE 0x2F -#define RESP_CURRENT_RSSI 0x30 -#define RESP_CHANNEL_BUSY 0x31 -#define RESP_AIRTIME 0x32 -#define RESP_NOISE_FLOOR 0x33 -#define RESP_STATS 0x34 -#define RESP_BATTERY 0x35 -#define RESP_PONG 0x36 -#define RESP_SENSORS 0x37 - -#define ERR_INVALID_LENGTH 0x01 -#define ERR_INVALID_PARAM 0x02 -#define ERR_NO_CALLBACK 0x03 -#define ERR_MAC_FAILED 0x04 -#define ERR_UNKNOWN_CMD 0x05 -#define ERR_ENCRYPT_FAILED 0x06 -#define ERR_TX_PENDING 0x07 +#define KISS_CMD_DATA 0x00 +#define KISS_CMD_TXDELAY 0x01 +#define KISS_CMD_PERSISTENCE 0x02 +#define KISS_CMD_SLOTTIME 0x03 +#define KISS_CMD_TXTAIL 0x04 +#define KISS_CMD_FULLDUPLEX 0x05 +#define KISS_CMD_SETHARDWARE 0x06 +#define KISS_CMD_RETURN 0xFF + +#define KISS_DEFAULT_TXDELAY 50 +#define KISS_DEFAULT_PERSISTENCE 63 +#define KISS_DEFAULT_SLOTTIME 10 + +#define HW_CMD_GET_IDENTITY 0x01 +#define HW_CMD_GET_RANDOM 0x02 +#define HW_CMD_VERIFY_SIGNATURE 0x03 +#define HW_CMD_SIGN_DATA 0x04 +#define HW_CMD_ENCRYPT_DATA 0x05 +#define HW_CMD_DECRYPT_DATA 0x06 +#define HW_CMD_KEY_EXCHANGE 0x07 +#define HW_CMD_HASH 0x08 +#define HW_CMD_SET_RADIO 0x09 +#define HW_CMD_SET_TX_POWER 0x0A +#define HW_CMD_GET_RADIO 0x0C +#define HW_CMD_GET_TX_POWER 0x0D +#define HW_CMD_GET_VERSION 0x0F +#define HW_CMD_GET_CURRENT_RSSI 0x10 +#define HW_CMD_IS_CHANNEL_BUSY 0x11 +#define HW_CMD_GET_AIRTIME 0x12 +#define HW_CMD_GET_NOISE_FLOOR 0x13 +#define HW_CMD_GET_STATS 0x14 +#define HW_CMD_GET_BATTERY 0x15 +#define HW_CMD_PING 0x16 +#define HW_CMD_GET_SENSORS 0x17 + +#define HW_RESP_IDENTITY 0x21 +#define HW_RESP_RANDOM 0x22 +#define HW_RESP_VERIFY 0x23 +#define HW_RESP_SIGNATURE 0x24 +#define HW_RESP_ENCRYPTED 0x25 +#define HW_RESP_DECRYPTED 0x26 +#define HW_RESP_SHARED_SECRET 0x27 +#define HW_RESP_HASH 0x28 +#define HW_RESP_OK 0x29 +#define HW_RESP_RADIO 0x2A +#define HW_RESP_TX_POWER 0x2B +#define HW_RESP_VERSION 0x2D +#define HW_RESP_ERROR 0x2E +#define HW_RESP_TX_DONE 0x2F +#define HW_RESP_CURRENT_RSSI 0x30 +#define HW_RESP_CHANNEL_BUSY 0x31 +#define HW_RESP_AIRTIME 0x32 +#define HW_RESP_NOISE_FLOOR 0x33 +#define HW_RESP_STATS 0x34 +#define HW_RESP_BATTERY 0x35 +#define HW_RESP_PONG 0x36 +#define HW_RESP_SENSORS 0x37 +#define HW_RESP_RX_META 0x38 + +#define HW_ERR_INVALID_LENGTH 0x01 +#define HW_ERR_INVALID_PARAM 0x02 +#define HW_ERR_NO_CALLBACK 0x03 +#define HW_ERR_MAC_FAILED 0x04 +#define HW_ERR_UNKNOWN_CMD 0x05 +#define HW_ERR_ENCRYPT_FAILED 0x06 #define KISS_FIRMWARE_VERSION 1 @@ -74,6 +86,9 @@ typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); typedef void (*SetTxPowerCallback)(uint8_t power); typedef float (*GetCurrentRssiCallback)(); typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors); +typedef void (*SendPacketCallback)(const uint8_t* data, uint16_t len); +typedef bool (*IsSendCompleteCallback)(); +typedef void (*OnSendFinishedCallback)(); struct RadioConfig { uint32_t freq_hz; @@ -83,6 +98,14 @@ struct RadioConfig { uint8_t tx_power; }; +enum TxState { + TX_IDLE, + TX_WAIT_CLEAR, + TX_SLOT_WAIT, + TX_DELAY, + TX_SENDING +}; + class KissModem { Stream& _serial; mesh::LocalIdentity& _identity; @@ -90,28 +113,43 @@ class KissModem { mesh::Radio& _radio; mesh::MainBoard& _board; SensorManager& _sensors; - + uint8_t _rx_buf[KISS_MAX_FRAME_SIZE]; uint16_t _rx_len; bool _rx_escaped; bool _rx_active; - + uint8_t _pending_tx[KISS_MAX_PACKET_SIZE]; uint16_t _pending_tx_len; bool _has_pending_tx; + uint8_t _txdelay; + uint8_t _persistence; + uint8_t _slottime; + uint8_t _txtail; + uint8_t _fullduplex; + + TxState _tx_state; + uint32_t _tx_timer; + SetRadioCallback _setRadioCallback; SetTxPowerCallback _setTxPowerCallback; GetCurrentRssiCallback _getCurrentRssiCallback; GetStatsCallback _getStatsCallback; - + SendPacketCallback _sendPacketCallback; + IsSendCompleteCallback _isSendCompleteCallback; + OnSendFinishedCallback _onSendFinishedCallback; + RadioConfig _config; void writeByte(uint8_t b); - void writeFrame(uint8_t cmd, const uint8_t* data, uint16_t len); - void writeErrorFrame(uint8_t error_code); + 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); + void writeHardwareError(uint8_t error_code); void processFrame(); - + void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len); + void processTx(); + void handleGetIdentity(); void handleGetRandom(const uint8_t* data, uint16_t len); void handleVerifySignature(const uint8_t* data, uint16_t len); @@ -137,16 +175,18 @@ class KissModem { public: KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng, mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors); - + void begin(); void loop(); - + void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; } void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; } void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; } - - bool getPacketToSend(uint8_t* packet, uint16_t* len); + void setSendPacketCallback(SendPacketCallback cb) { _sendPacketCallback = cb; } + void setIsSendCompleteCallback(IsSendCompleteCallback cb) { _isSendCompleteCallback = cb; } + void setOnSendFinishedCallback(OnSendFinishedCallback cb) { _onSendFinishedCallback = cb; } + void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len); - void onTxComplete(bool success); + bool isTxBusy() const { return _tx_state != TX_IDLE; } }; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 3a610460d..160adc691 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -12,14 +12,9 @@ #include #endif -#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 -#define AGC_RESET_INTERVAL_MS 30000 - 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) ; @@ -67,6 +62,18 @@ void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) { *errors = radio_driver.getPacketsRecvErrors(); } +void onSendPacket(const uint8_t* data, uint16_t len) { + radio_driver.startSendRaw(data, len); +} + +bool onIsSendComplete() { + return radio_driver.isSendComplete(); +} + +void onSendFinished() { + radio_driver.onSendFinished(); +} + void setup() { board.begin(); @@ -91,40 +98,25 @@ void setup() { modem->setTxPowerCallback(onSetTxPower); modem->setGetCurrentRssiCallback(onGetCurrentRssi); modem->setGetStatsCallback(onGetStats); + modem->setSendPacketCallback(onSendPacket); + modem->setIsSendCompleteCallback(onIsSendComplete); + modem->setOnSendFinishedCallback(onSendFinished); modem->begin(); } void loop() { modem->loop(); - uint8_t packet[KISS_MAX_PACKET_SIZE]; - uint16_t len; + if (!modem->isTxBusy()) { + uint8_t rx_buf[256]; + int rx_len = radio_driver.recvRaw(rx_buf, sizeof(rx_buf)); - // trigger noise floor calibration - if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { - radio_driver.triggerNoiseFloorCalibrate(0); - next_noise_floor_calib_ms = millis(); - } - radio_driver.loop(); - - if (modem->getPacketToSend(packet, &len)) { - radio_driver.startSendRaw(packet, len); - while (!radio_driver.isSendComplete()) { - delay(1); + 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.onSendFinished(); - modem->onTxComplete(true); } - if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { - radio_driver.resetAGC(); - 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/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 6e96018bc..fe2f546ee 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -107,4 +107,13 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} - +<../examples/simple_room_server/*.cpp> \ No newline at end of file + +<../examples/simple_room_server/*.cpp> + +[env:Xiao_nrf52_kiss_modem] +extends = Xiao_nrf52 +build_flags = + ${Xiao_nrf52.build_flags} +build_src_filter = ${Xiao_nrf52.build_src_filter} + +<../examples/kiss_modem/*.cpp> +lib_deps = + ${Xiao_nrf52.lib_deps} \ No newline at end of file From f78617dbdb9d2c8332f59633509685eebd80f9a6 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 6 Feb 2026 11:13:28 +0100 Subject: [PATCH 02/13] Add periodic noise floor calibration and AGC reset --- examples/kiss_modem/main.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 160adc691..62c1658f7 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 +#define AGC_RESET_INTERVAL_MS 30000 + 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) ; @@ -107,16 +112,24 @@ void setup() { void loop() { modem->loop(); + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { + radio_driver.triggerNoiseFloorCalibrate(0); + next_noise_floor_calib_ms = millis(); + } + radio_driver.loop(); + if (!modem->isTxBusy()) { + if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { + radio_driver.resetAGC(); + 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(); } From 203d86f87d7ccbd40997050e57af8815ae55917c Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 6 Feb 2026 11:19:16 +0100 Subject: [PATCH 03/13] Update documentation. --- docs/kiss_modem_protocol.md | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index e042053c6..9d8c31c65 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -168,6 +168,38 @@ All values little-endian. | SF | 1 byte | Spreading factor (5-12) | | CR | 1 byte | Coding rate (5-8) | +### Version (Version response) + +| Field | Size | Description | +|-------|------|-------------| +| Version | 1 byte | Firmware version | +| Reserved | 1 byte | Always 0 | + +### Encrypted (Encrypted response) + +| Field | Size | Description | +|-------|------|-------------| +| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes | +| Ciphertext | variable | AES-128-CBC encrypted data | + +### Airtime (Airtime response) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds | + +### Noise Floor (NoiseFloor response) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| Noise floor | 2 bytes | int16_t, dBm (signed) | + +The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds. + ### Stats (Stats response) All values little-endian. @@ -178,6 +210,14 @@ All values little-endian. | TX | 4 bytes | Packets transmitted | | Errors | 4 bytes | Receive errors | +### Battery (Battery response) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| Millivolts | 2 bytes | uint16_t, battery voltage in mV | + ### Sensor Permissions (GetSensors) | Bit | Value | Description | @@ -192,9 +232,19 @@ Use `0x07` for all permissions. Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing. +## Cryptographic Algorithms + +| Operation | Algorithm | +|-----------|-----------| +| Identity / Signing / Verification | Ed25519 | +| Key Exchange | X25519 (ECDH) | +| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) | +| Hashing | SHA-256 | + ## Notes - Modem generates identity on first boot (stored in flash) +- All multi-byte values are little-endian unless stated otherwise - SNR values in RxMeta are multiplied by 4 for 0.25 dB precision - TxDone is sent as a SetHardware event after each transmission - Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames From 02ddc05c30e781917863e26f68920839e9cf77bb Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 6 Feb 2026 11:43:59 +0100 Subject: [PATCH 04/13] Reorganise KISS protocol to close gaps. --- docs/kiss_modem_protocol.md | 79 ++++++++++++++++++++----------- examples/kiss_modem/KissModem.cpp | 31 ++++++++++++ examples/kiss_modem/KissModem.h | 58 +++++++++++++---------- 3 files changed, 116 insertions(+), 52 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 9d8c31c65..55e89129d 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -96,17 +96,20 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The | Hash | `0x08` | Data to hash | | SetRadio | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) | | SetTxPower | `0x0A` | Power dBm (1) | -| GetRadio | `0x0C` | - | -| GetTxPower | `0x0D` | - | -| GetVersion | `0x0F` | - | -| GetCurrentRssi | `0x10` | - | -| IsChannelBusy | `0x11` | - | -| GetAirtime | `0x12` | Packet length (1) | -| GetNoiseFloor | `0x13` | - | -| GetStats | `0x14` | - | -| GetBattery | `0x15` | - | -| Ping | `0x16` | - | -| GetSensors | `0x17` | Permissions (1) | +| GetRadio | `0x0B` | - | +| GetTxPower | `0x0C` | - | +| GetCurrentRssi | `0x0D` | - | +| IsChannelBusy | `0x0E` | - | +| GetAirtime | `0x0F` | Packet length (1) | +| GetNoiseFloor | `0x10` | - | +| GetVersion | `0x11` | - | +| GetStats | `0x12` | - | +| GetBattery | `0x13` | - | +| GetMCUTemp | `0x14` | - | +| GetSensors | `0x15` | Permissions (1) | +| GetDeviceName | `0x16` | - | +| Ping | `0x17` | - | +| Reboot | `0x18` | - | ### Response Sub-commands (TNC to Host) @@ -121,20 +124,22 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The | SharedSecret | `0x27` | Shared secret (32) | | Hash | `0x28` | SHA-256 hash (32) | | OK | `0x29` | - | -| Radio | `0x2A` | Freq (4) + BW (4) + SF (1) + CR (1) | -| TxPower | `0x2B` | Power dBm (1) | -| Version | `0x2D` | Version (1) + Reserved (1) | -| Error | `0x2E` | Error code (1) | -| TxDone | `0x2F` | Result (1): 0x00=failed, 0x01=success | -| CurrentRssi | `0x30` | RSSI dBm (1, signed) | -| ChannelBusy | `0x31` | Result (1): 0x00=clear, 0x01=busy | -| Airtime | `0x32` | Milliseconds (4) | -| NoiseFloor | `0x33` | dBm (2, signed) | -| Stats | `0x34` | RX (4) + TX (4) + Errors (4) | -| Battery | `0x35` | Millivolts (2) | -| Pong | `0x36` | - | -| Sensors | `0x37` | CayenneLPP payload | -| RxMeta | `0x38` | SNR (1) + RSSI (1) | +| Error | `0x2A` | Error code (1) | +| Radio | `0x2B` | Freq (4) + BW (4) + SF (1) + CR (1) | +| TxPower | `0x2C` | Power dBm (1) | +| CurrentRssi | `0x2D` | RSSI dBm (1, signed) | +| ChannelBusy | `0x2E` | Result (1): 0x00=clear, 0x01=busy | +| Airtime | `0x2F` | Milliseconds (4) | +| NoiseFloor | `0x30` | dBm (2, signed) | +| Version | `0x31` | Version (1) + Reserved (1) | +| Stats | `0x32` | RX (4) + TX (4) + Errors (4) | +| Battery | `0x33` | Millivolts (2) | +| MCUTemp | `0x34` | Temperature (2, signed) | +| Sensors | `0x35` | CayenneLPP payload | +| DeviceName | `0x36` | Name (variable, UTF-8) | +| Pong | `0x37` | - | +| TxDone | `0x38` | Result (1): 0x00=failed, 0x01=success | +| RxMeta | `0x39` | SNR (1) + RSSI (1) | ### Error Codes @@ -151,9 +156,9 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The The TNC sends these SetHardware frames without a preceding request: -**TxDone (0x2F)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. +**TxDone (0x38)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. -**RxMeta (0x38)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Standard KISS clients ignore this frame. +**RxMeta (0x39)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Standard KISS clients ignore this frame. ## Data Formats @@ -218,6 +223,26 @@ All values little-endian. |-------|------|-------------| | Millivolts | 2 bytes | uint16_t, battery voltage in mV | +### MCU Temperature (MCUTemp response) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| Temperature | 2 bytes | int16_t, tenths of °C (e.g., 253 = 25.3°C) | + +Returns `NoCallback` error if the board does not support temperature readings. + +### Device Name (DeviceName response) + +| Field | Size | Description | +|-------|------|-------------| +| Name | variable | UTF-8 string, no null terminator | + +### Reboot + +Sends an `OK` response, flushes serial, then reboots the device. The host should expect the connection to drop. + ### Sensor Permissions (GetSensors) | Bit | Value | Description | diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index 9915ec5ec..31bb8a1e6 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -225,6 +225,15 @@ void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint case HW_CMD_GET_SENSORS: handleGetSensors(data, len); break; + case HW_CMD_GET_MCU_TEMP: + handleGetMCUTemp(); + break; + case HW_CMD_REBOOT: + handleReboot(); + break; + case HW_CMD_GET_DEVICE_NAME: + handleGetDeviceName(); + break; default: writeHardwareError(HW_ERR_UNKNOWN_CMD); break; @@ -536,3 +545,25 @@ void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { writeHardwareFrame(HW_RESP_SENSORS, nullptr, 0); } } + +void KissModem::handleGetMCUTemp() { + float temp = _board.getMCUTemperature(); + if (isnan(temp)) { + writeHardwareError(HW_ERR_NO_CALLBACK); + return; + } + int16_t temp_tenths = (int16_t)(temp * 10.0f); + writeHardwareFrame(HW_RESP_MCU_TEMP, (uint8_t*)&temp_tenths, 2); +} + +void KissModem::handleReboot() { + writeHardwareFrame(HW_RESP_OK, nullptr, 0); + _serial.flush(); + delay(50); + _board.reboot(); +} + +void KissModem::handleGetDeviceName() { + const char* name = _board.getManufacturerName(); + writeHardwareFrame(HW_RESP_DEVICE_NAME, (const uint8_t*)name, strlen(name)); +} diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 98f3c3003..6b56b91e0 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -37,17 +37,20 @@ #define HW_CMD_HASH 0x08 #define HW_CMD_SET_RADIO 0x09 #define HW_CMD_SET_TX_POWER 0x0A -#define HW_CMD_GET_RADIO 0x0C -#define HW_CMD_GET_TX_POWER 0x0D -#define HW_CMD_GET_VERSION 0x0F -#define HW_CMD_GET_CURRENT_RSSI 0x10 -#define HW_CMD_IS_CHANNEL_BUSY 0x11 -#define HW_CMD_GET_AIRTIME 0x12 -#define HW_CMD_GET_NOISE_FLOOR 0x13 -#define HW_CMD_GET_STATS 0x14 -#define HW_CMD_GET_BATTERY 0x15 -#define HW_CMD_PING 0x16 -#define HW_CMD_GET_SENSORS 0x17 +#define HW_CMD_GET_RADIO 0x0B +#define HW_CMD_GET_TX_POWER 0x0C +#define HW_CMD_GET_CURRENT_RSSI 0x0D +#define HW_CMD_IS_CHANNEL_BUSY 0x0E +#define HW_CMD_GET_AIRTIME 0x0F +#define HW_CMD_GET_NOISE_FLOOR 0x10 +#define HW_CMD_GET_VERSION 0x11 +#define HW_CMD_GET_STATS 0x12 +#define HW_CMD_GET_BATTERY 0x13 +#define HW_CMD_GET_MCU_TEMP 0x14 +#define HW_CMD_GET_SENSORS 0x15 +#define HW_CMD_GET_DEVICE_NAME 0x16 +#define HW_CMD_PING 0x17 +#define HW_CMD_REBOOT 0x18 #define HW_RESP_IDENTITY 0x21 #define HW_RESP_RANDOM 0x22 @@ -58,20 +61,22 @@ #define HW_RESP_SHARED_SECRET 0x27 #define HW_RESP_HASH 0x28 #define HW_RESP_OK 0x29 -#define HW_RESP_RADIO 0x2A -#define HW_RESP_TX_POWER 0x2B -#define HW_RESP_VERSION 0x2D -#define HW_RESP_ERROR 0x2E -#define HW_RESP_TX_DONE 0x2F -#define HW_RESP_CURRENT_RSSI 0x30 -#define HW_RESP_CHANNEL_BUSY 0x31 -#define HW_RESP_AIRTIME 0x32 -#define HW_RESP_NOISE_FLOOR 0x33 -#define HW_RESP_STATS 0x34 -#define HW_RESP_BATTERY 0x35 -#define HW_RESP_PONG 0x36 -#define HW_RESP_SENSORS 0x37 -#define HW_RESP_RX_META 0x38 +#define HW_RESP_ERROR 0x2A +#define HW_RESP_RADIO 0x2B +#define HW_RESP_TX_POWER 0x2C +#define HW_RESP_CURRENT_RSSI 0x2D +#define HW_RESP_CHANNEL_BUSY 0x2E +#define HW_RESP_AIRTIME 0x2F +#define HW_RESP_NOISE_FLOOR 0x30 +#define HW_RESP_VERSION 0x31 +#define HW_RESP_STATS 0x32 +#define HW_RESP_BATTERY 0x33 +#define HW_RESP_MCU_TEMP 0x34 +#define HW_RESP_SENSORS 0x35 +#define HW_RESP_DEVICE_NAME 0x36 +#define HW_RESP_PONG 0x37 +#define HW_RESP_TX_DONE 0x38 +#define HW_RESP_RX_META 0x39 #define HW_ERR_INVALID_LENGTH 0x01 #define HW_ERR_INVALID_PARAM 0x02 @@ -171,6 +176,9 @@ class KissModem { void handleGetBattery(); void handlePing(); void handleGetSensors(const uint8_t* data, uint16_t len); + void handleGetMCUTemp(); + void handleReboot(); + void handleGetDeviceName(); public: KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng, From 1af013c741f1461d3179162ce21ab2f6f6a26721 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 6 Feb 2026 12:36:13 +0100 Subject: [PATCH 05/13] Clarify data frame limitations in KISS modem documentation. --- docs/kiss_modem_protocol.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 55e89129d..9652f976a 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -56,7 +56,7 @@ Maximum unescaped frame size: 512 bytes. |------|-------|------|-------------| | Data | `0x00` | Raw packet | Received packet from radio | -Data frames carry raw packet data only, with no metadata prepended. +Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. ### CSMA Behavior @@ -268,6 +268,7 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs. ## Notes +- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS “1024+ recommended” (that applies to general TNCs, not MeshCore) - Modem generates identity on first boot (stored in flash) - All multi-byte values are little-endian unless stated otherwise - SNR values in RxMeta are multiplied by 4 for 0.25 dB precision From f445b5acdc17ee6664cee77d5b113e2e54412eb2 Mon Sep 17 00:00:00 2001 From: agessaman Date: Fri, 6 Feb 2026 16:31:20 -0800 Subject: [PATCH 06/13] fix(kiss_modem): improve RX delivery and noise floor sampling --- examples/kiss_modem/KissModem.cpp | 5 +++++ examples/kiss_modem/KissModem.h | 2 ++ examples/kiss_modem/main.cpp | 32 +++++++++++++++++++++---------- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index 31bb8a1e6..94e4fe839 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -99,6 +99,11 @@ void KissModem::loop() { if (_rx_len < KISS_MAX_FRAME_SIZE) { _rx_buf[_rx_len++] = b; + } else { + /* Buffer full with no FEND; reset so we don't stay stuck ignoring input. */ + _rx_len = 0; + _rx_escaped = false; + _rx_active = false; } } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 6b56b91e0..674b68f76 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -197,4 +197,6 @@ class KissModem { void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len); bool isTxBusy() const { return _tx_state != TX_IDLE; } + /** True only when radio is actually transmitting; use to skip recvRaw in main loop. */ + bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; } }; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 62c1658f7..adb201460 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -112,16 +112,12 @@ void setup() { void loop() { modem->loop(); - if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { - radio_driver.triggerNoiseFloorCalibrate(0); - next_noise_floor_calib_ms = millis(); - } - radio_driver.loop(); - - if (!modem->isTxBusy()) { - if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { - radio_driver.resetAGC(); - next_agc_reset_ms = millis(); + if (!modem->isActuallyTransmitting()) { + if (!modem->isTxBusy()) { + if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { + radio_driver.resetAGC(); + next_agc_reset_ms = millis(); + } } uint8_t rx_buf[256]; @@ -131,5 +127,21 @@ void loop() { int8_t rssi = (int8_t)radio_driver.getLastRSSI(); modem->onPacketReceived(snr, rssi, rx_buf, rx_len); } + /* Sample noise floor right after drain: we're in STATE_RX so wrapper can collect. */ + for (int i = 0; i < 16; i++) { + radio_driver.loop(); + } + } + + /* Trigger starts a new 64-sample calibration window every 2s. */ + if ((uint32_t)(millis() - next_noise_floor_calib_ms) >= NOISE_FLOOR_CALIB_INTERVAL_MS) { + radio_driver.triggerNoiseFloorCalibrate(0); + next_noise_floor_calib_ms = millis(); + } + radio_driver.loop(); + if (!modem->isActuallyTransmitting()) { + for (int i = 0; i < 15; i++) { + radio_driver.loop(); + } } } From 49e7516145a750f0c0d11f6cdf5ddcc8973fb6ae Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 6 Feb 2026 14:17:54 +0100 Subject: [PATCH 07/13] Add KISS UART support --- examples/kiss_modem/main.cpp | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index adb201460..514897c3b 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -11,6 +11,9 @@ #elif defined(ESP32) #include #endif +#if defined(KISS_UART_RX) && defined(KISS_UART_TX) + #include +#endif #define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 #define AGC_RESET_INTERVAL_MS 30000 @@ -91,14 +94,35 @@ void setup() { rng.begin(radio_get_rng_seed()); loadOrCreateIdentity(); + sensors.begin(); + +#if defined(KISS_UART_RX) && defined(KISS_UART_TX) +#if defined(ESP32) + Serial1.setPins(KISS_UART_RX, KISS_UART_TX); + Serial1.begin(115200); +#elif defined(NRF52_PLATFORM) + ((Uart *)&Serial1)->setPins(KISS_UART_RX, KISS_UART_TX); + Serial1.begin(115200); +#elif defined(RP2040_PLATFORM) + ((SerialUART *)&Serial1)->setRX(KISS_UART_RX); + ((SerialUART *)&Serial1)->setTX(KISS_UART_TX); + Serial1.begin(115200); +#elif defined(STM32_PLATFORM) + ((HardwareSerial *)&Serial1)->setRx(KISS_UART_RX); + ((HardwareSerial *)&Serial1)->setTx(KISS_UART_TX); + Serial1.begin(115200); +#else + #error "KISS UART not supported on this platform" +#endif + modem = new KissModem(Serial1, identity, rng, radio_driver, board, sensors); +#else Serial.begin(115200); uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); - - sensors.begin(); - modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); +#endif + modem->setRadioCallback(onSetRadio); modem->setTxPowerCallback(onSetTxPower); modem->setGetCurrentRssiCallback(onGetCurrentRssi); From 7982d1ce1f0da6a97620a0fe4a223e1a77cf6dfc Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 7 Feb 2026 10:21:32 +0100 Subject: [PATCH 08/13] Use high-bit convention for hardware response codes --- examples/kiss_modem/KissModem.h | 57 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 674b68f76..f364f3ce5 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -52,31 +52,38 @@ #define HW_CMD_PING 0x17 #define HW_CMD_REBOOT 0x18 -#define HW_RESP_IDENTITY 0x21 -#define HW_RESP_RANDOM 0x22 -#define HW_RESP_VERIFY 0x23 -#define HW_RESP_SIGNATURE 0x24 -#define HW_RESP_ENCRYPTED 0x25 -#define HW_RESP_DECRYPTED 0x26 -#define HW_RESP_SHARED_SECRET 0x27 -#define HW_RESP_HASH 0x28 -#define HW_RESP_OK 0x29 -#define HW_RESP_ERROR 0x2A -#define HW_RESP_RADIO 0x2B -#define HW_RESP_TX_POWER 0x2C -#define HW_RESP_CURRENT_RSSI 0x2D -#define HW_RESP_CHANNEL_BUSY 0x2E -#define HW_RESP_AIRTIME 0x2F -#define HW_RESP_NOISE_FLOOR 0x30 -#define HW_RESP_VERSION 0x31 -#define HW_RESP_STATS 0x32 -#define HW_RESP_BATTERY 0x33 -#define HW_RESP_MCU_TEMP 0x34 -#define HW_RESP_SENSORS 0x35 -#define HW_RESP_DEVICE_NAME 0x36 -#define HW_RESP_PONG 0x37 -#define HW_RESP_TX_DONE 0x38 -#define HW_RESP_RX_META 0x39 +/* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */ +#define HW_RESP(cmd) ((cmd) | 0x80) + +#define HW_RESP_IDENTITY HW_RESP(HW_CMD_GET_IDENTITY) /* 0x81 */ +#define HW_RESP_RANDOM HW_RESP(HW_CMD_GET_RANDOM) /* 0x82 */ +#define HW_RESP_VERIFY HW_RESP(HW_CMD_VERIFY_SIGNATURE) /* 0x83 */ +#define HW_RESP_SIGNATURE HW_RESP(HW_CMD_SIGN_DATA) /* 0x84 */ +#define HW_RESP_ENCRYPTED HW_RESP(HW_CMD_ENCRYPT_DATA) /* 0x85 */ +#define HW_RESP_DECRYPTED HW_RESP(HW_CMD_DECRYPT_DATA) /* 0x86 */ +#define HW_RESP_SHARED_SECRET HW_RESP(HW_CMD_KEY_EXCHANGE) /* 0x87 */ +#define HW_RESP_HASH HW_RESP(HW_CMD_HASH) /* 0x88 */ +#define HW_RESP_RADIO HW_RESP(HW_CMD_GET_RADIO) /* 0x8B */ +#define HW_RESP_TX_POWER HW_RESP(HW_CMD_GET_TX_POWER) /* 0x8C */ +#define HW_RESP_CURRENT_RSSI HW_RESP(HW_CMD_GET_CURRENT_RSSI) /* 0x8D */ +#define HW_RESP_CHANNEL_BUSY HW_RESP(HW_CMD_IS_CHANNEL_BUSY) /* 0x8E */ +#define HW_RESP_AIRTIME HW_RESP(HW_CMD_GET_AIRTIME) /* 0x8F */ +#define HW_RESP_NOISE_FLOOR HW_RESP(HW_CMD_GET_NOISE_FLOOR) /* 0x90 */ +#define HW_RESP_VERSION HW_RESP(HW_CMD_GET_VERSION) /* 0x91 */ +#define HW_RESP_STATS HW_RESP(HW_CMD_GET_STATS) /* 0x92 */ +#define HW_RESP_BATTERY HW_RESP(HW_CMD_GET_BATTERY) /* 0x93 */ +#define HW_RESP_MCU_TEMP HW_RESP(HW_CMD_GET_MCU_TEMP) /* 0x94 */ +#define HW_RESP_SENSORS HW_RESP(HW_CMD_GET_SENSORS) /* 0x95 */ +#define HW_RESP_DEVICE_NAME HW_RESP(HW_CMD_GET_DEVICE_NAME) /* 0x96 */ +#define HW_RESP_PONG HW_RESP(HW_CMD_PING) /* 0x97 */ + +/* Generic responses (shared by multiple commands) */ +#define HW_RESP_OK 0xF0 +#define HW_RESP_ERROR 0xF1 + +/* Unsolicited notifications (no corresponding request) */ +#define HW_RESP_TX_DONE 0xF8 +#define HW_RESP_RX_META 0xF9 #define HW_ERR_INVALID_LENGTH 0x01 #define HW_ERR_INVALID_PARAM 0x02 From 5ccd99e25f6926e515280c5bb1d154d305948d48 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 7 Feb 2026 10:21:36 +0100 Subject: [PATCH 09/13] Add toggleable per-packet signal reporting --- examples/kiss_modem/KissModem.cpp | 28 ++++++++++++++++++++++++++-- examples/kiss_modem/KissModem.h | 6 ++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index 94e4fe839..ebe2e98f6 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -24,6 +24,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _isSendCompleteCallback = nullptr; _onSendFinishedCallback = nullptr; _config = {0, 0, 0, 0, 0}; + _signal_report_enabled = true; } void KissModem::begin() { @@ -239,6 +240,12 @@ void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint case HW_CMD_GET_DEVICE_NAME: handleGetDeviceName(); break; + case HW_CMD_SET_SIGNAL_REPORT: + handleSetSignalReport(data, len); + break; + case HW_CMD_GET_SIGNAL_REPORT: + handleGetSignalReport(); + break; default: writeHardwareError(HW_ERR_UNKNOWN_CMD); break; @@ -304,8 +311,10 @@ void KissModem::processTx() { void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { writeFrame(KISS_CMD_DATA, packet, len); - uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; - writeHardwareFrame(HW_RESP_RX_META, meta, 2); + if (_signal_report_enabled) { + uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; + writeHardwareFrame(HW_RESP_RX_META, meta, 2); + } } void KissModem::handleGetIdentity() { @@ -572,3 +581,18 @@ void KissModem::handleGetDeviceName() { const char* name = _board.getManufacturerName(); writeHardwareFrame(HW_RESP_DEVICE_NAME, (const uint8_t*)name, strlen(name)); } + +void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeHardwareError(HW_ERR_INVALID_LENGTH); + return; + } + _signal_report_enabled = (data[0] != 0x00); + uint8_t val = _signal_report_enabled ? 0x01 : 0x00; + writeHardwareFrame(HW_RESP_SIGNAL_REPORT, &val, 1); +} + +void KissModem::handleGetSignalReport() { + uint8_t val = _signal_report_enabled ? 0x01 : 0x00; + writeHardwareFrame(HW_RESP_SIGNAL_REPORT, &val, 1); +} diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index f364f3ce5..6a5e25dd0 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -51,6 +51,8 @@ #define HW_CMD_GET_DEVICE_NAME 0x16 #define HW_CMD_PING 0x17 #define HW_CMD_REBOOT 0x18 +#define HW_CMD_SET_SIGNAL_REPORT 0x19 +#define HW_CMD_GET_SIGNAL_REPORT 0x1A /* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */ #define HW_RESP(cmd) ((cmd) | 0x80) @@ -76,6 +78,7 @@ #define HW_RESP_SENSORS HW_RESP(HW_CMD_GET_SENSORS) /* 0x95 */ #define HW_RESP_DEVICE_NAME HW_RESP(HW_CMD_GET_DEVICE_NAME) /* 0x96 */ #define HW_RESP_PONG HW_RESP(HW_CMD_PING) /* 0x97 */ +#define HW_RESP_SIGNAL_REPORT HW_RESP(HW_CMD_GET_SIGNAL_REPORT) /* 0x9A */ /* Generic responses (shared by multiple commands) */ #define HW_RESP_OK 0xF0 @@ -153,6 +156,7 @@ class KissModem { OnSendFinishedCallback _onSendFinishedCallback; RadioConfig _config; + bool _signal_report_enabled; void writeByte(uint8_t b); void writeFrame(uint8_t type, const uint8_t* data, uint16_t len); @@ -186,6 +190,8 @@ class KissModem { void handleGetMCUTemp(); void handleReboot(); void handleGetDeviceName(); + void handleSetSignalReport(const uint8_t* data, uint16_t len); + void handleGetSignalReport(); public: KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng, From 362b5eb0a100a8e295fe1085df88d508466c4e37 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 7 Feb 2026 10:26:08 +0100 Subject: [PATCH 10/13] Update protocol docs for new response codes and signal reporting --- docs/kiss_modem_protocol.md | 59 ++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 9652f976a..6a08614fa 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -110,36 +110,41 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The | GetDeviceName | `0x16` | - | | Ping | `0x17` | - | | Reboot | `0x18` | - | +| SetSignalReport | `0x19` | Enable (1): 0x00=disable, nonzero=enable | +| GetSignalReport | `0x1A` | - | ### Response Sub-commands (TNC to Host) +Response codes use the high-bit convention: `response = command | 0x80`. Generic and unsolicited responses use the `0xF0`+ range. + | Sub-command | Value | Data | |-------------|-------|------| -| Identity | `0x21` | PubKey (32) | -| Random | `0x22` | Random bytes (1-64) | -| Verify | `0x23` | Result (1): 0x00=invalid, 0x01=valid | -| Signature | `0x24` | Signature (64) | -| Encrypted | `0x25` | MAC (2) + Ciphertext | -| Decrypted | `0x26` | Plaintext | -| SharedSecret | `0x27` | Shared secret (32) | -| Hash | `0x28` | SHA-256 hash (32) | -| OK | `0x29` | - | -| Error | `0x2A` | Error code (1) | -| Radio | `0x2B` | Freq (4) + BW (4) + SF (1) + CR (1) | -| TxPower | `0x2C` | Power dBm (1) | -| CurrentRssi | `0x2D` | RSSI dBm (1, signed) | -| ChannelBusy | `0x2E` | Result (1): 0x00=clear, 0x01=busy | -| Airtime | `0x2F` | Milliseconds (4) | -| NoiseFloor | `0x30` | dBm (2, signed) | -| Version | `0x31` | Version (1) + Reserved (1) | -| Stats | `0x32` | RX (4) + TX (4) + Errors (4) | -| Battery | `0x33` | Millivolts (2) | -| MCUTemp | `0x34` | Temperature (2, signed) | -| Sensors | `0x35` | CayenneLPP payload | -| DeviceName | `0x36` | Name (variable, UTF-8) | -| Pong | `0x37` | - | -| TxDone | `0x38` | Result (1): 0x00=failed, 0x01=success | -| RxMeta | `0x39` | SNR (1) + RSSI (1) | +| Identity | `0x81` | PubKey (32) | +| Random | `0x82` | Random bytes (1-64) | +| Verify | `0x83` | Result (1): 0x00=invalid, 0x01=valid | +| Signature | `0x84` | Signature (64) | +| Encrypted | `0x85` | MAC (2) + Ciphertext | +| Decrypted | `0x86` | Plaintext | +| SharedSecret | `0x87` | Shared secret (32) | +| Hash | `0x88` | SHA-256 hash (32) | +| Radio | `0x8B` | Freq (4) + BW (4) + SF (1) + CR (1) | +| TxPower | `0x8C` | Power dBm (1) | +| CurrentRssi | `0x8D` | RSSI dBm (1, signed) | +| ChannelBusy | `0x8E` | Result (1): 0x00=clear, 0x01=busy | +| Airtime | `0x8F` | Milliseconds (4) | +| NoiseFloor | `0x90` | dBm (2, signed) | +| Version | `0x91` | Version (1) + Reserved (1) | +| Stats | `0x92` | RX (4) + TX (4) + Errors (4) | +| Battery | `0x93` | Millivolts (2) | +| MCUTemp | `0x94` | Temperature (2, signed) | +| Sensors | `0x95` | CayenneLPP payload | +| DeviceName | `0x96` | Name (variable, UTF-8) | +| Pong | `0x97` | - | +| SignalReport | `0x9A` | Status (1): 0x00=disabled, 0x01=enabled | +| OK | `0xF0` | - | +| Error | `0xF1` | Error code (1) | +| TxDone | `0xF8` | Result (1): 0x00=failed, 0x01=success | +| RxMeta | `0xF9` | SNR (1) + RSSI (1) | ### Error Codes @@ -156,9 +161,9 @@ MeshCore-specific functionality uses the standard KISS SetHardware command. The The TNC sends these SetHardware frames without a preceding request: -**TxDone (0x38)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. +**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. -**RxMeta (0x39)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Standard KISS clients ignore this frame. +**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame. ## Data Formats From 00b44c41148e00e73b0289a78662987b44ae4809 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 7 Feb 2026 14:22:21 +0100 Subject: [PATCH 11/13] Remove redundant send/complete/finished callbacks, use Radio interface directly --- examples/kiss_modem/KissModem.cpp | 16 ++++------------ examples/kiss_modem/KissModem.h | 9 --------- examples/kiss_modem/main.cpp | 15 --------------- 3 files changed, 4 insertions(+), 36 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index ebe2e98f6..08ed9b906 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -20,9 +20,6 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _setTxPowerCallback = nullptr; _getCurrentRssiCallback = nullptr; _getStatsCallback = nullptr; - _sendPacketCallback = nullptr; - _isSendCompleteCallback = nullptr; - _onSendFinishedCallback = nullptr; _config = {0, 0, 0, 0, 0}; _signal_report_enabled = true; } @@ -287,19 +284,14 @@ void KissModem::processTx() { case TX_DELAY: if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) { - if (_sendPacketCallback) { - _sendPacketCallback(_pending_tx, _pending_tx_len); - _tx_state = TX_SENDING; - } else { - _has_pending_tx = false; - _tx_state = TX_IDLE; - } + _radio.startSendRaw(_pending_tx, _pending_tx_len); + _tx_state = TX_SENDING; } break; case TX_SENDING: - if (_isSendCompleteCallback && _isSendCompleteCallback()) { - if (_onSendFinishedCallback) _onSendFinishedCallback(); + if (_radio.isSendComplete()) { + _radio.onSendFinished(); uint8_t result = 0x01; writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); _has_pending_tx = false; diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 6a5e25dd0..88741e1f8 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -101,9 +101,6 @@ typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); typedef void (*SetTxPowerCallback)(uint8_t power); typedef float (*GetCurrentRssiCallback)(); typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors); -typedef void (*SendPacketCallback)(const uint8_t* data, uint16_t len); -typedef bool (*IsSendCompleteCallback)(); -typedef void (*OnSendFinishedCallback)(); struct RadioConfig { uint32_t freq_hz; @@ -151,9 +148,6 @@ class KissModem { SetTxPowerCallback _setTxPowerCallback; GetCurrentRssiCallback _getCurrentRssiCallback; GetStatsCallback _getStatsCallback; - SendPacketCallback _sendPacketCallback; - IsSendCompleteCallback _isSendCompleteCallback; - OnSendFinishedCallback _onSendFinishedCallback; RadioConfig _config; bool _signal_report_enabled; @@ -204,9 +198,6 @@ class KissModem { void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; } void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; } - void setSendPacketCallback(SendPacketCallback cb) { _sendPacketCallback = cb; } - void setIsSendCompleteCallback(IsSendCompleteCallback cb) { _isSendCompleteCallback = cb; } - void setOnSendFinishedCallback(OnSendFinishedCallback cb) { _onSendFinishedCallback = cb; } void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len); bool isTxBusy() const { return _tx_state != TX_IDLE; } diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 514897c3b..15888b905 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -70,18 +70,6 @@ void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) { *errors = radio_driver.getPacketsRecvErrors(); } -void onSendPacket(const uint8_t* data, uint16_t len) { - radio_driver.startSendRaw(data, len); -} - -bool onIsSendComplete() { - return radio_driver.isSendComplete(); -} - -void onSendFinished() { - radio_driver.onSendFinished(); -} - void setup() { board.begin(); @@ -127,9 +115,6 @@ void setup() { modem->setTxPowerCallback(onSetTxPower); modem->setGetCurrentRssiCallback(onGetCurrentRssi); modem->setGetStatsCallback(onGetStats); - modem->setSendPacketCallback(onSendPacket); - modem->setIsSendCompleteCallback(onIsSendComplete); - modem->setOnSendFinishedCallback(onSendFinished); modem->begin(); } From 5157daf1c1ae341a36d1efa21e307aef6c682321 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 7 Feb 2026 14:24:34 +0100 Subject: [PATCH 12/13] Remove individual HW_RESP_* defines, use HW_RESP() macro directly --- examples/kiss_modem/KissModem.cpp | 48 +++++++++++++++---------------- examples/kiss_modem/KissModem.h | 23 --------------- 2 files changed, 24 insertions(+), 47 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index 08ed9b906..b4251046b 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -310,7 +310,7 @@ void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, } void KissModem::handleGetIdentity() { - writeHardwareFrame(HW_RESP_IDENTITY, _identity.pub_key, PUB_KEY_SIZE); + writeHardwareFrame(HW_RESP(HW_CMD_GET_IDENTITY), _identity.pub_key, PUB_KEY_SIZE); } void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) { @@ -327,7 +327,7 @@ void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) { uint8_t buf[64]; _rng.random(buf, requested); - writeHardwareFrame(HW_RESP_RANDOM, buf, requested); + writeHardwareFrame(HW_RESP(HW_CMD_GET_RANDOM), buf, requested); } void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) { @@ -342,7 +342,7 @@ void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) { uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE; uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00; - writeHardwareFrame(HW_RESP_VERIFY, &result, 1); + writeHardwareFrame(HW_RESP(HW_CMD_VERIFY_SIGNATURE), &result, 1); } void KissModem::handleSignData(const uint8_t* data, uint16_t len) { @@ -353,7 +353,7 @@ void KissModem::handleSignData(const uint8_t* data, uint16_t len) { uint8_t signature[SIGNATURE_SIZE]; _identity.sign(signature, data, len); - writeHardwareFrame(HW_RESP_SIGNATURE, signature, SIGNATURE_SIZE); + writeHardwareFrame(HW_RESP(HW_CMD_SIGN_DATA), signature, SIGNATURE_SIZE); } void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) { @@ -370,7 +370,7 @@ void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) { int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len); if (encrypted_len > 0) { - writeHardwareFrame(HW_RESP_ENCRYPTED, buf, encrypted_len); + writeHardwareFrame(HW_RESP(HW_CMD_ENCRYPT_DATA), buf, encrypted_len); } else { writeHardwareError(HW_ERR_ENCRYPT_FAILED); } @@ -390,7 +390,7 @@ void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) { int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len); if (decrypted_len > 0) { - writeHardwareFrame(HW_RESP_DECRYPTED, buf, decrypted_len); + writeHardwareFrame(HW_RESP(HW_CMD_DECRYPT_DATA), buf, decrypted_len); } else { writeHardwareError(HW_ERR_MAC_FAILED); } @@ -404,7 +404,7 @@ void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) { uint8_t shared_secret[PUB_KEY_SIZE]; _identity.calcSharedSecret(shared_secret, data); - writeHardwareFrame(HW_RESP_SHARED_SECRET, shared_secret, PUB_KEY_SIZE); + writeHardwareFrame(HW_RESP(HW_CMD_KEY_EXCHANGE), shared_secret, PUB_KEY_SIZE); } void KissModem::handleHash(const uint8_t* data, uint16_t len) { @@ -415,7 +415,7 @@ void KissModem::handleHash(const uint8_t* data, uint16_t len) { uint8_t hash[32]; mesh::Utils::sha256(hash, 32, data, len); - writeHardwareFrame(HW_RESP_HASH, hash, 32); + writeHardwareFrame(HW_RESP(HW_CMD_HASH), hash, 32); } void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) { @@ -467,18 +467,18 @@ void KissModem::handleGetRadio() { memcpy(buf + 4, &_config.bw_hz, 4); buf[8] = _config.sf; buf[9] = _config.cr; - writeHardwareFrame(HW_RESP_RADIO, buf, 10); + writeHardwareFrame(HW_RESP(HW_CMD_GET_RADIO), buf, 10); } void KissModem::handleGetTxPower() { - writeHardwareFrame(HW_RESP_TX_POWER, &_config.tx_power, 1); + writeHardwareFrame(HW_RESP(HW_CMD_GET_TX_POWER), &_config.tx_power, 1); } void KissModem::handleGetVersion() { uint8_t buf[2]; buf[0] = KISS_FIRMWARE_VERSION; buf[1] = 0; - writeHardwareFrame(HW_RESP_VERSION, buf, 2); + writeHardwareFrame(HW_RESP(HW_CMD_GET_VERSION), buf, 2); } void KissModem::handleGetCurrentRssi() { @@ -489,12 +489,12 @@ void KissModem::handleGetCurrentRssi() { float rssi = _getCurrentRssiCallback(); int8_t rssi_byte = (int8_t)rssi; - writeHardwareFrame(HW_RESP_CURRENT_RSSI, (uint8_t*)&rssi_byte, 1); + writeHardwareFrame(HW_RESP(HW_CMD_GET_CURRENT_RSSI), (uint8_t*)&rssi_byte, 1); } void KissModem::handleIsChannelBusy() { uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00; - writeHardwareFrame(HW_RESP_CHANNEL_BUSY, &busy, 1); + writeHardwareFrame(HW_RESP(HW_CMD_IS_CHANNEL_BUSY), &busy, 1); } void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) { @@ -505,12 +505,12 @@ void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) { uint8_t packet_len = data[0]; uint32_t airtime = _radio.getEstAirtimeFor(packet_len); - writeHardwareFrame(HW_RESP_AIRTIME, (uint8_t*)&airtime, 4); + writeHardwareFrame(HW_RESP(HW_CMD_GET_AIRTIME), (uint8_t*)&airtime, 4); } void KissModem::handleGetNoiseFloor() { int16_t noise_floor = _radio.getNoiseFloor(); - writeHardwareFrame(HW_RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2); + writeHardwareFrame(HW_RESP(HW_CMD_GET_NOISE_FLOOR), (uint8_t*)&noise_floor, 2); } void KissModem::handleGetStats() { @@ -525,16 +525,16 @@ void KissModem::handleGetStats() { memcpy(buf, &rx, 4); memcpy(buf + 4, &tx, 4); memcpy(buf + 8, &errors, 4); - writeHardwareFrame(HW_RESP_STATS, buf, 12); + writeHardwareFrame(HW_RESP(HW_CMD_GET_STATS), buf, 12); } void KissModem::handleGetBattery() { uint16_t mv = _board.getBattMilliVolts(); - writeHardwareFrame(HW_RESP_BATTERY, (uint8_t*)&mv, 2); + writeHardwareFrame(HW_RESP(HW_CMD_GET_BATTERY), (uint8_t*)&mv, 2); } void KissModem::handlePing() { - writeHardwareFrame(HW_RESP_PONG, nullptr, 0); + writeHardwareFrame(HW_RESP(HW_CMD_PING), nullptr, 0); } void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { @@ -546,9 +546,9 @@ void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { uint8_t permissions = data[0]; CayenneLPP telemetry(255); if (_sensors.querySensors(permissions, telemetry)) { - writeHardwareFrame(HW_RESP_SENSORS, telemetry.getBuffer(), telemetry.getSize()); + writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), telemetry.getBuffer(), telemetry.getSize()); } else { - writeHardwareFrame(HW_RESP_SENSORS, nullptr, 0); + writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), nullptr, 0); } } @@ -559,7 +559,7 @@ void KissModem::handleGetMCUTemp() { return; } int16_t temp_tenths = (int16_t)(temp * 10.0f); - writeHardwareFrame(HW_RESP_MCU_TEMP, (uint8_t*)&temp_tenths, 2); + writeHardwareFrame(HW_RESP(HW_CMD_GET_MCU_TEMP), (uint8_t*)&temp_tenths, 2); } void KissModem::handleReboot() { @@ -571,7 +571,7 @@ void KissModem::handleReboot() { void KissModem::handleGetDeviceName() { const char* name = _board.getManufacturerName(); - writeHardwareFrame(HW_RESP_DEVICE_NAME, (const uint8_t*)name, strlen(name)); + writeHardwareFrame(HW_RESP(HW_CMD_GET_DEVICE_NAME), (const uint8_t*)name, strlen(name)); } void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) { @@ -581,10 +581,10 @@ void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) { } _signal_report_enabled = (data[0] != 0x00); uint8_t val = _signal_report_enabled ? 0x01 : 0x00; - writeHardwareFrame(HW_RESP_SIGNAL_REPORT, &val, 1); + writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1); } void KissModem::handleGetSignalReport() { uint8_t val = _signal_report_enabled ? 0x01 : 0x00; - writeHardwareFrame(HW_RESP_SIGNAL_REPORT, &val, 1); + writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1); } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 88741e1f8..60566add9 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -57,29 +57,6 @@ /* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */ #define HW_RESP(cmd) ((cmd) | 0x80) -#define HW_RESP_IDENTITY HW_RESP(HW_CMD_GET_IDENTITY) /* 0x81 */ -#define HW_RESP_RANDOM HW_RESP(HW_CMD_GET_RANDOM) /* 0x82 */ -#define HW_RESP_VERIFY HW_RESP(HW_CMD_VERIFY_SIGNATURE) /* 0x83 */ -#define HW_RESP_SIGNATURE HW_RESP(HW_CMD_SIGN_DATA) /* 0x84 */ -#define HW_RESP_ENCRYPTED HW_RESP(HW_CMD_ENCRYPT_DATA) /* 0x85 */ -#define HW_RESP_DECRYPTED HW_RESP(HW_CMD_DECRYPT_DATA) /* 0x86 */ -#define HW_RESP_SHARED_SECRET HW_RESP(HW_CMD_KEY_EXCHANGE) /* 0x87 */ -#define HW_RESP_HASH HW_RESP(HW_CMD_HASH) /* 0x88 */ -#define HW_RESP_RADIO HW_RESP(HW_CMD_GET_RADIO) /* 0x8B */ -#define HW_RESP_TX_POWER HW_RESP(HW_CMD_GET_TX_POWER) /* 0x8C */ -#define HW_RESP_CURRENT_RSSI HW_RESP(HW_CMD_GET_CURRENT_RSSI) /* 0x8D */ -#define HW_RESP_CHANNEL_BUSY HW_RESP(HW_CMD_IS_CHANNEL_BUSY) /* 0x8E */ -#define HW_RESP_AIRTIME HW_RESP(HW_CMD_GET_AIRTIME) /* 0x8F */ -#define HW_RESP_NOISE_FLOOR HW_RESP(HW_CMD_GET_NOISE_FLOOR) /* 0x90 */ -#define HW_RESP_VERSION HW_RESP(HW_CMD_GET_VERSION) /* 0x91 */ -#define HW_RESP_STATS HW_RESP(HW_CMD_GET_STATS) /* 0x92 */ -#define HW_RESP_BATTERY HW_RESP(HW_CMD_GET_BATTERY) /* 0x93 */ -#define HW_RESP_MCU_TEMP HW_RESP(HW_CMD_GET_MCU_TEMP) /* 0x94 */ -#define HW_RESP_SENSORS HW_RESP(HW_CMD_GET_SENSORS) /* 0x95 */ -#define HW_RESP_DEVICE_NAME HW_RESP(HW_CMD_GET_DEVICE_NAME) /* 0x96 */ -#define HW_RESP_PONG HW_RESP(HW_CMD_PING) /* 0x97 */ -#define HW_RESP_SIGNAL_REPORT HW_RESP(HW_CMD_GET_SIGNAL_REPORT) /* 0x9A */ - /* Generic responses (shared by multiple commands) */ #define HW_RESP_OK 0xF0 #define HW_RESP_ERROR 0xF1 From f6ebbd978e0b27f5db15f2013cb4b5e0410a8edd Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 7 Feb 2026 14:32:11 +0100 Subject: [PATCH 13/13] Remove redundant locals in handleSetRadio --- examples/kiss_modem/KissModem.cpp | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index b4251046b..5e8b00d52 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -428,21 +428,12 @@ void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) { return; } - uint32_t freq_hz, bw_hz; - memcpy(&freq_hz, data, 4); - memcpy(&bw_hz, data + 4, 4); - uint8_t sf = data[8]; - uint8_t cr = data[9]; + memcpy(&_config.freq_hz, data, 4); + memcpy(&_config.bw_hz, data + 4, 4); + _config.sf = data[8]; + _config.cr = data[9]; - _config.freq_hz = freq_hz; - _config.bw_hz = bw_hz; - _config.sf = sf; - _config.cr = cr; - - float freq = freq_hz / 1000000.0f; - float bw = bw_hz / 1000.0f; - - _setRadioCallback(freq, bw, sf, cr); + _setRadioCallback(_config.freq_hz / 1000000.0f, _config.bw_hz / 1000.0f, _config.sf, _config.cr); writeHardwareFrame(HW_RESP_OK, nullptr, 0); }