fix: reconnect a bonded peripheral instead of re-pairing it#40
Merged
Conversation
A peripheral that drops while held on this Mac (power cycle, briefly out of range, wake) stays bonded here — its link key is intact — so macOS reconnects it on its own. The auto-reconnect watcher, catching it reachable-but-not-yet-connected, ran IOBluetoothDevicePair.start() on it anyway. Re-pairing an already-bonded device re-runs bonding and forces a disconnect/reconnect cycle, and strands the menu at "(Pairing…)": the pair callback never fires for an already-connected device, and fetchConnectedPeripherals refuses to overwrite the in-flight .connecting, so macOS's own reconnect can't rescue the state. isPaired() separates the two reclaim semantics. A peripheral handed to the peer was `-remove`d (unregisterFromPC), so it is not bonded here and must be paired — the take-from-peer path, unchanged. A peripheral that merely dropped is still bonded, so it is ours: open the connection rather than re-pair. connectPeripheral now adopts/opens a bonded device for every caller, and the watcher skips the HOLDS_ONE peer query for a bonded device since the peer cannot hold one whose link key lives here.
|
🎉 This PR is included in version 2.11.3 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
MegaManSec
added a commit
that referenced
this pull request
Jun 3, 2026
…onded (#41) #40 let the auto-reconnect watcher skip the HOLDS_ONE peer check for a peripheral still bonded to this Mac, on the premise that "the peer can't hold a device whose link key lives here." That premise is wrong: Apple's Magic devices remember multiple hosts and stay bonded to *both* Macs (the README's setup step pairs each peripheral to both), so being paired here says nothing about which Mac is currently connected. The regression: if this Mac drops/sleeps while holding a peripheral and the peer takes it during the downtime (the peer can't reach a sleeping Mac to make it `-remove`, so the bond persists here), this Mac would wake and reconnect-reclaim it without asking — yanking it back from the peer that legitimately holds it. Revert the skip so every reclaim goes through `reclaimIfPeerIsFree` first. The gentle reconnect from #40 stays (connectPeripheral opens the connection instead of re-pairing a bonded device), but it now only runs after HOLDS_ONE confirms the peer is free — which is what actually fixed the reported re-pair/disconnect loop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A peripheral that drops while it is held on this Mac — power-cycling the keyboard, going briefly out of range, waking from sleep — stays bonded to this Mac (its link key is intact, since we never handed it off). macOS reconnects bonded devices on its own. But the auto-reconnect watcher (#36), catching the device reachable but not yet connected, ran
IOBluetoothDevicePair.start()on it anyway.Re-pairing an already-bonded device:
fetchConnectedPeripheralsdeliberately won'''t overwrite an in-flight.connecting, so macOS'''s own reconnect can'''t rescue the state.Fix
isPaired()cleanly separates the two reclaim semantics:unregisterFromPCused the private-removeselector to delete the bond, so the device is not bonded here → it must be paired (the take-from-peer path, unchanged).Two layers:
connectPeripheraladopts a live connection (oropenConnection()s a bonded-but-disconnected one) instead of pairing whenever the device is already bonded — protecting every caller (watcher, wake-reclaim, interactive).probeAndReclaimroutes a bonded device straight to that gentle reconnect and skips theHOLDS_ONEpeer query — the peer can'''t hold a device whose link key lives on this Mac.Testing
xcodebuild ... CODE_SIGNING_ALLOWED=NO buildsucceeds;swift format lintclean.