Three-tier configuration hierarchy: - [env:base] — RadioLib + default LoRa parameters - [soc_esp32/esp32s3/nrf52] — platform + framework per SoC - [env:board_name] — board-specific pins + chip selection 20 boards across 4 vendors: - Heltec: 11 boards (T114, CT62, E213, E290, Mesh Solar, T190, Tracker, Tracker V2, V2, V3, V4) - LilyGo: 4 boards (T-Beam 1W, sx1262, sx1276, supreme) - Seeed: 1 board (Xiao S3 + Wio SX1262 with verified pins) - RAK: 4 boards (RAK11310, RAK3112, RAK3401, RAK3x72, RAK4631) Known/verified pins: Heltec V2/V3/V4, RAK4631, Seeed Xiao S3 FIXME pins: all others (placeholders for future research) Source skeleton: - config.h — compile-time defaults + pin validation (#error checks) - kiss.h/c — KISS protocol implementation (C99) - radio.h/cpp — RadioLib wrapper with C API (extern "C" boundary) - main.cpp — Arduino entry point All files pass pre-commit (prettier, markdownlint, YAML check).
112 lines
3.1 KiB
C
112 lines
3.1 KiB
C
/* KISS protocol implementation — C99 */
|
|
#include "kiss.h"
|
|
|
|
void kiss_decoder_init(kiss_decoder_t *dec) {
|
|
dec->state = KISS_STATE_IDLE;
|
|
dec->len = 0;
|
|
}
|
|
|
|
bool kiss_decode(kiss_decoder_t *dec, uint8_t byte, kiss_frame_t *frame) {
|
|
switch (dec->state) {
|
|
case KISS_STATE_IDLE:
|
|
if (byte == KISS_FEND) {
|
|
dec->len = 0;
|
|
dec->state = KISS_STATE_IN_FRAME;
|
|
}
|
|
return false;
|
|
|
|
case KISS_STATE_IN_FRAME:
|
|
if (byte == KISS_FESC) {
|
|
dec->state = KISS_STATE_ESCAPE;
|
|
return false;
|
|
}
|
|
if (byte == KISS_FEND) {
|
|
if (dec->len > 0) {
|
|
/* Frame complete */
|
|
frame->port = dec->buf[0] & 0x0Fu;
|
|
frame->len = dec->len - 1;
|
|
if (frame->len > 0) {
|
|
for (size_t i = 0; i < frame->len; i++) {
|
|
frame->data[i] = dec->buf[i + 1];
|
|
}
|
|
}
|
|
dec->state = KISS_STATE_IDLE;
|
|
return true;
|
|
}
|
|
dec->state = KISS_STATE_IDLE;
|
|
return false;
|
|
}
|
|
if (dec->len < KISS_MAX_FRAME + 1) {
|
|
dec->buf[dec->len++] = byte;
|
|
}
|
|
return false;
|
|
|
|
case KISS_STATE_ESCAPE:
|
|
if (byte == KISS_TFEND) {
|
|
byte = KISS_FEND;
|
|
} else if (byte == KISS_TFESC) {
|
|
byte = KISS_FESC;
|
|
}
|
|
if (dec->len < KISS_MAX_FRAME + 1) {
|
|
dec->buf[dec->len++] = byte;
|
|
}
|
|
dec->state = KISS_STATE_IN_FRAME;
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
size_t kiss_encode(uint8_t port, const uint8_t *data, size_t len,
|
|
uint8_t *dst, size_t dst_cap) {
|
|
if (dst_cap < len + 3)
|
|
return 0; /* Need at least: FEND type data... FEND */
|
|
|
|
size_t pos = 0;
|
|
dst[pos++] = KISS_FEND;
|
|
|
|
/* Type byte: port in upper nibble, cmd in lower nibble (0 for data) */
|
|
uint8_t type = (port << 4) | 0x00;
|
|
if (type == KISS_FEND) {
|
|
dst[pos++] = KISS_FESC;
|
|
dst[pos++] = KISS_TFEND;
|
|
} else if (type == KISS_FESC) {
|
|
dst[pos++] = KISS_FESC;
|
|
dst[pos++] = KISS_TFESC;
|
|
} else {
|
|
dst[pos++] = type;
|
|
}
|
|
|
|
/* Payload with escaping */
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (pos + 2 > dst_cap)
|
|
return 0; /* Overflow */
|
|
if (data[i] == KISS_FEND) {
|
|
dst[pos++] = KISS_FESC;
|
|
dst[pos++] = KISS_TFEND;
|
|
} else if (data[i] == KISS_FESC) {
|
|
dst[pos++] = KISS_FESC;
|
|
dst[pos++] = KISS_TFESC;
|
|
} else {
|
|
dst[pos++] = data[i];
|
|
}
|
|
}
|
|
|
|
if (pos + 1 > dst_cap)
|
|
return 0;
|
|
dst[pos++] = KISS_FEND;
|
|
return pos;
|
|
}
|
|
|
|
size_t kiss_encode_quality(int8_t snr, int16_t rssi, uint8_t *dst,
|
|
size_t dst_cap) {
|
|
if (dst_cap < 5)
|
|
return 0; /* Need: FEND type snr rssi_hi rssi_lo FEND */
|
|
|
|
uint8_t payload[3];
|
|
payload[0] = (uint8_t)snr;
|
|
payload[1] = (uint8_t)((rssi >> 8) & 0xFF);
|
|
payload[2] = (uint8_t)(rssi & 0xFF);
|
|
|
|
return kiss_encode(KISS_PORT_QUALITY, payload, 3, dst, dst_cap);
|
|
}
|