From 54a923a413a93cbc69c547da04060e3ca0a8efc6 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 22 Jul 2026 18:59:51 -0500 Subject: [PATCH] Reconnect: on a failed attempt, engage a scan to recover an inert standing connect Field log (device-log diagnostic) showed the failure this fixes: after a restored-cache handshake failure ("device disconnected from us"), the re-armed standing connect was inert -- the peripheral sat .disconnected (peripheral.state=0) with iOS silently dropping the pending connect, and with no fallback it stalled for 15 minutes. The failed reconnect ATTEMPT is the error evidence, so in the re-arm's "disconnected, nothing pending" branch we now ALSO start a scan (guarded on .poweredOn). The scan makes iOS actively reacquire the sensor; a fresh discovery re-declares the connect on a fresh handle (handleScannerEvent(.didDiscover)), driving a fresh connect + fresh handshake. The scan is stopped the moment a session is established. Targeted (only after a failure), not an always-on scan. --- .../Pairing/LibreLoopCGMManager+Pairing.swift | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift b/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift index 4af7f85..51a75b8 100644 --- a/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift +++ b/LibreLoop/Pairing/LibreLoopCGMManager+Pairing.swift @@ -180,6 +180,8 @@ extension LibreLoopCGMManager { Task { await delegate?.retractAlert(identifier: id) } } self.monitor = monitor + // Session established -- stop any failure-recovery scan armed by the reconnect re-arm. + scanner.stopScan() self.connectedAt = Date() // Fresh session: hasn't streamed a reading yet. Drives the // connect→no-stream livelock accounting in handleMonitorDisconnect. @@ -895,13 +897,19 @@ extension LibreLoopCGMManager { // here too would be a duplicate within ms. scanner.cancelConnection(peripheral) } else { - // No active link, no in-flight Task, no CB event - // pending. Explicitly re-arm. scheduleReconnect - // always re-arms the CB connect intent at its top - // (idempotent), and its 0.5s debounce throttles - // Task spawning so this can't tight-loop with the - // events listener's own scheduleReconnect calls. - await MainActor.run { self.scheduleReconnect() } + // No active link, no in-flight Task, no CB event pending. Re-arm the standing connect -- + // BUT a bare standing connect can be inert here: a restored-cache handshake failure can leave + // the peripheral .disconnected with iOS silently dropping the pending connect (field log: + // 15-min stall, diagnostic showed peripheral.state=0). The failed ATTEMPT is our error + // evidence, so ALSO engage a scan: it makes iOS actively reacquire the sensor, and a fresh + // discovery re-declares the connect on a fresh handle (.didDiscover) -> a fresh connect. + // Stopped the moment a session is established (see `self.monitor = monitor`). + await MainActor.run { + self.scheduleReconnect() + if self.scanner.centralState == .poweredOn { + self.scanner.startScan() + } + } } } }