diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index b8e25ec..c02d679 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -89,15 +89,26 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line) #define MESH_EVENT_GPS_ACTION BIT(5) /* GPS state change (must run on main thread!) */ #define MESH_EVENT_TX_DRAIN BIT(6) /* Outbound packet delay expired, run checkSend */ #define MESH_EVENT_PREFS_DIRTY BIT(8) /* Prefs mutated off-main; main flushes to flash */ +#define MESH_EVENT_RTC_SAVE BIT(9) /* Hardware-RTC write requested off-main */ #ifdef ZEPHCORE_LORA /* Forward decl — data_store + companion_mesh_ptr statics are defined further * down in the file, so mesh_event_loop() can't reference them directly. */ static void save_prefs_to_flash(void); #endif + +/* Pending epoch for a deferred zephcore_rtc_save(). gps_fix_callback runs on + * the GNSS modem_chat worker thread, where the RTC's blocking I2C transactions + * (burst read/write, several ms each) would stall NMEA ingest — same hazard + * documented for prefs flash writes below. We stash the latest epoch and let + * the main thread perform the actual I2C write via MESH_EVENT_RTC_SAVE; + * concurrent posts coalesce into one save of the latest time. */ +static atomic_t pending_rtc_epoch = ATOMIC_INIT(0); + #define MESH_EVENT_BASE (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | \ MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | \ - MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_PREFS_DIRTY) + MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_PREFS_DIRTY | \ + MESH_EVENT_RTC_SAVE) #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) #define MESH_EVENT_JOYSTICK_LOOP BIT(7) /* Joystick UI loop tick (50 ms) */ #define MESH_EVENT_ALL (MESH_EVENT_BASE | MESH_EVENT_JOYSTICK_LOOP) @@ -111,6 +122,16 @@ static void save_prefs_to_flash(void); /* Event-driven mesh loop - k_event for signaling from ISR/callbacks */ static struct k_event mesh_events; +/* Defer a hardware-RTC write to the main thread (see pending_rtc_epoch above + * for why gps_fix_callback can't do this inline). Coalesces like prefs-dirty: + * only the latest epoch survives if multiple fixes land before the main loop + * services the event. */ +static void request_rtc_save(uint32_t epoch) +{ + atomic_set(&pending_rtc_epoch, (atomic_val_t)epoch); + k_event_post(&mesh_events, MESH_EVENT_RTC_SAVE); +} + /* Work items for event-driven processing */ static void rx_process_work_fn(struct k_work *work); static void contact_iter_work_fn(struct k_work *work); @@ -480,6 +501,13 @@ static void mesh_event_loop(void) } #endif + /* Off-main RTC write request (gps_fix_callback runs in modem_chat + * context — see request_rtc_save()). Perform the blocking I2C + * write here on the main thread instead. */ + if (events & MESH_EVENT_RTC_SAVE) { + zephcore_rtc_save((uint32_t)atomic_get(&pending_rtc_epoch)); + } + #if IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK) if (events & MESH_EVENT_JOYSTICK_LOOP) { joystick_ui_task.loop(); @@ -755,7 +783,10 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); time_sync_report(TIME_SYNC_GPS); - zephcore_rtc_save((uint32_t)utc_time); /* persist to hardware RTC */ + /* Defer the hardware-RTC write to the main thread — see + * request_rtc_save()/pending_rtc_epoch: blocking I2C here would + * stall NMEA ingest, same hazard as the prefs flash write below. */ + request_rtc_save((uint32_t)utc_time); } #ifdef ZEPHCORE_LORA diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 233db9c..c6331f2 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -88,7 +88,8 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); #define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */ #define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change (must run on main thread!) */ #define MESH_EVENT_TX_DRAIN BIT(5) /* Outbound packet delay expired, run checkSend */ -#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN) +#define MESH_EVENT_RTC_SAVE BIT(6) /* Hardware-RTC write requested off-main */ +#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_RTC_SAVE) /* Housekeeping interval - infrequent to preserve power savings */ #define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS @@ -96,6 +97,18 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); /* Event object for mesh loop */ static struct k_event mesh_events; +/* Pending epoch for a deferred zephcore_rtc_save() — gps_fix_callback runs on + * the GNSS modem_chat worker thread, where the RTC's blocking I2C transactions + * would stall NMEA ingest. Stash the latest epoch and let the main thread + * perform the write; concurrent posts coalesce into one save. */ +static atomic_t pending_rtc_epoch = ATOMIC_INIT(0); + +static void request_rtc_save(uint32_t epoch) +{ + atomic_set(&pending_rtc_epoch, (atomic_val_t)epoch); + k_event_post(&mesh_events, MESH_EVENT_RTC_SAVE); +} + /* USB CDC state */ static const struct device *usb_dev; static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE]; @@ -272,7 +285,10 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); - zephcore_rtc_save((uint32_t)utc_time); /* persist to hardware RTC */ + /* Defer the hardware-RTC write to the main thread — blocking I2C + * here would stall NMEA ingest (gps_fix_callback runs in the + * GNSS modem_chat worker context). */ + request_rtc_save((uint32_t)utc_time); } int lat_deg = (int)lat; @@ -385,6 +401,13 @@ static void repeater_event_loop(void) * a 30 s freshness guard — no periodic ADC fire here. */ #endif } + + /* Off-main RTC write request (gps_fix_callback runs in modem_chat + * context — see request_rtc_save()). Perform the blocking I2C + * write here on the main thread instead. */ + if (events & MESH_EVENT_RTC_SAVE) { + zephcore_rtc_save((uint32_t)atomic_get(&pending_rtc_epoch)); + } } } diff --git a/zephcore/src/main_room_server.cpp b/zephcore/src/main_room_server.cpp index 17d9cb1..feebfc4 100644 --- a/zephcore/src/main_room_server.cpp +++ b/zephcore/src/main_room_server.cpp @@ -89,7 +89,8 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); #define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change (must run on main thread!) */ #define MESH_EVENT_TX_DRAIN BIT(5) /* Outbound packet delay expired, run checkSend */ #define MESH_EVENT_PUSH_TICK BIT(6) /* Room server: drive the post-sync push engine */ -#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_PUSH_TICK) +#define MESH_EVENT_RTC_SAVE BIT(7) /* Hardware-RTC write requested off-main */ +#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_PUSH_TICK | MESH_EVENT_RTC_SAVE) /* Housekeeping interval - infrequent to preserve power savings */ #define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS @@ -97,6 +98,18 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios); /* Event object for mesh loop */ static struct k_event mesh_events; +/* Pending epoch for a deferred zephcore_rtc_save() — gps_fix_callback runs on + * the GNSS modem_chat worker thread, where the RTC's blocking I2C transactions + * would stall NMEA ingest. Stash the latest epoch and let the main thread + * perform the write; concurrent posts coalesce into one save. */ +static atomic_t pending_rtc_epoch = ATOMIC_INIT(0); + +static void request_rtc_save(uint32_t epoch) +{ + atomic_set(&pending_rtc_epoch, (atomic_val_t)epoch); + k_event_post(&mesh_events, MESH_EVENT_RTC_SAVE); +} + /* USB CDC state */ static const struct device *usb_dev; static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE]; @@ -285,7 +298,10 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) if (utc_time > 0) { LOG_INF("GPS fix: RTC sync time=%lld", utc_time); rtc_clock.setCurrentTime((uint32_t)utc_time); - zephcore_rtc_save((uint32_t)utc_time); /* persist to hardware RTC */ + /* Defer the hardware-RTC write to the main thread — blocking I2C + * here would stall NMEA ingest (gps_fix_callback runs in the + * GNSS modem_chat worker context). */ + request_rtc_save((uint32_t)utc_time); } int lat_deg = (int)lat; @@ -403,6 +419,13 @@ static void room_event_loop(void) * a 30 s freshness guard — no periodic ADC fire here. */ #endif } + + /* Off-main RTC write request (gps_fix_callback runs in modem_chat + * context — see request_rtc_save()). Perform the blocking I2C + * write here on the main thread instead. */ + if (events & MESH_EVENT_RTC_SAVE) { + zephcore_rtc_save((uint32_t)atomic_get(&pending_rtc_epoch)); + } } }