diff --git a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift index 95534db..fcd5601 100644 --- a/Magic Switch/Model/Store/BluetoothPeripheralStore.swift +++ b/Magic Switch/Model/Store/BluetoothPeripheralStore.swift @@ -337,9 +337,11 @@ final class BluetoothPeripheralStore: NSObject, ObservableObject, BluetoothPerip self.intentionalReleases.removeValue(forKey: id) guard let peripheral = self.peripherals.first(where: { $0.id == id }) else { continue } guard let device = NetworkDeviceStore.shared.networkDevices.first, + device.pendingFingerprint == nil, PairingStore.shared.isPaired else { - // No peer to ask — these are ours; take them back. + // No trusted peer to ask — none registered, or one flagged as a + // TOFU identity mismatch. Either way, reclaim locally. self.connectPeripheral(peripheral) continue } @@ -1221,9 +1223,12 @@ final class BluetoothPeripheralStore: NSObject, ObservableObject, BluetoothPerip private func reclaimIfPeerIsFree(_ peripheral: BluetoothPeripheral) { let id = peripheral.id guard PairingStore.shared.isPaired, - let device = NetworkDeviceStore.shared.networkDevices.first + let device = NetworkDeviceStore.shared.networkDevices.first, + device.pendingFingerprint == nil else { - // No peer to consult — it's ours; take it. + // No trusted peer to consult — none registered, or one flagged as a + // TOFU identity mismatch. Either way it's ours; reclaim locally rather + // than auto-probing an untrusted peer with our now-stale key. reconnectInFlight.remove(id) connectPeripheral(peripheral, announcePairTimeout: false) return diff --git a/Magic Switch/Model/Store/NetworkDeviceStore.swift b/Magic Switch/Model/Store/NetworkDeviceStore.swift index bdd3769..b1325d1 100644 --- a/Magic Switch/Model/Store/NetworkDeviceStore.swift +++ b/Magic Switch/Model/Store/NetworkDeviceStore.swift @@ -513,7 +513,12 @@ extension NetworkDeviceStore { on device: NetworkDevice, completion: @escaping (Result) -> Void ) { - sendTwoFrameCommand(.holdsOne, payload: address, to: device, completion: completion) + // `HOLDS_ONE` is only ever a background watcher/reclaim probe, never a user + // action — opt out of the outbound limiter so its fixed cadence can't trip + // the limiter that gates real switches (mirrors the reachability poll). + sendTwoFrameCommand( + .holdsOne, payload: address, to: device, countsTowardRateLimit: false, + completion: completion) } /// Shared helper for "opcode + single payload frame, await OP_SUCCESS". @@ -524,13 +529,16 @@ extension NetworkDeviceStore { _ command: DeviceCommand, payload: String, to device: NetworkDevice, + countsTowardRateLimit: Bool = true, completion: @escaping (Result) -> Void ) { guard PairingStore.shared.isPaired else { completion(.failure(.notPaired)) return } - let outgoing = OutgoingConnection(host: device.host, port: UInt16(device.port)) + let outgoing = OutgoingConnection( + host: device.host, port: UInt16(device.port), + countsTowardRateLimit: countsTowardRateLimit) outgoing.run( body: { channel, done in channel.send(Data(command.rawValue.utf8)) { err in