mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-23 12:12:46 +00:00
port lora e5 mini
This commit is contained in:
1 parent
c6b4816cc4
commit
2f86310500
13 files changed
+776
-19
No files matched your search
+10
-1
@@ -281,6 +281,8 @@ elseif(BOARD MATCHES ".*mg24.*" OR BOARD MATCHES ".*efr32.*")
|
||||
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf")
|
||||
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*mg24.*")
|
||||
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf")
|
||||
elseif(BOARD MATCHES "lora_e5" OR BOARD MATCHES ".*stm32wl.*")
|
||||
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/stm32wl_common.conf")
|
||||
elseif(BOARD MATCHES "native_sim" OR BOARD MATCHES "native_posix")
|
||||
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/linux_native/linux_common.conf")
|
||||
else()
|
||||
@@ -658,7 +660,8 @@ else()
|
||||
helpers/ui/ui_mesh_actions.cpp
|
||||
app/CompanionMesh.cpp
|
||||
)
|
||||
# Companion transport: TCP socket on native Linux, BLE NUS on MCU builds.
|
||||
# Companion transport: TCP socket on native Linux, serial (UART) on boards
|
||||
# with no Bluetooth controller (e.g. STM32WL), BLE NUS everywhere else.
|
||||
if(CONFIG_ZEPHCORE_TRANSPORT_TCP)
|
||||
message(STATUS "ZephCore Companion Transport: TCP (Linux)")
|
||||
target_sources(app PRIVATE adapters/transport/LinuxTCPTransport.c)
|
||||
@@ -669,6 +672,12 @@ else()
|
||||
target_include_directories(app PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui
|
||||
)
|
||||
elseif(NOT CONFIG_BT)
|
||||
# No BLE controller (STM32WL) — companion speaks MeshCore serial framing
|
||||
# over the chosen console UART. SerialCompanionTransport.c is a full
|
||||
# drop-in zephcore_ble_* provider, just like LinuxTCPTransport.c.
|
||||
message(STATUS "ZephCore Companion Transport: Serial (UART)")
|
||||
target_sources(app PRIVATE adapters/transport/SerialCompanionTransport.c)
|
||||
else()
|
||||
message(STATUS "ZephCore Companion Transport: BLE NUS")
|
||||
target_sources(app PRIVATE adapters/ble/ZephyrBLE.cpp)
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "ZephyrBLE.h"
|
||||
#include "companion_framing.h"
|
||||
|
||||
LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
|
||||
|
||||
@@ -45,9 +46,9 @@ LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
|
||||
#define LISTEN_PORT CONFIG_ZEPHCORE_LINUX_TCP_PORT
|
||||
#define LISTEN_BACKLOG 1
|
||||
|
||||
/* TX congestion control (mirrors ZephyrBLE.cpp). Push codes >= 0x80 are
|
||||
* lossy event signals; protocol response frames < 0x80 are lossless. */
|
||||
#define PUSH_CODE_BASE 0x80
|
||||
/* TX congestion control (mirrors ZephyrBLE.cpp). Lossy (>= 0x80) vs lossless
|
||||
* (< 0x80) classification is shared via companion_is_lossless_protocol_frame()
|
||||
* in companion_framing.h. */
|
||||
#define TX_OVERFLOW_RETRY_MS 250
|
||||
|
||||
/* Bound on a single blocking send. tx_drain_work_fn runs on the system
|
||||
@@ -57,10 +58,8 @@ LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
|
||||
* let the app reconnect rather than stall the node. */
|
||||
#define TX_SEND_TIMEOUT_SEC 2
|
||||
|
||||
struct frame {
|
||||
uint16_t len;
|
||||
uint8_t buf[MAX_FRAME_SIZE];
|
||||
};
|
||||
/* struct frame + framing constants + lossless classification are shared via
|
||||
* companion_framing.h (included above). */
|
||||
|
||||
K_MSGQ_DEFINE(ble_send_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
|
||||
K_MSGQ_DEFINE(ble_recv_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
|
||||
@@ -104,14 +103,6 @@ static bool overflow_pending;
|
||||
static void overflow_retry_work_fn(struct k_work *work);
|
||||
static K_WORK_DELAYABLE_DEFINE(overflow_retry_work, overflow_retry_work_fn);
|
||||
|
||||
static bool is_lossless_protocol_frame(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (!data || len == 0) {
|
||||
return false;
|
||||
}
|
||||
return data[0] < PUSH_CODE_BASE;
|
||||
}
|
||||
|
||||
static void close_client_locked(void)
|
||||
{
|
||||
if (client_fd >= 0) {
|
||||
@@ -469,7 +460,7 @@ size_t zephcore_ble_send(const uint8_t *data, uint16_t len)
|
||||
|
||||
/* Lossless protocol response frames are never dropped — report
|
||||
* failure so the caller retries instead of clobbering overflow. */
|
||||
if (is_lossless_protocol_frame(data, len)) {
|
||||
if (companion_is_lossless_protocol_frame(data, len)) {
|
||||
LOG_DBG("TX queue full for lossless frame hdr=0x%02x, retry later",
|
||||
data[0]);
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore serial (UART) companion transport — drop-in zephcore_ble_* provider.
|
||||
*
|
||||
* For boards with NO Bluetooth and NO USB device controller (e.g. the Seeed
|
||||
* LoRa-E5 / STM32WL family), the companion app talks to the node over a plain
|
||||
* UART — on the LoRa-E5 mini that UART is bridged to USB-C by an onboard
|
||||
* USB-UART chip, so the official MeshCore serial client connects to it exactly
|
||||
* like a native-USB companion.
|
||||
*
|
||||
* This file is the UART analogue of adapters/transport/LinuxTCPTransport.c: it
|
||||
* implements the full zephcore_ble_* C API (adapters/ble/ZephyrBLE.h) so the
|
||||
* companion application is transport-agnostic. It is compiled INSTEAD of
|
||||
* ZephyrBLE.cpp when CONFIG_BT=n and CONFIG_ZEPHCORE_TRANSPORT_TCP=n
|
||||
* (see the companion-transport selection in CMakeLists.txt). The MeshCore
|
||||
* '<'/'>' framing + struct frame + congestion classification are shared with
|
||||
* the TCP transport via companion_framing.h.
|
||||
*
|
||||
* I/O is interrupt-driven (uart_irq) on both directions so a contact-sync burst
|
||||
* (many ~148B frames back-to-back) drains at full line rate with real
|
||||
* back-pressure instead of blocking the system workqueue on uart_poll_out.
|
||||
*
|
||||
* Session model: a UART has no connect/disconnect line event (no DTR — the
|
||||
* official client never asserts it), and a connected companion may sit idle for
|
||||
* many minutes between frames. We therefore mark "connected" on the first
|
||||
* inbound frame (fires on_connected once, lights the UI indicator) and STAY
|
||||
* connected; we never infer a disconnect from idleness. Per-session reset on a
|
||||
* reconnecting client is driven by the protocol's CMD_APP_START handler, not by
|
||||
* any inactivity timer. The only timeout here is a parser-level resync for a
|
||||
* partial frame that never completes (below) — it touches the RX byte parser
|
||||
* only, never the session/connection state.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/sys/ring_buffer.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "ZephyrBLE.h"
|
||||
#include "companion_framing.h"
|
||||
|
||||
LOG_MODULE_REGISTER(serial_companion, LOG_LEVEL_INF);
|
||||
|
||||
#define FRAME_QUEUE_SIZE CONFIG_ZEPHCORE_BLE_QUEUE_SIZE
|
||||
#define TX_RING_SIZE 2048 /* ~13 contact frames of headroom */
|
||||
#define TX_OVERFLOW_RETRY_MS 250
|
||||
|
||||
/* Parser-only resync: if a frame has STARTED ('<' seen) but its payload never
|
||||
* completes within this window, the next inbound byte resets the parser to
|
||||
* IDLE so a lost/truncated frame can't swallow the following frame. This is NOT
|
||||
* a session/idle timeout — an idle-but-connected companion sits in RX_IDLE
|
||||
* between frames and never arms this, and it never affects connection state. */
|
||||
#define FRAME_PARTIAL_TIMEOUT_MS 2000
|
||||
|
||||
/* Companion UART — the chosen console UART (USART1 on the LoRa-E5 mini). */
|
||||
static const struct device *uart_dev;
|
||||
|
||||
K_MSGQ_DEFINE(ble_send_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
|
||||
K_MSGQ_DEFINE(ble_recv_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
|
||||
|
||||
static const struct ble_callbacks *transport_cbs;
|
||||
static enum zephcore_iface active_iface = ZEPHCORE_IFACE_NONE;
|
||||
static bool transport_enabled;
|
||||
static uint32_t passkey;
|
||||
|
||||
/* ---- RX byte-assembly state (runs in UART ISR) ---------------------------- */
|
||||
enum rx_state {
|
||||
RX_IDLE = 0, /* waiting for '<' */
|
||||
RX_LEN_LO,
|
||||
RX_LEN_HI,
|
||||
RX_PAYLOAD,
|
||||
};
|
||||
static enum rx_state rx_st;
|
||||
static uint16_t rx_idx;
|
||||
static uint16_t rx_expect;
|
||||
static uint32_t rx_frame_start; /* k_uptime_get_32() when the '<' arrived */
|
||||
static struct frame rx_frame;
|
||||
|
||||
/* ---- TX side: interrupt-driven ring (whole frames committed atomically) ---- */
|
||||
static uint8_t tx_ring_data[TX_RING_SIZE];
|
||||
static struct ring_buf tx_ring;
|
||||
static struct k_spinlock tx_lock;
|
||||
|
||||
/* Order-preserving holdover: when the ring is momentarily full, the popped
|
||||
* frame is parked here (NOT requeued at the tail, which would reorder it behind
|
||||
* later frames) and re-tried first on the next drain. Touched only by
|
||||
* tx_drain_work_fn, which runs solely on the system workqueue (never concurrent
|
||||
* with itself), so it needs no lock. */
|
||||
static struct frame held_frame;
|
||||
static bool held_pending;
|
||||
|
||||
/* ---- TX queue drain (moves frames from ble_send_queue into the TX ring) ----
|
||||
* Runs on the system workqueue; never blocks on the wire (the ISR does the
|
||||
* byte-level draining), so a full sync burst can't starve the workqueue. */
|
||||
static void tx_drain_work_fn(struct k_work *work);
|
||||
static K_WORK_DEFINE(tx_drain_work, tx_drain_work_fn);
|
||||
|
||||
/* on_connected is deferred off the ISR onto the system workqueue (it ends up
|
||||
* in ui_notify(), which is not guaranteed ISR-safe). */
|
||||
static void connect_work_fn(struct k_work *work);
|
||||
static K_WORK_DEFINE(connect_work, connect_work_fn);
|
||||
|
||||
/* ---- TX congestion control (mirrors LinuxTCPTransport.c / ZephyrBLE.cpp) ---- */
|
||||
static bool tx_congested;
|
||||
static struct frame overflow_frame;
|
||||
static bool overflow_pending;
|
||||
static void overflow_retry_work_fn(struct k_work *work);
|
||||
static K_WORK_DELAYABLE_DEFINE(overflow_retry_work, overflow_retry_work_fn);
|
||||
|
||||
static void connect_work_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
LOG_INF("serial companion session started");
|
||||
if (transport_cbs && transport_cbs->on_connected) {
|
||||
transport_cbs->on_connected();
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark the session connected on first inbound traffic (fires on_connected once
|
||||
* — the UI "connected" indicator). Called from the UART ISR, so the callback is
|
||||
* deferred to a workqueue rather than run inline. */
|
||||
static void mark_connected(void)
|
||||
{
|
||||
if (active_iface != ZEPHCORE_IFACE_NONE) {
|
||||
return;
|
||||
}
|
||||
active_iface = ZEPHCORE_IFACE_BLE; /* reuse the BLE path in main_companion */
|
||||
k_work_submit(&connect_work);
|
||||
}
|
||||
|
||||
/* ---- UART ISR: drains RX into the frame parser, fills TX from the ring ----- */
|
||||
static void uart_isr(const struct device *dev, void *user_data)
|
||||
{
|
||||
ARG_UNUSED(user_data);
|
||||
|
||||
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
|
||||
if (uart_irq_rx_ready(dev)) {
|
||||
uint8_t buf[64];
|
||||
int n = uart_fifo_read(dev, buf, sizeof(buf));
|
||||
|
||||
/* Parser-only resync: drop a partial frame that stalled
|
||||
* mid-payload so it can't consume the bytes of the next
|
||||
* frame. Connection state is untouched. */
|
||||
if (rx_st != RX_IDLE &&
|
||||
(k_uptime_get_32() - rx_frame_start) > FRAME_PARTIAL_TIMEOUT_MS) {
|
||||
rx_st = RX_IDLE;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint8_t b = buf[i];
|
||||
|
||||
switch (rx_st) {
|
||||
case RX_IDLE:
|
||||
if (b == COMPANION_FRAME_RX_SYNC) {
|
||||
rx_st = RX_LEN_LO;
|
||||
rx_frame_start = k_uptime_get_32();
|
||||
}
|
||||
break;
|
||||
case RX_LEN_LO:
|
||||
rx_expect = b;
|
||||
rx_st = RX_LEN_HI;
|
||||
break;
|
||||
case RX_LEN_HI:
|
||||
rx_expect |= ((uint16_t)b) << 8;
|
||||
rx_idx = 0;
|
||||
if (rx_expect == 0 || rx_expect > MAX_FRAME_SIZE) {
|
||||
rx_st = RX_IDLE; /* bad length, resync */
|
||||
} else {
|
||||
rx_st = RX_PAYLOAD;
|
||||
}
|
||||
break;
|
||||
default: /* RX_PAYLOAD */
|
||||
rx_frame.buf[rx_idx++] = b;
|
||||
if (rx_idx >= rx_expect) {
|
||||
rx_frame.len = rx_expect;
|
||||
mark_connected();
|
||||
/* on_rx_frame queues to ble_recv_queue and
|
||||
* wakes the mesh thread (ISR-safe: K_NO_WAIT
|
||||
* k_msgq_put + k_event_post), same as
|
||||
* ZephyrBLE's secure_nus_rx_write. */
|
||||
if (transport_cbs && transport_cbs->on_rx_frame) {
|
||||
transport_cbs->on_rx_frame(rx_frame.buf,
|
||||
rx_frame.len);
|
||||
}
|
||||
rx_st = RX_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uart_irq_tx_ready(dev)) {
|
||||
uint8_t *out;
|
||||
bool empty;
|
||||
k_spinlock_key_t key = k_spin_lock(&tx_lock);
|
||||
uint32_t claimed = ring_buf_get_claim(&tx_ring, &out, 64);
|
||||
|
||||
if (claimed > 0) {
|
||||
int sent = uart_fifo_fill(dev, out, claimed);
|
||||
ring_buf_get_finish(&tx_ring, sent > 0 ? sent : 0);
|
||||
}
|
||||
empty = ring_buf_is_empty(&tx_ring);
|
||||
if (empty) {
|
||||
/* Disable inside the lock so a concurrent enqueue
|
||||
* can't be stranded (it re-enables after its put). */
|
||||
uart_irq_tx_disable(dev);
|
||||
}
|
||||
k_spin_unlock(&tx_lock, key);
|
||||
|
||||
if (empty) {
|
||||
/* Ring drained — pull the next batch from the queue.
|
||||
* tx_drain_work fires on_tx_idle when fully drained. */
|
||||
k_work_submit(&tx_drain_work);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Queue one framed frame into the TX ring. Returns true if it fit (committed
|
||||
* atomically — header + payload together — so frames never tear). */
|
||||
static bool tx_ring_put_frame(const struct frame *f)
|
||||
{
|
||||
uint8_t hdr[3] = {
|
||||
COMPANION_FRAME_TX_SYNC,
|
||||
(uint8_t)(f->len & 0xFF),
|
||||
(uint8_t)((f->len >> 8) & 0xFF),
|
||||
};
|
||||
size_t total = sizeof(hdr) + f->len;
|
||||
|
||||
k_spinlock_key_t key = k_spin_lock(&tx_lock);
|
||||
if (ring_buf_space_get(&tx_ring) < total) {
|
||||
k_spin_unlock(&tx_lock, key);
|
||||
return false;
|
||||
}
|
||||
ring_buf_put(&tx_ring, hdr, sizeof(hdr));
|
||||
ring_buf_put(&tx_ring, f->buf, f->len);
|
||||
k_spin_unlock(&tx_lock, key);
|
||||
|
||||
uart_irq_tx_enable(uart_dev);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void tx_drain_work_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
struct frame f;
|
||||
|
||||
/* Re-try the order-preserving holdover first. If it still doesn't fit,
|
||||
* leave it parked — the TX ISR re-submits this work when the ring drains. */
|
||||
if (held_pending) {
|
||||
if (!tx_ring_put_frame(&held_frame)) {
|
||||
return;
|
||||
}
|
||||
held_pending = false;
|
||||
}
|
||||
|
||||
/* Move queued frames into the TX ring while it has room. On ring-full,
|
||||
* park the popped frame in the single holdover slot (preserving FIFO
|
||||
* order) rather than requeuing it behind later frames. */
|
||||
while (k_msgq_get(&ble_send_queue, &f, K_NO_WAIT) == 0) {
|
||||
if (!tx_ring_put_frame(&f)) {
|
||||
held_frame = f;
|
||||
held_pending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Clear congestion at the 1/3 low-water mark (hysteresis). */
|
||||
if (tx_congested &&
|
||||
k_msgq_num_used_get(&ble_send_queue) <= FRAME_QUEUE_SIZE / 3) {
|
||||
tx_congested = false;
|
||||
LOG_INF("tx: congestion cleared");
|
||||
}
|
||||
}
|
||||
|
||||
/* Queue + holdover fully drained into the ring. Fire on_tx_idle only once
|
||||
* the wire is also idle (ring empty) so the contact pump paces to real
|
||||
* throughput. */
|
||||
if (transport_cbs && transport_cbs->on_tx_idle && !held_pending) {
|
||||
bool ring_empty;
|
||||
k_spinlock_key_t key = k_spin_lock(&tx_lock);
|
||||
ring_empty = ring_buf_is_empty(&tx_ring);
|
||||
k_spin_unlock(&tx_lock, key);
|
||||
if (ring_empty) {
|
||||
transport_cbs->on_tx_idle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void overflow_retry_work_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
if (!overflow_pending) {
|
||||
return;
|
||||
}
|
||||
if (k_msgq_put(&ble_send_queue, &overflow_frame, K_NO_WAIT) == 0) {
|
||||
overflow_pending = false;
|
||||
k_work_submit(&tx_drain_work);
|
||||
} else {
|
||||
k_work_schedule(&overflow_retry_work, K_MSEC(TX_OVERFLOW_RETRY_MS));
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Public API (matches ZephyrBLE.h) ========== */
|
||||
|
||||
void zephcore_ble_init(const struct ble_callbacks *cbs)
|
||||
{
|
||||
transport_cbs = cbs;
|
||||
transport_enabled = true;
|
||||
ring_buf_init(&tx_ring, sizeof(tx_ring_data), tx_ring_data);
|
||||
}
|
||||
|
||||
void zephcore_ble_start(const char *device_name)
|
||||
{
|
||||
ARG_UNUSED(device_name);
|
||||
|
||||
uart_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
|
||||
if (!device_is_ready(uart_dev)) {
|
||||
LOG_ERR("companion UART not ready");
|
||||
uart_dev = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
uart_irq_callback_set(uart_dev, uart_isr);
|
||||
uart_irq_rx_enable(uart_dev);
|
||||
LOG_INF("serial companion transport ready on %s", uart_dev->name);
|
||||
}
|
||||
|
||||
size_t zephcore_ble_send(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (!uart_dev || len == 0 || len > MAX_FRAME_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct frame f;
|
||||
|
||||
f.len = len;
|
||||
memcpy(f.buf, data, len);
|
||||
|
||||
if (k_msgq_put(&ble_send_queue, &f, K_NO_WAIT) != 0) {
|
||||
/* Queue full — congestion mode (mirrors LinuxTCPTransport.c). */
|
||||
if (!tx_congested) {
|
||||
LOG_WRN("TX queue full (%u/%u), entering congestion",
|
||||
k_msgq_num_used_get(&ble_send_queue),
|
||||
(unsigned)FRAME_QUEUE_SIZE);
|
||||
tx_congested = true;
|
||||
}
|
||||
/* Lossless protocol responses are never dropped — report failure
|
||||
* so the caller retries instead of clobbering overflow. */
|
||||
if (companion_is_lossless_protocol_frame(data, len)) {
|
||||
return 0;
|
||||
}
|
||||
if (overflow_pending) {
|
||||
LOG_WRN("overflow full, dropping push hdr=0x%02x", data[0]);
|
||||
return 0;
|
||||
}
|
||||
overflow_frame = f;
|
||||
overflow_pending = true;
|
||||
k_work_schedule(&overflow_retry_work, K_MSEC(TX_OVERFLOW_RETRY_MS));
|
||||
return len;
|
||||
}
|
||||
|
||||
k_work_submit(&tx_drain_work);
|
||||
return len;
|
||||
}
|
||||
|
||||
void zephcore_ble_set_enabled(bool enable)
|
||||
{
|
||||
transport_enabled = enable;
|
||||
if (!uart_dev) {
|
||||
return;
|
||||
}
|
||||
/* Actually gate the link, not just a flag: stop accepting frames when
|
||||
* disabled (mirrors the TCP transport closing its socket). */
|
||||
if (enable) {
|
||||
uart_irq_rx_enable(uart_dev);
|
||||
} else {
|
||||
uart_irq_rx_disable(uart_dev);
|
||||
}
|
||||
}
|
||||
|
||||
bool zephcore_ble_is_enabled(void)
|
||||
{
|
||||
return transport_enabled;
|
||||
}
|
||||
|
||||
bool zephcore_ble_is_active(void)
|
||||
{
|
||||
return uart_dev != NULL && active_iface == ZEPHCORE_IFACE_BLE;
|
||||
}
|
||||
|
||||
bool zephcore_ble_is_connected(void)
|
||||
{
|
||||
return active_iface == ZEPHCORE_IFACE_BLE;
|
||||
}
|
||||
|
||||
bool zephcore_ble_is_congested(void)
|
||||
{
|
||||
return tx_congested;
|
||||
}
|
||||
|
||||
bool zephcore_ble_is_advertising(void)
|
||||
{
|
||||
/* A UART is "always advertising" once the transport is up — keeps the
|
||||
* companion housekeeping loop from trying to (re)start advertising. */
|
||||
return uart_dev != NULL;
|
||||
}
|
||||
|
||||
void zephcore_ble_set_passkey(uint32_t pk)
|
||||
{
|
||||
passkey = pk;
|
||||
}
|
||||
|
||||
uint32_t zephcore_ble_get_passkey(void)
|
||||
{
|
||||
return passkey;
|
||||
}
|
||||
|
||||
enum zephcore_iface zephcore_ble_get_active_iface(void)
|
||||
{
|
||||
return active_iface;
|
||||
}
|
||||
|
||||
void zephcore_ble_set_active_iface(enum zephcore_iface iface)
|
||||
{
|
||||
active_iface = iface;
|
||||
}
|
||||
|
||||
bool zephcore_ble_iface_try_claim(enum zephcore_iface who)
|
||||
{
|
||||
if (active_iface == ZEPHCORE_IFACE_NONE || active_iface == who) {
|
||||
active_iface = who;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct k_msgq *zephcore_ble_get_recv_queue(void)
|
||||
{
|
||||
return &ble_recv_queue;
|
||||
}
|
||||
|
||||
struct k_msgq *zephcore_ble_get_send_queue(void)
|
||||
{
|
||||
return &ble_send_queue;
|
||||
}
|
||||
|
||||
void zephcore_ble_kick_tx(void)
|
||||
{
|
||||
k_work_submit(&tx_drain_work);
|
||||
}
|
||||
|
||||
void zephcore_ble_disconnect(void)
|
||||
{
|
||||
active_iface = ZEPHCORE_IFACE_NONE;
|
||||
if (transport_cbs && transport_cbs->on_disconnected) {
|
||||
transport_cbs->on_disconnected();
|
||||
}
|
||||
}
|
||||
|
||||
void zephcore_ble_conn_params_ready(void)
|
||||
{
|
||||
/* No link-layer connection parameters on a UART — no-op. */
|
||||
}
|
||||
|
||||
void zephcore_ble_update_name(const char *new_name)
|
||||
{
|
||||
/* No advertising payload on a UART — no-op. */
|
||||
ARG_UNUSED(new_name);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Shared MeshCore companion serial framing — used by the non-BLE companion
|
||||
* transports (LinuxTCPTransport.c, SerialCompanionTransport.c).
|
||||
*
|
||||
* Wire format (MeshCore ArduinoSerialInterface):
|
||||
* App -> Node: '<' (0x3C) | len_LSB | len_MSB | payload...
|
||||
* Node -> App: '>' (0x3E) | len_LSB | len_MSB | payload...
|
||||
*
|
||||
* `struct frame` is the unit stored in the ble_send_queue/ble_recv_queue; its
|
||||
* layout must match what main_companion.cpp puts on those queues (len + buf).
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "ZephyrBLE.h" /* MAX_FRAME_SIZE */
|
||||
|
||||
#define COMPANION_FRAME_RX_SYNC '<'
|
||||
#define COMPANION_FRAME_TX_SYNC '>'
|
||||
|
||||
/* Push codes >= 0x80 are lossy event signals (droppable under congestion);
|
||||
* protocol responses < 0x80 are lossless and must never be silently dropped. */
|
||||
#define COMPANION_PUSH_CODE_BASE 0x80
|
||||
|
||||
struct frame {
|
||||
uint16_t len;
|
||||
uint8_t buf[MAX_FRAME_SIZE];
|
||||
};
|
||||
|
||||
static inline bool companion_is_lossless_protocol_frame(const uint8_t *data,
|
||||
uint16_t len)
|
||||
{
|
||||
return data != NULL && len > 0 && data[0] < COMPANION_PUSH_CODE_BASE;
|
||||
}
|
||||
@@ -1635,8 +1635,17 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
sendPacketError(ERR_ILLEGAL_ARG);
|
||||
return true;
|
||||
}
|
||||
// Reset contact iterator for fresh start
|
||||
// Reset per-session state for a fresh app session. BLE/USB also run
|
||||
// this cleanup on disconnect, but the serial transport has no
|
||||
// disconnect event, so APP_START is its authoritative session-reset
|
||||
// point: drop a stale contact iteration (a leftover PACKET_CONTACT_END
|
||||
// would confuse the new session's sync state machine), a half-finished
|
||||
// message sync, and free an abandoned Ed25519 sign buffer (else an 8KB
|
||||
// leak if the previous session dropped mid-CMD_SIGN). Idempotent — all
|
||||
// no-ops when the state is already clean.
|
||||
_contact_iter_active = false;
|
||||
cancelSyncPending();
|
||||
cleanupSignState();
|
||||
|
||||
// Return SELF_INFO
|
||||
uint8_t rsp[90]; // 58 fixed + up to 32 bytes name
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# STM32WL Platform Configuration (Seeed LoRa-E5 / STM32WLE5JC)
|
||||
#
|
||||
# Included by all STM32WL boards. STM32WL has NO Bluetooth radio and NO USB
|
||||
# peripheral, so these boards are UART-only: the repeater CLI (and the serial
|
||||
# companion) run over USART1 (chosen zephyr,console) through the board's
|
||||
# onboard USB-UART bridge. Flashing is over SWD/ST-Link (west flash / OpenOCD).
|
||||
#
|
||||
# Include order: prj.conf -> zephcore_common.conf -> stm32wl_common.conf -> board.conf
|
||||
# Only contains STM32WL-specific settings; common app/crypto/storage is in
|
||||
# zephcore_common.conf.
|
||||
|
||||
# ========== Boot / Flash layout ==========
|
||||
# No MCUboot: the application links at the flash origin via the single
|
||||
# slot0_partition defined in board.overlay (zephyr,code-partition).
|
||||
CONFIG_USE_DT_CODE_PARTITION=y
|
||||
|
||||
# ========== No Bluetooth ==========
|
||||
# STM32WL has no BLE radio. Disabling here makes every build on this platform
|
||||
# BT-free (the companion transport is serial, not NUS). Mirrors repeater.conf.
|
||||
CONFIG_BT=n
|
||||
|
||||
# ========== No USB device stack ==========
|
||||
# STM32WLE5JC has no USB peripheral. Console/CLI is plain UART
|
||||
# (UART_INTERRUPT_DRIVEN already set in prj.conf). No UF2 output.
|
||||
|
||||
# ========== Entropy / CSPRNG ==========
|
||||
# STM32WL provides only a raw TRNG (st,stm32-rng), not a hardware CSPRNG.
|
||||
# Enable the TRNG as the entropy source; with an entropy driver present,
|
||||
# CONFIG_CSPRNG_ENABLED defaults y and sys_csrand_get() is backed by the
|
||||
# hardware CS generator. ZephyrRNG additionally conditions identity seeds with
|
||||
# CPU-jitter + AES-256-CTR (see adapters/rng/ZephyrRNG.cpp), so a weak/slow
|
||||
# TRNG cannot silently produce correlated keys. CONFIG_CSPRNG_ENABLED (which
|
||||
# ZephyrRNG BUILD_ASSERTs on) is not directly assignable -- it defaults y once
|
||||
# an entropy driver is present (subsys/random/Kconfig), so enabling the TRNG
|
||||
# below is sufficient.
|
||||
CONFIG_ENTROPY_GENERATOR=y
|
||||
CONFIG_ENTROPY_STM32_RNG=y
|
||||
|
||||
# ========== Memory (64KB SRAM / 256KB flash) ==========
|
||||
# RAM is the binding constraint on STM32WL (flash sits at ~63%), so spend the
|
||||
# abundant flash to reclaim SRAM.
|
||||
#
|
||||
# AES lookup tables in ROM: the tf-psa-crypto builtin AES otherwise computes
|
||||
# ~8KB of forward/reverse tables (RT0..RT3, RSb, ...) into .bss at runtime.
|
||||
# ROM_TABLES makes them const (flash) instead. Single biggest SRAM win here.
|
||||
CONFIG_MBEDTLS_AES_ROM_TABLES=y
|
||||
|
||||
# PSA key slots: default 16 each cost ~40B+ of static slot state. The mesh only
|
||||
# juggles a couple of ephemeral AES keys at once; 8 is ample.
|
||||
CONFIG_MBEDTLS_PSA_KEY_SLOT_COUNT=8
|
||||
|
||||
# Trim the common 8KB stacks to fit 64KB SRAM. Crypto (Ed25519/PSA) and the
|
||||
# RNG jitter sampler run on the main thread, so keep main comfortable.
|
||||
CONFIG_MAIN_STACK_SIZE=6144
|
||||
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096
|
||||
@@ -134,6 +134,39 @@ pyocd pack install EFR32MG24B220F1536IM48
|
||||
|
||||
Flash with: `west flash --runner pyocd`
|
||||
|
||||
### STM32WL (Seeed LoRa-E5)
|
||||
|
||||
| Board | Build string | Flash |
|
||||
|----------------|---------------------------------------|--------------|
|
||||
| LoRa-E5 mini | `west build -b lora_e5_mini zephcore` | `west flash` |
|
||||
|
||||
The STM32WLE5JC integrates an SX1262-class sub-GHz radio (driven by the native
|
||||
SX126x driver via its STM32WL HAL over the internal `subghzspi` bus). RF
|
||||
front-end, RF switch (PA4/PA5), TCXO and the +22 dBm RFO_HP PA are described in
|
||||
upstream `zephyr/dts/arm/seeed_studio/lora-e5.dtsi`.
|
||||
|
||||
STM32WL caveats — different from every other ZephCore platform:
|
||||
- **No Bluetooth and no USB device.** `boards/common/stm32wl_common.conf` forces
|
||||
`CONFIG_BT=n`. The console/CLI and the companion protocol both run over
|
||||
**USART1**, bridged to USB-C by the onboard USB-UART chip.
|
||||
- **Repeater** uses the USART CLI (add `repeater.conf`). The **companion** speaks
|
||||
MeshCore serial framing over the same UART via `SerialCompanionTransport.c`
|
||||
(a drop-in `zephcore_ble_*` provider, auto-selected because `CONFIG_BT=n`) —
|
||||
no BLE pairing, the official serial client connects directly.
|
||||
- **RAM-bound, not flash-bound:** 64KB SRAM. The companion's contact/queue
|
||||
arrays are capped hard in `board.conf` (`MAX_CONTACTS=24`, `OFFLINE_QUEUE_SIZE=8`).
|
||||
AES tables live in ROM (`MBEDTLS_AES_ROM_TABLES`) to reclaim ~8KB SRAM.
|
||||
- **TRNG only (no HW CSPRNG):** the STM32 TRNG is enabled as the entropy source
|
||||
and `CSPRNG_ENABLED` auto-resolves on top; `ZephyrRNG` further conditions
|
||||
identity seeds with jitter + AES-CTR.
|
||||
- **No MCUboot / UF2:** single app partition at flash origin + a LittleFS volume
|
||||
(see `board.overlay`). Flash over SWD/ST-Link with `west flash` (OpenOCD).
|
||||
|
||||
Build the repeater:
|
||||
```
|
||||
west build -b lora_e5_mini zephcore -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
|
||||
```
|
||||
|
||||
### Building for Repeater Role
|
||||
|
||||
Append `-- -DEXTRA_CONF_FILE="boards/common/repeater.conf"` to any build command:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Seeed Studio LoRa-E5 mini (STM32WLE5JC, integrated SX126x sub-GHz radio)
|
||||
# Board-specific configuration.
|
||||
#
|
||||
# Hardware:
|
||||
# - STM32WLE5JC: Cortex-M4 @ 48MHz (MSI), 256KB flash, 64KB SRAM
|
||||
# - Integrated SX1262-class sub-GHz radio on the internal subghzspi bus
|
||||
# - RF front-end: RFO_HP, +22 dBm; RF switch on PA4 (TX) / PA5 (RX)
|
||||
# (radio node configured in zephyr/dts/arm/seeed_studio/lora-e5.dtsi)
|
||||
# - 32MHz TCXO (DIO3, 1.7V), 32.768kHz LSE
|
||||
# - USART1 (PB6/PB7) -> onboard USB-UART bridge: console + CLI
|
||||
# - No Bluetooth, no USB device (see boards/common/stm32wl_common.conf)
|
||||
#
|
||||
# Roles: repeater (USART CLI). Companion is serial-over-USART (no BLE).
|
||||
# Flash via SWD/ST-Link: west flash (OpenOCD runner, no UF2/DFU).
|
||||
|
||||
# Board identification
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="LoRa-E5 mini"
|
||||
|
||||
# Device Information Service model name (inert with BT off, kept for consistency)
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="LoRa-E5 mini"
|
||||
|
||||
# TX power: +22 dBm RFO_HP (clamped to rfo-hp-max-power=22 in the DT radio node).
|
||||
CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=22
|
||||
|
||||
# ========== Companion RAM capacity (64KB SRAM) ==========
|
||||
# The companion build allocates static contacts[] + offline_queue[] arrays.
|
||||
# The defaults (350 contacts ~66KB, 256-entry queue ~44KB) dwarf 64KB of SRAM,
|
||||
# so cap them hard. These only allocate in the companion role; the repeater
|
||||
# build ignores them. A serial companion on this constrained board is for
|
||||
# modest contact lists.
|
||||
CONFIG_ZEPHCORE_MAX_CONTACTS=24
|
||||
CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=8
|
||||
CONFIG_ZEPHCORE_MAX_CHANNELS=8
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Seeed Studio LoRa-E5 mini (STM32WLE5JC) -- ZephCore overlay
|
||||
*
|
||||
* The integrated sub-GHz radio (subghzspi "lora" node), RF switch (PA4/PA5),
|
||||
* TCXO and RFO_HP front-end are already fully described in
|
||||
* zephyr/dts/arm/seeed_studio/lora-e5.dtsi, and the lora0 alias + USART1
|
||||
* console come from the upstream lora_e5_mini board DTS. This overlay only:
|
||||
* 1. enables the TRNG entropy source, and
|
||||
* 2. repartitions the 256KB internal flash for a bootloader-less ZephCore
|
||||
* image plus a LittleFS data volume.
|
||||
*/
|
||||
|
||||
/*
|
||||
* STM32WL TRNG -- entropy source for the CSPRNG used by Ed25519 key
|
||||
* generation and LoRa contention jitter. STM32WL has only a raw TRNG;
|
||||
* CONFIG_CSPRNG_ENABLED (stm32wl_common.conf) layers the CS generator on top.
|
||||
* Matches the bare enable used by upstream nucleo_wl55jc.
|
||||
*/
|
||||
&rng {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
/*
|
||||
* Flash repartition -- no MCUboot (flashed over SWD/ST-Link; STM32WL has no
|
||||
* USB for native DFU/UF2). Total 256KB, 2KB erase pages.
|
||||
*
|
||||
* 0x00000 - 0x38000 (224KB) Application (slot0, links at flash origin)
|
||||
* 0x38000 - 0x40000 ( 32KB) LittleFS /lfs (identity, prefs, contacts)
|
||||
*
|
||||
* The upstream board reserves mcuboot(48K) + dual 96K slots + 16K storage,
|
||||
* whose 96K app slot cannot hold ZephCore -- so we delete and rebuild.
|
||||
*/
|
||||
/delete-node/ &boot_partition;
|
||||
/delete-node/ &slot0_partition;
|
||||
/delete-node/ &slot1_partition;
|
||||
/delete-node/ &storage_partition;
|
||||
|
||||
&flash0 {
|
||||
partitions {
|
||||
slot0_partition: partition@0 {
|
||||
label = "image-0";
|
||||
reg = <0x00000000 0x00038000>;
|
||||
};
|
||||
|
||||
lfs_partition: partition@38000 {
|
||||
label = "lfs";
|
||||
reg = <0x00038000 0x00008000>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/* LittleFS auto-mount at /lfs (defines the &lfs fstab node) */
|
||||
#include "../../common/filesystem.dtsi"
|
||||
@@ -8,6 +8,10 @@
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
|
||||
/* Neutralize STM32 CMSIS peripheral-instance macros (RNG, AES, ...) that
|
||||
* collide with portable mesh identifiers. No-op off STM32. */
|
||||
#include <mesh/stm32_cmsis_fixup.h>
|
||||
|
||||
#define MAX_HASH_SIZE 8 /* SHA256 truncated to 8 bytes for dedup */
|
||||
#define PUB_KEY_SIZE 32 /* Ed25519 public key */
|
||||
#define PRV_KEY_SIZE 64 /* Ed25519 expanded private key */
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* On STM32, CMSIS defines an object-like macro `RNG` that collides with the
|
||||
* mesh::RNG class below. The fixup header neutralizes it order-independently. */
|
||||
#include <mesh/stm32_cmsis_fixup.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class RNG {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* STM32 CMSIS peripheral-macro fixup for the portable mesh:: core.
|
||||
*
|
||||
* STM32 CMSIS device headers (e.g. stm32wle5xx.h, pulled in transitively via
|
||||
* <zephyr/kernel.h> -> soc.h) define object-like macros for every on-chip
|
||||
* peripheral instance: `RNG`, `AES`, `ADC`, `RTC`, `CRC`, ... Several collide
|
||||
* with generic identifiers in the portable mesh core -- most notably the
|
||||
* mesh::RNG class.
|
||||
*
|
||||
* The collision is order-sensitive: zephcore .cpp files include mesh headers
|
||||
* (which declare e.g. class RNG) and only afterwards include a Zephyr header
|
||||
* (e.g. <zephyr/logging/log.h>) that pulls CMSIS in. So a naive `#undef RNG`
|
||||
* placed next to the class declaration runs before the macro even exists.
|
||||
*
|
||||
* Fix: force the CMSIS device header to be processed NOW (via <soc.h>) so its
|
||||
* include guard trips, then undef the colliding instance macros. The later
|
||||
* pull through <zephyr/kernel.h> then re-enters a guarded, already-processed
|
||||
* header and is a no-op, so the macros stay undef'd for the rest of the
|
||||
* translation unit -- regardless of whether this header was reached before or
|
||||
* after the first Zephyr include.
|
||||
*
|
||||
* Only zephcore C++ TUs include this; Zephyr's own STM32 drivers (which use the
|
||||
* CMSIS peripheral pointers legitimately) are separate translation units and
|
||||
* are unaffected. No-op on every non-STM32 platform.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if defined(CONFIG_SOC_FAMILY_STM32)
|
||||
|
||||
#include <soc.h>
|
||||
|
||||
/* Peripheral-instance macros whose names can collide with portable identifiers.
|
||||
* #undef of an undefined macro is a harmless no-op, so no per-symbol guards. */
|
||||
#undef RNG
|
||||
#undef AES
|
||||
#undef ADC
|
||||
#undef DAC
|
||||
#undef RTC
|
||||
#undef CRC
|
||||
#undef PWR
|
||||
#undef PKA
|
||||
#undef TAMP
|
||||
#undef COMP
|
||||
|
||||
#endif /* CONFIG_SOC_FAMILY_STM32 */
|
||||
@@ -846,10 +846,11 @@ index 30243ba5dc7..69eefee6e0f 100644
|
||||
data->tx_async_signal = async;
|
||||
k_msgq_purge(&data->tx_msgq);
|
||||
|
||||
@@ -777,6 +1333,29 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
@@ -777,6 +1333,31 @@ static int sx126x_lora_send_async(const struct device *dev,
|
||||
/* Enable antenna and set TX path */
|
||||
sx126x_set_rf_path(dev, true, true);
|
||||
|
||||
+#ifdef CONFIG_LORA_SX126X_NATIVE_STANDALONE /* STM32WL HAL clamps in its own configure_tx_params; its config has no is_sx1261 */
|
||||
+ /* Sec 15.2 TX Clamp workaround (datasheet errata) -- SX1262 only.
|
||||
+ * Set bits [4:1] of register 0x08D8 before SetTx to prevent
|
||||
+ * PA overshoot that can damage the device. */
|
||||
@@ -872,6 +873,7 @@ index 30243ba5dc7..69eefee6e0f 100644
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+#endif /* CONFIG_LORA_SX126X_NATIVE_STANDALONE */
|
||||
+
|
||||
/* Start transmission with 10 second timeout */
|
||||
ret = sx126x_set_tx(dev, 10000);
|
||||
|
||||
Reference in new issue
Block a user