Files
ZephCore/zephcore/src/main_kiss_modem.cpp
T
Rastislav VysokyandClaude Opus 5.5 93c1a589d5 add KISS modem role (port of upstream examples/kiss_modem)
Raw LoRa packets to and from a host over the USB/console serial port, plus
upstream's SetHardware extensions (identity, crypto, radio config, stats).

- app/KissModem.cpp/h: upstream text, ZephCore changes fenced: an LBT
  refusal returns to CSMA until the wait-clear bound; a TX times out only
  once the radio has concluded (as Dispatcher::loop); SetRadio outside the
  ZC_RADIO_* ranges is refused; msUntilNextTx() for the event loop.
- src/main_kiss_modem.cpp: event-driven composition root; runs the radio
  maintenance the Dispatcher would. Identity in /lfs/repeater/_main.id.
- adapters/transport/ZephyrUartStream: Arduino Stream over IRQ RX/TX rings.
- boards/common/kiss_modem.conf: role, BT off, console driver off.
- LoRaRadio::getCurrentRSSI(), Print::availableForWrite().
- UI mesh-action stubs for every non-companion role.

Tested on 2x XIAO ESP32-C3 + Wio-SX1262 at 869.423 MHz SF7/62.5/CR5.
Built for xiao_esp32c3, rak4631, lora_e5_mini; host tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:44:36 +02:00

214 lines
5.9 KiB
C++

/*
* SPDX-License-Identifier: MIT
* ZephCore - KISS modem
*
* Composition root of the KISS modem role (upstream examples/kiss_modem): raw
* LoRa packets to and from a host over the USB/console serial port, plus
* MeshCore's SetHardware extensions (KissModem). No mesh, no prefs: the host
* configures the radio, the identity is the server roles' one.
*
* Event loop: serial input, LoRa RX/TX-done, host output drained and a one-shot
* for the soonest modem or radio-maintenance deadline each wake KissModem::loop().
*/
#include <zephyr/kernel.h>
#include <zephyr/devicetree.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_kiss_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
#define ZEPHCORE_USB_STACK \
(IS_ENABLED(CONFIG_USB_CDC_ACM) || IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS))
#if ZEPHCORE_USB_STACK && !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
#include <ZephyrUSBCDC.h>
#endif
#include <app/KissModem.h>
#include <app/RepeaterDataStore.h>
#include <src/RadioIncludes.h>
#include <src/mesh_events.h>
#include <helpers/boot_prefs.h>
#include <helpers/NodePrefs.h>
#include <adapters/transport/ZephyrUartStream.h>
#include <ZephyrSensorManager.h>
#include "../adapters/datastore/ZephyrFsFormat.h"
#define MESH_EVENT_HOST_TX_IDLE BIT(MESH_EVENT_ROLE_BASE) /* host output drained */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | \
MESH_EVENT_MAINTENANCE | MESH_EVENT_HOST_TX_IDLE)
static struct k_event mesh_events;
static void post_lora_rx(void *user_data)
{
ARG_UNUSED(user_data);
k_event_post(&mesh_events, MESH_EVENT_LORA_RX);
}
static void post_lora_tx_done(void *user_data)
{
ARG_UNUSED(user_data);
k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE);
}
static void post_host_rx(void)
{
k_event_post(&mesh_events, MESH_EVENT_CLI_RX);
}
static void post_host_tx_idle(void)
{
k_event_post(&mesh_events, MESH_EVENT_HOST_TX_IDLE);
}
static void wake_timer_fn(struct k_timer *timer)
{
ARG_UNUSED(timer);
k_event_post(&mesh_events, MESH_EVENT_MAINTENANCE);
}
K_TIMER_DEFINE(wake_timer, wake_timer_fn, NULL);
static mesh::ZephyrBoard board;
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LoRaRadio radio(lora_dev, board);
static mesh::ZephyrRNG rng;
static mesh::LocalIdentity identity;
static RepeaterDataStore data_store;
static ZephyrUartStream host;
/* RAM only: the radio reads its parameters here, the host sets them. */
static NodePrefs prefs;
static KissModem modem(host, identity, rng, radio, board, sensors);
static void apply_cad_prefs(void)
{
radio.setCadParams(prefs.cad_auto != 0, prefs.cad_offset, prefs.probe_interval,
prefs.cad_busycap, prefs.cad_base);
}
static void on_set_radio(float freq, float bw, uint8_t sf, uint8_t cr)
{
bool new_channel = freq != prefs.freq || bw != prefs.bw || sf != prefs.sf;
prefs.freq = freq;
prefs.bw = bw;
prefs.sf = sf;
prefs.cr = cr;
/* The learned CAD offset belongs to the old channel, as `set radio`. */
if (new_channel) {
apply_cad_prefs();
}
radio.reconfigure();
}
static void on_set_tx_power(uint8_t power)
{
/* The radio clamps to the board's limit when it next configures TX. */
prefs.tx_power_dbm = (int8_t)MIN(power, 30);
}
static float on_get_current_rssi(void)
{
return radio.getCurrentRSSI();
}
static void on_get_stats(uint32_t *rx, uint32_t *tx, uint32_t *errors)
{
*rx = radio.getPacketsRecv();
*tx = radio.getPacketsSent();
*errors = radio.getPacketsRecvErrors();
}
static void arm_wake(void)
{
uint32_t next = MIN(modem.msUntilNextTx(), radio.msUntilNextMaintenance());
if (next >= mesh::MAINTENANCE_IDLE) {
k_timer_stop(&wake_timer);
return;
}
k_timer_start(&wake_timer, K_MSEC(MAX(next, 1U)), K_NO_WAIT);
}
int main(void)
{
board.clearBootloaderMagic();
#if ZEPHCORE_USB_STACK && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) && \
!IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
zephcore_usbd_init();
const struct device *host_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
#else
const struct device *host_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
#endif
if (!data_store.hasRoleData()) {
LOG_WRN("Volume holds no data for this role - formatting before first boot");
if (!zephcore_fs_format_all(nullptr)) {
LOG_ERR("First-boot format failed - /lfs is not mounted");
}
}
if (!data_store.begin()) {
LOG_ERR("RepeaterDataStore init failed");
}
if (!data_store.loadIdentity(identity)) {
mesh::ZephyrRNG::generateFirstBootIdentity(identity);
data_store.saveIdentity(identity);
}
initNodePrefs(&prefs);
apply_boot_prefs(&prefs, false);
gps_park();
env_sensors_init();
k_event_init(&mesh_events);
radio.setRxCallback(post_lora_rx, nullptr);
radio.setTxDoneCallback(post_lora_tx_done, nullptr);
radio.setPrefs(&prefs);
radio.begin();
radio.setRxBoost(prefs.rx_boost != 0);
radio.setFemRxEnable(prefs.fem_rxgain != 0);
apply_cad_prefs();
if (!host.begin(host_dev, post_host_rx, post_host_tx_idle)) {
LOG_ERR("Host serial not ready");
}
modem.setRadioCallback(on_set_radio);
modem.setTxPowerCallback(on_set_tx_power);
modem.setGetCurrentRssiCallback(on_get_current_rssi);
modem.setGetStatsCallback(on_get_stats);
modem.begin();
for (;;) {
/* processTx() takes one state step per call; the second pass takes
* the step the first one made due (a new frame, a finished TX). */
modem.loop();
modem.loop();
uint8_t rx_buf[256];
int rx_len;
while (!modem.isHostOutputBackedUp() &&
(rx_len = radio.recvRaw(rx_buf, sizeof(rx_buf))) > 0) {
int8_t snr = (int8_t)(radio.getLastSNR() * 4);
int8_t rssi = (int8_t)radio.getLastRSSI();
modem.onPacketReceived(snr, rssi, rx_buf, rx_len);
}
radio.triggerNoiseFloorCalibrate(0);
radio.cadMaintenance();
radio.radioMaintenance();
arm_wake();
uint32_t ev = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER);
k_event_clear(&mesh_events, ev);
}
return 0;
}