diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 00b0bf90f..6a08614fa 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,156 @@ 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. 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 + +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 | `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` | - | +| 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 | `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 | 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 (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. + +**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 -### Radio Parameters (CMD_SET_RADIO / RESP_RADIO) +### Radio Parameters (SetRadio / Radio response) All values little-endian. @@ -111,27 +178,41 @@ All values little-endian. | SF | 1 byte | Spreading factor (5-12) | | CR | 1 byte | Coding rate (5-8) | -### Received Packet (CMD_DATA response) +### Version (Version response) | Field | Size | Description | |-------|------|-------------| -| SNR | 1 byte | Signal-to-noise × 4, signed | -| RSSI | 1 byte | Signal strength dBm, signed | -| Packet | variable | Raw MeshCore packet | +| Version | 1 byte | Firmware version | +| Reserved | 1 byte | Always 0 | -### Noise Floor (RESP_NOISE_FLOOR) +### Encrypted (Encrypted response) -Response to `CMD_GET_NOISE_FLOOR` (0x13). Little-endian. +| Field | Size | Description | +|-------|------|-------------| +| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes | +| Ciphertext | variable | AES-128-CBC encrypted data | -| Field | Size | Description | -|--------------|------|--------------------------------| -| Noise floor | 2 | int16_t, dBm (signed), e.g. -120 | +### Airtime (Airtime response) -The modem recalibrates the noise floor every two seconds with an AGC reset every 30 seconds. +All values little-endian. -### Stats (RESP_STATS) +| Field | Size | Description | +|-------|------|-------------| +| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds | -Response to `CMD_GET_STATS` (0x14). All values little-endian. +### 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. | Field | Size | Description | |-------|------|-------------| @@ -139,7 +220,35 @@ 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) +### Battery (Battery response) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| 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 | |-----|-------|-------------| @@ -149,14 +258,25 @@ 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. +## 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 +- 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) -- 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` +- 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 - 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..5e8b00d52 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -9,11 +9,19 @@ 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; _config = {0, 0, 0, 0, 0}; + _signal_report_enabled = true; } void KissModem::begin() { @@ -21,6 +29,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 +44,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 +80,376 @@ 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; + } else { + /* Buffer full with no FEND; reset so we don't stay stuck ignoring input. */ + _rx_len = 0; + _rx_escaped = false; + _rx_active = false; } } + + 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; + case HW_CMD_GET_MCU_TEMP: + handleGetMCUTemp(); + break; + case HW_CMD_REBOOT: + handleReboot(); + break; + 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: - 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) { + _radio.startSendRaw(_pending_tx, _pending_tx_len); + _tx_state = TX_SENDING; + } + break; + + case TX_SENDING: + if (_radio.isSendComplete()) { + _radio.onSendFinished(); + 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); + if (_signal_report_enabled) { + 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(HW_CMD_GET_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(HW_CMD_GET_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(HW_CMD_VERIFY_SIGNATURE), &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(HW_CMD_SIGN_DATA), 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(HW_CMD_ENCRYPT_DATA), 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(HW_CMD_DECRYPT_DATA), 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(HW_CMD_KEY_EXCHANGE), 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(HW_CMD_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); + + memcpy(&_config.freq_hz, data, 4); + memcpy(&_config.bw_hz, data + 4, 4); + _config.sf = data[8]; + _config.cr = data[9]; + + _setRadioCallback(_config.freq_hz / 1000000.0f, _config.bw_hz / 1000.0f, _config.sf, _config.cr); + 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 +458,124 @@ 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(HW_CMD_GET_RADIO), buf, 10); } void KissModem::handleGetTxPower() { - writeFrame(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; - 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(HW_CMD_GET_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(HW_CMD_GET_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(HW_CMD_IS_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(HW_CMD_GET_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(HW_CMD_GET_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(HW_CMD_GET_STATS), buf, 12); } void KissModem::handleGetBattery() { uint16_t mv = _board.getBattMilliVolts(); - writeFrame(RESP_BATTERY, (uint8_t*)&mv, 2); + writeHardwareFrame(HW_RESP(HW_CMD_GET_BATTERY), (uint8_t*)&mv, 2); } void KissModem::handlePing() { - writeFrame(RESP_PONG, nullptr, 0); + writeHardwareFrame(HW_RESP(HW_CMD_PING), 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(HW_CMD_GET_SENSORS), telemetry.getBuffer(), telemetry.getSize()); } else { - writeFrame(RESP_SENSORS, nullptr, 0); + writeHardwareFrame(HW_RESP(HW_CMD_GET_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(HW_CMD_GET_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(HW_CMD_GET_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(HW_CMD_GET_SIGNAL_REPORT), &val, 1); +} + +void KissModem::handleGetSignalReport() { + uint8_t val = _signal_report_enabled ? 0x01 : 0x00; + 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 170bb0c2a..60566add9 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -11,62 +11,66 @@ #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 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_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) + +/* 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 +#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 @@ -83,6 +87,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 +102,41 @@ 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; - + RadioConfig _config; + bool _signal_report_enabled; 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); @@ -133,20 +158,26 @@ class KissModem { void handleGetBattery(); void handlePing(); void handleGetSensors(const uint8_t* data, uint16_t len); + 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, 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 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; } + /** 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 3a610460d..15888b905 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -11,9 +11,12 @@ #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 +#define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 +#define AGC_RESET_INTERVAL_MS 30000 StdRNG rng; mesh::LocalIdentity identity; @@ -79,14 +82,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); @@ -97,34 +121,36 @@ void setup() { void loop() { modem->loop(); - uint8_t packet[KISS_MAX_PACKET_SIZE]; - uint16_t len; + 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]; + 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); + } + /* 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 noise floor calibration + /* 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->getPacketToSend(packet, &len)) { - radio_driver.startSendRaw(packet, len); - while (!radio_driver.isSendComplete()) { - delay(1); + if (!modem->isActuallyTransmitting()) { + for (int i = 0; i < 15; i++) { + radio_driver.loop(); } - 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); } } 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