From e77d79ec64ea04dc2501f13a6b52dd321d8123f5 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 9 Sep 2026 14:51:30 -0400 Subject: [PATCH 1/2] BLE workflow: add a guard service after the workflow services A peripheral reports its last primary service with End Group Handle 0xffff, and hosts cache that literally. A service that user code adds later lands inside that cached range, so hosts treat it as a change to the last workflow service: Chrome invalidates the page's object for it with no event to say so, and BlueZ drops it from its cache. On every host the web editor's REPL terminal went dead as soon as user code created a `_bleio.Service`. Create a placeholder service after the file transfer and serial services so that it, not a workflow service, is the one affected. Verified on nordic and espressif against BlueZ, CoreBluetooth and WinRT. Also move `circuitpython_base_uuid` from `serial.c` to `bluetooth.c`, since the guard uses it too, and factor the repeated `CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE` into `BLE_WORKFLOW`. Co-Authored-By: Claude Fable 5.1 --- supervisor/shared/bluetooth/bluetooth.c | 81 ++++++++++++++++++++++--- supervisor/shared/bluetooth/bluetooth.h | 3 + supervisor/shared/bluetooth/serial.c | 4 +- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/supervisor/shared/bluetooth/bluetooth.c b/supervisor/shared/bluetooth/bluetooth.c index 74edcbb46bb..d347392aed8 100644 --- a/supervisor/shared/bluetooth/bluetooth.c +++ b/supervisor/shared/bluetooth/bluetooth.c @@ -32,11 +32,20 @@ #include "supervisor/shared/bluetooth/serial.h" #endif +// BLE workflow exists if either or both of these are turned on. +#define BLE_WORKFLOW (CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE) + +#if BLE_WORKFLOW +#include "shared-bindings/_bleio/Characteristic.h" +#include "shared-bindings/_bleio/Service.h" +#include "shared-bindings/_bleio/UUID.h" +#endif + #if CIRCUITPY_STATUS_BAR #include "supervisor/shared/status_bar.h" #endif -#if (CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE || (CIRCUITPY_WEB_WORKFLOW && CIRCUITPY_WIFI)) && CIRCUITPY_SETTINGS_TOML +#if (BLE_WORKFLOW || (CIRCUITPY_WEB_WORKFLOW && CIRCUITPY_WIFI)) && CIRCUITPY_SETTINGS_TOML #include "supervisor/shared/settings.h" #endif @@ -67,7 +76,7 @@ const uint8_t private_advertising_data[] = { // This scan response advertises the full device name (if it fits.) uint8_t circuitpython_scan_response_data[31]; -#if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE +#if BLE_WORKFLOW static bool boot_in_discovery_mode = false; static bool advertising = false; static bool _private_advertising = false; @@ -180,13 +189,13 @@ static void supervisor_bluetooth_start_advertising(void) { advertising = status == 0; } -#endif // CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE +#endif // BLE_WORKFLOW #define BLE_DISCOVERY_DATA_GUARD 0xbb0000bb #define BLE_DISCOVERY_DATA_GUARD_MASK 0xff0000ff void supervisor_bluetooth_init(void) { - #if (CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE) + #if BLE_WORKFLOW #if CIRCUITPY_SETTINGS_TOML // Check if the user disabled BLE workflow in settings.toml. The default is that it's enabled. @@ -297,7 +306,7 @@ void supervisor_bluetooth_init(void) { } void supervisor_bluetooth_background(void) { - #if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE + #if BLE_WORKFLOW if (!ble_started) { return; } @@ -328,8 +337,21 @@ void supervisor_bluetooth_background(void) { #endif } +#if BLE_WORKFLOW +// adafXXXX-4369-7263-7569-74507974686e, stored little-endian. The XXXX bytes are +// filled in by common_hal_bleio_uuid_construct(). +const uint8_t circuitpython_base_uuid[16] = {0x6e, 0x68, 0x74, 0x79, 0x50, 0x74, 0x69, 0x75, 0x63, 0x72, 0x69, 0x43, 0x00, 0x00, 0xaf, 0xad }; + +static bleio_service_obj_t guard_service; +static bleio_uuid_obj_t guard_service_uuid; +static bleio_characteristic_obj_t guard_characteristic; +static bleio_uuid_obj_t guard_characteristic_uuid; +static mp_obj_list_t guard_characteristic_list; +static mp_obj_t guard_characteristic_list_items[1]; +#endif + void supervisor_start_bluetooth(void) { - #if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE + #if BLE_WORKFLOW if (workflow_state != WORKFLOW_ENABLED || ble_started) { return; @@ -345,6 +367,45 @@ void supervisor_start_bluetooth(void) { supervisor_start_bluetooth_serial(); #endif + // Create an unused service that will be in the GATT table immediately after + // the BLE workflow services. This protects the workflow services from being + // invalidated if user code creates services. + // + // A peripheral reports its last primary service with End Group Handle 0xffff, + // and hosts cache that literally. So a service that user code adds later lands + // inside the cached range of whichever service was last, and hosts treat the + // addition as a change to that service: Chrome invalidates the page's object + // for it (and has no event to report that to JavaScript), and BlueZ drops it + // from its cache entirely. This guard is created last so that it, + // not a workflow service, is the one affected, and file transfer and serial + // keep working. Costs three attribute handles. + guard_service_uuid.base.type = &bleio_uuid_type; + common_hal_bleio_uuid_construct(&guard_service_uuid, 0x0004, circuitpython_base_uuid); + guard_characteristic_list.base.type = &mp_type_list; + guard_characteristic_list.alloc = 1; + guard_characteristic_list.len = 0; + guard_characteristic_list.items = guard_characteristic_list_items; + guard_characteristic_list_items[0] = MP_OBJ_NULL; + guard_service.base.type = &bleio_service_type; + _common_hal_bleio_service_construct(&guard_service, &guard_service_uuid, false /* is secondary */, &guard_characteristic_list); + + // One read-only characteristic: we need at least one because on espressif + // a service is not registered with NimBLE until its first characteristic is added. + guard_characteristic_uuid.base.type = &bleio_uuid_type; + common_hal_bleio_uuid_construct(&guard_characteristic_uuid, 0x0005, circuitpython_base_uuid); + guard_characteristic.base.type = &bleio_characteristic_type; + common_hal_bleio_characteristic_construct(&guard_characteristic, + &guard_service, + 0, // handle (for remote only) + &guard_characteristic_uuid, + CHAR_PROP_READ, + SECURITY_MODE_ENC_NO_MITM, + SECURITY_MODE_NO_ACCESS, + 1, // max length + true, // fixed length + NULL, // no initial value + NULL); + // Mark as started so that the background call does something. ble_started = true; @@ -359,7 +420,7 @@ void supervisor_start_bluetooth(void) { } void supervisor_stop_bluetooth(void) { - #if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE + #if BLE_WORKFLOW if (!ble_started && workflow_state != WORKFLOW_ENABLED) { return; @@ -379,7 +440,7 @@ void supervisor_stop_bluetooth(void) { } void supervisor_bluetooth_enable_workflow(void) { - #if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE + #if BLE_WORKFLOW if (!ble_workflow_setting || workflow_state == WORKFLOW_DISABLED) { return; } @@ -388,13 +449,13 @@ void supervisor_bluetooth_enable_workflow(void) { } void supervisor_bluetooth_disable_workflow(void) { - #if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE + #if BLE_WORKFLOW workflow_state = WORKFLOW_DISABLED; #endif } bool supervisor_bluetooth_workflow_is_enabled(void) { - #if CIRCUITPY_BLE_FILE_SERVICE || CIRCUITPY_BLE_SERIAL_SERVICE + #if BLE_WORKFLOW if (workflow_state == WORKFLOW_ENABLED) { return true; } diff --git a/supervisor/shared/bluetooth/bluetooth.h b/supervisor/shared/bluetooth/bluetooth.h index 14e74a07c73..0050d702e02 100644 --- a/supervisor/shared/bluetooth/bluetooth.h +++ b/supervisor/shared/bluetooth/bluetooth.h @@ -7,6 +7,9 @@ #pragma once #include +#include + +extern const uint8_t circuitpython_base_uuid[16]; void supervisor_bluetooth_background(void); void supervisor_bluetooth_init(void); diff --git a/supervisor/shared/bluetooth/serial.c b/supervisor/shared/bluetooth/serial.c index c57dcdecf3b..037e5e3c7b7 100644 --- a/supervisor/shared/bluetooth/serial.c +++ b/supervisor/shared/bluetooth/serial.c @@ -15,6 +15,7 @@ #include "shared-bindings/_bleio/Service.h" #include "shared-bindings/_bleio/UUID.h" #include "shared-module/storage/__init__.h" +#include "supervisor/shared/bluetooth/bluetooth.h" #include "supervisor/shared/bluetooth/serial.h" #include "common-hal/_bleio/__init__.h" @@ -30,9 +31,6 @@ static bleio_uuid_obj_t supervisor_ble_circuitpython_tx_uuid; static bleio_characteristic_obj_t supervisor_ble_circuitpython_version_characteristic; static bleio_uuid_obj_t supervisor_ble_circuitpython_version_uuid; -// This is the base UUID for the CircuitPython service. -const uint8_t circuitpython_base_uuid[16] = {0x6e, 0x68, 0x74, 0x79, 0x50, 0x74, 0x69, 0x75, 0x63, 0x72, 0x69, 0x43, 0x00, 0x00, 0xaf, 0xad }; - static mp_obj_list_t characteristic_list; static mp_obj_t characteristic_list_items[3]; From 128b34cdbd9b727f0156db51d94acef34ebe88c3 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Thu, 10 Sep 2026 16:45:28 -0400 Subject: [PATCH 2/2] Tear down the guard service in supervisor_stop_bluetooth() zephyr-cp restarts the BLE workflow on every reload. The guard service was created on each start but never deinited on stop, so the second start re-registered a service still in Zephyr's service list, which corrupted the list and dropped the file transfer and serial services from the GATT table. Fixes the zephyr-tests failures in test_bsim_ble_workflow_nus and test_bsim_ble_workflow_file_transfer. Co-Authored-By: Claude Fable 5.1 --- supervisor/shared/bluetooth/bluetooth.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/supervisor/shared/bluetooth/bluetooth.c b/supervisor/shared/bluetooth/bluetooth.c index d347392aed8..15cddd7d28c 100644 --- a/supervisor/shared/bluetooth/bluetooth.c +++ b/supervisor/shared/bluetooth/bluetooth.c @@ -436,6 +436,10 @@ void supervisor_stop_bluetooth(void) { supervisor_stop_bluetooth_serial(); #endif + // Tear down the guard service too. + common_hal_bleio_characteristic_deinit(&guard_characteristic); + common_hal_bleio_service_deinit(&guard_service); + #endif }