fix: unstick taking peripherals from an absent peer - #108
Merged
Conversation
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.
…nnect (#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 PR is included in version 2.25.6 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
MegaManSec
added a commit
that referenced
this pull request
Aug 20, 2026
#103 shipped the bond refresh deliberately narrow — its title names the condition, "when a bonded device is in range but refuses to open." #108 extended it to takeovers by replacing that requirement with `(skipRangeCheck && autoReconnectIsOn)`. An absent peer and an absent peripheral are different facts. A registered peripheral that is merely switched off matches "bonded but refuses openConnection" exactly, so a takeover removed its healthy pairing record. `takeAllPeripherals` loops every registered peripheral, so the headline menu-bar flow reaches it, as do the display-dock trigger and ADOPT_RELEASED; auto-reconnect defaults to on, which is all the old gate required. The documented recovery cannot fire: `probeAndReclaim` gates on RSSI, and an unbonded Magic device stops answering until it is power cycled — see the comment above `directReclaimAfterWake` and README's double-sleep note. The refresh now requires the probe on every path, which also drops the last use of `autoReconnectIsOn`. Also from the same review: - removeStaleBond re-checks the attempt token immediately before `-remove`. The caller's gate runs a blocking `rssi()` after its own check, and a cancel landing in that window lost a bond it could not re-pair. - The bonded-open success arm takes the same `isCurrentAttempt` guard the failure arm three lines below already had, so a cancel during the blocking open is no longer silently undone. - bondsAwaitingRepair clears when the pair succeeds, not only on `.connected`. A re-pair that succeeded but whose openConnection failed reported "Pairing Was Reset" and told the user to rebuild a pairing that was intact — and README documents that stuck-device case as common. - cancelConnect reports a bond the attempt already removed. It stands the watcher down, so nothing else would have rebuilt it. - handlePairTimeout supersedes the attempt it just retired, so a Bluetooth-queue block still behind a blocking open cannot start a pair with no watchdog. - HOLDS_ONE answers from live Bluetooth state again. The registered-list precheck made this Mac reply "not holding" for a peripheral it was actively using, and the peer treats any failure as permission to reclaim; the two lists only converge on a manual Sync. - takeReleasesInFlight counts instead of flagging, so the first of two overlapping takes cannot clear the guard while the other is on the wire. - The connected-row adoption in fetchConnectedPeripherals goes through setConnectionState, so it completes the peer's connect waiter instead of leaving it to time out. - An unproven resolve can no longer relocate a record parked behind an Identity Mismatch, which also silently cleared the parking. - Corrects the comment claiming Magic devices stay bonded to both Macs.
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.
Taking peripherals from a locked or sleeping Mac could sit at "Pairing…" for minutes with no way out, while a manual connect from System Settings worked immediately.
openConnection()on the existing bond first (the same cheap connect System Settings does) and only remove + re-pair when the open is refused; the fresh pair pages continuously instead of gating on an RSSI probe an idle Magic device doesn't answer.Also fixes a shadowed
windowcheck inMenuRowControl.mouseDownthat made its stale-row guard a no-op.