ble race fixes

This commit is contained in:
liquidraver
2026-06-08 09:57:58 +02:00
parent 81a4365b0f
commit 7e00a6682d
5 changed files with 169 additions and 59 deletions
+11 -9
View File
@@ -67,8 +67,12 @@ static uint8_t usb_tx_ring_buf_data[USB_TX_RING_BUF_SIZE];
static struct ring_buf usb_tx_ring_buf;
static struct k_spinlock usb_tx_lock;
/* Work item for deferred V3 frame processing (set by init) */
static struct k_work *s_rx_work;
/* Main-thread wake for assembled binary frames (set by init). The byte
* assembly below runs on sysworkq, but V3-protocol parsing must happen on the
* main thread (handleProtocolFrame mutates mesh state shared with loop()), so
* we post this event instead of running the parser here. */
static struct k_event *s_mesh_events;
static uint32_t s_mesh_event_ble_rx;
/* Session start/end callbacks (mirror BLE on_connected / on_disconnected),
* set by main. start fires on first-frame claim, end on DTR drop. */
@@ -311,10 +315,10 @@ static void usb_rx_work_fn(struct k_work *work)
} f;
f.len = payload_len;
memcpy(f.buf, payload, payload_len);
/* sysworkq handles V3-protocol parsing; main only wakes
* if downstream LoRa work gets enqueued (via TX_DRAIN). */
/* Queue the frame and wake the main thread to parse it
* (parsing on sysworkq would race loop()). */
if (k_msgq_put(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) == 0) {
k_work_submit(s_rx_work);
k_event_post(s_mesh_events, s_mesh_event_ble_rx);
}
}
@@ -479,15 +483,13 @@ void zephcore_usb_companion_write_text(const char *text, size_t len)
}
void zephcore_usb_companion_init(struct k_event *mesh_events,
struct k_work *rx_work,
uint32_t mesh_event_ble_rx,
void *board)
{
ARG_UNUSED(board);
ARG_UNUSED(mesh_events);
ARG_UNUSED(mesh_event_ble_rx);
s_rx_work = rx_work;
s_mesh_events = mesh_events;
s_mesh_event_ble_rx = mesh_event_ble_rx;
/* The cdc_acm_uart DT node may be present without the class driver compiled
* (shared esp32s3_usb_otg.dtsi exposes the node unconditionally; the class is
+2 -3
View File
@@ -21,12 +21,11 @@ extern "C" {
* Call after device_is_ready() for the CDC ACM device.
*
* @param mesh_events Pointer to the k_event used by the mesh event loop
* @param rx_work Pointer to the rx_process work item (for BLE RX event)
* @param mesh_event_ble_rx Bitmask for MESH_EVENT_BLE_RX
* @param mesh_event_ble_rx Bitmask for MESH_EVENT_BLE_RX (posted to wake the
* main thread when a complete frame is assembled)
* @param board Opaque pointer to ZephyrBoard (for DFU reboot)
*/
void zephcore_usb_companion_init(struct k_event *mesh_events,
struct k_work *rx_work,
uint32_t mesh_event_ble_rx,
void *board);
+62 -21
View File
@@ -133,9 +133,12 @@ static void request_rtc_save(uint32_t epoch)
}
/* Work items for event-driven processing */
static void rx_process_work_fn(struct k_work *work);
static void process_companion_rx(void); /* runs on MAIN thread (see ble_on_rx_frame) */
static void contact_iter_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
#if ZEPHCORE_USB_STACK
static void companion_cli_run(const char *line); /* main-thread text-CLI exec */
#endif
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK)
static JoystickUITask joystick_ui_task;
@@ -151,9 +154,18 @@ static void joystick_signal_tx(void)
}
#endif
K_WORK_DEFINE(rx_process_work, rx_process_work_fn);
K_WORK_DEFINE(contact_iter_work, contact_iter_work_fn);
#if ZEPHCORE_USB_STACK
/* Completed USB text-CLI lines are run on the MAIN thread (the USB adapter
* assembles them on sysworkq). CommonCLI::handleCommand mutates mesh state
* shared with loop(), so it can't run on sysworkq — same hazard as the
* binary protocol path. Drained on MESH_EVENT_BLE_RX. */
#define CLI_LINE_BUF_SIZE 256
struct companion_cli_line { char buf[CLI_LINE_BUF_SIZE]; };
K_MSGQ_DEFINE(companion_cli_queue, sizeof(struct companion_cli_line), 4, 4);
#endif
#if ZEPHCORE_USB_STACK
/* USB TX-drained callback (mirrors BLE on_tx_idle): the TX ring emptied, so
* resume the contact pump to queue the next batch. Runs in the CDC TX
@@ -197,13 +209,12 @@ static void ble_on_rx_frame(const uint8_t *data, uint16_t len)
return;
}
/* Hand the frame to sysworkq for V3-protocol parsing. Main thread
* doesn't need a direct wake here: if handleProtocolFrame ends up
* enqueueing an outbound LoRa packet, notifyTxQueued() schedules
* tx_drain_work which posts MESH_EVENT_TX_DRAIN — that's the only
* signal main needs to drive the dispatcher. BLE-only commands
* (login, time, app-start, …) are handled entirely on sysworkq. */
k_work_submit(&rx_process_work);
/* Wake the MAIN thread to parse the frame. handleProtocolFrame()
* mutates the lock-free packet pool / dispatcher that loop() also
* touches, so it MUST run on the main thread — parsing on sysworkq
* races the main loop (the source of the stuck-"Sending…" bug when an
* inbound reply is processed while a send command is parsed). */
k_event_post(&mesh_events, MESH_EVENT_BLE_RX);
}
/* BLE TX idle callback — called when TX queue is empty.
@@ -340,10 +351,12 @@ static void push_callback(uint8_t code, const uint8_t *data, size_t len)
}
/* RX processing work - handles received BLE/USB frames */
static void rx_process_work_fn(struct k_work *work)
/* Parse inbound BLE/USB binary frames on the MAIN thread. Called from the
* event loop on MESH_EVENT_BLE_RX (formerly a sysworkq work item — moved to
* the main thread so handleProtocolFrame's mesh-state mutation can't race
* loop()). */
static void process_companion_rx(void)
{
ARG_UNUSED(work);
struct {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
@@ -458,6 +471,19 @@ static void mesh_event_loop(void)
gps_process_event();
}
/* Parse inbound BLE/USB frames + USB text-CLI lines HERE (main
* thread) before loop() drains any outbound they enqueued — keeps
* all mesh-state mutation on one thread (see ble_on_rx_frame). */
if (events & MESH_EVENT_BLE_RX) {
process_companion_rx();
#if ZEPHCORE_USB_STACK
struct companion_cli_line c;
while (k_msgq_get(&companion_cli_queue, &c, K_NO_WAIT) == 0) {
companion_cli_run(c.buf);
}
#endif
}
/* Packet processing — only on radio/BLE/TX events */
if (companion_mesh_ptr &&
(events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE |
@@ -733,7 +759,10 @@ static bool handle_autoshutdown_cli(const char *line, char *reply)
}
#endif
static void companion_cli_dispatch(const char *line)
/* Executes a text-CLI line — runs on the MAIN thread (drained from
* companion_cli_queue in the event loop). CommonCLI::handleCommand touches
* mesh state shared with loop(), so it must not run on sysworkq. */
static void companion_cli_run(const char *line)
{
char reply[CLI_REPLY_SIZE];
reply[0] = '\0';
@@ -754,6 +783,20 @@ static void companion_cli_dispatch(const char *line)
zephcore_usb_companion_write_text("\r\n", 2);
}
/* USB-adapter callback (runs on sysworkq). Just queue the line and wake the
* main thread — the actual handleCommand happens in companion_cli_run(). */
static void companion_cli_dispatch(const char *line)
{
struct companion_cli_line c;
strncpy(c.buf, line, sizeof(c.buf) - 1);
c.buf[sizeof(c.buf) - 1] = '\0';
if (k_msgq_put(&companion_cli_queue, &c, K_NO_WAIT) == 0) {
k_event_post(&mesh_events, MESH_EVENT_BLE_RX);
} else {
zephcore_usb_companion_write_text("\r\n -> busy\r\n", 13);
}
}
#endif /* ZEPHCORE_USB_STACK */
#endif
@@ -1093,7 +1136,7 @@ int main(void)
* via CONFIG_ZEPHCORE_COMPANION_USB (e.g. prod builds on USB-capable boards).
*/
#if ZEPHCORE_USB_STACK
zephcore_usb_companion_init(&mesh_events, &rx_process_work, MESH_EVENT_BLE_RX,
zephcore_usb_companion_init(&mesh_events, MESH_EVENT_BLE_RX,
&zephyr_board);
/* Mirror BLE connect/disconnect UI + cleanup for USB sessions. */
zephcore_usb_companion_set_session_start_cb(usb_on_session_start);
@@ -1123,16 +1166,14 @@ int main(void)
* - MESH_EVENT_LORA_RX: LoRa packet received (from RX async callback)
* - MESH_EVENT_LORA_TX_DONE: LoRa TX complete (from TX poll work -> callback)
* - MESH_EVENT_HOUSEKEEPING: Periodic maintenance (noise floor, etc.)
* - MESH_EVENT_TX_DRAIN: outbound LoRa packet queued (covers BLE/USB-driven
* mesh.send paths — sysworkq parses incoming frames and posts this
* when it actually enqueues something)
* - MESH_EVENT_BLE_RX: BLE/USB frame (or USB text-CLI line) ready to parse.
* The BLE callback / USB adapter only assemble + queue the frame off-main;
* process_companion_rx() (and companion_cli_run()) parse it HERE on the
* main thread so mesh-state mutation never races loop().
* - MESH_EVENT_TX_DRAIN: outbound LoRa packet queued — wakes loop() to drain
* - MESH_EVENT_UI_ACTION / GPS_ACTION / PREFS_DIRTY: deferred work
* from non-main threads
*
* BLE/USB RX frames are parsed entirely on sysworkq (rx_process_work) —
* no direct main-thread wake is needed; if the parsed command produces
* outbound LoRa traffic, notifyTxQueued() routes it via TX_DRAIN.
*
* Other processing still uses work queues:
* - BLE TX: write_frame -> tx_drain_work (event-driven via notify callback)
* - Contact iteration: contact_iter_work (on TX queue space)
+47 -13
View File
@@ -117,6 +117,14 @@ static char cli_line_buf[CLI_LINE_BUF_SIZE];
static char cli_reply_buf[256];
static uint16_t cli_line_idx;
/* Completed CLI lines are handed to the MAIN thread for execution. Byte
* assembly + echo (cli_rx_work_fn) runs on sysworkq and touches no mesh
* state, but handleCommand() mutates the lock-free packet pool / dispatcher
* that loop() also touches — running it on sysworkq races the main loop.
* So we queue the finished line and let the event loop run handleCommand(). */
struct cli_cmd_line { char buf[CLI_LINE_BUF_SIZE]; };
K_MSGQ_DEFINE(cli_cmd_queue, sizeof(struct cli_cmd_line), 4, 4);
/* Work items for event-driven processing */
static void cli_rx_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
@@ -178,22 +186,22 @@ static void cli_rx_work_fn(struct k_work *work)
LOG_INF("CLI cmd len=%d: %.40s%s", cli_line_idx,
cli_line_buf, cli_line_idx > 40 ? "..." : "");
/* Process CLI command */
#ifdef ZEPHCORE_LORA
if (repeater_mesh_ptr) {
cli_reply_buf[0] = '\0';
repeater_mesh_ptr->handleCommand(0, cli_line_buf, cli_reply_buf);
if (cli_reply_buf[0] != '\0') {
/* Arduino format: newline, then " -> reply" */
cli_print("\r\n -> ");
cli_print(cli_reply_buf);
}
/* Hand the command to the main thread (see cli_cmd_queue).
* The reply + trailing newline are emitted there, exactly
* matching the previous inline output order. */
struct cli_cmd_line c;
strncpy(c.buf, cli_line_buf, sizeof(c.buf) - 1);
c.buf[sizeof(c.buf) - 1] = '\0';
if (k_msgq_put(&cli_cmd_queue, &c, K_NO_WAIT) == 0) {
k_event_post(&mesh_events, MESH_EVENT_CLI_RX);
} else {
cli_print("\r\n -> busy\r\n");
}
#endif
cli_line_idx = 0;
} else {
/* Empty line — just emit the newline (no command to run) */
cli_print("\r\n");
}
/* New line for next command */
cli_print("\r\n");
} else if (byte == 0x7F || byte == 0x08) {
/* Backspace - echo backspace sequence */
if (cli_line_idx > 0) {
@@ -214,6 +222,25 @@ static void cli_rx_work_fn(struct k_work *work)
}
}
#ifdef ZEPHCORE_LORA
/* Run queued CLI commands on the MAIN thread (see cli_cmd_queue). Output
* order matches the old inline path exactly: "\r\n -> reply" when there is
* a reply, then a trailing "\r\n". */
static void process_cli_commands(void)
{
struct cli_cmd_line c;
while (repeater_mesh_ptr && k_msgq_get(&cli_cmd_queue, &c, K_NO_WAIT) == 0) {
cli_reply_buf[0] = '\0';
repeater_mesh_ptr->handleCommand(0, c.buf, cli_reply_buf);
if (cli_reply_buf[0] != '\0') {
cli_print("\r\n -> ");
cli_print(cli_reply_buf);
}
cli_print("\r\n");
}
}
#endif
/* Housekeeping timer callback - signals event to wake mesh loop periodically */
static void housekeeping_timer_fn(struct k_timer *timer)
{
@@ -358,6 +385,13 @@ static void repeater_event_loop(void)
}
#ifdef ZEPHCORE_LORA
/* Run queued CLI commands here (main thread) BEFORE loop() drains
* any outbound packets they enqueued — keeps all mesh-state
* mutation on the main thread (see cli_cmd_queue). */
if (events & MESH_EVENT_CLI_RX) {
process_cli_commands();
}
/* Packet processing — only on radio/CLI/TX events */
if (repeater_mesh_ptr &&
(events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE |
+47 -13
View File
@@ -118,6 +118,14 @@ static char cli_line_buf[CLI_LINE_BUF_SIZE];
static char cli_reply_buf[256];
static uint16_t cli_line_idx;
/* Completed CLI lines are handed to the MAIN thread for execution. Byte
* assembly + echo (cli_rx_work_fn) runs on sysworkq and touches no mesh
* state, but handleCommand() mutates the lock-free packet pool / dispatcher
* that loop() also touches — running it on sysworkq races the main loop.
* So we queue the finished line and let the event loop run handleCommand(). */
struct cli_cmd_line { char buf[CLI_LINE_BUF_SIZE]; };
K_MSGQ_DEFINE(cli_cmd_queue, sizeof(struct cli_cmd_line), 4, 4);
/* Work items for event-driven processing */
static void cli_rx_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
@@ -191,22 +199,22 @@ static void cli_rx_work_fn(struct k_work *work)
LOG_INF("CLI cmd len=%d: %.40s%s", cli_line_idx,
cli_line_buf, cli_line_idx > 40 ? "..." : "");
/* Process CLI command */
#ifdef ZEPHCORE_LORA
if (room_mesh_ptr) {
cli_reply_buf[0] = '\0';
room_mesh_ptr->handleCommand(0, cli_line_buf, cli_reply_buf);
if (cli_reply_buf[0] != '\0') {
/* Arduino format: newline, then " -> reply" */
cli_print("\r\n -> ");
cli_print(cli_reply_buf);
}
/* Hand the command to the main thread (see cli_cmd_queue).
* The reply + trailing newline are emitted there, exactly
* matching the previous inline output order. */
struct cli_cmd_line c;
strncpy(c.buf, cli_line_buf, sizeof(c.buf) - 1);
c.buf[sizeof(c.buf) - 1] = '\0';
if (k_msgq_put(&cli_cmd_queue, &c, K_NO_WAIT) == 0) {
k_event_post(&mesh_events, MESH_EVENT_CLI_RX);
} else {
cli_print("\r\n -> busy\r\n");
}
#endif
cli_line_idx = 0;
} else {
/* Empty line — just emit the newline (no command to run) */
cli_print("\r\n");
}
/* New line for next command */
cli_print("\r\n");
} else if (byte == 0x7F || byte == 0x08) {
/* Backspace - echo backspace sequence */
if (cli_line_idx > 0) {
@@ -227,6 +235,25 @@ static void cli_rx_work_fn(struct k_work *work)
}
}
#ifdef ZEPHCORE_LORA
/* Run queued CLI commands on the MAIN thread (see cli_cmd_queue). Output
* order matches the old inline path exactly: "\r\n -> reply" when there is
* a reply, then a trailing "\r\n". */
static void process_cli_commands(void)
{
struct cli_cmd_line c;
while (room_mesh_ptr && k_msgq_get(&cli_cmd_queue, &c, K_NO_WAIT) == 0) {
cli_reply_buf[0] = '\0';
room_mesh_ptr->handleCommand(0, c.buf, cli_reply_buf);
if (cli_reply_buf[0] != '\0') {
cli_print("\r\n -> ");
cli_print(cli_reply_buf);
}
cli_print("\r\n");
}
}
#endif
/* Housekeeping timer callback - signals event to wake mesh loop periodically */
static void housekeeping_timer_fn(struct k_timer *timer)
{
@@ -375,6 +402,13 @@ static void room_event_loop(void)
}
#ifdef ZEPHCORE_LORA
/* Run queued CLI commands here (main thread) BEFORE loop() drains
* any outbound packets they enqueued — keeps all mesh-state
* mutation on the main thread (see cli_cmd_queue). */
if (events & MESH_EVENT_CLI_RX) {
process_cli_commands();
}
/* Packet processing — only on radio/CLI/TX events */
if (room_mesh_ptr &&
(events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE |