mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-16 07:49:58 +00:00
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>
214 lines
5.9 KiB
C++
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;
|
|
}
|