fix(linux): duty-cycle BLE scan so other bonded LE devices can reconnect - #750
pedrosekine wants to merge 1 commit into
Conversation
BleManager set setLowEnergyDiscoveryTimeout(0), so the discovery session stayed open for as long as the scan was wanted - which is the whole time the AirPods are not connected. The Linux kernel will not re-arm the passive accept-list scan while any discovery session is active (hci_update_passive_scan_sync returns early unless discovery.state == DISCOVERY_STOPPED). That passive scan is what reconnects bonded LE peripherals, so while librepods was scanning, no other bonded LE device on the machine could auto-reconnect at all. On my laptop an MX Master 3 and MX Keys stopped reconnecting after suspend and had to be connected by hand every time. Scan in bounded 4s windows with an 8s idle gap instead. The idle gap lets BlueZ drop the discovery session so the kernel can re-arm passive scanning; the next window re-arms ours. This also makes onScanFinished() reachable. With a 0 timeout the agent never emitted finished(), and its restart was guarded by `if (discoveryAgent->isActive())` - a condition that cannot hold in a finished() handler, so the branch was dead either way. Detection coverage is unchanged: on my adapter a single 4s window sees the same 11 Apple manufacturer-data advertisers that a continuous scan does, since advertisers broadcast every 20ms-2s. The cost is up to 8s of extra latency before an advertising device is first noticed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bsha6PUDqP7yiNEMM8UG5a
|
Second independent reproduction of this exact failure, captured at the daemon level — hoping it helps get this merged. Setup: Arch Linux, BlueZ 5.87-2, Intel AX211 (btusb), MX Master 3S (bonded BLE), librepods running headless (vendored copy of the Linux daemon). What we captured (boot-window
So: any always-on discovery client permanently starves passive auto-connect of every other bonded LE peripheral on the machine. Here it reproduced on roughly every second boot, depending on whether the mouse advertised before librepods' session began. Two confirmations of details in this PR:
Applying this diff to my local build now; will report back with results on the AX211 + AirPods Pro 2 combo. |
Problem
BleManagersetssetLowEnergyDiscoveryTimeout(0)("continuous scanning"), so the BlueZ discovery session stays open for as long as scanning is wanted — which is the entire time the AirPods are not connected (initializeBluetooth(), and again on disconnect).The Linux kernel will not re-arm the passive accept-list scan while any discovery session is active —
hci_update_passive_scan_sync()returns early unlessdiscovery.state == DISCOVERY_STOPPED. That passive scan is precisely the mechanism that reconnects bonded LE peripherals.The result: while librepods is scanning, no other bonded LE device on the machine can auto-reconnect. On my laptop, an MX Master 3 mouse and MX Keys keyboard silently stopped reconnecting after suspend and had to be connected by hand every time. With the AirPods left in their case overnight, the adapter sat at
Discovering: yesfor 13 hours straight.It's an easy one to misattribute — it looks like a mouse, BlueZ, or kernel problem, and nothing points at the AirPods daemon.
Reproducing
Stop librepods and a bonded LE mouse reconnects on its own within seconds.
Fix
Scan in bounded 4s windows with an 8s idle gap. The idle gap lets BlueZ drop the discovery session so the kernel can re-arm passive scanning; the next window re-arms ours.
This also makes
onScanFinished()reachable. With a0timeout the agent never emittedfinished(), and the restart inside it was guarded byif (discoveryAgent->isActive())— a condition that cannot hold in afinished()handler, so the branch was dead regardless.isScanning()now also reportstrueacross the idle gap, since its one caller (onSystemGoingToSleep) means "are we in a scanning state", not "is the radio scanning this instant".Detection coverage
Unchanged. On my adapter a single 4s window sees the same 11 Apple manufacturer-data advertisers that a continuous scan does — BLE advertisers broadcast every 20ms–2s, so a 4s window is far longer than needed. The cost is up to 8s of extra latency before an advertising device is first noticed. Once connected, everything runs over the AAP L2CAP link and is untouched by this change.
Happy to make the two constants configurable, or to tune them, if you'd prefer different values.
Testing
On Arch Linux, BlueZ 5.87, Qt 6, MediaTek MT7922 adapter, AirPods Pro:
0x07proximity-pairing parse path. My AirPods never emitted a0x07advert during testing — not in 20s of continuous scanning, nor a 2-minute capture across case open/close cycles — so I had nothing to feed the parser. This patch doesn't touch that code, and a continuous scan saw no0x07either, so I don't believe it's affected. Flagging it rather than implying coverage I don't have.Unrelated issues noticed
Not touched here, happy to file separately:
main()callsQLocalServer::removeServer("app_server")and deletes/tmp/app_serverimmediately before probing it withsocket_check.connectToServer("app_server"), so the connect always fails.--headlessisn't parsed — the arg loop only handles--debugand--hide, though the flag appears in the systemd unit in the README.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bsha6PUDqP7yiNEMM8UG5a