diff --git a/OmnipodKit/Bluetooth/BluetoothManager.swift b/OmnipodKit/Bluetooth/BluetoothManager.swift index 9d11aba..7d508fa 100644 --- a/OmnipodKit/Bluetooth/BluetoothManager.swift +++ b/OmnipodKit/Bluetooth/BluetoothManager.swift @@ -725,8 +725,8 @@ class BluetoothManager: NSObject { // Disconnect from all devices not in our connection list for device in devices { let peripheral = device.manager.peripheral - if !autoConnectIDs.contains(peripheral.identifier.uuidString) && - (peripheral.state == .connected || peripheral.state == .connecting) + if peripheral.state == .connecting || + (peripheral.state == .connected && !autoConnectIDs.contains(peripheral.identifier.uuidString)) { log.default("Disconnecting from peripheral: %{public}@", peripheral) manager.cancelPeripheralConnection(peripheral) @@ -833,9 +833,15 @@ class BluetoothManager: NSObject { manager.stopScan() for device in devices { let peripheral = device.manager.peripheral - if peripheral.state == .disconnected || peripheral.state == .disconnecting { + switch peripheral.state { + case .connecting where !isConnectWatchdogActive(peripheral): + log.info("discoverPods: Cancelling stale connect: %{public}@", peripheral) + manager.cancelPeripheralConnection(peripheral) + case .disconnected, .disconnecting: log.info("discoverPods: Connecting to peripheral: %{public}@", peripheral) - timedConnect(peripheral) // pairing/discovery — an explicit connect, not auto-reconnect + timedConnect(peripheral) + default: + break } } startScanning() @@ -1570,30 +1576,13 @@ extension BluetoothManager: CBCentralManagerDelegate { connectionDelegate?.omnipodLogDeviceEvent("[pairing] heard pod \(peripheral.identifier.uuidString) pairable=\(podAdvertisement.pairable) state=\(peripheral.state.rawValue)") } if discoveryModeEnabled && podAdvertisement.pairable { - // We've heard our target pairable pod — stop the discovery scan so it doesn't starve the - // connect (an active allowDuplicates scan wedges the connect in .connecting, which is - // what stalled pairing), then connect if it's disconnected. A watchdog-managed connect - // in flight is left alone (it's supervised and will retry itself). + // Stop the scan so it doesn't starve the connect, then connect if disconnected. + // Anything already .connecting is ours: discoverPods cancels stale connects first. if manager.isScanning { manager.stopScan() } if peripheral.state == .disconnected { log.default("Connecting to pairable device %{public}@ in discovery mode", peripheral) connectionDelegate?.omnipodLogDeviceEvent("[pairing] connecting to pairable pod \(peripheral.identifier.uuidString)") - timedConnect(peripheral) // pairing — an explicit connect, not auto-reconnect - } else if peripheral.state == .connecting && !isConnectWatchdogActive(peripheral) { - // ZOMBIE pending connect: we just HEARD this pod advertise, so it is not in a live - // connection — a stale, unsupervised connect request (e.g. from an abandoned pairing - // attempt) is pinning it in .connecting. Field failure mode: every rescan reported - // "heard pod ... state=1" and then declined to connect, so pairing never succeeded. - // Cancel the zombie and connect fresh (re-arming the watchdog) once teardown lands. - log.default("[pairing] pairable pod %{public}@ stuck in .connecting with no watchdog — cancelling zombie connect", peripheral.identifier.uuidString) - connectionDelegate?.omnipodLogDeviceEvent("[pairing] zombie connect on pairable pod — cancelling and reconnecting") - manager.cancelPeripheralConnection(peripheral) - managerQueue.asyncAfter(deadline: .now() + BluetoothManager.eagerConnectTeardownSeconds) { [weak self] in - guard let self = self, self.discoveryModeEnabled, peripheral.state != .connected else { return } - self.log.default("[pairing] reconnecting to pairable pod %{public}@ after zombie teardown", peripheral.identifier.uuidString) - self.connectionDelegate?.omnipodLogDeviceEvent("[pairing] connecting to pairable pod \(peripheral.identifier.uuidString) (post-zombie)") - self.timedConnect(peripheral) - } + timedConnect(peripheral) } } else if autoConnectIDs.contains(peripheral.identifier.uuidString) && peripheral.state == .disconnected { log.debug("Reconnecting to autoconnect device") diff --git a/OmnipodKit/PumpManager/OmniPumpManager.swift b/OmnipodKit/PumpManager/OmniPumpManager.swift index 45a6176..d203605 100644 --- a/OmnipodKit/PumpManager/OmniPumpManager.swift +++ b/OmnipodKit/PumpManager/OmniPumpManager.swift @@ -341,7 +341,16 @@ public class OmniPumpManager: RileyLinkPumpManager { let mustProvide = request != nil let desc = request.map { "last=\($0.lastCGMReadingDate.map { String(describing: $0) } ?? "nil") interval=\(Int($0.expectedCGMReadingInterval))s" } ?? "nil" logDeviceCommunication("[heartbeat] pid=\(pid) setBLEHeartbeatRequest(\(desc))", type: .connection) - if self.state.podType.mayUseRileyLink { + // `mayUseRileyLink` is true for DASH as well as Eros, because DASH *can* use a RileyLink + // under the Pod Keep Alive option. That is the wrong question here: what matters is whether + // a RileyLink is actually in the loop to tick. A DASH pod on direct BLE with Pod Keep Alive + // off took the RileyLink branch, so `provideHeartbeat` was never set and the BLE pod never + // got its heartbeat request -- Loop asked for a heartbeat, we logged it, and dropped it. + // Only bites when the CGM cannot provide the heartbeat itself (a remote/networked CGM such + // as Nightscout); a BLE Dexcom masks it, which is why it went unnoticed. + let rileyLinkIsInUse = self.state.podType.isEros + || (self.state.podType.mayUseRileyLink && self.state.podKeepAlive == .rileyLink) + if rileyLinkIsInUse { rileyLinkDeviceProvider.timerTickEnabled = self.state.isPumpDataStale || mustProvide || /// RL ticks needed for traditional BLE wakups self.state.podKeepAlive == .rileyLink // RL ticks needed for PodKeepAlive rileyLink option