Scaffold PlatformIO project with 20 board configs and C99/C++ source skeleton
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).
This commit is contained in:
51
src/config.h
Normal file
51
src/config.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
/* Default LoRa parameters — override per-board in hardware/.../platformio.ini */
|
||||
#ifndef LORA_FREQ_KHZ
|
||||
# define LORA_FREQ_KHZ 869525UL /* 869.525 MHz */
|
||||
#endif
|
||||
#ifndef LORA_BW_HZ
|
||||
# define LORA_BW_HZ 125000UL /* 125 kHz */
|
||||
#endif
|
||||
#ifndef LORA_SF
|
||||
# define LORA_SF 7
|
||||
#endif
|
||||
#ifndef LORA_CR
|
||||
# define LORA_CR 5 /* denominator: coding rate = 4/CR */
|
||||
#endif
|
||||
#ifndef LORA_POWER_DBM
|
||||
# define LORA_POWER_DBM 14
|
||||
#endif
|
||||
#ifndef LORA_SYNCWORD
|
||||
# define LORA_SYNCWORD 0x34 /* LoRa public */
|
||||
#endif
|
||||
|
||||
#ifndef KISS_BAUD
|
||||
# define KISS_BAUD 115200
|
||||
#endif
|
||||
|
||||
/* Pin validation — boards must define all required pins via build_flags */
|
||||
#ifndef LORA_PIN_NSS
|
||||
# error "LORA_PIN_NSS not defined — add to hardware/<vendor>/<board>/platformio.ini"
|
||||
#endif
|
||||
#ifndef LORA_PIN_RESET
|
||||
# error "LORA_PIN_RESET not defined"
|
||||
#endif
|
||||
|
||||
#if defined(LORA_CHIP_SX1276)
|
||||
# ifndef LORA_PIN_DIO0
|
||||
# error "LORA_PIN_DIO0 not defined (required for SX1276)"
|
||||
# endif
|
||||
#else
|
||||
# ifndef LORA_PIN_DIO1
|
||||
# error "LORA_PIN_DIO1 not defined (required for SX1262/LR1110)"
|
||||
# endif
|
||||
# ifndef LORA_PIN_BUSY
|
||||
# error "LORA_PIN_BUSY not defined (required for SX1262/LR1110)"
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#if !defined(LORA_CHIP_SX1276) && !defined(LORA_CHIP_SX1262) && \
|
||||
!defined(LORA_CHIP_LR1110)
|
||||
# error "No LoRa chip defined — set LORA_CHIP_SX1276, LORA_CHIP_SX1262, or LORA_CHIP_LR1110"
|
||||
#endif
|
||||
111
src/kiss.c
Normal file
111
src/kiss.c
Normal file
@@ -0,0 +1,111 @@
|
||||
/* 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);
|
||||
}
|
||||
70
src/kiss.h
Normal file
70
src/kiss.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* KISS special bytes */
|
||||
#define KISS_FEND 0xC0u
|
||||
#define KISS_FESC 0xDBu
|
||||
#define KISS_TFEND 0xDCu
|
||||
#define KISS_TFESC 0xDDu
|
||||
|
||||
/* Port assignments */
|
||||
#define KISS_PORT_DATA 0u
|
||||
#define KISS_PORT_QUALITY 1u
|
||||
#define KISS_PORT_CONFIG 2u
|
||||
|
||||
/* Configuration command opcodes (port 2) */
|
||||
#define KISS_CMD_RESERVED 0x00u
|
||||
#define KISS_CMD_RES_OK 0x01u
|
||||
#define KISS_CMD_RES_ERROR 0x02u
|
||||
#define KISS_CMD_GET_RADIO 0x10u
|
||||
#define KISS_CMD_SET_RADIO 0x11u
|
||||
#define KISS_CMD_GET_FREQ 0x12u
|
||||
#define KISS_CMD_SET_FREQ 0x13u
|
||||
#define KISS_CMD_GET_BW 0x14u
|
||||
#define KISS_CMD_SET_BW 0x15u
|
||||
#define KISS_CMD_GET_SF 0x16u
|
||||
#define KISS_CMD_SET_SF 0x17u
|
||||
#define KISS_CMD_GET_CR 0x18u
|
||||
#define KISS_CMD_SET_CR 0x19u
|
||||
#define KISS_CMD_GET_POWER 0x1Au
|
||||
#define KISS_CMD_SET_POWER 0x1Bu
|
||||
#define KISS_CMD_GET_SYNCWORD 0x1Cu
|
||||
#define KISS_CMD_SET_SYNCWORD 0x1Du
|
||||
|
||||
#define KISS_MAX_FRAME 256u
|
||||
|
||||
typedef struct {
|
||||
uint8_t port;
|
||||
uint8_t data[KISS_MAX_FRAME];
|
||||
uint16_t len;
|
||||
} kiss_frame_t;
|
||||
|
||||
typedef enum {
|
||||
KISS_STATE_IDLE,
|
||||
KISS_STATE_IN_FRAME,
|
||||
KISS_STATE_ESCAPE,
|
||||
} kiss_state_t;
|
||||
|
||||
typedef struct {
|
||||
kiss_state_t state;
|
||||
uint8_t buf[KISS_MAX_FRAME + 1u]; /* +1 for type byte */
|
||||
uint16_t len;
|
||||
} kiss_decoder_t;
|
||||
|
||||
void kiss_decoder_init(kiss_decoder_t *dec);
|
||||
|
||||
/* Feed one byte into the decoder. Returns true when a complete frame is
|
||||
ready in *frame. frame must not be NULL when return value is checked. */
|
||||
bool kiss_decode(kiss_decoder_t *dec, uint8_t byte, kiss_frame_t *frame);
|
||||
|
||||
/* Encode port+data into a KISS frame. Returns bytes written, or 0 on
|
||||
overflow. */
|
||||
size_t kiss_encode(uint8_t port, const uint8_t *data, size_t len,
|
||||
uint8_t *dst, size_t dst_cap);
|
||||
|
||||
/* Encode a 3-byte signal quality frame for port 1. Big-endian: int8 snr,
|
||||
int16 rssi. */
|
||||
size_t kiss_encode_quality(int8_t snr, int16_t rssi, uint8_t *dst,
|
||||
size_t dst_cap);
|
||||
48
src/main.cpp
Normal file
48
src/main.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "kiss.h"
|
||||
#include "radio.h"
|
||||
|
||||
static kiss_decoder_t rx_decoder; /* host → modem (serial in) */
|
||||
static uint8_t tx_buf[KISS_MAX_FRAME];
|
||||
static uint8_t radio_buf[KISS_MAX_FRAME];
|
||||
|
||||
void setup() {
|
||||
Serial.begin(KISS_BAUD);
|
||||
kiss_decoder_init(&rx_decoder);
|
||||
radio_init();
|
||||
radio_rx_start();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
kiss_frame_t frame;
|
||||
|
||||
/* Serial → radio: decode incoming KISS frames and transmit */
|
||||
while (Serial.available()) {
|
||||
if (kiss_decode(&rx_decoder, (uint8_t)Serial.read(), &frame)) {
|
||||
if (frame.port == KISS_PORT_DATA) {
|
||||
radio_tx(frame.data, frame.len);
|
||||
}
|
||||
/* port 2 config handling goes here */
|
||||
}
|
||||
}
|
||||
|
||||
/* Radio → serial: forward received packets as KISS frames */
|
||||
if (radio_rx_available()) {
|
||||
radio_rx_info_t info;
|
||||
int n =
|
||||
radio_rx_read(radio_buf, sizeof(radio_buf), &info);
|
||||
if (n > 0) {
|
||||
/* Port 1: signal quality */
|
||||
size_t qlen = kiss_encode_quality(info.snr, info.rssi, tx_buf,
|
||||
sizeof(tx_buf));
|
||||
Serial.write(tx_buf, qlen);
|
||||
|
||||
/* Port 0: raw packet */
|
||||
size_t dlen = kiss_encode(KISS_PORT_DATA, radio_buf, (size_t)n,
|
||||
tx_buf, sizeof(tx_buf));
|
||||
Serial.write(tx_buf, dlen);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/radio.cpp
Normal file
106
src/radio.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "radio.h"
|
||||
#include "config.h"
|
||||
#include <RadioLib.h>
|
||||
|
||||
/* ── Chip instantiation ─────────────────────────────────────────────────── */
|
||||
#if defined(LORA_CHIP_SX1262)
|
||||
static SX1262 radio(new Module(LORA_PIN_NSS, LORA_PIN_DIO1, LORA_PIN_RESET,
|
||||
LORA_PIN_BUSY));
|
||||
#elif defined(LORA_CHIP_LR1110)
|
||||
static LR1110 radio(new Module(LORA_PIN_NSS, LORA_PIN_DIO1, LORA_PIN_RESET,
|
||||
LORA_PIN_BUSY));
|
||||
#elif defined(LORA_CHIP_SX1276)
|
||||
static SX1276 radio(new Module(LORA_PIN_NSS, LORA_PIN_DIO0, LORA_PIN_RESET,
|
||||
RADIOLIB_NC));
|
||||
#else
|
||||
# error "No LoRa chip defined"
|
||||
#endif
|
||||
|
||||
static radio_config_t current_cfg;
|
||||
|
||||
/* ── Chip-specific overloads ─────────────────────────────────────────────
|
||||
Use C++ overloading to isolate per-chip API deviations. */
|
||||
|
||||
#if defined(LORA_CHIP_SX1262)
|
||||
static int16_t chip_begin(SX1262 &r, const radio_config_t &cfg) {
|
||||
return r.begin(cfg.freq_khz / 1000.0f, cfg.bw_hz / 1000.0f, cfg.sf,
|
||||
cfg.cr, cfg.syncword, cfg.power_dbm);
|
||||
}
|
||||
#elif defined(LORA_CHIP_LR1110)
|
||||
static int16_t chip_begin(LR1110 &r, const radio_config_t &cfg) {
|
||||
return r.begin(cfg.freq_khz / 1000.0f, cfg.bw_hz / 1000.0f, cfg.sf,
|
||||
cfg.cr, cfg.syncword, cfg.power_dbm);
|
||||
}
|
||||
#elif defined(LORA_CHIP_SX1276)
|
||||
static int16_t chip_begin(SX1276 &r, const radio_config_t &cfg) {
|
||||
return r.begin(cfg.freq_khz / 1000.0f, cfg.bw_hz / 1000.0f, cfg.sf,
|
||||
cfg.cr, cfg.syncword, cfg.power_dbm);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ── C API implementation ────────────────────────────────────────────────── */
|
||||
|
||||
int radio_init(void) {
|
||||
current_cfg = {
|
||||
.freq_khz = LORA_FREQ_KHZ,
|
||||
.bw_hz = LORA_BW_HZ,
|
||||
.sf = LORA_SF,
|
||||
.cr = LORA_CR,
|
||||
.power_dbm = LORA_POWER_DBM,
|
||||
.syncword = LORA_SYNCWORD,
|
||||
};
|
||||
int16_t err = chip_begin(radio, current_cfg);
|
||||
return (err == RADIOLIB_ERR_NONE) ? 0 : (int)err;
|
||||
}
|
||||
|
||||
int radio_tx(const uint8_t *data, size_t len) {
|
||||
int16_t err = radio.transmit(data, len);
|
||||
if (err == RADIOLIB_ERR_NONE) {
|
||||
radio.startReceive();
|
||||
}
|
||||
return (err == RADIOLIB_ERR_NONE) ? 0 : (int)err;
|
||||
}
|
||||
|
||||
int radio_rx_start(void) {
|
||||
int16_t err = radio.startReceive();
|
||||
return (err == RADIOLIB_ERR_NONE) ? 0 : (int)err;
|
||||
}
|
||||
|
||||
bool radio_rx_available(void) {
|
||||
return radio.available();
|
||||
}
|
||||
|
||||
int radio_rx_read(uint8_t *buf, size_t buf_cap, radio_rx_info_t *info) {
|
||||
int16_t err = radio.readData(buf, (size_t)buf_cap);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
return (int)err;
|
||||
if (info) {
|
||||
info->snr = (int8_t)radio.getSNR();
|
||||
info->rssi = (int16_t)radio.getRSSI();
|
||||
}
|
||||
radio.startReceive();
|
||||
return (int)radio.getPacketLength();
|
||||
}
|
||||
|
||||
int radio_set_config(const radio_config_t *cfg) {
|
||||
int16_t err;
|
||||
if ((err = radio.setFrequency(cfg->freq_khz / 1000.0f)) !=
|
||||
RADIOLIB_ERR_NONE)
|
||||
return err;
|
||||
if ((err = radio.setBandwidth(cfg->bw_hz / 1000.0f)) != RADIOLIB_ERR_NONE)
|
||||
return err;
|
||||
if ((err = radio.setSpreadingFactor(cfg->sf)) != RADIOLIB_ERR_NONE)
|
||||
return err;
|
||||
if ((err = radio.setCodingRate(cfg->cr)) != RADIOLIB_ERR_NONE)
|
||||
return err;
|
||||
if ((err = radio.setOutputPower(cfg->power_dbm)) != RADIOLIB_ERR_NONE)
|
||||
return err;
|
||||
if ((err = radio.setSyncWord(cfg->syncword)) != RADIOLIB_ERR_NONE)
|
||||
return err;
|
||||
current_cfg = *cfg;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void radio_get_config(radio_config_t *cfg) {
|
||||
*cfg = current_cfg;
|
||||
}
|
||||
48
src/radio.h
Normal file
48
src/radio.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint32_t freq_khz;
|
||||
uint32_t bw_hz;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
int8_t power_dbm;
|
||||
uint8_t syncword;
|
||||
} radio_config_t;
|
||||
|
||||
typedef struct {
|
||||
int8_t snr;
|
||||
int16_t rssi;
|
||||
} radio_rx_info_t;
|
||||
|
||||
/* Initialise radio using config.h macro defaults. Returns 0 on success. */
|
||||
int radio_init(void);
|
||||
|
||||
/* Transmit packet. Blocks until TX complete. Returns 0 on success. */
|
||||
int radio_tx(const uint8_t *data, size_t len);
|
||||
|
||||
/* Enter continuous RX mode. Returns 0 on success. */
|
||||
int radio_rx_start(void);
|
||||
|
||||
/* True if a packet is waiting in the RX buffer. */
|
||||
bool radio_rx_available(void);
|
||||
|
||||
/* Read received packet into buf. Fills *info if non-NULL.
|
||||
Returns byte count or negative error code. */
|
||||
int radio_rx_read(uint8_t *buf, size_t buf_cap, radio_rx_info_t *info);
|
||||
|
||||
/* Apply new configuration. Returns 0 on success. */
|
||||
int radio_set_config(const radio_config_t *cfg);
|
||||
|
||||
/* Copy current configuration into *cfg. */
|
||||
void radio_get_config(radio_config_t *cfg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user