diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 2df637f27..4f2d6dc64 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -310,26 +310,40 @@ final class DeviceDataManager { func instantiateDeviceManagers() { if let pumpManagerRawValue = rawPumpManager ?? UserDefaults.appGroup?.legacyPumpManagerRawValue { - pumpManager = pumpManagerFromRawValue(pumpManagerRawValue) - // Update lastPumpEventsReconciliation on DoseStore - if let lastSync = pumpManager?.lastSync { - Task { - try? await doseStore.addPumpEvents([], lastReconciliation: lastSync) + if let restoredPumpManager = pumpManagerFromRawValue(pumpManagerRawValue) { + pumpManager = restoredPumpManager + // Update lastPumpEventsReconciliation on DoseStore + if let lastSync = pumpManager?.lastSync { + Task { + try? await doseStore.addPumpEvents([], lastReconciliation: lastSync) + } } - } - if let status = pumpManager?.status { - updatePumpIsAllowingAutomation(status: status) + if let status = pumpManager?.status { + updatePumpIsAllowingAutomation(status: status) + } + } else { + // We have saved pump state, but its plugin couldn't be instantiated in this build (e.g. + // the app was built without that pump manager plugin). Do NOT assign pumpManager = nil — + // that fires the didSet, which writes rawPumpManager = nil and DELETES the persisted + // PumpManagerState, permanently losing the pod pairing/session keys. Leave the saved + // state on disk so a build that includes the plugin can restore it on a later launch. + log.error("Saved pump manager plugin unavailable; preserving persisted PumpManagerState for a future launch") } } else { pumpManager = nil } if let cgmManagerRawValue = rawCGMManager ?? UserDefaults.appGroup?.legacyCGMManagerRawValue { - cgmManager = cgmManagerFromRawValue(cgmManagerRawValue) - - // Handle case of PumpManager providing CGM - if cgmManager == nil && pumpManagerTypeFromRawValue(cgmManagerRawValue) != nil { + if let restoredCGMManager = cgmManagerFromRawValue(cgmManagerRawValue) { + cgmManager = restoredCGMManager + } else if pumpManagerTypeFromRawValue(cgmManagerRawValue) != nil { + // Handle case of PumpManager providing CGM cgmManager = pumpManager as? CGMManager + } else { + // Saved CGM state exists but its plugin couldn't be instantiated in this build. Don't + // assign cgmManager = nil — that would delete the persisted CGMManagerState (same + // footgun as the pump path above). Preserve it for a future launch with the plugin. + log.error("Saved CGM manager plugin unavailable; preserving persisted CGMManagerState for a future launch") } } } @@ -837,7 +851,17 @@ extension DeviceDataManager { } func updatePumpManagerBLEHeartbeatPreference() { - pumpManager?.setMustProvideBLEHeartbeat(pumpManagerMustProvideBLEHeartbeat) + guard pumpManagerMustProvideBLEHeartbeat else { + pumpManager?.setBLEHeartbeatRequest(nil) + return + } + // Tell the pump when the last CGM reading landed and how often readings are expected, so it can + // schedule its next heartbeat to arrive just after the next reading is due (the pump adds its own + // buffer for the remote CGM value to be fetched and stored). + let request = PumpHeartbeatRequest( + lastCGMReadingDate: glucoseStore.latestGlucose?.startDate, + expectedCGMReadingInterval: cgmManager?.expectedGlucoseSampleInterval ?? .minutes(5)) + pumpManager?.setBLEHeartbeatRequest(request) } } diff --git a/Loop/Managers/Live Activity/LiveActivityManager.swift b/Loop/Managers/Live Activity/LiveActivityManager.swift index 179b4a053..9f480877d 100644 --- a/Loop/Managers/Live Activity/LiveActivityManager.swift +++ b/Loop/Managers/Live Activity/LiveActivityManager.swift @@ -115,7 +115,7 @@ class LiveActivityManager : LiveActivityManagerProxy { let statusContext = UserDefaults.appGroup?.statusExtensionContext let glucoseFormatter = NumberFormatter.glucoseFormatter(for: unit) - let glucoseSamples = self.getGlucoseSample(unit: unit) + let glucoseSamples = await self.getGlucoseSample(unit: unit) guard let currentGlucose = glucoseSamples.last else { print("ERROR: No glucose sample found...") return @@ -327,24 +327,19 @@ class LiveActivityManager : LiveActivityManagerProxy { return iobFormatter.string(from: iob) ?? "??" } - private func getGlucoseSample(unit: LoopUnit) -> [StoredGlucoseSample] { - let updateGroup = DispatchGroup() - var samples: [StoredGlucoseSample] = [] - + private func getGlucoseSample(unit: LoopUnit) async -> [StoredGlucoseSample] { // When in spacious mode, we want to show the predictive line // In compact mode, we only want to show the history let timeInterval: TimeInterval = self.settings.addPredictiveLine ? .hours(-2) : .hours(-6) - updateGroup.enter() - Task { - samples = (try? await self.glucoseStore.getGlucoseSamples( - start: adjustedChartStart(Date.now.addingTimeInterval(timeInterval)), - end: Date.now - )) ?? [] - updateGroup.leave() - } - - _ = updateGroup.wait(timeout: .distantFuture) - return samples + + // NOTE: Previously this bridged async→sync via DispatchGroup.wait(.distantFuture), + // which blocked a Swift Concurrency cooperative-pool thread. Because update() itself + // runs on that pool, repeated calls starved every cooperative thread and deadlocked + // the whole concurrency runtime (Loop stopped looping). Await directly instead. + return (try? await self.glucoseStore.getGlucoseSamples( + start: adjustedChartStart(Date.now.addingTimeInterval(timeInterval)), + end: Date.now + )) ?? [] } // If the chart start falls past the half-hour mark (HH:31–HH:59), pull it back to HH:30