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:
Maze X
2026-03-27 17:15:30 +01:00
parent 777014f375
commit 8883ee3e94
33 changed files with 824 additions and 15 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
node_modules/ node_modules/
.cache/ .cache/
.pio/
.vscode/
*.pyc

View File

@@ -146,22 +146,26 @@ Not all bandwidths are legal for all frequency bands — consult local regulatio
## Project Structure ## Project Structure
Three-tier configuration hierarchy: base + SoC + board. Pin definitions are compile-time
macros in board configs (no separate pins.h).
```text ```text
loramodem/ loramodem/
├── platformio.ini # Board environments and build configuration ├── platformio.ini # [platformio] + [env:base]
├── PROJECT.md # This file ├── soc/ # SoC shared configs
├── src/ │ ├── esp32/platformio.ini
│ ├── main.cpp # Entry point; Arduino setup() and loop() │ ├── esp32s3/platformio.ini
── kiss.h / kiss.cpp # KISS frame encoder/decoder ── nrf52/platformio.ini
│ ├── radio.h / radio.cpp # RadioLib wrapper (init, TX, RX, config) ├── hardware/ # 20 board configs
── config.h # Default radio parameters and pin definitions ── heltec/ (11 boards)
├── include/ # Shared headers (if needed) │ ├── lilygo/ (4 boards)
├── lib/ # Local libraries (if needed) │ ├── seeed/ (1 board)
├── variants/ # Board-specific hardware definitions │ └── rak/ (4 boards)
│ ├── heltec_wifi_lora_32_v3/pins.h └── src/
├── rak4631/pins.h ├── main.cpp # Arduino setup()/loop() — calls C APIs
── [other boards]/pins.h ── kiss.h / kiss.c # KISS protocol — C99
└── test/ # PlatformIO Unity test suite ├── radio.h / radio.cpp # RadioLib wrapper — C++ (extern "C" API)
└── config.h # Compile-time defaults + pin validation
``` ```
## Building ## Building
@@ -171,7 +175,9 @@ Prerequisites: [PlatformIO](https://platformio.org/) CLI or IDE extension.
**Build for a specific board:** **Build for a specific board:**
```sh ```sh
pio run -e heltec_wifi_lora_32_v3 pio run -e heltec_v3
pio run -e rak_rak4631
pio run -e lilygo_t_beam_1w
``` ```
**Upload to a connected board:** **Upload to a connected board:**

View File

@@ -0,0 +1,14 @@
; Heltec CT62 — nRF52840 (likely)
; FIXME: verify board ID, chip, and pin mappings
[env:heltec_ct62]
extends = soc_nrf52, env:base
board = heltec_ct62
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_CT62
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec E213 — SoC/chip unknown
; FIXME: identify SoC (ESP32/nRF52?) and LoRa chip, get pin mappings
[env:heltec_e213]
extends = soc_esp32, env:base
board = heltec_e213
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_E213
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec E290 — SoC/chip unknown
; FIXME: identify SoC and LoRa chip, get pin mappings
[env:heltec_e290]
extends = soc_esp32, env:base
board = heltec_e290
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_E290
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec Mesh Solar — SoC/chip unknown
; FIXME: identify SoC and LoRa chip, get pin mappings
[env:heltec_mesh_solar]
extends = soc_esp32, env:base
board = heltec_mesh_solar
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_MESH_SOLAR
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec T114 — nRF52840, SX1262 (likely)
; FIXME: verify board ID, chip, and pin mappings from hardware datasheet
[env:heltec_t114]
extends = soc_nrf52, env:base
board = heltec_t114
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_T114
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec T190 — nRF52840 (likely)
; FIXME: verify board ID, chip, and pin mappings
[env:heltec_t190]
extends = soc_nrf52, env:base
board = heltec_t190
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_T190
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec Tracker — nRF52840, SX1262 (likely)
; FIXME: verify board ID and pin mappings from hardware datasheet
[env:heltec_tracker]
extends = soc_nrf52, env:base
board = heltec_tracker
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_TRACKER
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; Heltec Tracker V2 — nRF52840, SX1262 (likely)
; FIXME: verify board ID and pin mappings
[env:heltec_tracker_v2]
extends = soc_nrf52, env:base
board = heltec_tracker_v2
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_TRACKER_V2
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,12 @@
; Heltec WiFi LoRa 32 V2 — ESP32, SX1276, 868 MHz
[env:heltec_v2]
extends = soc_esp32, env:base
board = heltec_wifi_lora_32
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_V2
-DLORA_CHIP_SX1276
-DLORA_PIN_NSS=18
-DLORA_PIN_DIO0=26
-DLORA_PIN_RESET=14

View File

@@ -0,0 +1,13 @@
; Heltec WiFi LoRa 32 V3 — ESP32-S3, SX1262, 868 MHz
[env:heltec_v3]
extends = soc_esp32s3, env:base
board = heltec_wifi_lora_32_v3
build_flags =
${soc_esp32s3.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_V3
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=8
-DLORA_PIN_DIO1=14
-DLORA_PIN_RESET=12
-DLORA_PIN_BUSY=13

View File

@@ -0,0 +1,13 @@
; Heltec WiFi LoRa 32 V4 — ESP32-S3, SX1262, 868 MHz
[env:heltec_v4]
extends = soc_esp32s3, env:base
board = heltec_wifi_lora_32_v4
build_flags =
${soc_esp32s3.build_flags}
${env:base.build_flags}
-DBOARD_HELTEC_V4
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=8
-DLORA_PIN_DIO1=14
-DLORA_PIN_RESET=12
-DLORA_PIN_BUSY=13

View File

@@ -0,0 +1,14 @@
; LilyGo T-Beam 1W — ESP32, SX1262 (likely)
; FIXME: verify board ID and pin mappings from hardware datasheet
[env:lilygo_t_beam_1w]
extends = soc_esp32, env:base
board = lilygo_t_beam_1w
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_LILYGO_T_BEAM_1W
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; LilyGo T-Beam Supreme (SX1262) — ESP32, SX1262
; FIXME: verify board ID and pin mappings
[env:lilygo_t_beam_supreme]
extends = soc_esp32, env:base
board = lilygo_t_beam_supreme
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_LILYGO_T_BEAM_SUPREME
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; LilyGo T-Beam with SX1262 — ESP32, SX1262
; FIXME: verify board ID and pin mappings
[env:lilygo_t_beam_sx1262]
extends = soc_esp32, env:base
board = lilygo_t_beam_sx1262
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_LILYGO_T_BEAM_SX1262
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,13 @@
; LilyGo T-Beam with SX1276 — ESP32, SX1276
; FIXME: verify board ID and pin mappings
[env:lilygo_t_beam_sx1276]
extends = soc_esp32, env:base
board = lilygo_t_beam_sx1276
build_flags =
${soc_esp32.build_flags}
${env:base.build_flags}
-DBOARD_LILYGO_T_BEAM_SX1276
-DLORA_CHIP_SX1276
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO0=0
-DLORA_PIN_RESET=0

View File

@@ -0,0 +1,14 @@
; RAK11310 — RP2040-based (different platform)
; FIXME: determine platform (likely rp2040), chip, and pin mappings
[env:rak_rak11310]
extends = soc_nrf52, env:base
board = rak11310
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_RAK_RAK11310
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; RAK3112 — nRF52840 (likely), SX1262 (likely)
; FIXME: verify board ID, chip, and pin mappings
[env:rak_rak3112]
extends = soc_nrf52, env:base
board = rak3112
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_RAK_RAK3112
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; RAK3401 — nRF52840 (likely), SX1262 (likely)
; FIXME: verify board ID, chip, and pin mappings
[env:rak_rak3401]
extends = soc_nrf52, env:base
board = rak3401
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_RAK_RAK3401
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,14 @@
; RAK3x72 — nRF52840 (likely), SX1262 (likely)
; FIXME: verify which variants (3172, 3272, 3372?), board IDs, and pin mappings
[env:rak_rak3x72]
extends = soc_nrf52, env:base
board = rak3x72
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_RAK_RAK3X72
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=0
-DLORA_PIN_DIO1=0
-DLORA_PIN_RESET=0
-DLORA_PIN_BUSY=0

View File

@@ -0,0 +1,13 @@
; RAK4631 — nRF52840, SX1262, 868 MHz
[env:rak_rak4631]
extends = soc_nrf52, env:base
board = wiscore_rak4631
build_flags =
${soc_nrf52.build_flags}
${env:base.build_flags}
-DBOARD_RAK_RAK4631
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=42
-DLORA_PIN_DIO1=47
-DLORA_PIN_RESET=38
-DLORA_PIN_BUSY=46

View File

@@ -0,0 +1,21 @@
; Seeed Xiao S3 + Wio SX1262 — ESP32-S3, SX1262
[env:seeed_xiao_s3_wio_sx1262]
extends = soc_esp32s3, env:base
board = seeed_xiao_s3
build_flags =
${soc_esp32s3.build_flags}
${env:base.build_flags}
-DBOARD_SEEED_XIAO_S3_WIO_SX1262
-DLORA_CHIP_SX1262
-DLORA_PIN_NSS=41
-DLORA_PIN_DIO1=39
-DLORA_PIN_RESET=42
-DLORA_PIN_BUSY=40
; SX1262 RF switch on DIO2
-DSX126X_DIO2_AS_RF_SWITCH=true
-DSX126X_DIO3_TCXO_VOLTAGE=1.8
-DSX126X_CURRENT_LIMIT=140
; SPI pins
-DLORA_PIN_SCLK=7
-DLORA_PIN_MISO=8
-DLORA_PIN_MOSI=9

56
platformio.ini Normal file
View File

@@ -0,0 +1,56 @@
; Root PlatformIO configuration for LoRa KISS modem (20 boards)
; Board-specific environments are in hardware/<vendor>/<board>/platformio.ini
; SoC shared settings are in soc/<soc>/platformio.ini
[platformio]
default_envs =
heltec_v3,
rak_rak4631,
heltec_v2
extra_configs =
soc/esp32/platformio.ini
soc/esp32s3/platformio.ini
soc/nrf52/platformio.ini
hardware/heltec/t114/platformio.ini
hardware/heltec/ct62/platformio.ini
hardware/heltec/e213/platformio.ini
hardware/heltec/e290/platformio.ini
hardware/heltec/mesh_solar/platformio.ini
hardware/heltec/t190/platformio.ini
hardware/heltec/tracker/platformio.ini
hardware/heltec/tracker_v2/platformio.ini
hardware/heltec/v2/platformio.ini
hardware/heltec/v3/platformio.ini
hardware/heltec/v4/platformio.ini
hardware/lilygo/t_beam_1w/platformio.ini
hardware/lilygo/t_beam_sx1262/platformio.ini
hardware/lilygo/t_beam_sx1276/platformio.ini
hardware/lilygo/t_beam_supreme/platformio.ini
hardware/seeed/xiao_s3_wio_sx1262/platformio.ini
hardware/rak/rak11310/platformio.ini
hardware/rak/rak3112/platformio.ini
hardware/rak/rak3401/platformio.ini
hardware/rak/rak3x72/platformio.ini
hardware/rak/rak4631/platformio.ini
; ------------------------------------------------------------------
; Base environment — all board envs extend this.
; Provides: RadioLib dependency, default LoRa parameters.
; ------------------------------------------------------------------
[env:base]
lib_deps =
jgromes/RadioLib@^6.6.0
monitor_speed = 115200
build_flags =
; Default LoRa radio parameters — override per-board as needed
-DLORA_FREQ_KHZ=869525UL
-DLORA_BW_HZ=125000UL
-DLORA_SF=7
-DLORA_CR=5
-DLORA_POWER_DBM=14
-DLORA_SYNCWORD=0x34
; KISS serial baud rate
-DKISS_BAUD=115200

5
soc/esp32/platformio.ini Normal file
View File

@@ -0,0 +1,5 @@
[soc_esp32]
platform = espressif32
framework = arduino
build_flags =
-DARCH_ESP32

View File

@@ -0,0 +1,5 @@
[soc_esp32s3]
platform = espressif32
framework = arduino
build_flags =
-DARCH_ESP32S3

5
soc/nrf52/platformio.ini Normal file
View File

@@ -0,0 +1,5 @@
[soc_nrf52]
platform = nordicnrf52
framework = arduino
build_flags =
-DARCH_NRF52

51
src/config.h Normal file
View 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
View 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
View 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
View 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
View 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
View 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