From 5be57b84f44a11bae05a25b153f3461725533c3b Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Mon, 24 Aug 2026 17:40:32 -0400 Subject: [PATCH 1/5] Keep the BLE workflow connection alive across VM restarts Typing at the "Press any key to enter the REPL" prompt over BLE workflow serial ended the session after exactly one character: the first byte breaks main.c's wait loop, which calls bleio_reset(), and the full stack disable/enable cycle there drops every connection, the workflow's included. Ctrl-D reloads dropped it the same way. bleio_user_reset() now tears down user BLE state individually on every VM reset: it stops user scanning and advertising (as before), and now also disconnects the connections that user code initiated or accepted with its own advertising. Connections are marked as user-owned when they are created: only user code connects in the central role, and a peripheral connection belongs to whoever started the advertising it answered. The BLE workflow connection is not user-owned and stays up. This does espressif's old "TODO: Don't stop BLE workflow connection." bleio_reset() still runs the full stack cycle, but only when user code created a GATT service, tracked by a flag set in the shared-bindings Service constructor. The cycle exists only to clear user services from the GATT table: the SoftDevice can do that no other way, and espressif for now matches it, though its NimBLE can delete individual services (a possible follow-up). When no user services exist, there is nothing to tear down and the workflow session survives. The supervisor constructs its workflow services through common-hal directly and does not set the flag. Since the full cycle no longer always runs, bleio_user_reset() also clears the adapter's and connections' pointers into the VM heap (connection objects, remote service lists, nordic's advertising data buffers), which the cycle used to clear implicitly. The adapter struct is a GC root, so a pointer left over from a dead heap would be scanned as a live object in the next VM. Also make the Service Changed indication work, so a bonded host discards its cached GATT table when a reload changed the services. nordic sent it only when the bonded system-attribute load succeeded, and the call never worked anyway: sd_ble_gatts_service_changed() rejects handles outside the application-populated attribute range, and it was called with (0xC, 0xFFFF), the error ignored. The populated range is now tracked as attributes are added and indicated on every bonded reconnect. espressif never signaled at all; it now calls ble_svc_gatt_changed() after the cycle rebuilds the table, which indicates connected peers immediately and bonded peers when they reconnect. Verified with btmon on both ports: the indication goes out with the real range, and the host confirms, rediscovers, and resubscribes. Applies to nordic and espressif, the ports that run the BLE workflow. Co-Authored-By: Claude Fable 5 --- main.c | 11 +++-- ports/espressif/common-hal/_bleio/Adapter.c | 12 +++-- .../espressif/common-hal/_bleio/Connection.h | 5 ++ ports/espressif/common-hal/_bleio/__init__.c | 49 +++++++++++++++++-- ports/nordic/common-hal/_bleio/Adapter.c | 11 +++++ ports/nordic/common-hal/_bleio/Adapter.h | 5 ++ .../nordic/common-hal/_bleio/Characteristic.c | 2 + ports/nordic/common-hal/_bleio/Connection.c | 22 ++++++--- ports/nordic/common-hal/_bleio/Connection.h | 5 ++ ports/nordic/common-hal/_bleio/Service.c | 4 ++ ports/nordic/common-hal/_bleio/__init__.c | 37 ++++++++++++++ ports/nordic/common-hal/_bleio/__init__.h | 8 +++ shared-bindings/_bleio/Service.c | 4 ++ shared-bindings/_bleio/__init__.c | 14 ++++++ shared-bindings/_bleio/__init__.h | 15 ++++++ 15 files changed, 186 insertions(+), 18 deletions(-) diff --git a/main.c b/main.c index 2020cb8c307..e2322524ab1 100644 --- a/main.c +++ b/main.c @@ -775,8 +775,10 @@ static bool __attribute__((noinline)) run_code_py(safe_mode_t safe_mode, bool *s // Done waiting, start the board back up. - // We delay resetting BLE until after the wait in case we're transferring - // more files over. + // Resetting BLE is delayed until here, after the wait, in case files were being + // transferred over the BLE workflow during the wait. The reset cycles the BLE + // stack, dropping the workflow connection, only if user code created GATT + // services; otherwise it is a no-op and the connection continues. #if CIRCUITPY_BLEIO bleio_reset(); #endif @@ -992,8 +994,9 @@ static int run_repl(safe_mode_t safe_mode) { #endif cleanup_after_vm(MP_OBJ_SENTINEL); - // Also reset bleio. The above call omits it in case workflows should continue. In this case, - // we're switching straight to another VM so we want to reset. + // Also reset bleio, which cleanup_after_vm() above omits so workflows can + // continue between VMs. As in run_code_py(), this cycles the BLE stack only if + // user code created GATT services. #if CIRCUITPY_BLEIO bleio_reset(); #endif diff --git a/ports/espressif/common-hal/_bleio/Adapter.c b/ports/espressif/common-hal/_bleio/Adapter.c index 61abdb219be..68be5b082c5 100644 --- a/ports/espressif/common-hal/_bleio/Adapter.c +++ b/ports/espressif/common-hal/_bleio/Adapter.c @@ -339,7 +339,7 @@ static int _mtu_reply(uint16_t conn_handle, return 0; } -static void _new_connection(uint16_t conn_handle) { +static void _new_connection(uint16_t conn_handle, bool user_owned) { // Set the tx_power for the connection higher than the advertisement. esp_ble_tx_power_set(conn_handle, ESP_PWR_LVL_N0); @@ -362,6 +362,7 @@ static void _new_connection(uint16_t conn_handle) { connection->connection_obj = mp_const_none; connection->pair_status = PAIR_NOT_PAIRED; connection->mtu = 0; + connection->user_owned = user_owned; ble_gattc_exchange_mtu(conn_handle, _mtu_reply, connection); @@ -379,7 +380,8 @@ static int _connect_event(struct ble_gap_event *event, void *self_in) { case BLE_GAP_EVENT_CONNECT: if (event->connect.status == 0) { // This triggers an MTU exchange. Its reply will exit the loop waiting for a connection. - _new_connection(event->connect.conn_handle); + // Only user code connects in the central role. + _new_connection(event->connect.conn_handle, true); // Set connections objs back to NULL since we have a new // connection and need a new tuple. self->connection_objs = NULL; @@ -498,7 +500,8 @@ static int _advertising_event(struct ble_gap_event *event, void *self_in) { #if !MYNEWT_VAL(BLE_EXT_ADV) if (event->connect.status == NIMBLE_OK) { - _new_connection(event->connect.conn_handle); + // The connection belongs to whoever started the advertising it answered. + _new_connection(event->connect.conn_handle, self->user_advertising); // Set connections objs back to NULL since we have a new // connection and need a new tuple. self->connection_objs = NULL; @@ -511,7 +514,8 @@ static int _advertising_event(struct ble_gap_event *event, void *self_in) { case BLE_GAP_EVENT_ADV_COMPLETE: #if MYNEWT_VAL(BLE_EXT_ADV) if (event->adv_complete.reason == NIMBLE_OK) { - _new_connection(event->adv_complete.conn_handle); + // The connection belongs to whoever started the advertising it answered. + _new_connection(event->adv_complete.conn_handle, self->user_advertising); // Set connections objs back to NULL since we have a new // connection and need a new tuple. self->connection_objs = NULL; diff --git a/ports/espressif/common-hal/_bleio/Connection.h b/ports/espressif/common-hal/_bleio/Connection.h index 5f33eb43b5d..176ed0ebfe4 100644 --- a/ports/espressif/common-hal/_bleio/Connection.h +++ b/ports/espressif/common-hal/_bleio/Connection.h @@ -31,6 +31,11 @@ typedef enum { typedef struct { uint16_t conn_handle; bool is_central; + // True if user code initiated or accepted this connection: it connected in the + // central role, or a central answered user code's advertising. User-owned + // connections are disconnected when the VM resets; the BLE workflow connection + // is not user-owned and stays up. + bool user_owned; // Remote services discovered when this peripheral is acting as a client. mp_obj_list_t *remote_service_list; // The advertising data and scan response buffers are held by us, not by the SD, so we must diff --git a/ports/espressif/common-hal/_bleio/__init__.c b/ports/espressif/common-hal/_bleio/__init__.c index 552ba0575b3..7a8fd2f36a5 100644 --- a/ports/espressif/common-hal/_bleio/__init__.c +++ b/ports/espressif/common-hal/_bleio/__init__.c @@ -24,6 +24,8 @@ #include "common-hal/_bleio/__init__.h" #include "common-hal/_bleio/ble_events.h" +#include "services/gatt/ble_svc_gatt.h" + #include "nvs_flash.h" static volatile int _completion_status; @@ -35,10 +37,29 @@ void bleio_user_reset(void) { if (!common_hal_bleio_adapter_get_enabled(&common_hal_bleio_adapter_obj)) { return; } - // Stop any user scanning or advertising, and stop all connections. - // TODO: Don't stop BLE workflow connection. - bleio_adapter_reset(&common_hal_bleio_adapter_obj); + // Stop any user scanning or advertising. + common_hal_bleio_adapter_stop_scan(&common_hal_bleio_adapter_obj); + common_hal_bleio_adapter_stop_advertising(&common_hal_bleio_adapter_obj); + + // Disconnect the connections that user code initiated or accepted with its own + // advertising. Keep the BLE workflow connection if present. + // + // Remove each connection's pointers into the VM heap before disconnecting: + // the heap is about to go away, and a disconnect completes asynchronously. + for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) { + bleio_connection_internal_t *connection = &bleio_connections[i]; + connection->connection_obj = mp_const_none; + connection->remote_service_list = NULL; + if (connection->conn_handle != BLEIO_HANDLE_INVALID && connection->user_owned) { + common_hal_bleio_connection_disconnect(connection); + } + } + + // Now clear the adapter's remaining heap pointer, since it will be stale + // when the VM stops. + common_hal_bleio_adapter_obj.connection_objs = NULL; + // Also clean up event handlers that are on the heap. ble_event_remove_heap_handlers(); // Maybe start advertising the BLE workflow. @@ -52,11 +73,33 @@ void bleio_reset(void) { return; } + // The disable/enable cycle below clears user-created services from the GATT + // table, and it drops every connection, the BLE workflow's included. So run it + // only when user code created services. All other user BLE state has already + // been torn down individually by bleio_user_reset(). + // + // ESP-IDF NimBLE can delete individual services (ble_gatts_delete_svc(), used + // in Service.c), so a possible refinement is for bleio_user_reset() to delete + // user services one by one and never cycle the stack at all. For now this port + // matches nordic, whose SoftDevice can only clear services with a full cycle. + if (!bleio_user_services_created()) { + return; + } + bleio_clear_user_services_created(); + supervisor_stop_bluetooth(); ble_event_reset(); bleio_adapter_reset(&common_hal_bleio_adapter_obj); common_hal_bleio_adapter_set_enabled(&common_hal_bleio_adapter_obj, false); supervisor_start_bluetooth(); + + // The cycle above rebuilt the GATT table, so bonded peers' cached tables are + // stale. Signal Service Changed over the whole handle range: NimBLE indicates + // connected subscribed peers immediately (there are none right after the + // cycle) and records the change for bonded peers, indicating them when they + // reconnect. Without this, a bonded host trusts its cache indefinitely and + // can look up characteristics at stale handles. + ble_svc_gatt_changed(0x0001, 0xffff); } // The singleton _bleio.Adapter object, bound to _bleio.adapter diff --git a/ports/nordic/common-hal/_bleio/Adapter.c b/ports/nordic/common-hal/_bleio/Adapter.c index 05edd832a8b..cd34212192b 100644 --- a/ports/nordic/common-hal/_bleio/Adapter.c +++ b/ports/nordic/common-hal/_bleio/Adapter.c @@ -248,6 +248,10 @@ static bool adapter_on_ble_evt(ble_evt_t *ble_evt, void *self_in) { connection->connection_obj = mp_const_none; connection->pair_status = PAIR_NOT_PAIRED; connection->mtu = 0; + // Only user code connects in the central role, and a peripheral + // connection belongs to whoever started the advertising it answered. + connection->user_owned = connected->role == BLE_GAP_ROLE_CENTRAL || + self->advertising_started_by_user; // Clear leftover bond state; connection slots are recycled. The // SoftDevice fills in only the keys the new peer distributes, so a // stale keyset could mix the previous peer's keys into this peer's @@ -385,6 +389,9 @@ void common_hal_bleio_adapter_set_enabled(bleio_adapter_obj_t *self, bool enable ble_drv_remove_event_handler(connection_on_ble_evt, connection); connection->conn_handle = BLE_CONN_HANDLE_INVALID; } + // The SoftDevice's GATT table is empty after an enable. + bleio_gatts_min_handle = 0xFFFF; + bleio_gatts_max_handle = 0; self->background_callback.fun = bluetooth_adapter_background; self->background_callback.data = self; bleio_adapter_reset_name(self); @@ -748,6 +755,9 @@ uint32_t _common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, if (self->current_advertising_data != NULL && self->current_advertising_data == self->advertising_data) { return NRF_ERROR_BUSY; } + // The supervisor calls this internal function directly; user code arrives via + // common_hal_bleio_adapter_start_advertising(), which overrides this to true. + self->advertising_started_by_user = false; // If the current advertising data isn't owned by the adapter then it must be an internal // advertisement that we should stop. @@ -928,6 +938,7 @@ void common_hal_bleio_adapter_start_advertising(bleio_adapter_obj_t *self, bool tx_power, directed_to)); self->user_advertising = true; + self->advertising_started_by_user = true; } void common_hal_bleio_adapter_stop_advertising(bleio_adapter_obj_t *self) { diff --git a/ports/nordic/common-hal/_bleio/Adapter.h b/ports/nordic/common-hal/_bleio/Adapter.h index 5bc1805d072..22e9e6dbded 100644 --- a/ports/nordic/common-hal/_bleio/Adapter.h +++ b/ports/nordic/common-hal/_bleio/Adapter.h @@ -38,6 +38,11 @@ typedef struct { ble_drv_evt_handler_entry_t advertising_handler_entry; background_callback_t background_callback; bool user_advertising; + // Whether the current (or most recent) advertising was started by user code + // rather than the supervisor. Unlike user_advertising, this is not cleared when + // advertising stops, so the connected-event handler can still read it: the + // advertising event handler runs first and has already stopped the advertising. + bool advertising_started_by_user; } bleio_adapter_obj_t; void bleio_adapter_gc_collect(bleio_adapter_obj_t *adapter); diff --git a/ports/nordic/common-hal/_bleio/Characteristic.c b/ports/nordic/common-hal/_bleio/Characteristic.c index 2e6042e48e7..413c97a3d6e 100644 --- a/ports/nordic/common-hal/_bleio/Characteristic.c +++ b/ports/nordic/common-hal/_bleio/Characteristic.c @@ -240,6 +240,8 @@ void common_hal_bleio_characteristic_add_descriptor(bleio_characteristic_obj_t * }; check_nrf_error(sd_ble_gatts_descriptor_add(self->handle, &desc_attr, &descriptor->handle)); + bleio_gatts_min_handle = MIN(bleio_gatts_min_handle, descriptor->handle); + bleio_gatts_max_handle = MAX(bleio_gatts_max_handle, descriptor->handle); mp_obj_list_append(MP_OBJ_FROM_PTR(self->descriptor_list), MP_OBJ_FROM_PTR(descriptor)); diff --git a/ports/nordic/common-hal/_bleio/Connection.c b/ports/nordic/common-hal/_bleio/Connection.c index f32034582b4..fbd3263b725 100644 --- a/ports/nordic/common-hal/_bleio/Connection.c +++ b/ports/nordic/common-hal/_bleio/Connection.c @@ -289,15 +289,23 @@ bool connection_on_ble_evt(ble_evt_t *ble_evt, void *self_in) { // mode >=1 and/or level >=1 means encryption is set up self->pair_status = PAIR_NOT_PAIRED; } else { - if (bonding_load_cccd_info(self->is_central, self->conn_handle, self->ediv)) { - // Did an sd_ble_gatts_sys_attr_set() with the stored sys_attr values. - // Indicate ATTR table change because we may have reloaded since the peer last - // connected. - sd_ble_gatts_service_changed(self->conn_handle, 0xC, 0xFFFF); - } else { - // No matching bonding found, so use fresh system attributes. + if (!bonding_load_cccd_info(self->is_central, self->conn_handle, self->ediv)) { + // No stored system attributes (CCCD values) found, so use fresh ones. sd_ble_gatts_sys_attr_set(self->conn_handle, NULL, 0, 0); } + if (self->ediv != EDIV_INVALID && bleio_gatts_min_handle <= bleio_gatts_max_handle) { + // The peer reconnected using a stored bond, and the GATT table may + // have changed since it last connected: a reload can add or remove + // user services. Indicate Service Changed so the peer discards its + // cached table. This must not be conditional on the sys-attr load + // above succeeding: a peer that missed the indication would trust a + // stale cache indefinitely, and saved system attributes are the + // less reliable of the two records. The error is ignored if the + // peer hasn't subscribed to indications yet. Both handles must be + // within the application-populated range: the SoftDevice rejects + // 0xFFFF, and also 0xC, which is inside its own GATT service. + (void)sd_ble_gatts_service_changed(self->conn_handle, bleio_gatts_min_handle, bleio_gatts_max_handle); + } self->pair_status = PAIR_PAIRED; } break; diff --git a/ports/nordic/common-hal/_bleio/Connection.h b/ports/nordic/common-hal/_bleio/Connection.h index ea1edf17603..78e2ccd5314 100644 --- a/ports/nordic/common-hal/_bleio/Connection.h +++ b/ports/nordic/common-hal/_bleio/Connection.h @@ -31,6 +31,11 @@ typedef enum { typedef struct { uint16_t conn_handle; bool is_central; + // True if user code initiated or accepted this connection: it connected in the + // central role, or a central answered user code's advertising. User-owned + // connections are disconnected when the VM resets; the BLE workflow connection + // is not user-owned and stays up. + bool user_owned; // Remote services discovered when this peripheral is acting as a client. mp_obj_list_t *remote_service_list; // The advertising data and scan response buffers are held by us, not by the SD, so we must diff --git a/ports/nordic/common-hal/_bleio/Service.c b/ports/nordic/common-hal/_bleio/Service.c index 1bd75f8a481..c187c54178d 100644 --- a/ports/nordic/common-hal/_bleio/Service.c +++ b/ports/nordic/common-hal/_bleio/Service.c @@ -46,6 +46,8 @@ uint32_t _common_hal_bleio_service_construct(bleio_service_obj_t *self, bleio_uu uint32_t result = sd_ble_gatts_service_add(service_type, &nordic_uuid, &self->handle); // Do a service changed indication to all connected peers. if (result == NRF_SUCCESS) { + bleio_gatts_min_handle = MIN(bleio_gatts_min_handle, self->handle); + bleio_gatts_max_handle = MAX(bleio_gatts_max_handle, self->handle); _indicate_service_change(self->handle, self->handle); } @@ -169,6 +171,8 @@ void common_hal_bleio_service_add_characteristic(bleio_service_obj_t *self, _expand_range(char_handles.cccd_handle, &start, &end); _expand_range(char_handles.sccd_handle, &start, &end); _expand_range(char_handles.user_desc_handle, &start, &end); + bleio_gatts_min_handle = MIN(bleio_gatts_min_handle, start); + bleio_gatts_max_handle = MAX(bleio_gatts_max_handle, end); _indicate_service_change(start, end); #if CIRCUITPY_VERBOSE_BLE diff --git a/ports/nordic/common-hal/_bleio/__init__.c b/ports/nordic/common-hal/_bleio/__init__.c index 3d76b93ff15..7928123c173 100644 --- a/ports/nordic/common-hal/_bleio/__init__.c +++ b/ports/nordic/common-hal/_bleio/__init__.c @@ -77,6 +77,9 @@ void check_sec_status(uint8_t sec_status) { } } +uint16_t bleio_gatts_min_handle = 0xFFFF; +uint16_t bleio_gatts_max_handle; + void common_hal_bleio_init(void) { } @@ -85,6 +88,30 @@ void bleio_user_reset(void) { // Stop any user scanning or advertising. common_hal_bleio_adapter_stop_scan(&common_hal_bleio_adapter_obj); common_hal_bleio_adapter_stop_advertising(&common_hal_bleio_adapter_obj); + + // Disconnect the connections that user code initiated or accepted with its + // own advertising. The BLE workflow connection is not user-owned and stays up. + // + // Clear each connection's pointers into the VM heap first: the heap is about + // to go away, and a disconnect completes asynchronously, possibly after it + // has. The disconnect event handler dereferences connection_obj unless it is + // mp_const_none. + for (size_t i = 0; i < BLEIO_TOTAL_CONNECTION_COUNT; i++) { + bleio_connection_internal_t *connection = &bleio_connections[i]; + connection->connection_obj = mp_const_none; + connection->remote_service_list = NULL; + if (connection->conn_handle != BLE_CONN_HANDLE_INVALID && connection->user_owned) { + common_hal_bleio_connection_disconnect(connection); + } + } + + // Clear the adapter's remaining pointers into the VM heap. The full stack + // reset in bleio_reset() used to do this implicitly, but it no longer always + // runs. The adapter struct is a GC root, so a pointer left over from this + // heap would be scanned as a live object in the next VM's heap. + common_hal_bleio_adapter_obj.connection_objs = NULL; + common_hal_bleio_adapter_obj.advertising_data = NULL; + common_hal_bleio_adapter_obj.scan_response_data = NULL; } ble_drv_remove_heap_handlers(); @@ -101,6 +128,16 @@ void bleio_reset(void) { return; } + // The SoftDevice cannot remove individual services from its GATT table: the + // disable/enable cycle below is the only way to clear them, and it drops every + // connection, the BLE workflow's included. So run it only when user code created + // services. All other user BLE state has already been torn down individually by + // bleio_user_reset(). + if (!bleio_user_services_created()) { + return; + } + bleio_clear_user_services_created(); + supervisor_stop_bluetooth(); bleio_adapter_reset(&common_hal_bleio_adapter_obj); common_hal_bleio_adapter_set_enabled(&common_hal_bleio_adapter_obj, false); diff --git a/ports/nordic/common-hal/_bleio/__init__.h b/ports/nordic/common-hal/_bleio/__init__.h index caf287f9204..955c73ad8e5 100644 --- a/ports/nordic/common-hal/_bleio/__init__.h +++ b/ports/nordic/common-hal/_bleio/__init__.h @@ -8,6 +8,14 @@ void bleio_background(void); +// The range of attribute handles the application has populated in the +// SoftDevice's GATT table. Kept because sd_ble_gatts_service_changed() rejects +// handles outside that range (BLE_ERROR_INVALID_ATTR_HANDLE): 0xFFFF is beyond +// it, and handles at or below 0xC are in the SoftDevice's own GAP and GATT +// services, not the application's. +extern uint16_t bleio_gatts_min_handle; +extern uint16_t bleio_gatts_max_handle; + typedef struct { ble_gap_enc_key_t own_enc; ble_gap_enc_key_t peer_enc; diff --git a/shared-bindings/_bleio/Service.c b/shared-bindings/_bleio/Service.c index cea9a68e694..57285dab924 100644 --- a/shared-bindings/_bleio/Service.c +++ b/shared-bindings/_bleio/Service.c @@ -8,6 +8,7 @@ #include "py/objproperty.h" #include "py/runtime.h" +#include "shared-bindings/_bleio/__init__.h" #include "shared-bindings/_bleio/Characteristic.h" #include "shared-bindings/_bleio/Service.h" #include "shared-bindings/_bleio/UUID.h" @@ -45,6 +46,9 @@ static mp_obj_t bleio_service_make_new(const mp_obj_type_t *type, size_t n_args, bleio_service_obj_t *service = mp_obj_malloc_with_finaliser(bleio_service_obj_t, &bleio_service_type); common_hal_bleio_service_construct(service, uuid, is_secondary); + // The service is now in the stack's GATT table, and only a full stack reset can + // remove it. bleio_reset() checks this flag to decide whether to do that reset. + bleio_set_user_services_created(); return MP_OBJ_FROM_PTR(service); } diff --git a/shared-bindings/_bleio/__init__.c b/shared-bindings/_bleio/__init__.c index 1ec9fd96e76..bc6b82873c6 100644 --- a/shared-bindings/_bleio/__init__.c +++ b/shared-bindings/_bleio/__init__.c @@ -91,6 +91,20 @@ MP_NORETURN void mp_raise_bleio_SecurityError(mp_rom_error_text_t fmt, ...) { nlr_raise(exception); } +static bool _user_services_created; + +bool bleio_user_services_created(void) { + return _user_services_created; +} + +void bleio_set_user_services_created(void) { + _user_services_created = true; +} + +void bleio_clear_user_services_created(void) { + _user_services_created = false; +} + // Called when _bleio is imported. static mp_obj_t bleio___init__(void) { // HCI cannot be enabled on import, because we need to setup the HCI adapter first. diff --git a/shared-bindings/_bleio/__init__.h b/shared-bindings/_bleio/__init__.h index b55db201f2e..ae7d781c550 100644 --- a/shared-bindings/_bleio/__init__.h +++ b/shared-bindings/_bleio/__init__.h @@ -39,6 +39,21 @@ void bleio_user_reset(void); // Completely resets the BLE stack including BLE connections. void bleio_reset(void); +// True if user code created a local GATT service during this VM run. Set by the +// shared-bindings Service constructor; the supervisor constructs its BLE workflow +// services through common-hal directly and does not set it. User-created services +// can only be removed from the stack's GATT table by a full stack reset, so a +// port's bleio_reset() uses this to decide whether that reset, which drops all +// connections, is needed at all. +bool bleio_user_services_created(void); + +// Record that user code created a local GATT service. +void bleio_set_user_services_created(void); + +// Clear the flag reported by bleio_user_services_created(). Call after a full +// stack reset, so the next VM run starts out clean. +void bleio_clear_user_services_created(void); + // Init any state needed before calling any bleio functions including those // having to do with bonding. This doesn't enable the BLE adapter though. void common_hal_bleio_init(void); From 82d2c3e7e034650f823c6cca6489deba668fc460 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 26 Aug 2026 13:35:34 -0400 Subject: [PATCH 2/5] Clean up BLE reset leftovers in main.c, ble_hci, and silabs Delete main.c's reset_devices(), which only handled CIRCUITPY_BLEIO_HCI. At VM cleanup it double-reset those builds, because ble_hci's bleio_user_reset() does a full reset itself; at startup it stood in for HCI initialization, which now happens in common_hal_bleio_init() when user code imports _bleio. HCI is unusable before user code supplies an adapter, so import time is early enough, and bleio_reset() no longer re-creates the CCCD UUID that init now owns. Remove silabs declarations of vm_used_ble and cccd_uuid, copied long ago from ble_hci and never defined or used in that port. Co-Authored-By: Claude Fable 5 --- devices/ble_hci/common-hal/_bleio/__init__.c | 11 +++++++---- main.c | 13 +------------ ports/silabs/common-hal/_bleio/__init__.h | 6 ------ 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/devices/ble_hci/common-hal/_bleio/__init__.c b/devices/ble_hci/common-hal/_bleio/__init__.c index bfe39888753..29078944aaa 100644 --- a/devices/ble_hci/common-hal/_bleio/__init__.c +++ b/devices/ble_hci/common-hal/_bleio/__init__.c @@ -38,6 +38,13 @@ bool vm_used_ble; // } void common_hal_bleio_init(void) { + // Called on every import of _bleio. This is the only initialization HCI gets: + // unlike other ports, nothing runs at board startup, because HCI is unusable + // until user code supplies an adapter (UART, etc.) anyway. + // Create a UUID object for all CCCD's. + cccd_uuid.base.type = &bleio_uuid_type; + common_hal_bleio_uuid_construct(&cccd_uuid, BLE_UUID_CCCD, NULL); + bleio_hci_reset(); } void bleio_user_reset(void) { @@ -47,10 +54,6 @@ void bleio_user_reset(void) { // Turn off BLE on a reset or reload. void bleio_reset(void) { - // Create a UUID object for all CCCD's. - cccd_uuid.base.type = &bleio_uuid_type; - common_hal_bleio_uuid_construct(&cccd_uuid, BLE_UUID_CCCD, NULL); - bleio_hci_reset(); if (!common_hal_bleio_adapter_get_enabled(&common_hal_bleio_adapter_obj)) { diff --git a/main.c b/main.c index e2322524ab1..590bf6a3e4d 100644 --- a/main.c +++ b/main.c @@ -121,12 +121,6 @@ uint8_t value_out = 0; #include "supervisor/shared/settings.h" #endif -static void reset_devices(void) { - #if CIRCUITPY_BLEIO_HCI - bleio_reset(); - #endif -} - static uint8_t *_heap; static uint8_t *_pystack; static volatile bool _vm_is_running = false; @@ -374,9 +368,6 @@ static void cleanup_after_vm(mp_obj_t exception) { } } - // Reset port-independent devices, like CIRCUITPY_BLEIO_HCI. - reset_devices(); - #if CIRCUITPY_ATEXIT atexit_reset(); #endif @@ -390,7 +381,7 @@ static void cleanup_after_vm(mp_obj_t exception) { memorymonitor_reset(); #endif - // Disable user related BLE state that uses the micropython heap. + // Disable user related BLE state that uses the VM heap. Leave BLE workflow running if it's in use. #if CIRCUITPY_BLEIO bleio_user_reset(); #endif @@ -1088,8 +1079,6 @@ int __attribute__((used)) main(void) { // Reset everything and prep MicroPython to run boot.py. reset_port(); - // Port-independent devices, like CIRCUITPY_BLEIO_HCI. - reset_devices(); reset_board(); // displays init after filesystem, since they could share the flash SPI diff --git a/ports/silabs/common-hal/_bleio/__init__.h b/ports/silabs/common-hal/_bleio/__init__.h index 63eda59d84f..4ecd6456b8b 100644 --- a/ports/silabs/common-hal/_bleio/__init__.h +++ b/ports/silabs/common-hal/_bleio/__init__.h @@ -61,12 +61,6 @@ // Maximum length for variable length Attribute Values. #define BLE_GATTS_VAR_ATTR_LEN_MAX (512) -// Track if the user code modified the BLE state -// to know if we need to undo it on reload. -extern bool vm_used_ble; - -// UUID shared by all CCCD's. -extern bleio_uuid_obj_t cccd_uuid; extern void bleio_reset(); extern osMutexId_t bluetooth_connection_mutex_id; From 029089f661056808615731b03734e2e1850f14de Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 26 Aug 2026 14:12:48 -0400 Subject: [PATCH 3/5] Clarify reset and Service Changed comments Co-Authored-By: Claude Fable 5 --- main.c | 4 ++-- ports/espressif/common-hal/_bleio/__init__.c | 22 +++++++++----------- ports/nordic/common-hal/_bleio/__init__.h | 8 +++---- shared-bindings/_bleio/Service.c | 4 ++-- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/main.c b/main.c index 590bf6a3e4d..845d55ca21e 100644 --- a/main.c +++ b/main.c @@ -767,7 +767,7 @@ static bool __attribute__((noinline)) run_code_py(safe_mode_t safe_mode, bool *s // Done waiting, start the board back up. // Resetting BLE is delayed until here, after the wait, in case files were being - // transferred over the BLE workflow during the wait. The reset cycles the BLE + // transferred over the BLE workflow during the wait. The reset restarts the BLE // stack, dropping the workflow connection, only if user code created GATT // services; otherwise it is a no-op and the connection continues. #if CIRCUITPY_BLEIO @@ -986,7 +986,7 @@ static int run_repl(safe_mode_t safe_mode) { cleanup_after_vm(MP_OBJ_SENTINEL); // Also reset bleio, which cleanup_after_vm() above omits so workflows can - // continue between VMs. As in run_code_py(), this cycles the BLE stack only if + // continue between VMs. As in run_code_py(), this restarts the BLE stack only if // user code created GATT services. #if CIRCUITPY_BLEIO bleio_reset(); diff --git a/ports/espressif/common-hal/_bleio/__init__.c b/ports/espressif/common-hal/_bleio/__init__.c index 7a8fd2f36a5..ae2687fa076 100644 --- a/ports/espressif/common-hal/_bleio/__init__.c +++ b/ports/espressif/common-hal/_bleio/__init__.c @@ -73,15 +73,15 @@ void bleio_reset(void) { return; } - // The disable/enable cycle below clears user-created services from the GATT - // table, and it drops every connection, the BLE workflow's included. So run it + // The stop/start cycle below clears user-created services from the GATT + // table, and drops every connection, BLE workflow included. So run it // only when user code created services. All other user BLE state has already // been torn down individually by bleio_user_reset(). // - // ESP-IDF NimBLE can delete individual services (ble_gatts_delete_svc(), used - // in Service.c), so a possible refinement is for bleio_user_reset() to delete - // user services one by one and never cycle the stack at all. For now this port - // matches nordic, whose SoftDevice can only clear services with a full cycle. + // TODO: ESP-IDF NimBLE can delete individual services (ble_gatts_delete_svc(), + // used in Service.c), so bleio_user_reset() could delete user services one by + // one and never restart the BLE stack at all. For now this port matches + // nordic, whose SoftDevice can only clear services with a full cycle. if (!bleio_user_services_created()) { return; } @@ -93,12 +93,10 @@ void bleio_reset(void) { common_hal_bleio_adapter_set_enabled(&common_hal_bleio_adapter_obj, false); supervisor_start_bluetooth(); - // The cycle above rebuilt the GATT table, so bonded peers' cached tables are - // stale. Signal Service Changed over the whole handle range: NimBLE indicates - // connected subscribed peers immediately (there are none right after the - // cycle) and records the change for bonded peers, indicating them when they - // reconnect. Without this, a bonded host trusts its cache indefinitely and - // can look up characteristics at stale handles. + // The stop/start above rebuilt the GATT table, so now any bonded peers' cached + // tables are stale. Signal Service Changed over the whole handle range. + // Without Service Changed, a bonded host trusts its cache indefinitely and can + // look up characteristics at stale handles. ble_svc_gatt_changed(0x0001, 0xffff); } diff --git a/ports/nordic/common-hal/_bleio/__init__.h b/ports/nordic/common-hal/_bleio/__init__.h index 955c73ad8e5..eecbc32275f 100644 --- a/ports/nordic/common-hal/_bleio/__init__.h +++ b/ports/nordic/common-hal/_bleio/__init__.h @@ -8,11 +8,9 @@ void bleio_background(void); -// The range of attribute handles the application has populated in the -// SoftDevice's GATT table. Kept because sd_ble_gatts_service_changed() rejects -// handles outside that range (BLE_ERROR_INVALID_ATTR_HANDLE): 0xFFFF is beyond -// it, and handles at or below 0xC are in the SoftDevice's own GAP and GATT -// services, not the application's. +// Remember the range of attribute handles the application has populated in the +// SoftDevice's GATT table. Required to call sd_ble_gatts_service_changed() +// with valid arguments: it fails if the args are outside that range. extern uint16_t bleio_gatts_min_handle; extern uint16_t bleio_gatts_max_handle; diff --git a/shared-bindings/_bleio/Service.c b/shared-bindings/_bleio/Service.c index 57285dab924..920d66a3de2 100644 --- a/shared-bindings/_bleio/Service.c +++ b/shared-bindings/_bleio/Service.c @@ -46,8 +46,8 @@ static mp_obj_t bleio_service_make_new(const mp_obj_type_t *type, size_t n_args, bleio_service_obj_t *service = mp_obj_malloc_with_finaliser(bleio_service_obj_t, &bleio_service_type); common_hal_bleio_service_construct(service, uuid, is_secondary); - // The service is now in the stack's GATT table, and only a full stack reset can - // remove it. bleio_reset() checks this flag to decide whether to do that reset. + // Remember that there are services that were created by the user. + // bleio_reset() needs this to know what kind of reset to do. bleio_set_user_services_created(); return MP_OBJ_FROM_PTR(service); From 650bd9a08ec6a9cd522fbadc2a54225832ce0fe1 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 26 Aug 2026 16:25:00 -0400 Subject: [PATCH 4/5] Rename bleio_user_services_created() to bleio_get_user_services_created() Per review. Co-Authored-By: Claude Fable 5 --- ports/espressif/common-hal/_bleio/__init__.c | 2 +- ports/nordic/common-hal/_bleio/__init__.c | 2 +- shared-bindings/_bleio/__init__.c | 2 +- shared-bindings/_bleio/__init__.h | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ports/espressif/common-hal/_bleio/__init__.c b/ports/espressif/common-hal/_bleio/__init__.c index ae2687fa076..9fe86405498 100644 --- a/ports/espressif/common-hal/_bleio/__init__.c +++ b/ports/espressif/common-hal/_bleio/__init__.c @@ -82,7 +82,7 @@ void bleio_reset(void) { // used in Service.c), so bleio_user_reset() could delete user services one by // one and never restart the BLE stack at all. For now this port matches // nordic, whose SoftDevice can only clear services with a full cycle. - if (!bleio_user_services_created()) { + if (!bleio_get_user_services_created()) { return; } bleio_clear_user_services_created(); diff --git a/ports/nordic/common-hal/_bleio/__init__.c b/ports/nordic/common-hal/_bleio/__init__.c index 7928123c173..554bbf200bc 100644 --- a/ports/nordic/common-hal/_bleio/__init__.c +++ b/ports/nordic/common-hal/_bleio/__init__.c @@ -133,7 +133,7 @@ void bleio_reset(void) { // connection, the BLE workflow's included. So run it only when user code created // services. All other user BLE state has already been torn down individually by // bleio_user_reset(). - if (!bleio_user_services_created()) { + if (!bleio_get_user_services_created()) { return; } bleio_clear_user_services_created(); diff --git a/shared-bindings/_bleio/__init__.c b/shared-bindings/_bleio/__init__.c index bc6b82873c6..202542c3d05 100644 --- a/shared-bindings/_bleio/__init__.c +++ b/shared-bindings/_bleio/__init__.c @@ -93,7 +93,7 @@ MP_NORETURN void mp_raise_bleio_SecurityError(mp_rom_error_text_t fmt, ...) { static bool _user_services_created; -bool bleio_user_services_created(void) { +bool bleio_get_user_services_created(void) { return _user_services_created; } diff --git a/shared-bindings/_bleio/__init__.h b/shared-bindings/_bleio/__init__.h index ae7d781c550..0e8a9212e6b 100644 --- a/shared-bindings/_bleio/__init__.h +++ b/shared-bindings/_bleio/__init__.h @@ -45,12 +45,12 @@ void bleio_reset(void); // can only be removed from the stack's GATT table by a full stack reset, so a // port's bleio_reset() uses this to decide whether that reset, which drops all // connections, is needed at all. -bool bleio_user_services_created(void); +bool bleio_get_user_services_created(void); // Record that user code created a local GATT service. void bleio_set_user_services_created(void); -// Clear the flag reported by bleio_user_services_created(). Call after a full +// Clear the flag reported by bleio_get_user_services_created(). Call after a full // stack reset, so the next VM run starts out clean. void bleio_clear_user_services_created(void); From 4bbbe2a6f2f4507511ed26f817fa509955c55b9a Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 26 Aug 2026 16:37:07 -0400 Subject: [PATCH 5/5] shrink pca10100 --- ports/nordic/boards/pca10100/mpconfigboard.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/ports/nordic/boards/pca10100/mpconfigboard.mk b/ports/nordic/boards/pca10100/mpconfigboard.mk index 34bcc47cf9f..9997abb8bfe 100644 --- a/ports/nordic/boards/pca10100/mpconfigboard.mk +++ b/ports/nordic/boards/pca10100/mpconfigboard.mk @@ -10,3 +10,4 @@ INTERNAL_FLASH_FILESYSTEM = 1 CIRCUITPY_ONEWIREIO = 0 CIRCUITPY_AUDIOMIXER = 0 CIRCUITPY_RAINBOWIO = 0 +CIRCUITPY_USB_MIDI = 0