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).
8.6 KiB
Micro controller LoRa KISS modem
This project is a LoRa KISS modem for common LoRa capable development boards, such as created by vendors like Heltec, RAK, Seeed, etc.
It is using the USB-serial interface (optionally hardware serial interface) to allow the user to send and receive LoRa packets using the KISS modem protocol.
KISS Modem
KISS Framing
KISS (Keep It Simple, Stupid) is a standard TNC protocol for packet radio. Frames use special bytes for delimiting:
| Symbol | Value | Meaning |
|---|---|---|
| FEND | 0xC0 |
Frame end/begin delimiter |
| FESC | 0xDB |
Frame escape |
| TFEND | 0xDC |
Transposed FEND (follows FESC) |
| TFESC | 0xDD |
Transposed FESC (follows FESC) |
A frame has the structure:
FEND <type byte> <data bytes...> FEND
The type byte encodes the port number (upper nibble) and command (lower nibble):
type = (port << 4) | cmd
For host-to-modem data frames, cmd = 0x00. Any FEND or FESC bytes
appearing in the data payload must be escaped as FESC TFEND or FESC TFESC
respectively.
Ports
The KISS modem uses three ports for communication:
| Port | Direction | Purpose |
|---|---|---|
| 0 | bidirectional | Raw LoRa packet data |
| 1 | TNC → host | Signal quality (SNR + RSSI) |
| 2 | bidirectional | Radio configuration commands |
Port 1 — Signal Quality
Before each received LoRa packet delivered on port 0, the modem emits a signal quality frame on port 1. The payload is 3 bytes, big-endian:
| Offset | Size | Type | Field | Description |
|---|---|---|---|---|
| 0 | 1 | int8 | SNR | Signal-to-noise ratio in dB (rounded) |
| 1 | 2 | int16 | RSSI | Received signal strength in dBm |
Example: FEND 0x11 [-7] [0xFF 0x99] FEND = SNR = -7 dB, RSSI = -103 dBm
Configuration Commands (Port 2)
Configuration commands allow the host to query and control the modem's LoRa
parameters. Each configuration frame's data payload (after the KISS type byte)
begins with a 1-byte command byte. The modem responds to every SET_* command
with either RES_OK (0x01) or RES_ERROR (0x02) on port 2.
Commands:
| Cmd | Name | Dir | Payload |
|---|---|---|---|
| 0x00 | Reserved | — | — |
| 0x01 | RES_OK | ← | (none) |
| 0x02 | RES_ERROR | ← | (none) |
| 0x10 | GET_RADIO | → | (none) |
| 0x10 | resp | ← | freq_kHz, bw_hz, sf, cr, power |
| 0x11 | SET_RADIO | → | freq_kHz, bw_hz, sf, cr, power |
| 0x12 | GET_FREQUENCY | → | (none) |
| 0x12 | resp | ← | freq_kHz |
| 0x13 | SET_FREQUENCY | → | freq_kHz |
| 0x14 | GET_BANDWIDTH | → | (none) |
| 0x14 | resp | ← | bw_hz |
| 0x15 | SET_BANDWIDTH | → | bw_hz |
| 0x16 | GET_SF | → | (none) |
| 0x16 | resp | ← | sf |
| 0x17 | SET_SF | → | sf |
| 0x18 | GET_CR | → | (none) |
| 0x18 | resp | ← | cr |
| 0x19 | SET_CR | → | cr |
| 0x1A | GET_POWER | → | (none) |
| 0x1A | resp | ← | power_dBm |
| 0x1B | SET_POWER | → | power_dBm |
| 0x1C | GET_SYNCWORD | → | (none) |
| 0x1C | resp | ← | syncword |
| 0x1D | SET_SYNCWORD | → | syncword |
Legend: → = host → TNC, ← = TNC → host
Units
All integer fields are transmitted in big-endian byte order.
| Parameter | Type | Unit | Encoding | Example |
|---|---|---|---|---|
| Frequency | uint32 | kHz | 4 bytes | 869.618 MHz → 869618 |
| Bandwidth | uint32 | Hz | 4 bytes | 62.5 kHz → 62500 |
| SF | uint8 | — | 1 byte (5–12) | SF 7 → 0x07 |
| CR | uint8 | — | 1 byte (5–8) | 4/5 → 0x05 |
| Power | int8 | dBm | 1 byte | 22 dBm → 0x16 |
| Sync word | uint8 | — | 1 byte | 0x34 → public |
LoRa
Hardware
Common LoRa hardware is supported via the RadioLib library:
| Module | Frequency band | Notes |
|---|---|---|
| SX1262 | 868 / 915 MHz | Used on Heltec, RAK, Seeed boards |
| SX1268 | 433 / 470 MHz | 433 MHz ISM band variants |
| LR1110 | 150–960 MHz | Semtech LR11xx family |
| LR1120 | 150–960 MHz, 2.4 GHz | Semtech LR11xx family |
| SX1276 / SX1278 | 137–1020 MHz | Older boards; supported via RadioLib |
Bandwidths
All LoRa bandwidths supported by RadioLib are accepted. SX126x-based boards support:
| Bandwidth | Wire value (uint32 Hz) |
|---|---|
| 7.8 kHz | 7800 |
| 10.4 kHz | 10400 |
| 15.6 kHz | 15600 |
| 20.8 kHz | 20800 |
| 31.25 kHz | 31250 |
| 41.7 kHz | 41700 |
| 62.5 kHz | 62500 |
| 125 kHz | 125000 |
| 250 kHz | 250000 |
| 500 kHz | 500000 |
Note: SX1272/SX1273-based boards support only 125, 250, and 500 kHz. Not all bandwidths are legal for all frequency bands — consult local regulations.
Project Structure
Three-tier configuration hierarchy: base + SoC + board. Pin definitions are compile-time macros in board configs (no separate pins.h).
loramodem/
├── platformio.ini # [platformio] + [env:base]
├── soc/ # SoC shared configs
│ ├── esp32/platformio.ini
│ ├── esp32s3/platformio.ini
│ └── nrf52/platformio.ini
├── hardware/ # 20 board configs
│ ├── heltec/ (11 boards)
│ ├── lilygo/ (4 boards)
│ ├── seeed/ (1 board)
│ └── rak/ (4 boards)
└── src/
├── main.cpp # Arduino setup()/loop() — calls C APIs
├── kiss.h / kiss.c # KISS protocol — C99
├── radio.h / radio.cpp # RadioLib wrapper — C++ (extern "C" API)
└── config.h # Compile-time defaults + pin validation
Building
Prerequisites: PlatformIO CLI or IDE extension.
Build for a specific board:
pio run -e heltec_v3
pio run -e rak_rak4631
pio run -e lilygo_t_beam_1w
Upload to a connected board:
pio run -e heltec_wifi_lora_32_v3 -t upload
Monitor serial output:
pio device monitor -b 115200
Note: KISS frames are binary. Use a KISS-capable TNC client (e.g., Direwolf, socat) to interact with the modem over serial.
Available board environments are defined in platformio.ini.
Configuration Defaults
The modem initializes with these LoRa parameters at power-on:
| Parameter | Default value |
|---|---|
| Frequency | TBD MHz |
| Bandwidth | 125 kHz |
| SF | TBD |
| CR | 4/5 |
| Power | TBD dBm |
| Sync word | 0x34 (LoRa public) |
These defaults may be overridden at compile time via config.h. Parameters changed
via KISS port 2 commands take effect immediately but are not persisted across
power cycles unless persistent storage (EEPROM/NVS) is implemented.
Framework
This project uses PlatformIO as the build system with
RadioLib as the LoRa driver library.
Board-specific pin assignments and hardware configurations are isolated in the
variants/ directory, allowing the core KISS modem logic to remain board-agnostic.
The USB-CDC serial interface is used by default for KISS communication. On supported
boards, a hardware UART may be selected instead via compile-time configuration in config.h.