Initial import

This commit is contained in:
2026-03-25 18:14:45 +01:00
commit d074cd2e43
99 changed files with 3781 additions and 0 deletions

25
app/CMakeLists.txt Normal file
View File

@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.20.0)
# Set BOARD_ROOT and DTS_ROOT to the repo root so that boards/<vendor>/<board>/
# is searched for out-of-tree board definitions.
# Must be set BEFORE find_package(Zephyr).
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_LIST_DIR}/..)
list(APPEND DTS_ROOT ${CMAKE_CURRENT_LIST_DIR}/..)
# Register the LR11xx out-of-tree driver as an extra Zephyr module.
# It is only compiled when a semtech,lr11xx DTS node is enabled.
list(APPEND ZEPHYR_EXTRA_MODULES
${CMAKE_CURRENT_LIST_DIR}/../drivers/lora/lr11xx
)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(loramodem LANGUAGES C)
target_sources(app PRIVATE
src/main.c
src/kiss.c
src/lora_modem.c
)
target_include_directories(app PRIVATE src)

71
app/Kconfig Normal file
View File

@@ -0,0 +1,71 @@
mainmenu "LoRa KISS Modem Configuration"
source "Kconfig.zephyr"
menu "KISS Interface"
config LORAMODEM_KISS_IFACE_USB
bool "Use USB CDC-ACM as KISS serial interface"
depends on USB_DEVICE_STACK || USB_DEVICE_STACK_NEXT
help
Use USB CDC-ACM virtual serial port for the KISS interface.
Requires the host to enumerate the device before framing begins.
The board overlay must define the loramodem,kiss-uart chosen node
pointing to a cdc_acm_uart instance.
config LORAMODEM_KISS_IFACE_UART
bool "Use hardware UART as KISS serial interface"
default y if !LORAMODEM_KISS_IFACE_USB
help
Use a hardware UART for the KISS interface.
The board overlay must define the loramodem,kiss-uart chosen node
pointing to the desired uart instance.
endmenu
menu "LoRa Radio Parameters"
config LORAMODEM_LORA_FREQ_HZ
int "Center frequency (Hz)"
default 915000000
help
LoRa radio center frequency in Hz.
Common values: 915000000 (US/AU), 868000000 (EU), 433000000 (Asia).
config LORAMODEM_LORA_SF
int "Spreading factor"
default 9
range 7 12
help
LoRa spreading factor. Higher values increase range and time-on-air.
config LORAMODEM_LORA_BW
int "Bandwidth (kHz)"
default 125
help
LoRa bandwidth in kHz. Valid values: 125, 250, 500.
config LORAMODEM_LORA_CR
int "Coding rate denominator"
default 5
range 5 8
help
LoRa coding rate as the denominator N of 4/N.
5 = 4/5 (lowest overhead), 8 = 4/8 (highest redundancy).
config LORAMODEM_LORA_TX_POWER_DBM
int "TX power (dBm)"
default 14
range 2 22
config LORAMODEM_LORA_PREAMBLE_LEN
int "Preamble length (symbols)"
default 8
range 6 65535
endmenu
config LORAMODEM_MAX_PACKET_SIZE
int "Maximum LoRa packet payload size (bytes)"
default 255
range 1 255

28
app/prj.conf Normal file
View File

@@ -0,0 +1,28 @@
# LoRa KISS Modem — base project configuration
# Board-specific .conf overlays extend or override these settings.
# Logging
CONFIG_LOG=y
CONFIG_PRINTK=y
# LoRa subsystem
CONFIG_LORA=y
# UART (interrupt-driven for non-blocking byte-by-byte processing)
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Heap for KISS frame buffers and dynamic allocations
CONFIG_HEAP_MEM_POOL_SIZE=4096
# Zephyr kernel threads
CONFIG_MAIN_STACK_SIZE=2048
# Default radio parameters (US 915 MHz, SF9, BW125, CR 4/5)
CONFIG_LORAMODEM_LORA_FREQ_HZ=915000000
CONFIG_LORAMODEM_LORA_SF=9
CONFIG_LORAMODEM_LORA_BW=125
CONFIG_LORAMODEM_LORA_CR=5
CONFIG_LORAMODEM_LORA_TX_POWER_DBM=14
CONFIG_LORAMODEM_LORA_PREAMBLE_LEN=8
CONFIG_LORAMODEM_MAX_PACKET_SIZE=255

109
app/src/kiss.c Normal file
View File

@@ -0,0 +1,109 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* KISS protocol encoder and decoder implementation.
*/
#include "kiss.h"
#include <errno.h>
#include <string.h>
int kiss_encode_cmd(uint8_t cmd, const uint8_t *data, size_t data_len,
uint8_t *out_buf, size_t out_size)
{
size_t out_idx = 0;
/* Worst-case output: FEND + CMD + (2 * data_len) + FEND */
if (out_size < (2 + 2 * data_len + 1)) {
return -ENOMEM;
}
out_buf[out_idx++] = KISS_FEND;
out_buf[out_idx++] = cmd;
for (size_t i = 0; i < data_len; i++) {
if (data[i] == KISS_FEND) {
out_buf[out_idx++] = KISS_FESC;
out_buf[out_idx++] = KISS_TFEND;
} else if (data[i] == KISS_FESC) {
out_buf[out_idx++] = KISS_FESC;
out_buf[out_idx++] = KISS_TFESC;
} else {
out_buf[out_idx++] = data[i];
}
}
out_buf[out_idx++] = KISS_FEND;
return (int)out_idx;
}
int kiss_encode(const uint8_t *data, size_t data_len,
uint8_t *out_buf, size_t out_size)
{
return kiss_encode_cmd(KISS_CMD_DATA, data, data_len, out_buf, out_size);
}
void kiss_decoder_init(struct kiss_decoder_ctx *ctx,
uint8_t *buf, size_t size)
{
ctx->frame_buf = buf;
ctx->frame_buf_size = size;
ctx->frame_len = 0;
ctx->in_frame = false;
ctx->in_escape = false;
ctx->cmd_received = false;
ctx->cmd = 0;
}
bool kiss_decoder_feed(struct kiss_decoder_ctx *ctx, uint8_t byte)
{
if (byte == KISS_FEND) {
if (ctx->in_frame && ctx->frame_len > 0) {
/* Closing delimiter — frame complete */
ctx->in_frame = false;
ctx->in_escape = false;
ctx->cmd_received = false;
return true;
}
/* Opening delimiter (or spurious FEND) — start fresh */
ctx->in_frame = true;
ctx->in_escape = false;
ctx->cmd_received = false;
ctx->frame_len = 0;
return false;
}
if (!ctx->in_frame) {
return false;
}
/* Handle escape sequences */
if (byte == KISS_FESC) {
ctx->in_escape = true;
return false;
}
if (ctx->in_escape) {
ctx->in_escape = false;
if (byte == KISS_TFEND) {
byte = KISS_FEND;
} else if (byte == KISS_TFESC) {
byte = KISS_FESC;
}
/* Unknown escape sequences: discard the escape, keep the byte */
}
/* First unescaped byte after opening FEND is the CMD byte */
if (!ctx->cmd_received) {
ctx->cmd = byte;
ctx->cmd_received = true;
return false;
}
/* Append to payload buffer (silently drop if full) */
if (ctx->frame_len < ctx->frame_buf_size) {
ctx->frame_buf[ctx->frame_len++] = byte;
}
return false;
}

125
app/src/kiss.h Normal file
View File

@@ -0,0 +1,125 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* KISS (Keep It Simple Stupid) protocol encoder/decoder.
*
* Frame format:
* FEND | CMD | <data with escaping> | FEND
*
* Escape sequences (inside frame data only):
* 0xC0 → 0xDB 0xDC (FESC TFEND)
* 0xDB → 0xDB 0xDD (FESC TFESC)
*/
#ifndef KISS_H
#define KISS_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
/* Special bytes */
#define KISS_FEND 0xC0u /* Frame delimiter */
#define KISS_FESC 0xDBu /* Escape byte */
#define KISS_TFEND 0xDCu /* Escaped FEND */
#define KISS_TFESC 0xDDu /* Escaped FESC */
/* Command codes (upper nibble = port 0) */
#define KISS_CMD_DATA 0x00u
#define KISS_CMD_TXDELAY 0x01u
#define KISS_CMD_PERSISTENCE 0x02u
#define KISS_CMD_SLOTTIME 0x03u
#define KISS_CMD_TXTAIL 0x04u
#define KISS_CMD_FULLDUPLEX 0x05u
#define KISS_CMD_SETHARDWARE 0x06u
#define KISS_CMD_RETURN 0xFFu
/*
* SetHardware (0x06) sub-commands — device-specific radio configuration.
*
* Frame payload format: [sub-cmd 1 byte] [params...]
*
* SET commands change a parameter and apply it immediately.
* GET commands cause the device to respond with a SetHardware frame
* containing the same sub-command byte followed by the current value(s).
*
* Frequency: uint32_t big-endian, Hz (e.g. 0x36892580 = 915000000)
* BW: uint16_t big-endian, kHz (125, 250, or 500)
* SF: uint8_t 7..12
* CR: uint8_t coding-rate denominator 5..8 (meaning 4/5 .. 4/8)
* TX power: int8_t dBm
*/
#define KISS_HW_SET_FREQ 0x01u /* payload: 4 bytes freq_hz (BE uint32) */
#define KISS_HW_GET_FREQ 0x02u /* no params; response: sub-cmd + 4 bytes */
#define KISS_HW_SET_SF 0x03u /* payload: 1 byte sf (7-12) */
#define KISS_HW_SET_BW 0x04u /* payload: 2 bytes bw_khz (BE uint16: 125/250/500) */
#define KISS_HW_SET_CR 0x05u /* payload: 1 byte cr denom (5-8) */
#define KISS_HW_SET_TXPWR 0x06u /* payload: 1 byte tx_power_dbm (int8) */
#define KISS_HW_GET_PARAMS 0x07u /* no params; response: sub-cmd + all params (10 bytes) */
/*
* GET_PARAMS response payload (10 bytes after sub-cmd byte):
* [0..3] freq_hz BE uint32
* [4] sf uint8
* [5..6] bw_khz BE uint16
* [7] cr uint8 (denominator)
* [8] tx_power int8 (dBm)
* [9] preamble uint8
*/
/**
* @brief Encode data into a KISS frame with an explicit command byte.
*
* Writes FEND + cmd + escaped data + FEND into @p out_buf.
*
* @param cmd KISS command byte (e.g. KISS_CMD_DATA, KISS_CMD_SETHARDWARE).
* @param data Payload bytes (may be NULL when data_len == 0).
* @param data_len Length of payload.
* @param out_buf Output buffer for the encoded frame.
* @param out_size Size of the output buffer.
* @return Number of bytes written, or -ENOMEM if @p out_buf is too small.
*/
int kiss_encode_cmd(uint8_t cmd, const uint8_t *data, size_t data_len,
uint8_t *out_buf, size_t out_size);
/**
* @brief Encode raw data into a KISS data frame (CMD = 0x00).
*
* Convenience wrapper around kiss_encode_cmd() using KISS_CMD_DATA.
*/
int kiss_encode(const uint8_t *data, size_t data_len,
uint8_t *out_buf, size_t out_size);
/**
* @brief KISS decoder context. Maintains state between calls to kiss_decoder_feed().
*/
struct kiss_decoder_ctx {
uint8_t *frame_buf; /**< Caller-supplied buffer for decoded payload. */
size_t frame_buf_size; /**< Size of frame_buf. */
size_t frame_len; /**< Number of payload bytes decoded so far. */
bool in_frame; /**< True after the opening FEND has been seen. */
bool in_escape; /**< True after FESC has been seen. */
bool cmd_received; /**< True once the CMD byte has been consumed. */
uint8_t cmd; /**< KISS command byte of the current frame. */
};
/**
* @brief Initialize a KISS decoder context.
*
* @param ctx Context to initialize.
* @param buf Buffer for decoded frame payload.
* @param size Size of @p buf.
*/
void kiss_decoder_init(struct kiss_decoder_ctx *ctx,
uint8_t *buf, size_t size);
/**
* @brief Feed one byte into the KISS decoder state machine.
*
* @param ctx Decoder context.
* @param byte Next byte from the serial stream.
* @return true when a complete frame has been received.
* ctx->frame_buf[0..ctx->frame_len-1] holds the payload.
* ctx->cmd holds the KISS command byte.
*/
bool kiss_decoder_feed(struct kiss_decoder_ctx *ctx, uint8_t byte);
#endif /* KISS_H */

218
app/src/lora_modem.c Normal file
View File

@@ -0,0 +1,218 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* LoRa modem abstraction layer.
* Maps runtime lora_radio_params to Zephyr's lora_modem_config enums
* and provides a simple send/recv API for the KISS bridge.
*
* Runtime parameters are stored in g_params, protected by g_params_mutex.
* send() and recv() snapshot the params under the mutex before configuring
* the radio, so a concurrent frequency change takes effect on the next call.
*/
#include "lora_modem.h"
#include <zephyr/drivers/lora.h>
#include <zephyr/devicetree.h>
#include <zephyr/logging/log.h>
#include <string.h>
LOG_MODULE_REGISTER(lora_modem, LOG_LEVEL_INF);
#define LORA_DEV DEVICE_DT_GET(DT_ALIAS(lora0))
/* ── Runtime parameter state ─────────────────────────────────────────────── */
static struct lora_radio_params g_params;
static K_MUTEX_DEFINE(g_params_mutex);
/* ── Enum conversion helpers ─────────────────────────────────────────────── */
static enum lora_signal_bandwidth bw_to_enum(uint16_t bw_khz)
{
switch (bw_khz) {
case 500: return BW_500_KHZ;
case 250: return BW_250_KHZ;
case 125: /* fall through */
default: return BW_125_KHZ;
}
}
static enum lora_datarate sf_to_enum(uint8_t sf)
{
switch (sf) {
case 6: return SF_6;
case 7: return SF_7;
case 8: return SF_8;
case 9: return SF_9;
case 10: return SF_10;
case 11: return SF_11;
case 12: /* fall through */
default: return SF_12;
}
}
static enum lora_coding_rate cr_to_enum(uint8_t cr_denom)
{
switch (cr_denom) {
case 5: return CR_4_5;
case 6: return CR_4_6;
case 7: return CR_4_7;
case 8: /* fall through */
default: return CR_4_8;
}
}
/* Build a lora_modem_config from a params snapshot */
static void params_to_cfg(const struct lora_radio_params *p,
struct lora_modem_config *cfg, bool tx)
{
cfg->frequency = p->freq_hz;
cfg->bandwidth = bw_to_enum(p->bw_khz);
cfg->datarate = sf_to_enum(p->sf);
cfg->coding_rate = cr_to_enum(p->cr);
cfg->preamble_len = p->preamble_len;
cfg->tx_power = p->tx_power;
cfg->tx = tx;
}
/* ── Public API ──────────────────────────────────────────────────────────── */
int lora_modem_init(void)
{
const struct device *lora_dev = LORA_DEV;
if (!device_is_ready(lora_dev)) {
LOG_ERR("LoRa device not ready");
return -ENODEV;
}
/* Load compile-time defaults into runtime params */
g_params.freq_hz = CONFIG_LORAMODEM_LORA_FREQ_HZ;
g_params.sf = CONFIG_LORAMODEM_LORA_SF;
g_params.bw_khz = CONFIG_LORAMODEM_LORA_BW;
g_params.cr = CONFIG_LORAMODEM_LORA_CR;
g_params.tx_power = CONFIG_LORAMODEM_LORA_TX_POWER_DBM;
g_params.preamble_len = CONFIG_LORAMODEM_LORA_PREAMBLE_LEN;
LOG_INF("LoRa init: %u Hz SF%u BW%ukHz CR4/%u %d dBm",
g_params.freq_hz, g_params.sf, g_params.bw_khz,
g_params.cr, g_params.tx_power);
struct lora_modem_config cfg;
params_to_cfg(&g_params, &cfg, true);
return lora_config(lora_dev, &cfg);
}
void lora_modem_get_params(struct lora_radio_params *params)
{
k_mutex_lock(&g_params_mutex, K_FOREVER);
*params = g_params;
k_mutex_unlock(&g_params_mutex);
}
int lora_modem_set_params(const struct lora_radio_params *params)
{
if (params->sf < 6 || params->sf > 12) {
return -EINVAL;
}
if (params->bw_khz != 125 && params->bw_khz != 250 &&
params->bw_khz != 500) {
return -EINVAL;
}
if (params->cr < 5 || params->cr > 8) {
return -EINVAL;
}
if (params->freq_hz == 0) {
return -EINVAL;
}
k_mutex_lock(&g_params_mutex, K_FOREVER);
g_params = *params;
k_mutex_unlock(&g_params_mutex);
LOG_INF("Params updated: %u Hz SF%u BW%ukHz CR4/%u %d dBm",
params->freq_hz, params->sf, params->bw_khz,
params->cr, params->tx_power);
return 0;
}
int lora_modem_set_freq(uint32_t freq_hz)
{
if (freq_hz == 0) {
return -EINVAL;
}
k_mutex_lock(&g_params_mutex, K_FOREVER);
g_params.freq_hz = freq_hz;
k_mutex_unlock(&g_params_mutex);
LOG_INF("Frequency set to %u Hz", freq_hz);
return 0;
}
int lora_modem_send(const uint8_t *data, size_t len)
{
const struct device *lora_dev = LORA_DEV;
struct lora_radio_params snap;
struct lora_modem_config tx_cfg;
int ret;
k_mutex_lock(&g_params_mutex, K_FOREVER);
snap = g_params;
k_mutex_unlock(&g_params_mutex);
params_to_cfg(&snap, &tx_cfg, true);
ret = lora_config(lora_dev, &tx_cfg);
if (ret < 0) {
LOG_ERR("lora_config(TX) failed: %d", ret);
return ret;
}
ret = lora_send(lora_dev, (uint8_t *)data, (uint32_t)len);
if (ret < 0) {
LOG_ERR("lora_send failed: %d", ret);
}
return ret;
}
int lora_modem_recv(uint8_t *buf, size_t buf_size, k_timeout_t timeout,
int16_t *rssi_out, int8_t *snr_out)
{
const struct device *lora_dev = LORA_DEV;
struct lora_radio_params snap;
struct lora_modem_config rx_cfg;
int ret;
k_mutex_lock(&g_params_mutex, K_FOREVER);
snap = g_params;
k_mutex_unlock(&g_params_mutex);
params_to_cfg(&snap, &rx_cfg, false);
ret = lora_config(lora_dev, &rx_cfg);
if (ret < 0) {
LOG_ERR("lora_config(RX) failed: %d", ret);
return ret;
}
int16_t rssi = 0;
int8_t snr = 0;
ret = lora_recv(lora_dev, buf, (uint8_t)buf_size, timeout, &rssi, &snr);
if (ret >= 0) {
LOG_DBG("RX %d bytes RSSI=%d SNR=%d", ret, rssi, snr);
if (rssi_out) {
*rssi_out = rssi;
}
if (snr_out) {
*snr_out = snr;
}
}
return ret;
}

92
app/src/lora_modem.h Normal file
View File

@@ -0,0 +1,92 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* LoRa modem abstraction layer.
* Wraps Zephyr's lora_driver_api with application-level configuration.
*/
#ifndef LORA_MODEM_H
#define LORA_MODEM_H
#include <stdint.h>
#include <stddef.h>
#include <zephyr/kernel.h>
/**
* @brief Runtime radio parameters.
*
* All fields are in user-friendly units. Modified at runtime via
* lora_modem_set_params() / lora_modem_set_freq(); read via
* lora_modem_get_params().
*/
struct lora_radio_params {
uint32_t freq_hz; /**< Center frequency in Hz */
uint8_t sf; /**< Spreading factor (7-12) */
uint16_t bw_khz; /**< Bandwidth in kHz (125, 250, or 500) */
uint8_t cr; /**< Coding rate denominator (5..8, meaning 4/5..4/8) */
int8_t tx_power; /**< TX power in dBm */
uint8_t preamble_len; /**< Preamble length in symbols */
};
/**
* @brief Initialize and configure the LoRa radio.
*
* Loads compile-time Kconfig defaults into the runtime params struct
* and applies them. Call once at startup before any send/recv.
*
* @return 0 on success, negative errno on failure.
*/
int lora_modem_init(void);
/**
* @brief Get the current radio parameters.
*
* @param params Output: filled with current settings.
*/
void lora_modem_get_params(struct lora_radio_params *params);
/**
* @brief Set all radio parameters at once.
*
* Parameters are stored for use on the next send/recv call.
* The radio is NOT immediately reconfigured; the change takes effect
* at the start of the next lora_modem_send() or lora_modem_recv() call.
*
* @param params New parameter values.
* @return 0 on success, -EINVAL if a parameter is out of range.
*/
int lora_modem_set_params(const struct lora_radio_params *params);
/**
* @brief Change only the center frequency.
*
* Convenience wrapper around lora_modem_set_params() for the common
* case of a frequency-only change.
*
* @param freq_hz New center frequency in Hz.
* @return 0 on success, -EINVAL if out of range.
*/
int lora_modem_set_freq(uint32_t freq_hz);
/**
* @brief Transmit a packet over LoRa.
*
* @param data Payload bytes.
* @param len Payload length (must be <= CONFIG_LORAMODEM_MAX_PACKET_SIZE).
* @return 0 on success, negative errno on failure.
*/
int lora_modem_send(const uint8_t *data, size_t len);
/**
* @brief Receive a packet from LoRa (blocking).
*
* @param buf Buffer for received payload.
* @param buf_size Size of @p buf.
* @param timeout How long to wait (use K_FOREVER to wait indefinitely).
* @param rssi_out If non-NULL, receives RSSI of the packet (dBm).
* @param snr_out If non-NULL, receives SNR of the packet (dB).
* @return Number of bytes received (>= 0), or negative errno on error/timeout.
*/
int lora_modem_recv(uint8_t *buf, size_t buf_size, k_timeout_t timeout,
int16_t *rssi_out, int8_t *snr_out);
#endif /* LORA_MODEM_H */

362
app/src/main.c Normal file
View File

@@ -0,0 +1,362 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* LoRa KISS Modem — main application.
*
* Two threads bridge a KISS serial interface to a LoRa radio:
*
* kiss_rx_thread: reads bytes from the KISS UART, decodes KISS frames,
* dispatches data frames to lora_modem_send(), and
* handles SetHardware (0x06) frames for radio parameter
* changes (frequency, SF, BW, CR, TX power).
*
* lora_rx_thread: loops with a 1-second timeout on lora_modem_recv();
* on each successful receive it encodes the payload as
* a KISS data frame and writes it to the KISS UART.
* The finite timeout ensures frequency changes applied
* by kiss_rx_thread take effect within 1 second.
*
* SetHardware sub-command protocol (see kiss.h for KISS_HW_* defines):
*
* Host → device: FEND | 0x06 | <sub-cmd> [params...] | FEND
* Device → host: FEND | 0x06 | <sub-cmd> [current-value...] | FEND
* (only sent in response to GET commands)
*
* SET_FREQ (0x01): payload = 4 bytes freq_hz big-endian uint32
* GET_FREQ (0x02): no params; response = sub-cmd + 4-byte freq
* SET_SF (0x03): payload = 1 byte sf (7-12)
* SET_BW (0x04): payload = 2 bytes bw_khz big-endian uint16 (125/250/500)
* SET_CR (0x05): payload = 1 byte cr denominator (5-8)
* SET_TXPWR(0x06): payload = 1 byte tx_power_dbm (int8)
* GET_PARAMS(0x07): no params; response = sub-cmd + 10-byte param block
*
* GET_PARAMS response (10 bytes after sub-cmd):
* [0..3] freq_hz BE uint32
* [4] sf uint8
* [5..6] bw_khz BE uint16
* [7] cr uint8 (denominator)
* [8] tx_power int8 (dBm)
* [9] preamble uint8
*/
#include "kiss.h"
#include "lora_modem.h"
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
/* Resolve the KISS UART from the board overlay */
#define KISS_UART_NODE DT_CHOSEN(loramodem_kiss_uart)
#define KISS_UART DEVICE_DT_GET(KISS_UART_NODE)
/* Thread stack sizes */
#define KISS_RX_STACK_SIZE 2048
#define LORA_RX_STACK_SIZE 2048
/* lora_rx_fn polls with this timeout so frequency changes take effect promptly */
#define LORA_RX_TIMEOUT_MS 1000
/* Packet buffers */
static uint8_t lora_rx_buf[CONFIG_LORAMODEM_MAX_PACKET_SIZE];
/* kiss_tx_buf is shared by both threads — protected by uart_tx_mutex */
static uint8_t kiss_tx_buf[CONFIG_LORAMODEM_MAX_PACKET_SIZE * 2 + 4];
/* Mutex protecting writes to the KISS UART (used by both threads) */
static K_MUTEX_DEFINE(uart_tx_mutex);
/* ── UART helpers ─────────────────────────────────────────────────────────── */
static void uart_write_bytes(const struct device *uart,
const uint8_t *buf, size_t len)
{
for (size_t i = 0; i < len; i++) {
uart_poll_out(uart, buf[i]);
}
}
/* Encode and send a KISS SetHardware response frame to the host */
static void send_hw_response(const struct device *uart,
const uint8_t *payload, size_t payload_len)
{
k_mutex_lock(&uart_tx_mutex, K_FOREVER);
int enc = kiss_encode_cmd(KISS_CMD_SETHARDWARE,
payload, payload_len,
kiss_tx_buf, sizeof(kiss_tx_buf));
if (enc > 0) {
uart_write_bytes(uart, kiss_tx_buf, (size_t)enc);
}
k_mutex_unlock(&uart_tx_mutex);
}
/* ── SetHardware dispatcher ──────────────────────────────────────────────── */
static void handle_sethardware(const struct device *uart,
const uint8_t *payload, size_t len)
{
if (len == 0) {
LOG_WRN("SetHardware: empty payload");
return;
}
uint8_t sub_cmd = payload[0];
const uint8_t *args = payload + 1;
size_t args_len = len - 1;
switch (sub_cmd) {
case KISS_HW_SET_FREQ: {
if (args_len < 4) {
LOG_WRN("SET_FREQ: need 4 bytes, got %zu", args_len);
return;
}
uint32_t freq = ((uint32_t)args[0] << 24) |
((uint32_t)args[1] << 16) |
((uint32_t)args[2] << 8) |
((uint32_t)args[3]);
if (lora_modem_set_freq(freq) < 0) {
LOG_WRN("SET_FREQ: invalid frequency %u Hz", freq);
}
break;
}
case KISS_HW_GET_FREQ: {
struct lora_radio_params p;
lora_modem_get_params(&p);
uint8_t resp[5];
resp[0] = KISS_HW_GET_FREQ;
resp[1] = (uint8_t)(p.freq_hz >> 24);
resp[2] = (uint8_t)(p.freq_hz >> 16);
resp[3] = (uint8_t)(p.freq_hz >> 8);
resp[4] = (uint8_t)(p.freq_hz);
send_hw_response(uart, resp, sizeof(resp));
break;
}
case KISS_HW_SET_SF: {
if (args_len < 1) {
LOG_WRN("SET_SF: need 1 byte");
return;
}
struct lora_radio_params p;
lora_modem_get_params(&p);
p.sf = args[0];
if (lora_modem_set_params(&p) < 0) {
LOG_WRN("SET_SF: invalid SF %u", args[0]);
}
break;
}
case KISS_HW_SET_BW: {
if (args_len < 2) {
LOG_WRN("SET_BW: need 2 bytes");
return;
}
uint16_t bw = ((uint16_t)args[0] << 8) | args[1];
struct lora_radio_params p;
lora_modem_get_params(&p);
p.bw_khz = bw;
if (lora_modem_set_params(&p) < 0) {
LOG_WRN("SET_BW: invalid BW %u kHz", bw);
}
break;
}
case KISS_HW_SET_CR: {
if (args_len < 1) {
LOG_WRN("SET_CR: need 1 byte");
return;
}
struct lora_radio_params p;
lora_modem_get_params(&p);
p.cr = args[0];
if (lora_modem_set_params(&p) < 0) {
LOG_WRN("SET_CR: invalid CR %u", args[0]);
}
break;
}
case KISS_HW_SET_TXPWR: {
if (args_len < 1) {
LOG_WRN("SET_TXPWR: need 1 byte");
return;
}
struct lora_radio_params p;
lora_modem_get_params(&p);
p.tx_power = (int8_t)args[0];
lora_modem_set_params(&p);
break;
}
case KISS_HW_GET_PARAMS: {
struct lora_radio_params p;
lora_modem_get_params(&p);
uint8_t resp[11];
resp[0] = KISS_HW_GET_PARAMS;
resp[1] = (uint8_t)(p.freq_hz >> 24);
resp[2] = (uint8_t)(p.freq_hz >> 16);
resp[3] = (uint8_t)(p.freq_hz >> 8);
resp[4] = (uint8_t)(p.freq_hz);
resp[5] = p.sf;
resp[6] = (uint8_t)(p.bw_khz >> 8);
resp[7] = (uint8_t)(p.bw_khz);
resp[8] = p.cr;
resp[9] = (uint8_t)p.tx_power;
resp[10] = p.preamble_len;
send_hw_response(uart, resp, sizeof(resp));
break;
}
default:
LOG_WRN("SetHardware: unknown sub-cmd 0x%02x", sub_cmd);
break;
}
}
/* ── KISS RX thread: serial → LoRa ────────────────────────────────────────── */
K_THREAD_STACK_DEFINE(kiss_rx_stack, KISS_RX_STACK_SIZE);
static struct k_thread kiss_rx_thread;
static void kiss_rx_fn(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
const struct device *uart = KISS_UART;
struct kiss_decoder_ctx dec;
static uint8_t dec_buf[CONFIG_LORAMODEM_MAX_PACKET_SIZE];
kiss_decoder_init(&dec, dec_buf, sizeof(dec_buf));
#if defined(CONFIG_LORAMODEM_KISS_IFACE_USB)
uint32_t dtr = 0;
LOG_INF("Waiting for USB host...");
while (!dtr) {
uart_line_ctrl_get(uart, UART_LINE_CTRL_DTR, &dtr);
k_sleep(K_MSEC(100));
}
LOG_INF("USB host connected");
#endif
LOG_INF("KISS RX thread started");
uint8_t byte;
while (true) {
if (uart_poll_in(uart, &byte) == 0) {
if (kiss_decoder_feed(&dec, byte)) {
if (dec.cmd == KISS_CMD_DATA &&
dec.frame_len > 0) {
LOG_DBG("TX %zu bytes via LoRa",
dec.frame_len);
lora_modem_send(dec.frame_buf,
dec.frame_len);
} else if (dec.cmd == KISS_CMD_SETHARDWARE) {
handle_sethardware(uart,
dec.frame_buf,
dec.frame_len);
}
kiss_decoder_init(&dec, dec_buf,
sizeof(dec_buf));
}
} else {
k_yield();
}
}
}
/* ── LoRa RX thread: LoRa → serial ────────────────────────────────────────── */
K_THREAD_STACK_DEFINE(lora_rx_stack, LORA_RX_STACK_SIZE);
static struct k_thread lora_rx_thread;
static void lora_rx_fn(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
const struct device *uart = KISS_UART;
int16_t rssi;
int8_t snr;
LOG_INF("LoRa RX thread started");
while (true) {
/*
* Use a finite timeout so any frequency change applied by
* kiss_rx_thread takes effect within LORA_RX_TIMEOUT_MS.
* lora_modem_recv() snapshots g_params at the start of each
* call, so the new frequency is picked up automatically on
* the next iteration.
*/
int len = lora_modem_recv(lora_rx_buf, sizeof(lora_rx_buf),
K_MSEC(LORA_RX_TIMEOUT_MS),
&rssi, &snr);
if (len == -ETIMEDOUT) {
continue;
}
if (len < 0) {
LOG_WRN("lora_modem_recv error: %d", len);
continue;
}
LOG_DBG("RX %d bytes RSSI=%d SNR=%d", len, rssi, snr);
k_mutex_lock(&uart_tx_mutex, K_FOREVER);
int enc_len = kiss_encode(lora_rx_buf, (size_t)len,
kiss_tx_buf, sizeof(kiss_tx_buf));
if (enc_len > 0) {
uart_write_bytes(uart, kiss_tx_buf, (size_t)enc_len);
} else {
LOG_WRN("kiss_encode error: %d", enc_len);
}
k_mutex_unlock(&uart_tx_mutex);
}
}
/* ── Entry point ──────────────────────────────────────────────────────────── */
int main(void)
{
int ret;
LOG_INF("LoRa KISS Modem starting");
ret = lora_modem_init();
if (ret < 0) {
LOG_ERR("LoRa modem init failed: %d", ret);
return ret;
}
k_thread_create(&kiss_rx_thread, kiss_rx_stack,
K_THREAD_STACK_SIZEOF(kiss_rx_stack),
kiss_rx_fn, NULL, NULL, NULL,
K_PRIO_COOP(5), 0, K_NO_WAIT);
k_thread_name_set(&kiss_rx_thread, "kiss_rx");
k_thread_create(&lora_rx_thread, lora_rx_stack,
K_THREAD_STACK_SIZEOF(lora_rx_stack),
lora_rx_fn, NULL, NULL, NULL,
K_PRIO_COOP(5), 0, K_NO_WAIT);
k_thread_name_set(&lora_rx_thread, "lora_rx");
return 0;
}