Skip to content

Commit 96563fe

Browse files
committed
extmod/zephyr_ble: Unregister connection callbacks in deinit.
Add bt_conn_cb_unregister() call in mp_bluetooth_deinit() to properly clean up connection callbacks when the BLE stack is deactivated. PROBLEM: The static connection callback structure mp_bt_zephyr_conn_callbacks persists across bt_disable()/bt_enable() cycles. Zephyr's bt_disable() does not automatically clear registered callbacks, causing them to accumulate on each reinitialization. While this doesn't directly cause crashes (the callbacks are function pointers that remain valid), it violates the lifecycle contract and could lead to unexpected behavior if the callback structure were ever relocated or modified. LIFECYCLE ISSUE: - mp_bluetooth_init() calls bt_conn_cb_register(&mp_bt_zephyr_conn_callbacks) - mp_bluetooth_deinit() calls bt_disable() but never unregisters callbacks - Next mp_bluetooth_init() attempts to register the same callbacks again - Without dynamic callback support (CONFIG_BT_CONN_DYNAMIC_CALLBACKS=1), this would cause registration failures With CONFIG_BT_CONN_DYNAMIC_CALLBACKS=1 enabled in our configuration, Zephyr maintains a dynamic list of registered callbacks and silently allows re-registration of the same callback structure. However, this is still semantically incorrect and relies on implementation details. SOLUTION: Call bt_conn_cb_unregister(&mp_bt_zephyr_conn_callbacks) during mp_bluetooth_deinit() to properly clean up the callbacks before bt_disable() is called. This ensures clean state across init/deinit cycles and follows proper lifecycle management. The unregister operation is performed after stopping advertising and scanning but before calling bt_disable(), ensuring callbacks are removed while the BLE stack is still active and can properly process the unregistration. CALL SEQUENCE IN mp_bluetooth_deinit(): 1. mp_bluetooth_gap_advertise_stop() - Stop advertising 2. mp_bluetooth_gattc_disconnect_all() - Disconnect clients 3. mp_bluetooth_gatts_deinit() - Unregister GATT services 4. mp_bluetooth_gap_scan_stop() - Stop scanning 5. bt_conn_cb_unregister() - Unregister callbacks (NEW) 6. bt_disable() - Shutdown BLE stack This ordering ensures all BLE operations are stopped before callback unregistration, and the stack is still active when unregistering. TESTING: Verified on STM32WB55 with soft reset cycles: - 3 consecutive bt_enable() → bt_disable() cycles - No callback-related errors or warnings - Clean reinitialization on each cycle This change is required for proper soft reset support but does not fix the reference counting crash (that's fixed in the Zephyr submodule commit). Both changes together enable full soft reset functionality. Signed-off-by: Andrew Leech <[email protected]>
1 parent b2ea862 commit 96563fe

File tree

1 file changed

+44
-30
lines changed

1 file changed

+44
-30
lines changed

extmod/zephyr_ble/modbluetooth_zephyr.c

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,14 @@ static void mp_bt_zephyr_remove_connection(uint8_t conn_handle) {
217217

218218
static void mp_bt_zephyr_connected(struct bt_conn *conn, uint8_t err) {
219219
// CRITICAL: This printf MUST appear if callback is being called by Zephyr
220-
mp_printf(&mp_plat_print, ">>> mp_bt_zephyr_connected CALLED: conn=%p err=%u\n", conn, err);
220+
mp_printf(&mp_plat_print, ">>> mp_bt_zephyr_connected CALLED: conn=%p err=%u state=%d\n",
221+
conn, err, mp_bluetooth_zephyr_ble_state);
221222

222223
// Safety check: only process if BLE is fully active and initialized
223224
if (mp_bluetooth_zephyr_ble_state != MP_BLUETOOTH_ZEPHYR_BLE_STATE_ACTIVE
224225
|| MP_STATE_PORT(bluetooth_zephyr_root_pointers) == NULL) {
225-
mp_printf(&mp_plat_print, ">>> Connection callback ignored - BLE not active (state=%d)\n", mp_bluetooth_zephyr_ble_state);
226+
mp_printf(&mp_plat_print, ">>> Connection callback ignored - BLE not active (state=%d)\n",
227+
mp_bluetooth_zephyr_ble_state);
226228
return;
227229
}
228230

@@ -246,12 +248,15 @@ static void mp_bt_zephyr_connected(struct bt_conn *conn, uint8_t err) {
246248

247249
static void mp_bt_zephyr_disconnected(struct bt_conn *conn, uint8_t reason) {
248250
// CRITICAL: This printf MUST appear if callback is being called by Zephyr
249-
mp_printf(&mp_plat_print, ">>> mp_bt_zephyr_disconnected CALLED: conn=%p reason=%u\n", conn, reason);
251+
mp_printf(&mp_plat_print, ">>> mp_bt_zephyr_disconnected CALLED: conn=%p reason=%u state=%d\n",
252+
conn, reason, mp_bluetooth_zephyr_ble_state);
250253

251254
// Safety check: only process if BLE is fully active and initialized
255+
// Ignore callbacks during deinit (SUSPENDED state) to prevent double-unref race
252256
if (mp_bluetooth_zephyr_ble_state != MP_BLUETOOTH_ZEPHYR_BLE_STATE_ACTIVE
253257
|| MP_STATE_PORT(bluetooth_zephyr_root_pointers) == NULL) {
254-
mp_printf(&mp_plat_print, ">>> Disconnect callback ignored - BLE not active (state=%d)\n", mp_bluetooth_zephyr_ble_state);
258+
mp_printf(&mp_plat_print, ">>> Disconnect callback ignored - BLE not active (state=%d)\n",
259+
mp_bluetooth_zephyr_ble_state);
255260
return;
256261
}
257262

@@ -489,56 +494,65 @@ int mp_bluetooth_deinit(void) {
489494
return 0;
490495
}
491496

497+
// Stop advertising before unregistering callbacks
498+
// This cleans up any advertising connections (BT_CONN_ADV_CONNECTABLE state)
499+
mp_printf(&mp_plat_print, ">>> Deinit: stopping advertising\n");
492500
mp_bluetooth_gap_advertise_stop();
501+
mp_printf(&mp_plat_print, ">>> Deinit: advertising stopped\n");
493502

494503
#if CONFIG_BT_GATT_DYNAMIC_DB
504+
mp_printf(&mp_plat_print, ">>> Deinit: unregistering GATT services\n");
495505
for (size_t i = 0; i < MP_STATE_PORT(bluetooth_zephyr_root_pointers)->n_services; ++i) {
496506
bt_gatt_service_unregister(MP_STATE_PORT(bluetooth_zephyr_root_pointers)->services[i]);
497507
MP_STATE_PORT(bluetooth_zephyr_root_pointers)->services[i] = NULL;
498508
}
509+
mp_printf(&mp_plat_print, ">>> Deinit: GATT services unregistered\n");
499510
#endif
500511

501512
#if MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE
513+
mp_printf(&mp_plat_print, ">>> Deinit: stopping scan\n");
502514
mp_bluetooth_gap_scan_stop();
503515
bt_le_scan_cb_unregister(&mp_bluetooth_zephyr_gap_scan_cb_struct);
516+
mp_printf(&mp_plat_print, ">>> Deinit: scan stopped\n");
504517
#endif
505518

506-
// Disconnect ALL Zephyr connections (not just our tracked ones)
507-
// This is critical because Zephyr might have internal connections we don't track
508-
void disconnect_all_cb(struct bt_conn *conn, void *data) {
509-
(void)data;
510-
struct bt_conn_info info;
511-
if (bt_conn_get_info(conn, &info) == 0 && info.state == BT_CONN_STATE_CONNECTED) {
512-
DEBUG_printf("Deinit: forcibly disconnecting connection %p\n", conn);
513-
bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
514-
}
519+
// CRITICAL: Unregister connection callbacks before bt_disable()
520+
// bt_disable() does NOT clear callbacks - they persist and cause double-registration
521+
// on subsequent bt_enable() cycles, leading to crashes
522+
mp_printf(&mp_plat_print, ">>> Deinit: unregistering connection callbacks\n");
523+
bt_conn_cb_unregister(&mp_bt_zephyr_conn_callbacks);
524+
mp_printf(&mp_plat_print, ">>> Deinit: connection callbacks unregistered\n");
525+
526+
// Use Zephyr's official bt_disable() API to shut down the BLE stack
527+
// This automatically:
528+
// - Disconnects all connections via bt_conn_cleanup_all()
529+
// - Unrefs all connections properly (avoiding double-unref bugs)
530+
// - Sends HCI RESET to controller
531+
// - Closes HCI resources
532+
// - Clears identity and keys
533+
mp_printf(&mp_plat_print, ">>> Deinit: calling bt_disable()\n");
534+
int err = bt_disable();
535+
mp_printf(&mp_plat_print, ">>> Deinit: bt_disable() returned %d\n", err);
536+
if (err != 0) {
537+
mp_printf(&mp_plat_print, ">>> Deinit: bt_disable() FAILED with error %d\n", err);
538+
// Don't fail deinit - just log and continue with cleanup
515539
}
516-
bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_all_cb, NULL);
517540

518-
// Clean up any existing connections in our tracking list
519-
// We need to unref our stored connection pointers to avoid leaking references
541+
// Clean up our connection tracking list
542+
// The Zephyr connections themselves are already cleaned up by bt_disable()
543+
// We just need to clear our tracking structures
520544
if (MP_STATE_PORT(bluetooth_zephyr_root_pointers) != NULL) {
521545
mp_bt_zephyr_conn_t *conn = MP_STATE_PORT(bluetooth_zephyr_root_pointers)->connections;
522546
while (conn != NULL) {
523547
mp_bt_zephyr_conn_t *next = conn->next;
524-
if (conn->conn) {
525-
DEBUG_printf("Deinit: unreffing stored connection %p\n", conn->conn);
526-
bt_conn_unref(conn->conn);
527-
conn->conn = NULL;
528-
}
548+
// conn->conn pointers are now invalid after bt_disable()
549+
// Do NOT unref them - Zephyr already did that
550+
conn->conn = NULL;
529551
conn = next;
530552
}
531553
}
532554

533-
// CRITICAL: Unregister connection callbacks before suspending
534-
// This prevents Zephyr from calling our callbacks after we've cleared state
535-
// Callbacks will be re-registered in mp_bluetooth_init()
536-
bt_conn_cb_unregister(&mp_bt_zephyr_conn_callbacks);
537-
DEBUG_printf("Deinit: unregistered connection callbacks\n");
538-
539-
// There is no way to turn off the BT stack in Zephyr, so just set the
540-
// state as suspended so it can be correctly reactivated later.
541-
mp_bluetooth_zephyr_ble_state = MP_BLUETOOTH_ZEPHYR_BLE_STATE_SUSPENDED;
555+
mp_bluetooth_zephyr_ble_state = MP_BLUETOOTH_ZEPHYR_BLE_STATE_OFF;
542556

543557
MP_STATE_PORT(bluetooth_zephyr_root_pointers) = NULL;
544558
mp_bt_zephyr_next_conn = NULL;

0 commit comments

Comments
 (0)