From 9975dc34007c6d10c89fc492b41c48ca1a5c8f59 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Wed, 3 Jun 2026 16:45:38 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20adopt=20already-connected=20peripheral?= =?UTF-8?q?=20instead=20of=20stranding=20at=20"(Pairing=E2=80=A6)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto-reconnect watcher's reclaim path does a HOLDS_ONE network round-trip to the peer before deciding to re-pair. macOS often reconnects the bonded device on its own during that window, so by the time connectPeripheral runs the device is already connected. Starting an IOBluetoothDevicePair on an already-connected device never fires devicePairingFinished, stranding the state machine at .connecting — the menu shows "(Pairing…)" forever even though the peripheral works. Short-circuit in connectPeripheral: if the device is already connected, adopt the live connection (set .connected, register the disconnect observer) instead of pairing. Covers the interactive take/menu paths too. --- .../Model/Store/BluetoothPeripheralStore.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift index ad5df9c..becf92c 100644 --- a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift +++ b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift @@ -582,6 +582,18 @@ final class BluetoothPeripheralStore: NSObject, ObservableObject, BluetoothPerip return } + // Already connected — e.g. macOS reconnected this bonded peripheral on + // its own while the auto-reconnect watcher's HOLDS_ONE probe was in + // flight, so by the time we got here there's nothing left to pair. + // Starting an `IOBluetoothDevicePair` on an already-connected device + // never fires `devicePairingFinished`, stranding the UI at `.connecting` + // ("(Pairing…)"). Adopt the live connection instead. + if btDevice.isConnected() { + self.setConnectionState(.connected, for: peripheral.id) + self.registerForDisconnect(device: btDevice, address: peripheral.id) + return + } + if btDevice.rssi() == Constants.invalidRSSI { print("\(peripheral.name) is out of range or not responding") self.setConnectionState(.disconnected, for: peripheral.id)