mirror of https://github.com/meshcore-dev/MeshCore
committed by
GitHub
5 changed files with 860 additions and 0 deletions
@ -0,0 +1,152 @@ |
|||||
|
# 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. |
||||
|
|
||||
|
## Serial Configuration |
||||
|
|
||||
|
115200 baud, 8N1, no flow control. |
||||
|
|
||||
|
## Frame Format |
||||
|
|
||||
|
Standard KISS framing with byte stuffing. |
||||
|
|
||||
|
| Byte | Name | Description | |
||||
|
|------|------|-------------| |
||||
|
| `0xC0` | FEND | Frame delimiter | |
||||
|
| `0xDB` | FESC | Escape character | |
||||
|
| `0xDC` | TFEND | Escaped FEND (FESC + TFEND = 0xC0) | |
||||
|
| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) | |
||||
|
|
||||
|
``` |
||||
|
┌──────┬─────────┬──────────────┬──────┐ |
||||
|
│ FEND │ Command │ Data (escaped)│ FEND │ |
||||
|
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ |
||||
|
└──────┴─────────┴──────────────┴──────┘ |
||||
|
``` |
||||
|
|
||||
|
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) | |
||||
|
| `CMD_SET_SYNC_WORD` | `0x0B` | Sync word (1) | |
||||
|
| `CMD_GET_RADIO` | `0x0C` | - | |
||||
|
| `CMD_GET_TX_POWER` | `0x0D` | - | |
||||
|
| `CMD_GET_SYNC_WORD` | `0x0E` | - | |
||||
|
| `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) | |
||||
|
| `RESP_SYNC_WORD` | `0x2C` | Sync word (1) | |
||||
|
| `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 |
||||
|
|
||||
|
| 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 | |
||||
|
|
||||
|
## Data Formats |
||||
|
|
||||
|
### Radio Parameters (CMD_SET_RADIO / RESP_RADIO) |
||||
|
|
||||
|
All values little-endian. |
||||
|
|
||||
|
| Field | Size | Description | |
||||
|
|-------|------|-------------| |
||||
|
| Frequency | 4 bytes | Hz (e.g., 869618000) | |
||||
|
| Bandwidth | 4 bytes | Hz (e.g., 62500) | |
||||
|
| 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 | |
||||
|
|
||||
|
### Stats (RESP_STATS) |
||||
|
|
||||
|
All values little-endian. |
||||
|
|
||||
|
| Field | Size | Description | |
||||
|
|-------|------|-------------| |
||||
|
| RX | 4 bytes | Packets received | |
||||
|
| TX | 4 bytes | Packets transmitted | |
||||
|
| Errors | 4 bytes | Receive errors | |
||||
|
|
||||
|
### Sensor Permissions (CMD_GET_SENSORS) |
||||
|
|
||||
|
| Bit | Value | Description | |
||||
|
|-----|-------|-------------| |
||||
|
| 0 | `0x01` | Base (battery) | |
||||
|
| 1 | `0x02` | Location (GPS) | |
||||
|
| 2 | `0x04` | Environment (temp, humidity, pressure) | |
||||
|
|
||||
|
Use `0x07` for all permissions. |
||||
|
|
||||
|
### Sensor Data (RESP_SENSORS) |
||||
|
|
||||
|
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` |
||||
|
- See [packet_structure.md](./packet_structure.md) for packet format |
||||
@ -0,0 +1,437 @@ |
|||||
|
#include "KissModem.h" |
||||
|
#include <CayenneLPP.h> |
||||
|
|
||||
|
KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng, |
||||
|
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors) |
||||
|
: _serial(serial), _identity(identity), _rng(rng), _radio(radio), _board(board), _sensors(sensors) { |
||||
|
_rx_len = 0; |
||||
|
_rx_escaped = false; |
||||
|
_rx_active = false; |
||||
|
_has_pending_tx = false; |
||||
|
_pending_tx_len = 0; |
||||
|
_setRadioCallback = nullptr; |
||||
|
_setTxPowerCallback = nullptr; |
||||
|
_getCurrentRssiCallback = nullptr; |
||||
|
_getStatsCallback = nullptr; |
||||
|
_config = {0, 0, 0, 0, 0}; |
||||
|
} |
||||
|
|
||||
|
void KissModem::begin() { |
||||
|
_rx_len = 0; |
||||
|
_rx_escaped = false; |
||||
|
_rx_active = false; |
||||
|
_has_pending_tx = false; |
||||
|
} |
||||
|
|
||||
|
void KissModem::writeByte(uint8_t b) { |
||||
|
if (b == KISS_FEND) { |
||||
|
_serial.write(KISS_FESC); |
||||
|
_serial.write(KISS_TFEND); |
||||
|
} else if (b == KISS_FESC) { |
||||
|
_serial.write(KISS_FESC); |
||||
|
_serial.write(KISS_TFESC); |
||||
|
} else { |
||||
|
_serial.write(b); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KissModem::writeFrame(uint8_t cmd, const uint8_t* data, uint16_t len) { |
||||
|
_serial.write(KISS_FEND); |
||||
|
writeByte(cmd); |
||||
|
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::loop() { |
||||
|
while (_serial.available()) { |
||||
|
uint8_t b = _serial.read(); |
||||
|
|
||||
|
if (b == KISS_FEND) { |
||||
|
if (_rx_active && _rx_len > 0) { |
||||
|
processFrame(); |
||||
|
} |
||||
|
_rx_len = 0; |
||||
|
_rx_escaped = false; |
||||
|
_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; |
||||
|
} |
||||
|
|
||||
|
if (_rx_len < KISS_MAX_FRAME_SIZE) { |
||||
|
_rx_buf[_rx_len++] = b; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KissModem::processFrame() { |
||||
|
if (_rx_len < 1) return; |
||||
|
|
||||
|
uint8_t cmd = _rx_buf[0]; |
||||
|
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 { |
||||
|
memcpy(_pending_tx, data, data_len); |
||||
|
_pending_tx_len = data_len; |
||||
|
_has_pending_tx = true; |
||||
|
} |
||||
|
break; |
||||
|
case CMD_GET_IDENTITY: |
||||
|
handleGetIdentity(); |
||||
|
break; |
||||
|
case CMD_GET_RANDOM: |
||||
|
handleGetRandom(data, data_len); |
||||
|
break; |
||||
|
case CMD_VERIFY_SIGNATURE: |
||||
|
handleVerifySignature(data, data_len); |
||||
|
break; |
||||
|
case CMD_SIGN_DATA: |
||||
|
handleSignData(data, data_len); |
||||
|
break; |
||||
|
case CMD_ENCRYPT_DATA: |
||||
|
handleEncryptData(data, data_len); |
||||
|
break; |
||||
|
case CMD_DECRYPT_DATA: |
||||
|
handleDecryptData(data, data_len); |
||||
|
break; |
||||
|
case CMD_KEY_EXCHANGE: |
||||
|
handleKeyExchange(data, data_len); |
||||
|
break; |
||||
|
case CMD_HASH: |
||||
|
handleHash(data, data_len); |
||||
|
break; |
||||
|
case CMD_SET_RADIO: |
||||
|
handleSetRadio(data, data_len); |
||||
|
break; |
||||
|
case CMD_SET_TX_POWER: |
||||
|
handleSetTxPower(data, data_len); |
||||
|
break; |
||||
|
case CMD_GET_RADIO: |
||||
|
handleGetRadio(); |
||||
|
break; |
||||
|
case CMD_GET_TX_POWER: |
||||
|
handleGetTxPower(); |
||||
|
break; |
||||
|
case CMD_GET_VERSION: |
||||
|
handleGetVersion(); |
||||
|
break; |
||||
|
case CMD_GET_CURRENT_RSSI: |
||||
|
handleGetCurrentRssi(); |
||||
|
break; |
||||
|
case CMD_IS_CHANNEL_BUSY: |
||||
|
handleIsChannelBusy(); |
||||
|
break; |
||||
|
case CMD_GET_AIRTIME: |
||||
|
handleGetAirtime(data, data_len); |
||||
|
break; |
||||
|
case CMD_GET_NOISE_FLOOR: |
||||
|
handleGetNoiseFloor(); |
||||
|
break; |
||||
|
case CMD_GET_STATS: |
||||
|
handleGetStats(); |
||||
|
break; |
||||
|
case CMD_GET_BATTERY: |
||||
|
handleGetBattery(); |
||||
|
break; |
||||
|
case CMD_PING: |
||||
|
handlePing(); |
||||
|
break; |
||||
|
case CMD_GET_SENSORS: |
||||
|
handleGetSensors(data, data_len); |
||||
|
break; |
||||
|
default: |
||||
|
writeErrorFrame(ERR_UNKNOWN_CMD); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetIdentity() { |
||||
|
writeFrame(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); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
uint8_t requested = data[0]; |
||||
|
if (requested < 1 || requested > 64) { |
||||
|
writeErrorFrame(ERR_INVALID_PARAM); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
uint8_t buf[64]; |
||||
|
_rng.random(buf, requested); |
||||
|
writeFrame(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); |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleSignData(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 1) { |
||||
|
writeErrorFrame(ERR_INVALID_LENGTH); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
uint8_t signature[SIGNATURE_SIZE]; |
||||
|
_identity.sign(signature, data, len); |
||||
|
writeFrame(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); |
||||
|
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); |
||||
|
} else { |
||||
|
writeErrorFrame(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); |
||||
|
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); |
||||
|
} else { |
||||
|
writeErrorFrame(ERR_MAC_FAILED); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) { |
||||
|
if (len < PUB_KEY_SIZE) { |
||||
|
writeErrorFrame(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); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleHash(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 1) { |
||||
|
writeErrorFrame(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); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 10) { |
||||
|
writeErrorFrame(ERR_INVALID_LENGTH); |
||||
|
return; |
||||
|
} |
||||
|
if (!_setRadioCallback) { |
||||
|
writeErrorFrame(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); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 1) { |
||||
|
writeErrorFrame(ERR_INVALID_LENGTH); |
||||
|
return; |
||||
|
} |
||||
|
if (!_setTxPowerCallback) { |
||||
|
writeErrorFrame(ERR_NO_CALLBACK); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_config.tx_power = data[0]; |
||||
|
_setTxPowerCallback(data[0]); |
||||
|
writeFrame(RESP_OK, nullptr, 0); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetRadio() { |
||||
|
uint8_t buf[10]; |
||||
|
memcpy(buf, &_config.freq_hz, 4); |
||||
|
memcpy(buf + 4, &_config.bw_hz, 4); |
||||
|
buf[8] = _config.sf; |
||||
|
buf[9] = _config.cr; |
||||
|
writeFrame(RESP_RADIO, buf, 10); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetTxPower() { |
||||
|
writeFrame(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); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetCurrentRssi() { |
||||
|
if (!_getCurrentRssiCallback) { |
||||
|
writeErrorFrame(ERR_NO_CALLBACK); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
float rssi = _getCurrentRssiCallback(); |
||||
|
int8_t rssi_byte = (int8_t)rssi; |
||||
|
writeFrame(RESP_CURRENT_RSSI, (uint8_t*)&rssi_byte, 1); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleIsChannelBusy() { |
||||
|
uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00; |
||||
|
writeFrame(RESP_CHANNEL_BUSY, &busy, 1); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 1) { |
||||
|
writeErrorFrame(ERR_INVALID_LENGTH); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
uint8_t packet_len = data[0]; |
||||
|
uint32_t airtime = _radio.getEstAirtimeFor(packet_len); |
||||
|
writeFrame(RESP_AIRTIME, (uint8_t*)&airtime, 4); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetNoiseFloor() { |
||||
|
int16_t noise_floor = _radio.getNoiseFloor(); |
||||
|
writeFrame(RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetStats() { |
||||
|
if (!_getStatsCallback) { |
||||
|
writeErrorFrame(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); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetBattery() { |
||||
|
uint16_t mv = _board.getBattMilliVolts(); |
||||
|
writeFrame(RESP_BATTERY, (uint8_t*)&mv, 2); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handlePing() { |
||||
|
writeFrame(RESP_PONG, nullptr, 0); |
||||
|
} |
||||
|
|
||||
|
void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 1) { |
||||
|
writeErrorFrame(ERR_INVALID_LENGTH); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
uint8_t permissions = data[0]; |
||||
|
CayenneLPP telemetry(255); |
||||
|
if (_sensors.querySensors(permissions, telemetry)) { |
||||
|
writeFrame(RESP_SENSORS, telemetry.getBuffer(), telemetry.getSize()); |
||||
|
} else { |
||||
|
writeFrame(RESP_SENSORS, nullptr, 0); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,152 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <Arduino.h> |
||||
|
#include <Identity.h> |
||||
|
#include <Utils.h> |
||||
|
#include <Mesh.h> |
||||
|
#include <helpers/SensorManager.h> |
||||
|
|
||||
|
#define KISS_FEND 0xC0 |
||||
|
#define KISS_FESC 0xDB |
||||
|
#define KISS_TFEND 0xDC |
||||
|
#define KISS_TFESC 0xDD |
||||
|
|
||||
|
#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_FIRMWARE_VERSION 1 |
||||
|
|
||||
|
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); |
||||
|
|
||||
|
struct RadioConfig { |
||||
|
uint32_t freq_hz; |
||||
|
uint32_t bw_hz; |
||||
|
uint8_t sf; |
||||
|
uint8_t cr; |
||||
|
uint8_t tx_power; |
||||
|
}; |
||||
|
|
||||
|
class KissModem { |
||||
|
Stream& _serial; |
||||
|
mesh::LocalIdentity& _identity; |
||||
|
mesh::RNG& _rng; |
||||
|
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; |
||||
|
|
||||
|
SetRadioCallback _setRadioCallback; |
||||
|
SetTxPowerCallback _setTxPowerCallback; |
||||
|
GetCurrentRssiCallback _getCurrentRssiCallback; |
||||
|
GetStatsCallback _getStatsCallback; |
||||
|
|
||||
|
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 processFrame(); |
||||
|
|
||||
|
void handleGetIdentity(); |
||||
|
void handleGetRandom(const uint8_t* data, uint16_t len); |
||||
|
void handleVerifySignature(const uint8_t* data, uint16_t len); |
||||
|
void handleSignData(const uint8_t* data, uint16_t len); |
||||
|
void handleEncryptData(const uint8_t* data, uint16_t len); |
||||
|
void handleDecryptData(const uint8_t* data, uint16_t len); |
||||
|
void handleKeyExchange(const uint8_t* data, uint16_t len); |
||||
|
void handleHash(const uint8_t* data, uint16_t len); |
||||
|
void handleSetRadio(const uint8_t* data, uint16_t len); |
||||
|
void handleSetTxPower(const uint8_t* data, uint16_t len); |
||||
|
void handleGetRadio(); |
||||
|
void handleGetTxPower(); |
||||
|
void handleGetVersion(); |
||||
|
void handleGetCurrentRssi(); |
||||
|
void handleIsChannelBusy(); |
||||
|
void handleGetAirtime(const uint8_t* data, uint16_t len); |
||||
|
void handleGetNoiseFloor(); |
||||
|
void handleGetStats(); |
||||
|
void handleGetBattery(); |
||||
|
void handlePing(); |
||||
|
void handleGetSensors(const uint8_t* data, uint16_t len); |
||||
|
|
||||
|
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); |
||||
|
}; |
||||
@ -0,0 +1,117 @@ |
|||||
|
#include <Arduino.h> |
||||
|
#include <target.h> |
||||
|
#include <helpers/ArduinoHelpers.h> |
||||
|
#include <helpers/IdentityStore.h> |
||||
|
#include "KissModem.h" |
||||
|
|
||||
|
#if defined(NRF52_PLATFORM) |
||||
|
#include <InternalFileSystem.h> |
||||
|
#elif defined(RP2040_PLATFORM) |
||||
|
#include <LittleFS.h> |
||||
|
#elif defined(ESP32) |
||||
|
#include <SPIFFS.h> |
||||
|
#endif |
||||
|
|
||||
|
StdRNG rng; |
||||
|
mesh::LocalIdentity identity; |
||||
|
KissModem* modem; |
||||
|
|
||||
|
void halt() { |
||||
|
while (1) ; |
||||
|
} |
||||
|
|
||||
|
void loadOrCreateIdentity() { |
||||
|
#if defined(NRF52_PLATFORM) |
||||
|
InternalFS.begin(); |
||||
|
IdentityStore store(InternalFS, ""); |
||||
|
#elif defined(ESP32) |
||||
|
SPIFFS.begin(true); |
||||
|
IdentityStore store(SPIFFS, "/identity"); |
||||
|
#elif defined(RP2040_PLATFORM) |
||||
|
LittleFS.begin(); |
||||
|
IdentityStore store(LittleFS, "/identity"); |
||||
|
store.begin(); |
||||
|
#else |
||||
|
#error "Filesystem not defined" |
||||
|
#endif |
||||
|
|
||||
|
if (!store.load("_main", identity)) { |
||||
|
identity = radio_new_identity(); |
||||
|
while (identity.pub_key[0] == 0x00 || identity.pub_key[0] == 0xFF) { |
||||
|
identity = radio_new_identity(); |
||||
|
} |
||||
|
store.save("_main", identity); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) { |
||||
|
radio_set_params(freq, bw, sf, cr); |
||||
|
} |
||||
|
|
||||
|
void onSetTxPower(uint8_t power) { |
||||
|
radio_set_tx_power(power); |
||||
|
} |
||||
|
|
||||
|
float onGetCurrentRssi() { |
||||
|
return radio_driver.getCurrentRSSI(); |
||||
|
} |
||||
|
|
||||
|
void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) { |
||||
|
*rx = radio_driver.getPacketsRecv(); |
||||
|
*tx = radio_driver.getPacketsSent(); |
||||
|
*errors = radio_driver.getPacketsRecvErrors(); |
||||
|
} |
||||
|
|
||||
|
void setup() { |
||||
|
board.begin(); |
||||
|
|
||||
|
if (!radio_init()) { |
||||
|
halt(); |
||||
|
} |
||||
|
|
||||
|
radio_driver.begin(); |
||||
|
|
||||
|
rng.begin(radio_get_rng_seed()); |
||||
|
loadOrCreateIdentity(); |
||||
|
|
||||
|
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); |
||||
|
modem->setRadioCallback(onSetRadio); |
||||
|
modem->setTxPowerCallback(onSetTxPower); |
||||
|
modem->setGetCurrentRssiCallback(onGetCurrentRssi); |
||||
|
modem->setGetStatsCallback(onGetStats); |
||||
|
modem->begin(); |
||||
|
} |
||||
|
|
||||
|
void loop() { |
||||
|
modem->loop(); |
||||
|
|
||||
|
uint8_t packet[KISS_MAX_PACKET_SIZE]; |
||||
|
uint16_t len; |
||||
|
|
||||
|
if (modem->getPacketToSend(packet, &len)) { |
||||
|
radio_driver.startSendRaw(packet, len); |
||||
|
while (!radio_driver.isSendComplete()) { |
||||
|
delay(1); |
||||
|
} |
||||
|
radio_driver.onSendFinished(); |
||||
|
modem->onTxComplete(true); |
||||
|
} |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
Loading…
Reference in new issue