Fix a number of BLE workflow and bonding issues - #11178
Conversation
Original motivation was to fix BLE workflow serial dropping the link on the first keystroke. Typing at the "Press any key to enter the REPL" prompt over the BLE workflow serial ended the session after exactly one character. The first byte breaks `main.c`'s wait loop, which then calls `bleio_reset()`, and on nordic restarts the SoftDevice. The BLE bonding survives that restart, but the link does not. The main debugging was done in the nordic port. Behaviour changes: * `bleio_reset()` now returns early when user code never imported `_bleio`. The SoftDevice restart was there only to drop leftover user-created GATT services. The SoftDevice offers no way to remove those individually, so it is unnecessary when the VM cannot have created any in this VM instantiation. Tracked by a flag set in `bleio___init__()`. * A bonded central may distribute no identity address; this is true for BlueZ with its default `Privacy=off`, and on Windows. In that case, store the address it connected from in `peer_id.id_addr_info` and reconnect by aiming `ADV_DIRECT_IND` at it. Such a central does not use privacy, so it cannot resolve a private address and will never recognise our undirected advertisement, but it does connect from a stable address we can target. Centrals that do distribute an IRK (iOS, macOS, Android) keep undirected private advertising, which is what works for them and what Apple's accessory guidelines require. Various bugs found during debugging are now fixed. Similar bugs in other ports (due to code copying) were fixed after the fixes were vetted in nordic. They have not been tested yet. * `bleio_adapter_reset()` mistakenly waited zero milliseconds for disconnects to complete: the loop read `while (any_connected && ...)` with `any_connected` initialised to `false`. The SoftDevice was then disabled before the disconnect PDU went out, so the central saw a link supervision timeout rather than a disconnect reason. Now a `do/while`. Same bug was also fixed in espressif and silabs. * Anonymous advertising set `private_addr_cycle_s` to `timeout + 1`, and the workflow advertises with an unlimited timeout encoded as zero, so the resolvable private address rotated every second -- way too fast for a central to resolve an address and still connect to it. Passing zero selects the SoftDevice default of 15 minutes, which is also the maximum rotation period Microsoft's accessory guidelines allow. * Directed advertising selected the high duty cycle type for an unlimited timeout, for the same "zero means unlimited" reason. The spec caps high duty cycle at 1.28 seconds. Also fixed in espressif. * `ble_drv_remove_heap_handlers()` mistakenly stopped after the first handler it removed, because `ble_drv_remove_event_handler()` clears the removed entry's next pointer. Same bug fixed in espressif's `ble_event_remove_heap_handlers()`. * `common_hal_bleio_packet_buffer_deinit()` never cleared `self->characteristic`, so `common_hal_bleio_packet_buffer_deinited()` always reported false and the guards in `supervisor/shared/bluetooth/serial.c` never took effect. It also removed the client event handler for server-side buffers. Same bug fixed in silabs and ble_hci. * The BLE serial RX ringbuf was mistakenly given a size of `sizeof(_incoming) * sizeof(uint32_t)`, four times the 256 bytes it actually has. * `bonding_load_identities()` returned peers that had distributed no IRK, mistakenly handing an all-zero identity to `sd_ble_gap_device_identities_set()`. * Connection slots mistakenly retained the previous peer's keys, because `bonding_keys` was cleared only on adapter enable and not per connection, so a recycled slot could accidentally store one peer's IRK with another's LTK. Now they are cleared. `_common_hal_bleio_adapter_start_advertising()` now takes `directed_to` as a raw `bleio_raw_address_t` instead of a `bleio_address_obj_t`, which allows moving `mp_get_buffer_raise()` up into `shared-bindings`. Now the code in `supervisor/shared` calls the internal function and cannot raise an exception. Some typos about the workflow UUIDs were fixed in `docs/workflows.md`. Tested on a Feather nRF52840 Express. A simple terminal program using bleak was developed for testing. It is now in `tools/workflow/ble_terminal.py`. Linux with the bleak terminal reconnects in about 140 ms by directed advertising. https://code.circuitpython.org only works properly on macOS Chrome now. It reconnects by undirected private advertising. Still to fix: Chrome on Windows spins on startup while trying to read `boot_out.txt` for board information. Chrome on Linux doesn't even get that far: it stops with the initial BLE workflow popup visible. Commit message edited by @dhalbert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chrome registers no org.bluez.Agent1, so on Linux a browser cannot complete
pairing with the BLE workflow. BlueZ has no pairing UI of its own; it delegates
every pairing interaction to an agent that some application must register, and
with nothing else providing one bluetoothd refuses outright:
src/device.c:new_auth() No agent available for request type 2
device_confirm_passkey: Operation not permitted
The link then drops. Since both the file transfer and serial services are
`SECURITY_MODE_ENC_NO_MITM`, nothing works past the connect. macOS and Windows
are unaffected because the operating system owns pairing there and shows its
own dialog. This also matches Chromium's own support matrix, which lists
on-demand device pairing as unimplemented on Linux.
Run `python3 tools/workflow/ble_agent.py` and leave it running while pairing.
It registers a NoInputNoOutput agent, so BlueZ selects Just Works and asks
nothing -- the same security the workflow already assumes, so this does not
weaken anything. `ble_terminal.py` registers exactly this agent for itself;
this splits it out so it can be used with a browser.
Only pairing needs the agent. Once the board is bonded, reconnects need
nothing. It does not help with the separate Linux kernel problem where a
connect to an unbonded peripheral often times out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude wrote this: Follow-up on the three-platform testing, since the description above is now out of date about where the remaining failures actually live. Short version: macOS Chrome works, Linux Chrome does not and cannot be made to from our side, and Windows still needs investigation. Struck-through text below was wrong and is superseded by the measurements that follow it. macOS Chrome worksVerified through https://code.circuitpython.org on a Feather nRF52840 Express: the chooser finds the board, pairing completes, both services enumerate, serial works, and typing at the "Press any key to enter the REPL" prompt no longer drops the link — the REPL banner arrives over the same connection. After a page reload it reconnects on its own by undirected private advertising. Linux Chrome does not, because of two host defectsNeither is fixable in CircuitPython, and both are worth studying because they affect anyone trying to use the BLE workflow from a browser on Linux. 1. Chrome registers no BlueZ pairing agent, so pairing cannot complete. BlueZ has no pairing UI of its own; it delegates every pairing interaction over D-Bus to an The link then drops, and since both the file transfer and serial services are
Later measurement inverted that conclusion. Driving
Holding a discovery session open is what breaks the connect, not what makes it work. Arm B has never failed: 36/36 across two controllers, both kernel connect paths, every signal level tried and both orderings. The failure also looks different from the trace above. Two things anyone trying to reproduce this needs to know. The failing state is established by booting and cleared by a host suspend/resume — a 19-second suspend was sufficient, confirmed in both directions across three boots — so on a machine that has been suspended since boot it will not reproduce at all. And it did not reproduce on a CSR dongle in the same host on the same boot, bracketed either side by failing runs on the built-in MediaTek controller ( The practical consequence for the web editor is the opposite of what the struck text implied: abort any This symptom family is reported upstream but not fixed anywhere: bluez/bluez#2309, bluez/bluez#2356, hbldh/bleak#1244, and https://bugzilla.kernel.org/show_bug.cgi?id=199111 which has been open since 2018. None of them identifies the discovery-state dependency or the boot/suspend toggle. Tested on kernel 7.0.0-29-generic with BlueZ 5.85. Separately, Windows needs further researchWindows Chrome did connect and run serial successfully once a stale host pairing was cleared, but the original failure — spinning while reading An orphaned-pairing trap worth fixing separatelyNot platform-specific, and arguably the most user-visible problem found. Entering discovery mode calls Since the discovery-mode button press is the documented way to make a board discoverable again, users will hit this routinely, and nothing anywhere tells them what happened. There are bugs in the web editorThe debugging sessions today exposed several bugs in web-editor and ble-file-transfer-js as well, and I'll submit issues and PR's for those later. --@dhalbert |
🤖 Generated with Claude Code
Draft: I want to test espressif and make further commits before this is ready for review.
The original bug
Typing at the "Press any key to enter the REPL" prompt over the BLE workflow serial ended the session after exactly one character. The first byte breaks
main.c's wait loop, which then callsbleio_reset(), and on nordic that restarts the SoftDevice. The bonding survives that restart; the link does not.bleio_reset()now returns early when user code never imported_bleio. The stack restart is only there to drop leftover user-created GATT services, which the SoftDevice offers no way to remove individually, so it is unnecessary when the VM cannot have created any.Reconnecting when the restart does have to happen
The peripheral now chooses its reconnect advertising strategy from what the bonded central distributed at pairing time:
Privacy=off), WindowsADV_DIRECT_INDaimed at the address it connected fromA central that distributed no IRK is not using privacy. It cannot resolve our private address, so it will never recognise an undirected advertisement as us — but it does connect from a stable address, which we now store with the bond and can target. Centrals that do use privacy keep undirected advertising, which is what works for them and what Apple's accessory guidelines require.
Bugs found along the way
The commit message has the full list with details. In brief:
bleio_adapter_reset()waited zero milliseconds, so the stack was torn down before the disconnect went out and the central saw a supervision timeoutble_drv_remove_heap_handlers()stopped after the first handler it removed, leaving the rest pointing into a heap about to be freedcommon_hal_bleio_packet_buffer_deinited()could never return true, so the guards insupervisor/shared/bluetooth/serial.cnever took effectSeveral of these had been copied into other ports and are fixed there too.
Testing
On a Feather nRF52840 Express:
tools/workflow/ble_terminal.py, added in this PR: reconnects in about 140 ms by directed advertising.Not tested: espressif, silabs, ble_hci. Those changes are the copied bug fixes plus a mechanical signature change.
Still broken, and verified as pre-existing against a stock 10.3.0-alpha.4 build, so not regressions from this PR: Chrome on Windows spins while reading
boot_out.txtfor board information, and Chrome on Linux stops with the initial BLE workflow popup still visible.Where to look hardest
_common_hal_bleio_adapter_start_advertising()now takesdirected_toas a rawbleio_raw_address_tinstead of ableio_address_obj_t, movingmp_get_buffer_raise()up into the user-facing wrapper. That touches all four implementations. The reason is thatsupervisor/sharedcalls the internal function and must not raise.peer_id.id_addr_infonow carries two meanings, distinguished by whetherpeer_id.id_infoholds an IRK: either an identity address the peer distributed, or merely the address the peer connected from.bonding_load_identities()skips entries with no IRK so the two cannot be confused.Related issues
bleio_reset()has the identical structure. Not fixed here. It appears to have a second cause as well:bleio_user_reset()there disconnects the workflow connection on every VM exit, which is what its own// TODO: Don't stop BLE workflow connection.is about.ble_drv_remove_heap_handlers()class of bug fixed here, which would be worth a retest.