fix: stop the unbond settle from stalling every other peripheral's connect - #111
Merged
MegaManSec merged 2 commits intoAug 20, 2026
Merged
Conversation
…nnect `removeStaleBond` slept 0.5s on `bluetoothQueue` to let `-remove` settle in the daemon. That queue is serial and shared by every in-flight connect, so a full-set switch served peripherals one at a time, each waiting out its predecessors' unbonds — the later rows spending longest bonded to no Mac at all, which is the window where macOS answers the peripheral's own reconnect with its "Connection Request" panel instead of our pair session (#109). Wait by yielding the queue: `removeStaleBond` re-enqueues via `asyncAfter` and hands the re-fetched handle to a completion, moving the pair start into `startDevicePair`. An escalated refresh now pairs blind instead of re-probing RSSI after the teardown — the gate above it already established reachability, and the second probe only widened the window while also being able to abandon the attempt with the bond already gone. Failing after a teardown also stops being silent: it leaves the peripheral paired to nothing, which no retry cadence undoes for the user, so it reports through the announce and watcher gates that suppress an ordinary failure.
The takeover escalation removes a local bond without an RSSI check, on the reasoning that a peer which just released (or vanished) leaves a device that's free but too idle to answer the probe. That holds, and it is what unsticks a take from a locked peer — but it is a guess, and two things stopped it from being a safe one. The watcher's retries are what pay for a wrong guess, and `armReconnect` preserves an existing entry's arm time so `reconnectMaxWindow` counts from the original drop. A teardown minutes into an existing watch therefore inherited almost no time to re-pair. `armReconnectForBondRepair` restarts the window from the teardown, and arms a peripheral whose caller never did; it leaves an entry's reclaim/adoption flavour alone, so the adoption cap still applies. Those retries also don't exist when "reconnect peripherals if they drop" is off, so with the setting off nothing would re-pair the device at all. Make the blind escalation conditional on it and keep the bond otherwise. Read via `UserDefaults` rather than the `@AppStorage` wrapper, which the Bluetooth queue can't safely touch.
MegaManSec
added a commit
that referenced
this pull request
Aug 20, 2026
* fix: unstick taking peripherals from an absent peer Take-from-peer connects open the existing bond first (the connect System Settings performs) and only remove + re-pair when that open is refused; the fresh pair pages continuously instead of gating on an RSSI probe an idle Magic device doesn't answer. A "Pairing…" dropdown row is clickable to cancel the attempt. Live snapshots adopt a connect made outside the app instead of holding "Pairing…" until the watchdog fires. * fix: stop the unbond settle from stalling every other peripheral's connect (#111) * fix: stop the unbond settle from stalling every other peripheral's connect `removeStaleBond` slept 0.5s on `bluetoothQueue` to let `-remove` settle in the daemon. That queue is serial and shared by every in-flight connect, so a full-set switch served peripherals one at a time, each waiting out its predecessors' unbonds — the later rows spending longest bonded to no Mac at all, which is the window where macOS answers the peripheral's own reconnect with its "Connection Request" panel instead of our pair session (#109). Wait by yielding the queue: `removeStaleBond` re-enqueues via `asyncAfter` and hands the re-fetched handle to a completion, moving the pair start into `startDevicePair`. An escalated refresh now pairs blind instead of re-probing RSSI after the teardown — the gate above it already established reachability, and the second probe only widened the window while also being able to abandon the attempt with the bond already gone. Failing after a teardown also stops being silent: it leaves the peripheral paired to nothing, which no retry cadence undoes for the user, so it reports through the announce and watcher gates that suppress an ordinary failure. * fix: make a blind bond teardown recoverable before taking one on The takeover escalation removes a local bond without an RSSI check, on the reasoning that a peer which just released (or vanished) leaves a device that's free but too idle to answer the probe. That holds, and it is what unsticks a take from a locked peer — but it is a guess, and two things stopped it from being a safe one. The watcher's retries are what pay for a wrong guess, and `armReconnect` preserves an existing entry's arm time so `reconnectMaxWindow` counts from the original drop. A teardown minutes into an existing watch therefore inherited almost no time to re-pair. `armReconnectForBondRepair` restarts the window from the teardown, and arms a peripheral whose caller never did; it leaves an entry's reclaim/adoption flavour alone, so the adoption cap still applies. Those retries also don't exist when "reconnect peripherals if they drop" is off, so with the setting off nothing would re-pair the device at all. Make the blind escalation conditional on it and keep the bond otherwise. Read via `UserDefaults` rather than the `@AppStorage` wrapper, which the Bluetooth queue can't safely touch.
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.
Stacked on #108 — review that first; this targets its branch and will retarget to
mainwhen it merges.Unbond settle stops stalling the queue
removeStaleBondslept 0.5s onbluetoothQueueto let-removesettle. That queue is serial and shared by every in-flight connect, so a full-set switch served peripherals one at a time, each waiting out its predecessors' unbonds. The later rows spent longest bonded to no Mac at all — the window in which macOS answers the peripheral's own reconnect with the "Connection Request" panel from #109 instead of our pair session.removeStaleBondre-enqueues viaasyncAfterand hands the re-fetched handle to a completion. The pair start moves intostartDevicePairso both paths share it, and the completion re-checksisCurrentAttemptsince the gap is now supersedable.The blind teardown becomes recoverable
#108's takeover escalation removes a bond with no RSSI check, reasoning that a peer which just released (or vanished) leaves a device that's free but too idle to answer the probe. That's right, and it's what unsticks a take from a locked peer — but it's a guess, and two things stopped it being a safe one.
armReconnectpreserves an existing entry's arm time, soreconnectMaxWindowcounts from the original drop. A teardown minutes into an existing watch inherited almost no time to re-pair.armReconnectForBondRepairrestarts the window from the teardown, and arms a peripheral whose caller never did. It leaves the entry's reclaim/adoption flavour alone, so the adoption attempt cap still applies.UserDefaultsrather than the@AppStoragewrapper, which the Bluetooth queue can't safely touch.Verified with
swift formatonly — no Xcode on this machine, so this hasn't been built or run against hardware.