Skip to content

Commit 1bcb52b

Browse files
Add new commands and responses for RSSI, channel status, airtime, noise floor, statistics, battery, and sensors.
1 parent c786cfe commit 1bcb52b

4 files changed

Lines changed: 294 additions & 32 deletions

File tree

docs/kiss_modem_protocol.md

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,38 +48,55 @@ Maximum unescaped frame size: 512 bytes.
4848
| `CMD_GET_TX_POWER` | `0x0D` | - |
4949
| `CMD_GET_SYNC_WORD` | `0x0E` | - |
5050
| `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) |
5159

5260
### Response Commands (Modem → Host)
5361

5462
| Command | Value | Data |
5563
|---------|-------|------|
5664
| `CMD_DATA` | `0x00` | SNR (1) + RSSI (1) + Packet |
57-
| `RESP_IDENTITY` | `0x11` | PubKey (32) |
58-
| `RESP_RANDOM` | `0x12` | Random bytes (1-64) |
59-
| `RESP_VERIFY` | `0x13` | Result (1): 0x00=invalid, 0x01=valid |
60-
| `RESP_SIGNATURE` | `0x14` | Signature (64) |
61-
| `RESP_ENCRYPTED` | `0x15` | MAC (2) + Ciphertext |
62-
| `RESP_DECRYPTED` | `0x16` | Plaintext |
63-
| `RESP_SHARED_SECRET` | `0x17` | Shared secret (32) |
64-
| `RESP_HASH` | `0x18` | SHA-256 hash (32) |
65-
| `RESP_OK` | `0x19` | - |
66-
| `RESP_RADIO` | `0x1A` | Freq (4) + BW (4) + SF (1) + CR (1) |
67-
| `RESP_TX_POWER` | `0x1B` | Power dBm (1) |
68-
| `RESP_SYNC_WORD` | `0x1C` | Sync word (1) |
69-
| `RESP_VERSION` | `0x1D` | Version (1) + Reserved (1) |
70-
| `RESP_ERROR` | `0x1E` | Error code (1) |
71-
| `RESP_TX_DONE` | `0x1F` | Result (1): 0x00=failed, 0x01=success |
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+
| `RESP_SYNC_WORD` | `0x2C` | Sync word (1) |
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 |
7288

7389
## Error Codes
7490

7591
| Code | Value | Description |
7692
|------|-------|-------------|
7793
| `ERR_INVALID_LENGTH` | `0x01` | Request data too short |
7894
| `ERR_INVALID_PARAM` | `0x02` | Invalid parameter value |
79-
| `ERR_NO_CALLBACK` | `0x03` | Radio callback not set |
95+
| `ERR_NO_CALLBACK` | `0x03` | Feature not available |
8096
| `ERR_MAC_FAILED` | `0x04` | MAC verification failed |
8197
| `ERR_UNKNOWN_CMD` | `0x05` | Unknown command |
8298
| `ERR_ENCRYPT_FAILED` | `0x06` | Encryption failed |
99+
| `ERR_TX_PENDING` | `0x07` | TX already pending |
83100

84101
## Data Formats
85102

@@ -102,9 +119,34 @@ All values little-endian.
102119
| RSSI | 1 byte | Signal strength dBm, signed |
103120
| Packet | variable | Raw MeshCore packet |
104121

122+
### Stats (RESP_STATS)
123+
124+
All values little-endian.
125+
126+
| Field | Size | Description |
127+
|-------|------|-------------|
128+
| RX | 4 bytes | Packets received |
129+
| TX | 4 bytes | Packets transmitted |
130+
| Errors | 4 bytes | Receive errors |
131+
132+
### Sensor Permissions (CMD_GET_SENSORS)
133+
134+
| Bit | Value | Description |
135+
|-----|-------|-------------|
136+
| 0 | `0x01` | Base (battery) |
137+
| 1 | `0x02` | Location (GPS) |
138+
| 2 | `0x04` | Environment (temp, humidity, pressure) |
139+
140+
Use `0x07` for all permissions.
141+
142+
### Sensor Data (RESP_SENSORS)
143+
144+
Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing.
145+
105146
## Notes
106147

107148
- Modem generates identity on first boot (stored in flash)
108149
- SNR values multiplied by 4 for 0.25 dB precision
109150
- Wait for `RESP_TX_DONE` before sending next packet
151+
- Sending `CMD_DATA` while TX is pending returns `ERR_TX_PENDING`
110152
- See [packet_structure.md](./packet_structure.md) for packet format

examples/kiss_modem/KissModem.cpp

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r
1010
_setRadioCallback = nullptr;
1111
_setTxPowerCallback = nullptr;
1212
_setSyncWordCallback = nullptr;
13+
_getCurrentRssiCallback = nullptr;
14+
_isChannelBusyCallback = nullptr;
15+
_getAirtimeCallback = nullptr;
16+
_getNoiseFloorCallback = nullptr;
17+
_getStatsCallback = nullptr;
18+
_getBatteryCallback = nullptr;
19+
_getSensorsCallback = nullptr;
1320
_config = {0, 0, 0, 0, 0, 0x12};
1421
}
1522

@@ -91,6 +98,8 @@ void KissModem::processFrame() {
9198
writeErrorFrame(ERR_INVALID_LENGTH);
9299
} else if (data_len > KISS_MAX_PACKET_SIZE) {
93100
writeErrorFrame(ERR_INVALID_LENGTH);
101+
} else if (_has_pending_tx) {
102+
writeErrorFrame(ERR_TX_PENDING);
94103
} else {
95104
memcpy(_pending_tx, data, data_len);
96105
_pending_tx_len = data_len;
@@ -142,6 +151,30 @@ void KissModem::processFrame() {
142151
case CMD_GET_VERSION:
143152
handleGetVersion();
144153
break;
154+
case CMD_GET_CURRENT_RSSI:
155+
handleGetCurrentRssi();
156+
break;
157+
case CMD_IS_CHANNEL_BUSY:
158+
handleIsChannelBusy();
159+
break;
160+
case CMD_GET_AIRTIME:
161+
handleGetAirtime(data, data_len);
162+
break;
163+
case CMD_GET_NOISE_FLOOR:
164+
handleGetNoiseFloor();
165+
break;
166+
case CMD_GET_STATS:
167+
handleGetStats();
168+
break;
169+
case CMD_GET_BATTERY:
170+
handleGetBattery();
171+
break;
172+
case CMD_PING:
173+
handlePing();
174+
break;
175+
case CMD_GET_SENSORS:
176+
handleGetSensors(data, data_len);
177+
break;
145178
default:
146179
writeErrorFrame(ERR_UNKNOWN_CMD);
147180
break;
@@ -360,3 +393,98 @@ void KissModem::onTxComplete(bool success) {
360393
uint8_t result = success ? 0x01 : 0x00;
361394
writeFrame(RESP_TX_DONE, &result, 1);
362395
}
396+
397+
void KissModem::handleGetCurrentRssi() {
398+
if (!_getCurrentRssiCallback) {
399+
writeErrorFrame(ERR_NO_CALLBACK);
400+
return;
401+
}
402+
403+
float rssi = _getCurrentRssiCallback();
404+
int8_t rssi_byte = (int8_t)rssi;
405+
writeFrame(RESP_CURRENT_RSSI, (uint8_t*)&rssi_byte, 1);
406+
}
407+
408+
void KissModem::handleIsChannelBusy() {
409+
if (!_isChannelBusyCallback) {
410+
writeErrorFrame(ERR_NO_CALLBACK);
411+
return;
412+
}
413+
414+
uint8_t busy = _isChannelBusyCallback() ? 0x01 : 0x00;
415+
writeFrame(RESP_CHANNEL_BUSY, &busy, 1);
416+
}
417+
418+
void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) {
419+
if (len < 1) {
420+
writeErrorFrame(ERR_INVALID_LENGTH);
421+
return;
422+
}
423+
if (!_getAirtimeCallback) {
424+
writeErrorFrame(ERR_NO_CALLBACK);
425+
return;
426+
}
427+
428+
uint8_t packet_len = data[0];
429+
uint32_t airtime = _getAirtimeCallback(packet_len);
430+
writeFrame(RESP_AIRTIME, (uint8_t*)&airtime, 4);
431+
}
432+
433+
void KissModem::handleGetNoiseFloor() {
434+
if (!_getNoiseFloorCallback) {
435+
writeErrorFrame(ERR_NO_CALLBACK);
436+
return;
437+
}
438+
439+
int16_t noise_floor = _getNoiseFloorCallback();
440+
writeFrame(RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2);
441+
}
442+
443+
void KissModem::handleGetStats() {
444+
if (!_getStatsCallback) {
445+
writeErrorFrame(ERR_NO_CALLBACK);
446+
return;
447+
}
448+
449+
uint32_t rx, tx, errors;
450+
_getStatsCallback(&rx, &tx, &errors);
451+
uint8_t buf[12];
452+
memcpy(buf, &rx, 4);
453+
memcpy(buf + 4, &tx, 4);
454+
memcpy(buf + 8, &errors, 4);
455+
writeFrame(RESP_STATS, buf, 12);
456+
}
457+
458+
void KissModem::handleGetBattery() {
459+
if (!_getBatteryCallback) {
460+
writeErrorFrame(ERR_NO_CALLBACK);
461+
return;
462+
}
463+
464+
uint16_t mv = _getBatteryCallback();
465+
writeFrame(RESP_BATTERY, (uint8_t*)&mv, 2);
466+
}
467+
468+
void KissModem::handlePing() {
469+
writeFrame(RESP_PONG, nullptr, 0);
470+
}
471+
472+
void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) {
473+
if (len < 1) {
474+
writeErrorFrame(ERR_INVALID_LENGTH);
475+
return;
476+
}
477+
if (!_getSensorsCallback) {
478+
writeErrorFrame(ERR_NO_CALLBACK);
479+
return;
480+
}
481+
482+
uint8_t permissions = data[0];
483+
uint8_t buf[255];
484+
uint8_t result_len = _getSensorsCallback(permissions, buf, 255);
485+
if (result_len > 0) {
486+
writeFrame(RESP_SENSORS, buf, result_len);
487+
} else {
488+
writeFrame(RESP_SENSORS, nullptr, 0);
489+
}
490+
}

examples/kiss_modem/KissModem.h

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,59 @@
2828
#define CMD_GET_TX_POWER 0x0D
2929
#define CMD_GET_SYNC_WORD 0x0E
3030
#define CMD_GET_VERSION 0x0F
31+
#define CMD_GET_CURRENT_RSSI 0x10
32+
#define CMD_IS_CHANNEL_BUSY 0x11
33+
#define CMD_GET_AIRTIME 0x12
34+
#define CMD_GET_NOISE_FLOOR 0x13
35+
#define CMD_GET_STATS 0x14
36+
#define CMD_GET_BATTERY 0x15
37+
#define CMD_PING 0x16
38+
#define CMD_GET_SENSORS 0x17
3139

32-
#define RESP_IDENTITY 0x11
33-
#define RESP_RANDOM 0x12
34-
#define RESP_VERIFY 0x13
35-
#define RESP_SIGNATURE 0x14
36-
#define RESP_ENCRYPTED 0x15
37-
#define RESP_DECRYPTED 0x16
38-
#define RESP_SHARED_SECRET 0x17
39-
#define RESP_HASH 0x18
40-
#define RESP_OK 0x19
41-
#define RESP_RADIO 0x1A
42-
#define RESP_TX_POWER 0x1B
43-
#define RESP_SYNC_WORD 0x1C
44-
#define RESP_VERSION 0x1D
45-
#define RESP_ERROR 0x1E
46-
#define RESP_TX_DONE 0x1F
40+
#define RESP_IDENTITY 0x21
41+
#define RESP_RANDOM 0x22
42+
#define RESP_VERIFY 0x23
43+
#define RESP_SIGNATURE 0x24
44+
#define RESP_ENCRYPTED 0x25
45+
#define RESP_DECRYPTED 0x26
46+
#define RESP_SHARED_SECRET 0x27
47+
#define RESP_HASH 0x28
48+
#define RESP_OK 0x29
49+
#define RESP_RADIO 0x2A
50+
#define RESP_TX_POWER 0x2B
51+
#define RESP_SYNC_WORD 0x2C
52+
#define RESP_VERSION 0x2D
53+
#define RESP_ERROR 0x2E
54+
#define RESP_TX_DONE 0x2F
55+
#define RESP_CURRENT_RSSI 0x30
56+
#define RESP_CHANNEL_BUSY 0x31
57+
#define RESP_AIRTIME 0x32
58+
#define RESP_NOISE_FLOOR 0x33
59+
#define RESP_STATS 0x34
60+
#define RESP_BATTERY 0x35
61+
#define RESP_PONG 0x36
62+
#define RESP_SENSORS 0x37
4763

4864
#define ERR_INVALID_LENGTH 0x01
4965
#define ERR_INVALID_PARAM 0x02
5066
#define ERR_NO_CALLBACK 0x03
5167
#define ERR_MAC_FAILED 0x04
5268
#define ERR_UNKNOWN_CMD 0x05
5369
#define ERR_ENCRYPT_FAILED 0x06
70+
#define ERR_TX_PENDING 0x07
5471

55-
#define KISS_FIRMWARE_VERSION 1
72+
#define KISS_FIRMWARE_VERSION 2
5673

5774
typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr);
5875
typedef void (*SetTxPowerCallback)(uint8_t power);
5976
typedef void (*SetSyncWordCallback)(uint8_t syncWord);
77+
typedef float (*GetCurrentRssiCallback)();
78+
typedef bool (*IsChannelBusyCallback)();
79+
typedef uint32_t (*GetAirtimeCallback)(uint8_t len);
80+
typedef int16_t (*GetNoiseFloorCallback)();
81+
typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors);
82+
typedef uint16_t (*GetBatteryCallback)();
83+
typedef uint8_t (*GetSensorsCallback)(uint8_t permissions, uint8_t* buffer, uint8_t max_len);
6084

6185
struct RadioConfig {
6286
uint32_t freq_hz;
@@ -84,6 +108,13 @@ class KissModem {
84108
SetRadioCallback _setRadioCallback;
85109
SetTxPowerCallback _setTxPowerCallback;
86110
SetSyncWordCallback _setSyncWordCallback;
111+
GetCurrentRssiCallback _getCurrentRssiCallback;
112+
IsChannelBusyCallback _isChannelBusyCallback;
113+
GetAirtimeCallback _getAirtimeCallback;
114+
GetNoiseFloorCallback _getNoiseFloorCallback;
115+
GetStatsCallback _getStatsCallback;
116+
GetBatteryCallback _getBatteryCallback;
117+
GetSensorsCallback _getSensorsCallback;
87118

88119
RadioConfig _config;
89120

@@ -107,6 +138,14 @@ class KissModem {
107138
void handleGetTxPower();
108139
void handleGetSyncWord();
109140
void handleGetVersion();
141+
void handleGetCurrentRssi();
142+
void handleIsChannelBusy();
143+
void handleGetAirtime(const uint8_t* data, uint16_t len);
144+
void handleGetNoiseFloor();
145+
void handleGetStats();
146+
void handleGetBattery();
147+
void handlePing();
148+
void handleGetSensors(const uint8_t* data, uint16_t len);
110149

111150
public:
112151
KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng);
@@ -117,6 +156,13 @@ class KissModem {
117156
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; }
118157
void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; }
119158
void setSyncWordCallback(SetSyncWordCallback cb) { _setSyncWordCallback = cb; }
159+
void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; }
160+
void setIsChannelBusyCallback(IsChannelBusyCallback cb) { _isChannelBusyCallback = cb; }
161+
void setGetAirtimeCallback(GetAirtimeCallback cb) { _getAirtimeCallback = cb; }
162+
void setGetNoiseFloorCallback(GetNoiseFloorCallback cb) { _getNoiseFloorCallback = cb; }
163+
void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; }
164+
void setGetBatteryCallback(GetBatteryCallback cb) { _getBatteryCallback = cb; }
165+
void setGetSensorsCallback(GetSensorsCallback cb) { _getSensorsCallback = cb; }
120166

121167
bool getPacketToSend(uint8_t* packet, uint16_t* len);
122168
void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len);

0 commit comments

Comments
 (0)