Skip to content

Commit 248c310

Browse files
authored
Merge pull request #1605 from ViezeVingertjes/kiss-modem-spec-compliance
Kiss modem spec compliance
2 parents b1094c2 + e4ef6db commit 248c310

5 files changed

Lines changed: 656 additions & 336 deletions

File tree

docs/kiss_modem_protocol.md

Lines changed: 212 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# MeshCore KISS Modem Protocol
22

3-
Serial protocol for the KISS modem firmware. Enables sending/receiving MeshCore packets over LoRa and cryptographic operations using the modem's identity.
3+
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.
44

55
## Serial Configuration
66

77
115200 baud, 8N1, no flow control.
88

99
## Frame Format
1010

11-
Standard KISS framing with byte stuffing.
11+
Standard KISS framing per the KA9Q/K3MC specification.
1212

1313
| Byte | Name | Description |
1414
|------|------|-------------|
@@ -18,89 +18,156 @@ Standard KISS framing with byte stuffing.
1818
| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
1919

2020
```
21-
┌──────┬─────────┬──────────────┬──────┐
22-
│ FEND │ Command │ Data (escaped)│ FEND │
23-
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
24-
└──────┴─────────┴──────────────┴──────┘
21+
┌──────┬───────────┬──────────────┬──────┐
22+
│ FEND │ Type Byte │ Data (escaped)│ FEND │
23+
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
24+
└──────┴───────────┴──────────────┴──────┘
2525
```
2626

27+
### Type Byte
28+
29+
The type byte is split into two nibbles:
30+
31+
| Bits | Field | Description |
32+
|------|-------|-------------|
33+
| 7-4 | Port | Port number (0 for single-port TNC) |
34+
| 3-0 | Command | Command number |
35+
2736
Maximum unescaped frame size: 512 bytes.
2837

29-
## Commands
30-
31-
### Request Commands (Host → Modem)
32-
33-
| Command | Value | Data |
34-
|---------|-------|------|
35-
| `CMD_DATA` | `0x00` | Packet (2-255 bytes) |
36-
| `CMD_GET_IDENTITY` | `0x01` | - |
37-
| `CMD_GET_RANDOM` | `0x02` | Length (1 byte, 1-64) |
38-
| `CMD_VERIFY_SIGNATURE` | `0x03` | PubKey (32) + Signature (64) + Data |
39-
| `CMD_SIGN_DATA` | `0x04` | Data to sign |
40-
| `CMD_ENCRYPT_DATA` | `0x05` | Key (32) + Plaintext |
41-
| `CMD_DECRYPT_DATA` | `0x06` | Key (32) + MAC (2) + Ciphertext |
42-
| `CMD_KEY_EXCHANGE` | `0x07` | Remote PubKey (32) |
43-
| `CMD_HASH` | `0x08` | Data to hash |
44-
| `CMD_SET_RADIO` | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) |
45-
| `CMD_SET_TX_POWER` | `0x0A` | Power dBm (1) |
46-
| *reserved* | `0x0B` | *(not implemented)* |
47-
| `CMD_GET_RADIO` | `0x0C` | - |
48-
| `CMD_GET_TX_POWER` | `0x0D` | - |
49-
| *reserved* | `0x0E` | *(not implemented)* |
50-
| `CMD_GET_VERSION` | `0x0F` | - |
51-
| `CMD_GET_CURRENT_RSSI` | `0x10` | - |
52-
| `CMD_IS_CHANNEL_BUSY` | `0x11` | - |
53-
| `CMD_GET_AIRTIME` | `0x12` | Packet length (1) |
54-
| `CMD_GET_NOISE_FLOOR` | `0x13` | - |
55-
| `CMD_GET_STATS` | `0x14` | - |
56-
| `CMD_GET_BATTERY` | `0x15` | - |
57-
| `CMD_PING` | `0x16` | - |
58-
| `CMD_GET_SENSORS` | `0x17` | Permissions (1) |
59-
60-
### Response Commands (Modem → Host)
61-
62-
| Command | Value | Data |
63-
|---------|-------|------|
64-
| `CMD_DATA` | `0x00` | SNR (1) + RSSI (1) + Packet |
65-
| `RESP_IDENTITY` | `0x21` | PubKey (32) |
66-
| `RESP_RANDOM` | `0x22` | Random bytes (1-64) |
67-
| `RESP_VERIFY` | `0x23` | Result (1): 0x00=invalid, 0x01=valid |
68-
| `RESP_SIGNATURE` | `0x24` | Signature (64) |
69-
| `RESP_ENCRYPTED` | `0x25` | MAC (2) + Ciphertext |
70-
| `RESP_DECRYPTED` | `0x26` | Plaintext |
71-
| `RESP_SHARED_SECRET` | `0x27` | Shared secret (32) |
72-
| `RESP_HASH` | `0x28` | SHA-256 hash (32) |
73-
| `RESP_OK` | `0x29` | - |
74-
| `RESP_RADIO` | `0x2A` | Freq (4) + BW (4) + SF (1) + CR (1) |
75-
| `RESP_TX_POWER` | `0x2B` | Power dBm (1) |
76-
| *reserved* | `0x2C` | *(not implemented)* |
77-
| `RESP_VERSION` | `0x2D` | Version (1) + Reserved (1) |
78-
| `RESP_ERROR` | `0x2E` | Error code (1) |
79-
| `RESP_TX_DONE` | `0x2F` | Result (1): 0x00=failed, 0x01=success |
80-
| `RESP_CURRENT_RSSI` | `0x30` | RSSI dBm (1, signed) |
81-
| `RESP_CHANNEL_BUSY` | `0x31` | Result (1): 0x00=clear, 0x01=busy |
82-
| `RESP_AIRTIME` | `0x32` | Milliseconds (4) |
83-
| `RESP_NOISE_FLOOR` | `0x33` | dBm (2, signed) |
84-
| `RESP_STATS` | `0x34` | RX (4) + TX (4) + Errors (4) |
85-
| `RESP_BATTERY` | `0x35` | Millivolts (2) |
86-
| `RESP_PONG` | `0x36` | - |
87-
| `RESP_SENSORS` | `0x37` | CayenneLPP payload |
88-
89-
## Error Codes
38+
## Standard KISS Commands
39+
40+
### Host to TNC
41+
42+
| Command | Value | Data | Description |
43+
|---------|-------|------|-------------|
44+
| Data | `0x00` | Raw packet | Queue packet for transmission |
45+
| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
46+
| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
47+
| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
48+
| TXtail | `0x04` | Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) |
49+
| FullDuplex | `0x05` | Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) |
50+
| SetHardware | `0x06` | Sub-command + data | MeshCore extensions (see below) |
51+
| Return | `0xFF` | - | Exit KISS mode (no-op) |
52+
53+
### TNC to Host
54+
55+
| Type | Value | Data | Description |
56+
|------|-------|------|-------------|
57+
| Data | `0x00` | Raw packet | Received packet from radio |
58+
59+
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.
60+
61+
### CSMA Behavior
62+
63+
The TNC implements p-persistent CSMA for half-duplex operation:
64+
65+
1. When a packet is queued, monitor carrier detect
66+
2. When the channel clears, generate a random value 0-255
67+
3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
68+
4. Otherwise, wait SlotTime and repeat from step 1
69+
70+
In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
71+
72+
## SetHardware Extensions (0x06)
73+
74+
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.
75+
76+
### Frame Format
77+
78+
```
79+
┌──────┬──────┬─────────────┬──────────────┬──────┐
80+
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
81+
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
82+
└──────┴──────┴─────────────┴──────────────┴──────┘
83+
```
84+
85+
### Request Sub-commands (Host to TNC)
86+
87+
| Sub-command | Value | Data |
88+
|-------------|-------|------|
89+
| GetIdentity | `0x01` | - |
90+
| GetRandom | `0x02` | Length (1 byte, 1-64) |
91+
| VerifySignature | `0x03` | PubKey (32) + Signature (64) + Data |
92+
| SignData | `0x04` | Data to sign |
93+
| EncryptData | `0x05` | Key (32) + Plaintext |
94+
| DecryptData | `0x06` | Key (32) + MAC (2) + Ciphertext |
95+
| KeyExchange | `0x07` | Remote PubKey (32) |
96+
| Hash | `0x08` | Data to hash |
97+
| SetRadio | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) |
98+
| SetTxPower | `0x0A` | Power dBm (1) |
99+
| GetRadio | `0x0B` | - |
100+
| GetTxPower | `0x0C` | - |
101+
| GetCurrentRssi | `0x0D` | - |
102+
| IsChannelBusy | `0x0E` | - |
103+
| GetAirtime | `0x0F` | Packet length (1) |
104+
| GetNoiseFloor | `0x10` | - |
105+
| GetVersion | `0x11` | - |
106+
| GetStats | `0x12` | - |
107+
| GetBattery | `0x13` | - |
108+
| GetMCUTemp | `0x14` | - |
109+
| GetSensors | `0x15` | Permissions (1) |
110+
| GetDeviceName | `0x16` | - |
111+
| Ping | `0x17` | - |
112+
| Reboot | `0x18` | - |
113+
| SetSignalReport | `0x19` | Enable (1): 0x00=disable, nonzero=enable |
114+
| GetSignalReport | `0x1A` | - |
115+
116+
### Response Sub-commands (TNC to Host)
117+
118+
Response codes use the high-bit convention: `response = command | 0x80`. Generic and unsolicited responses use the `0xF0`+ range.
119+
120+
| Sub-command | Value | Data |
121+
|-------------|-------|------|
122+
| Identity | `0x81` | PubKey (32) |
123+
| Random | `0x82` | Random bytes (1-64) |
124+
| Verify | `0x83` | Result (1): 0x00=invalid, 0x01=valid |
125+
| Signature | `0x84` | Signature (64) |
126+
| Encrypted | `0x85` | MAC (2) + Ciphertext |
127+
| Decrypted | `0x86` | Plaintext |
128+
| SharedSecret | `0x87` | Shared secret (32) |
129+
| Hash | `0x88` | SHA-256 hash (32) |
130+
| Radio | `0x8B` | Freq (4) + BW (4) + SF (1) + CR (1) |
131+
| TxPower | `0x8C` | Power dBm (1) |
132+
| CurrentRssi | `0x8D` | RSSI dBm (1, signed) |
133+
| ChannelBusy | `0x8E` | Result (1): 0x00=clear, 0x01=busy |
134+
| Airtime | `0x8F` | Milliseconds (4) |
135+
| NoiseFloor | `0x90` | dBm (2, signed) |
136+
| Version | `0x91` | Version (1) + Reserved (1) |
137+
| Stats | `0x92` | RX (4) + TX (4) + Errors (4) |
138+
| Battery | `0x93` | Millivolts (2) |
139+
| MCUTemp | `0x94` | Temperature (2, signed) |
140+
| Sensors | `0x95` | CayenneLPP payload |
141+
| DeviceName | `0x96` | Name (variable, UTF-8) |
142+
| Pong | `0x97` | - |
143+
| SignalReport | `0x9A` | Status (1): 0x00=disabled, 0x01=enabled |
144+
| OK | `0xF0` | - |
145+
| Error | `0xF1` | Error code (1) |
146+
| TxDone | `0xF8` | Result (1): 0x00=failed, 0x01=success |
147+
| RxMeta | `0xF9` | SNR (1) + RSSI (1) |
148+
149+
### Error Codes
90150

91151
| Code | Value | Description |
92152
|------|-------|-------------|
93-
| `ERR_INVALID_LENGTH` | `0x01` | Request data too short |
94-
| `ERR_INVALID_PARAM` | `0x02` | Invalid parameter value |
95-
| `ERR_NO_CALLBACK` | `0x03` | Feature not available |
96-
| `ERR_MAC_FAILED` | `0x04` | MAC verification failed |
97-
| `ERR_UNKNOWN_CMD` | `0x05` | Unknown command |
98-
| `ERR_ENCRYPT_FAILED` | `0x06` | Encryption failed |
99-
| `ERR_TX_PENDING` | `0x07` | TX already pending |
153+
| InvalidLength | `0x01` | Request data too short |
154+
| InvalidParam | `0x02` | Invalid parameter value |
155+
| NoCallback | `0x03` | Feature not available |
156+
| MacFailed | `0x04` | MAC verification failed |
157+
| UnknownCmd | `0x05` | Unknown sub-command |
158+
| EncryptFailed | `0x06` | Encryption failed |
159+
160+
### Unsolicited Events
161+
162+
The TNC sends these SetHardware frames without a preceding request:
163+
164+
**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure.
165+
166+
**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.
100167

101168
## Data Formats
102169

103-
### Radio Parameters (CMD_SET_RADIO / RESP_RADIO)
170+
### Radio Parameters (SetRadio / Radio response)
104171

105172
All values little-endian.
106173

@@ -111,35 +178,77 @@ All values little-endian.
111178
| SF | 1 byte | Spreading factor (5-12) |
112179
| CR | 1 byte | Coding rate (5-8) |
113180

114-
### Received Packet (CMD_DATA response)
181+
### Version (Version response)
115182

116183
| Field | Size | Description |
117184
|-------|------|-------------|
118-
| SNR | 1 byte | Signal-to-noise × 4, signed |
119-
| RSSI | 1 byte | Signal strength dBm, signed |
120-
| Packet | variable | Raw MeshCore packet |
185+
| Version | 1 byte | Firmware version |
186+
| Reserved | 1 byte | Always 0 |
121187

122-
### Noise Floor (RESP_NOISE_FLOOR)
188+
### Encrypted (Encrypted response)
123189

124-
Response to `CMD_GET_NOISE_FLOOR` (0x13). Little-endian.
190+
| Field | Size | Description |
191+
|-------|------|-------------|
192+
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
193+
| Ciphertext | variable | AES-128-CBC encrypted data |
125194

126-
| Field | Size | Description |
127-
|--------------|------|--------------------------------|
128-
| Noise floor | 2 | int16_t, dBm (signed), e.g. -120 |
195+
### Airtime (Airtime response)
129196

130-
The modem recalibrates the noise floor every two seconds with an AGC reset every 30 seconds.
197+
All values little-endian.
131198

132-
### Stats (RESP_STATS)
199+
| Field | Size | Description |
200+
|-------|------|-------------|
201+
| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds |
133202

134-
Response to `CMD_GET_STATS` (0x14). All values little-endian.
203+
### Noise Floor (NoiseFloor response)
204+
205+
All values little-endian.
206+
207+
| Field | Size | Description |
208+
|-------|------|-------------|
209+
| Noise floor | 2 bytes | int16_t, dBm (signed) |
210+
211+
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
212+
213+
### Stats (Stats response)
214+
215+
All values little-endian.
135216

136217
| Field | Size | Description |
137218
|-------|------|-------------|
138219
| RX | 4 bytes | Packets received |
139220
| TX | 4 bytes | Packets transmitted |
140221
| Errors | 4 bytes | Receive errors |
141222

142-
### Sensor Permissions (CMD_GET_SENSORS)
223+
### Battery (Battery response)
224+
225+
All values little-endian.
226+
227+
| Field | Size | Description |
228+
|-------|------|-------------|
229+
| Millivolts | 2 bytes | uint16_t, battery voltage in mV |
230+
231+
### MCU Temperature (MCUTemp response)
232+
233+
All values little-endian.
234+
235+
| Field | Size | Description |
236+
|-------|------|-------------|
237+
| Temperature | 2 bytes | int16_t, tenths of °C (e.g., 253 = 25.3°C) |
238+
239+
Returns `NoCallback` error if the board does not support temperature readings.
240+
241+
### Device Name (DeviceName response)
242+
243+
| Field | Size | Description |
244+
|-------|------|-------------|
245+
| Name | variable | UTF-8 string, no null terminator |
246+
247+
### Reboot
248+
249+
Sends an `OK` response, flushes serial, then reboots the device. The host should expect the connection to drop.
250+
251+
### Sensor Permissions (GetSensors)
143252

144253
| Bit | Value | Description |
145254
|-----|-------|-------------|
@@ -149,14 +258,25 @@ Response to `CMD_GET_STATS` (0x14). All values little-endian.
149258

150259
Use `0x07` for all permissions.
151260

152-
### Sensor Data (RESP_SENSORS)
261+
### Sensor Data (Sensors response)
153262

154263
Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing.
155264

265+
## Cryptographic Algorithms
266+
267+
| Operation | Algorithm |
268+
|-----------|-----------|
269+
| Identity / Signing / Verification | Ed25519 |
270+
| Key Exchange | X25519 (ECDH) |
271+
| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) |
272+
| Hashing | SHA-256 |
273+
156274
## Notes
157275

276+
- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS “1024+ recommended” (that applies to general TNCs, not MeshCore)
158277
- Modem generates identity on first boot (stored in flash)
159-
- SNR values multiplied by 4 for 0.25 dB precision
160-
- Wait for `RESP_TX_DONE` before sending next packet
161-
- Sending `CMD_DATA` while TX is pending returns `ERR_TX_PENDING`
278+
- All multi-byte values are little-endian unless stated otherwise
279+
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
280+
- TxDone is sent as a SetHardware event after each transmission
281+
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
162282
- See [packet_structure.md](./packet_structure.md) for packet format

0 commit comments

Comments
 (0)