mirror of https://github.com/meshcore-dev/MeshCore
5 changed files with 706 additions and 0 deletions
@ -0,0 +1,110 @@ |
|||||
|
# 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` | - | |
||||
|
|
||||
|
### Response Commands (Modem → Host) |
||||
|
|
||||
|
| Command | Value | Data | |
||||
|
|---------|-------|------| |
||||
|
| `CMD_DATA` | `0x00` | SNR (1) + RSSI (1) + Packet | |
||||
|
| `RESP_IDENTITY` | `0x11` | PubKey (32) | |
||||
|
| `RESP_RANDOM` | `0x12` | Random bytes (1-64) | |
||||
|
| `RESP_VERIFY` | `0x13` | Result (1): 0x00=invalid, 0x01=valid | |
||||
|
| `RESP_SIGNATURE` | `0x14` | Signature (64) | |
||||
|
| `RESP_ENCRYPTED` | `0x15` | MAC (2) + Ciphertext | |
||||
|
| `RESP_DECRYPTED` | `0x16` | Plaintext | |
||||
|
| `RESP_SHARED_SECRET` | `0x17` | Shared secret (32) | |
||||
|
| `RESP_HASH` | `0x18` | SHA-256 hash (32) | |
||||
|
| `RESP_OK` | `0x19` | - | |
||||
|
| `RESP_RADIO` | `0x1A` | Freq (4) + BW (4) + SF (1) + CR (1) | |
||||
|
| `RESP_TX_POWER` | `0x1B` | Power dBm (1) | |
||||
|
| `RESP_SYNC_WORD` | `0x1C` | Sync word (1) | |
||||
|
| `RESP_VERSION` | `0x1D` | Version (1) + Reserved (1) | |
||||
|
| `RESP_ERROR` | `0x1E` | Error code (1) | |
||||
|
| `RESP_TX_DONE` | `0x1F` | Result (1): 0x00=failed, 0x01=success | |
||||
|
|
||||
|
## Error Codes |
||||
|
|
||||
|
| Code | Value | Description | |
||||
|
|------|-------|-------------| |
||||
|
| `ERR_INVALID_LENGTH` | `0x01` | Request data too short | |
||||
|
| `ERR_INVALID_PARAM` | `0x02` | Invalid parameter value | |
||||
|
| `ERR_NO_CALLBACK` | `0x03` | Radio callback not set | |
||||
|
| `ERR_MAC_FAILED` | `0x04` | MAC verification failed | |
||||
|
| `ERR_UNKNOWN_CMD` | `0x05` | Unknown command | |
||||
|
| `ERR_ENCRYPT_FAILED` | `0x06` | Encryption failed | |
||||
|
|
||||
|
## 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 | |
||||
|
|
||||
|
## 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 |
||||
|
- See [packet_structure.md](./packet_structure.md) for packet format |
||||
@ -0,0 +1,362 @@ |
|||||
|
#include "KissModem.h" |
||||
|
|
||||
|
KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng) |
||||
|
: _serial(serial), _identity(identity), _rng(rng) { |
||||
|
_rx_len = 0; |
||||
|
_rx_escaped = false; |
||||
|
_rx_active = false; |
||||
|
_has_pending_tx = false; |
||||
|
_pending_tx_len = 0; |
||||
|
_setRadioCallback = nullptr; |
||||
|
_setTxPowerCallback = nullptr; |
||||
|
_setSyncWordCallback = nullptr; |
||||
|
_config = {0, 0, 0, 0, 0, 0x12}; |
||||
|
} |
||||
|
|
||||
|
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 { |
||||
|
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_SET_SYNC_WORD: |
||||
|
handleSetSyncWord(data, data_len); |
||||
|
break; |
||||
|
case CMD_GET_RADIO: |
||||
|
handleGetRadio(); |
||||
|
break; |
||||
|
case CMD_GET_TX_POWER: |
||||
|
handleGetTxPower(); |
||||
|
break; |
||||
|
case CMD_GET_SYNC_WORD: |
||||
|
handleGetSyncWord(); |
||||
|
break; |
||||
|
case CMD_GET_VERSION: |
||||
|
handleGetVersion(); |
||||
|
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::handleSetSyncWord(const uint8_t* data, uint16_t len) { |
||||
|
if (len < 1) { |
||||
|
writeErrorFrame(ERR_INVALID_LENGTH); |
||||
|
return; |
||||
|
} |
||||
|
if (!_setSyncWordCallback) { |
||||
|
writeErrorFrame(ERR_NO_CALLBACK); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_config.sync_word = data[0]; |
||||
|
_setSyncWordCallback(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::handleGetSyncWord() { |
||||
|
writeFrame(RESP_SYNC_WORD, &_config.sync_word, 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); |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <Arduino.h> |
||||
|
#include <Identity.h> |
||||
|
#include <Utils.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_SET_SYNC_WORD 0x0B |
||||
|
#define CMD_GET_RADIO 0x0C |
||||
|
#define CMD_GET_TX_POWER 0x0D |
||||
|
#define CMD_GET_SYNC_WORD 0x0E |
||||
|
#define CMD_GET_VERSION 0x0F |
||||
|
|
||||
|
#define RESP_IDENTITY 0x11 |
||||
|
#define RESP_RANDOM 0x12 |
||||
|
#define RESP_VERIFY 0x13 |
||||
|
#define RESP_SIGNATURE 0x14 |
||||
|
#define RESP_ENCRYPTED 0x15 |
||||
|
#define RESP_DECRYPTED 0x16 |
||||
|
#define RESP_SHARED_SECRET 0x17 |
||||
|
#define RESP_HASH 0x18 |
||||
|
#define RESP_OK 0x19 |
||||
|
#define RESP_RADIO 0x1A |
||||
|
#define RESP_TX_POWER 0x1B |
||||
|
#define RESP_SYNC_WORD 0x1C |
||||
|
#define RESP_VERSION 0x1D |
||||
|
#define RESP_ERROR 0x1E |
||||
|
#define RESP_TX_DONE 0x1F |
||||
|
|
||||
|
#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 KISS_FIRMWARE_VERSION 1 |
||||
|
|
||||
|
typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); |
||||
|
typedef void (*SetTxPowerCallback)(uint8_t power); |
||||
|
typedef void (*SetSyncWordCallback)(uint8_t syncWord); |
||||
|
|
||||
|
struct RadioConfig { |
||||
|
uint32_t freq_hz; |
||||
|
uint32_t bw_hz; |
||||
|
uint8_t sf; |
||||
|
uint8_t cr; |
||||
|
uint8_t tx_power; |
||||
|
uint8_t sync_word; |
||||
|
}; |
||||
|
|
||||
|
class KissModem { |
||||
|
Stream& _serial; |
||||
|
mesh::LocalIdentity& _identity; |
||||
|
mesh::RNG& _rng; |
||||
|
|
||||
|
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; |
||||
|
SetSyncWordCallback _setSyncWordCallback; |
||||
|
|
||||
|
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 handleSetSyncWord(const uint8_t* data, uint16_t len); |
||||
|
void handleGetRadio(); |
||||
|
void handleGetTxPower(); |
||||
|
void handleGetSyncWord(); |
||||
|
void handleGetVersion(); |
||||
|
|
||||
|
public: |
||||
|
KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng); |
||||
|
|
||||
|
void begin(); |
||||
|
void loop(); |
||||
|
|
||||
|
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; } |
||||
|
void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } |
||||
|
void setSyncWordCallback(SetSyncWordCallback cb) { _setSyncWordCallback = 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,108 @@ |
|||||
|
#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); |
||||
|
} |
||||
|
|
||||
|
void onSetSyncWord(uint8_t sync_word) { |
||||
|
radio_set_sync_word(sync_word); |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
|
||||
|
modem = new KissModem(Serial, identity, rng); |
||||
|
modem->setRadioCallback(onSetRadio); |
||||
|
modem->setTxPowerCallback(onSetTxPower); |
||||
|
modem->setSyncWordCallback(onSetSyncWord); |
||||
|
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