Skip to content

fix(bindings): reset a BlueZ controller the kernel lost track of - #577

Merged
bahdotsh merged 1 commit into
mainfrom
fix/python-ble-advertising-wedged-controller
Oct 16, 2026
Merged

bahdotsh merged 1 commit into
mainfrom
fix/python-ble-advertising-wedged-controller

Conversation

@bahdotsh

Copy link
Copy Markdown
Member

Problem

On two Raspberry Pi 4s (Linux 6.18, BlueZ 5.87) the Python peripheral failed every start with Failed to register advertisement, and every replacement service failed the same way. It looked like advertisements left behind by replaced containers were filling the slots. They weren't: BlueZ reported ActiveInstances 0 and SupportedInstances 5 while it was failing, and a full set of slots gets a different answer ("Maximum advertisements reached").

Root cause

Linux loses track of the controller's advertising state. hci_event.c (le_conn_complete_evt, line 5650 in v6.18) clears HCI_LE_ADV on every LE Connection Complete, assuming the controller stopped advertising. A controller only stops for a link in which it is the peripheral.

Captured in the HCI trace on the Pi (13:41:40 UTC):

  1. Our central dials a peer, and the kernel pauses advertising.
  2. While the dial is still pending, a discovery restart makes the kernel enable advertising again.
  3. LE Connection Complete, role Central. The kernel clears its flag, but the controller keeps advertising.
  4. From then on the kernel never disables advertising. Every LE Set Advertising Parameters and LE Set Random Address is answered Command Disallowed (0x0c), so every new advertisement is refused until the controller is reset.

Fix

  • BlePeripheral._bring_up: on Linux, when the start fails with Failed to register advertisement and BlueZ reports a free slot, it powers the adapter off and on through Adapter1.Powered (which sends an HCI Reset and clears the stale advertising), then starts once more. It does this once only, because the reset drops every link on the adapter. Any other refusal is raised unchanged.
  • bluez_restarts: an adapter's Powered turning true now triggers the same re-registration as a bluetoothd restart. BlueZ drops every discovery session on power-off, so without this the central in the same process stopped scanning after the reset. The same applies to any power toggle by another process.
  • Docs: docs/bridges/python.md P17, the bridges README row, and a CHANGELOG entry.

Verification

  • pytest bindings/python/tests: 1128 passed. There are new tests for the reset path (it resets once, any other error is left alone, the free-slot check, the power-off-then-on order) and for the power-on signal. Each of 7 deliberate code breakages makes a test fail.
  • Hardware: redeployed on pi-a while it was in the broken state. The start reset the adapter once, the central re-registered, and the peripheral was running about 1.5 s later. The HCI trace shows one last Command Disallowed, then Reset, then Set Advertising Parameters and Set Advertise Enable succeeding.
  • Full lab rerun with the fix: lan, outage and a 4-member group including an Android phone pass. The Pi-to-Pi BLE phase still fails because pi-b's LE radio has stopped working (no advertising reports while actively scanning, not visible to any other device, and an HCI Reset doesn't help). That is a hardware or firmware problem outside this change, and a rerun after a power cycle is owed.

Not covered

  • If the controller gets into this state while the peripheral is already running, it keeps advertising our existing data, so peers can still find us. The reset only runs at the next start.
  • The kernel race itself is upstream. This change recovers from it but doesn't prevent it.

On Linux 6.18 with BlueZ 5.87 a Raspberry Pi 4 stopped accepting any new
advertisement ("Failed to register advertisement") while BlueZ reported
every slot free, and the state outlived the service: each replacement
container failed the same way until the controller was reset.

The kernel clears its advertising flag on every LE connection complete,
assuming the controller stopped. A controller only stops for a link in
which it is the peripheral. When advertising is on while our central's
dial is pending (a discovery restart re-enables it), the dial completes
in the central role, the controller keeps advertising, and the kernel
believes it stopped. From then on every LE Set Advertising Parameters is
answered Command Disallowed. Captured in the HCI trace on the Pi.

- BlePeripheral resets the adapter once (Adapter1.Powered off and on,
  which sends an HCI Reset) when BlueZ refuses an advertisement that way
  and reports a free slot, then starts again. Once only: the reset drops
  every link on the adapter.
- bluez_restarts also re-registers a running role when an adapter powers
  back on. BlueZ drops every discovery session on power-off, so without
  it the central in the same process went deaf after the reset.

Verified on a wedged Pi: the start reset the adapter once and was
advertising a second later, with the controller accepting the advertising
parameters again.
@bahdotsh
bahdotsh merged commit d73d6a5 into main Oct 16, 2026
27 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant