polscm32 1 anno fa
parent
commit
902dbe01cb

+ 118 - 107
Trio/Sources/APS/APSManager.swift

@@ -20,8 +20,8 @@ protocol APSManager {
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     var isManualTempBasal: Bool { get }
     func enactTempBasal(rate: Double, duration: TimeInterval) async
-    func determineBasal() async throws -> Bool
-    func determineBasalSync() async
+    func determineBasal() async throws
+    func determineBasalSync() async throws
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination?
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
@@ -194,105 +194,131 @@ final class BaseAPSManager: APSManager, Injectable {
         Task { [weak self] in
             guard let self else { return }
 
-            // check the last start of looping is more the loopInterval but the previous loop was completed
-            if self.lastLoopDate > self.lastLoopStartDate {
-                guard self.lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
-                    debug(.apsManager, "too close to do a loop : \(self.lastLoopStartDate)")
-                    return
+            // Check if we can start a new loop
+            guard await self.canStartNewLoop() else { return }
+
+            // Setup loop and background task
+            var (loopStatRecord, backgroundTask) = await self.setupLoop()
+
+            do {
+                // Execute loop logic
+                try await self.executeLoop(loopStatRecord: &loopStatRecord)
+
+                // Upload data to Nightscout if available
+                if let nightscoutManager = self.nightscout {
+                    await nightscoutManager.uploadCarbs()
+                    await nightscoutManager.uploadPumpHistory()
+                    await nightscoutManager.uploadOverrides()
+                    await nightscoutManager.uploadTempTargets()
                 }
+            } catch {
+                var updatedStats = loopStatRecord
+                updatedStats.end = Date()
+                updatedStats.duration = roundDouble((updatedStats.end! - updatedStats.start).timeInterval / 60, 2)
+                updatedStats.loopStatus = error.localizedDescription
+                await loopCompleted(error: error, loopStatRecord: updatedStats)
+                debug(.apsManager, "\(DebuggingIdentifiers.failed) Failed to complete Loop: \(error.localizedDescription)")
             }
 
-            guard !self.isLooping.value else {
-                warning(.apsManager, "Loop already in progress. Skip recommendation.")
-                return
+            // Cleanup background task
+            if let backgroundTask = backgroundTask {
+                await UIApplication.shared.endBackgroundTask(backgroundTask)
+                self.backGroundTaskID = .invalid
             }
+        }
+    }
 
-            // start background time extension
-            self.backGroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") { [weak self] in
-                guard let self, let backgroundTask = self.backGroundTaskID else { return }
-                Task {
-                    UIApplication.shared.endBackgroundTask(backgroundTask)
-                }
-                self.backGroundTaskID = .invalid
+    private func canStartNewLoop() async -> Bool {
+        // Check if too soon for next loop
+        if lastLoopDate > lastLoopStartDate {
+            guard lastLoopStartDate.addingTimeInterval(Config.loopInterval) < Date() else {
+                debug(.apsManager, "too close to do a loop : \(lastLoopStartDate)")
+                return false
             }
+        }
 
-            self.lastLoopStartDate = Date()
+        // Check if loop already in progress
+        guard !isLooping.value else {
+            warning(.apsManager, "Loop already in progress. Skip recommendation.")
+            return false
+        }
 
-            var previousLoop = [LoopStatRecord]()
-            var interval: Double?
+        return true
+    }
 
-            do {
-                try await self.privateContext.perform {
-                    let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
-                    let sortStats = NSSortDescriptor(key: "end", ascending: false)
-                    requestStats.sortDescriptors = [sortStats]
-                    requestStats.fetchLimit = 1
-                    previousLoop = try self.privateContext.fetch(requestStats)
-
-                    if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
-                        interval = self.roundDouble(
-                            (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
-                            1
-                        )
-                    }
-                }
-            } catch let error as NSError {
-                debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error.userInfo)"
-                )
+    private func setupLoop() async -> (LoopStats, UIBackgroundTaskIdentifier?) {
+        // Start background task
+        let backgroundTask = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") { [weak self] in
+            guard let self, let backgroundTask = self.backGroundTaskID else { return }
+            Task {
+                UIApplication.shared.endBackgroundTask(backgroundTask)
             }
+            self.backGroundTaskID = .invalid
+        }
+        backGroundTaskID = backgroundTask
 
-            var loopStatRecord = LoopStats(
-                start: self.lastLoopStartDate,
-                loopStatus: "Starting",
-                interval: interval
-            )
+        // Set loop start time
+        lastLoopStartDate = Date()
 
-            self.isLooping.send(true)
+        // Calculate interval from previous loop
+        let interval = await calculateLoopInterval()
 
-            do {
-                if try await !self.determineBasal() {
-                    throw APSError.apsError(message: "Determine basal failed")
-                }
+        // Create initial loop stats record
+        let loopStatRecord = LoopStats(
+            start: lastLoopStartDate,
+            loopStatus: "Starting",
+            interval: interval
+        )
 
-                // Open loop completed
-                guard self.settings.closedLoop else {
-                    loopStatRecord.end = Date()
-                    loopStatRecord.duration = self.roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                    loopStatRecord.loopStatus = "Success"
-                    await self.loopCompleted(loopStatRecord: loopStatRecord)
-                    return
-                }
+        isLooping.send(true)
 
-                // Closed loop - enact Determination
-                try await self.enactDetermination()
-                loopStatRecord.end = Date()
-                loopStatRecord.duration = self.roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                loopStatRecord.loopStatus = "Success"
-                await self.loopCompleted(loopStatRecord: loopStatRecord)
-            } catch {
-                loopStatRecord.end = Date()
-                loopStatRecord.duration = self.roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
-                loopStatRecord.loopStatus = error.localizedDescription
-                await self.loopCompleted(error: error, loopStatRecord: loopStatRecord)
-            }
+        return (loopStatRecord, backgroundTask)
+    }
 
-            if let nightscoutManager = self.nightscout {
-                await nightscoutManager.uploadCarbs()
-                await nightscoutManager.uploadPumpHistory()
-                await nightscoutManager.uploadOverrides()
-                await nightscoutManager.uploadTempTargets()
-            }
+    private func executeLoop(loopStatRecord: inout LoopStats) async throws {
+        try await determineBasal()
 
-            // End background task after all the operations are completed
-            if let backgroundTask = self.backGroundTaskID {
-                await UIApplication.shared.endBackgroundTask(backgroundTask)
-                self.backGroundTaskID = .invalid
+        // Handle open loop
+        guard settings.closedLoop else {
+            loopStatRecord.end = Date()
+            loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+            loopStatRecord.loopStatus = "Success"
+            await loopCompleted(loopStatRecord: loopStatRecord)
+            return
+        }
+
+        // Handle closed loop
+        try await enactDetermination()
+        loopStatRecord.end = Date()
+        loopStatRecord.duration = roundDouble((loopStatRecord.end! - loopStatRecord.start).timeInterval / 60, 2)
+        loopStatRecord.loopStatus = "Success"
+        await loopCompleted(loopStatRecord: loopStatRecord)
+    }
+
+    private func calculateLoopInterval() async -> Double? {
+        do {
+            return try await privateContext.perform {
+                let requestStats = LoopStatRecord.fetchRequest() as NSFetchRequest<LoopStatRecord>
+                let sortStats = NSSortDescriptor(key: "end", ascending: false)
+                requestStats.sortDescriptors = [sortStats]
+                requestStats.fetchLimit = 1
+                let previousLoop = try self.privateContext.fetch(requestStats)
+
+                if (previousLoop.first?.end ?? .distantFuture) < self.lastLoopStartDate {
+                    return self.roundDouble(
+                        (self.lastLoopStartDate - (previousLoop.first?.end ?? Date())).timeInterval / 60,
+                        1
+                    )
+                }
+                return nil
             }
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch the last loop with error: \(error)")
+            return nil
         }
     }
 
-//     Loop exit point
+    // Loop exit point
     private func loopCompleted(error: Error? = nil, loopStatRecord: LoopStats) async {
         isLooping.send(false)
 
@@ -355,7 +381,7 @@ final class BaseAPSManager: APSManager, Injectable {
         return false
     }
 
-    func determineBasal() async throws -> Bool {
+    func determineBasal() async throws {
         debug(.apsManager, "Start determine basal")
 
         // Fetch glucose asynchronously
@@ -390,7 +416,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard isValidGlucoseData else {
             debug(.apsManager, "Glucose validation failed")
             processError(APSError.glucoseError(message: "Glucose validation failed"))
-            return false
+            return
         }
 
         do {
@@ -412,25 +438,14 @@ final class BaseAPSManager: APSManager, Injectable {
                         $0.determinationDidUpdate(determination)
                     }
                 }
-                return true
-            } else {
-                return false
             }
         } catch {
-            debug(.apsManager, "Error determining basal: \(error)")
-            return false
+            throw APSError.apsError(message: "Error determining basal: \(error.localizedDescription)")
         }
     }
 
-    func determineBasalSync() async {
-        do {
-            _ = try await determineBasal()
-        } catch {
-            debug(
-                .apsManager,
-                "\(DebuggingIdentifiers.failed) Error performing determine basal sync: \(error.localizedDescription)"
-            )
-        }
+    func determineBasalSync() async throws {
+        _ = try await determineBasal()
     }
 
     func simulateDetermineBasal(simulatedCarbsAmount: Decimal, simulatedBolusAmount: Decimal) async -> Determination? {
@@ -467,13 +482,11 @@ final class BaseAPSManager: APSManager, Injectable {
 
         if let error = verifyStatus() {
             processError(error)
+            // Capture broadcaster and queue before async context
             let broadcaster = self.broadcaster
-            let processQueue = self.processQueue
             Task { @MainActor in
-                if let broadcaster = broadcaster {
-                    broadcaster.notify(BolusFailureObserver.self, on: processQueue) {
-                        $0.bolusDidFail()
-                    }
+                broadcaster?.notify(BolusFailureObserver.self, on: .main) {
+                    $0.bolusDidFail()
                 }
             }
             callback?(false, "Error! Failed to enact bolus.")
@@ -490,7 +503,7 @@ final class BaseAPSManager: APSManager, Injectable {
             try await pump.enactBolus(units: roundedAmount, automatic: isSMB)
             debug(.apsManager, "Bolus succeeded")
             if !isSMB {
-                await determineBasalSync()
+                try await determineBasalSync()
             }
             bolusProgress.send(0)
             callback?(true, "Bolus enacted successfully.")
@@ -498,13 +511,11 @@ final class BaseAPSManager: APSManager, Injectable {
             warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
             processError(APSError.pumpError(error))
             if !isSMB {
+                // Use MainActor to handle broadcaster notification
                 let broadcaster = self.broadcaster
-                let processQueue = self.processQueue
                 Task { @MainActor in
-                    if let broadcaster = broadcaster {
-                        broadcaster.notify(BolusFailureObserver.self, on: processQueue) {
-                            $0.bolusDidFail()
-                        }
+                    broadcaster?.notify(BolusFailureObserver.self, on: .main) {
+                        $0.bolusDidFail()
                     }
                 }
             }

+ 1 - 1
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -366,7 +366,7 @@ final class OpenAPS {
 
             return determination
         } else {
-            return nil
+            throw APSError.apsError(message: "Determination is nil")
         }
     }
 

+ 26 - 18
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -105,16 +105,20 @@ extension DataTable {
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
             Task {
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
+                do {
+                    try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
 
-                await MainActor.run {
-                    carbEntryDeleted = true
-                    waitForSuggestion = true
+                    await MainActor.run {
+                        carbEntryDeleted = true
+                        waitForSuggestion = true
+                    }
+                } catch {
+                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error.localizedDescription)")
                 }
             }
         }
 
-        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async {
+        func deleteCarbs(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) async throws {
             // Delete from Nightscout/Apple Health/Tidepool
             await deleteFromServices(treatmentObjectID, isFPUDeletion: isFpuOrComplexMeal)
 
@@ -122,7 +126,7 @@ extension DataTable {
             await carbsStorage.deleteCarbsEntryStored(treatmentObjectID)
 
             // Perform a determine basal sync to update cob
-            await apsManager.determineBasalSync()
+            try await apsManager.determineBasalSync()
         }
 
         /// Deletes carb and FPU entries from all connected services (Nightscout, HealthKit, Tidepool)
@@ -208,16 +212,20 @@ extension DataTable {
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeInsulinDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
             Task {
-                await invokeInsulinDeletion(treatmentObjectID)
+                do {
+                    try await invokeInsulinDeletion(treatmentObjectID)
 
-                await MainActor.run {
-                    insulinEntryDeleted = true
-                    waitForSuggestion = true
+                    await MainActor.run {
+                        insulinEntryDeleted = true
+                        waitForSuggestion = true
+                    }
+                } catch {
+                    debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete insulin entry: \(error)")
                 }
             }
         }
 
-        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async {
+        func invokeInsulinDeletion(_ treatmentObjectID: NSManagedObjectID) async throws {
             do {
                 let authenticated = try await unlockmanager.unlock()
 
@@ -233,7 +241,7 @@ extension DataTable {
                 await CoreDataStack.shared.deleteObject(identifiedBy: treatmentObjectID)
 
                 // Perform a determine basal sync to update iob
-                await apsManager.determineBasalSync()
+                try await apsManager.determineBasalSync()
             } catch {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
@@ -295,7 +303,7 @@ extension DataTable {
                 guard let originalEntry = await getOriginalEntryValues(treatmentObjectID) else { return }
 
                 // Deletion logic for carb and FPU entries
-                await deleteOldEntries(
+                try await deleteOldEntries(
                     treatmentObjectID,
                     originalEntry: originalEntry,
                     newCarbs: newCarbs,
@@ -314,7 +322,7 @@ extension DataTable {
 
                 await syncWithServices()
                 // Perform a determine basal sync to update cob
-                await apsManager.determineBasalSync()
+                try await apsManager.determineBasalSync()
             }
         }
 
@@ -360,24 +368,24 @@ extension DataTable {
             newFat _: Decimal,
             newProtein _: Decimal,
             newNote _: String
-        ) async {
+        ) async throws {
             if ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
                 ((originalEntry.entryValues?.carbs ?? 0) == 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
             {
                 // Delete the zero-carb-entry and all its carb equivalents connected by the same fpuID from remote services and Core Data
                 // Use fpuID
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
             } else if ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.fat ?? 0) > 0) ||
                 ((originalEntry.entryValues?.carbs ?? 0) > 0 && (originalEntry.entryValues?.protein ?? 0) > 0)
             {
                 // Delete carb entry and carb equivalents that are all connected by the same fpuID from remote services and Core Data
                 // Use fpuID
-                await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
+                try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: true)
 
             } else {
                 // Delete just the carb entry since there are no carb equivalents
                 // Use NSManagedObjectID
-                await deleteCarbs(treatmentObjectID)
+                try await deleteCarbs(treatmentObjectID)
             }
         }
 

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -508,7 +508,7 @@ extension Home {
                 await apsManager.cancelBolus(nil)
 
                 // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
-                await apsManager.determineBasalSync()
+                try await apsManager.determineBasalSync()
             }
         }
 

+ 10 - 25
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -40,13 +40,11 @@ extension Treatments {
         var minDelta: Decimal = 0
         var expectedDelta: Decimal = 0
         var minPredBG: Decimal = 0
-        var waitForSuggestion: Bool = false
+        var isAwaitingDeterminationResult: Bool = false
         var carbRatio: Decimal = 0
 
         var addButtonPressed: Bool = false
 
-        var waitForSuggestionInitial: Bool = false
-
         var target: Decimal = 0
         var cob: Int16 = 0
         var iob: Decimal = 0
@@ -185,19 +183,6 @@ extension Treatments {
                             self.registerObservers()
                         }
 
-                        if self.waitForSuggestionInitial {
-                            group.addTask {
-                                let isDetermineBasalSuccessful = try await self.apsManager.determineBasal()
-                                if !isDetermineBasalSuccessful {
-                                    await MainActor.run {
-                                        self.waitForSuggestion = false
-                                        self.insulinRequired = 0
-                                        self.insulinRecommended = 0
-                                    }
-                                }
-                            }
-                        }
-
                         // Wait for all tasks to complete
                         try await group.waitForAll()
                     }
@@ -440,7 +425,7 @@ extension Treatments {
 
                 guard glucoseStorage.isGlucoseDataFresh(date) else {
                     await MainActor.run {
-                        waitForSuggestion = false
+                        isAwaitingDeterminationResult = false
                     }
                     return hideModal()
                 }
@@ -458,7 +443,7 @@ extension Treatments {
             }
 
             await MainActor.run {
-                self.waitForSuggestion = true
+                self.isAwaitingDeterminationResult = true
             }
         }
 
@@ -480,7 +465,7 @@ extension Treatments {
             } catch {
                 print("authentication error for pump bolus: \(error.localizedDescription)")
                 await MainActor.run {
-                    self.waitForSuggestion = false
+                    self.isAwaitingDeterminationResult = false
                     if self.addButtonPressed {
                         self.hideModal()
                     }
@@ -506,14 +491,14 @@ extension Treatments {
                     // store external dose to pump history
                     await pumpHistoryStorage.storeExternalInsulinEvent(amount: amount, timestamp: date)
                     // perform determine basal sync
-                    await apsManager.determineBasalSync()
+                    try await apsManager.determineBasalSync()
                 } else {
                     print("authentication failed")
                 }
             } catch {
                 print("authentication error for external insulin: \(error.localizedDescription)")
                 await MainActor.run {
-                    self.waitForSuggestion = false
+                    self.isAwaitingDeterminationResult = false
                     if self.addButtonPressed {
                         self.hideModal()
                     }
@@ -551,9 +536,9 @@ extension Treatments {
                 // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
                 if amount <= 0 {
                     await MainActor.run {
-                        self.waitForSuggestion = true
+                        self.isAwaitingDeterminationResult = true
                     }
-                    await apsManager.determineBasalSync()
+                    try await apsManager.determineBasalSync()
                 }
             } catch {
                 debug(.default, "\(DebuggingIdentifiers.failed) Failed to save carbs: \(error.localizedDescription)")
@@ -612,7 +597,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
 
         DispatchQueue.main.async {
             debug(.bolusState, "determinationDidUpdate fired")
-            self.waitForSuggestion = false
+            self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
                 self.hideModal()
             }
@@ -622,7 +607,7 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
     func bolusDidFail() {
         DispatchQueue.main.async {
             debug(.bolusState, "bolusDidFail fired")
-            self.waitForSuggestion = false
+            self.isAwaitingDeterminationResult = false
             if self.addButtonPressed {
                 self.hideModal()
             }

+ 2 - 16
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -308,9 +308,9 @@ extension Treatments {
                     }
                     .listSectionSpacing(sectionSpacing)
                 }
-                .blur(radius: state.waitForSuggestion ? 5 : 0)
+                .blur(radius: state.isAwaitingDeterminationResult ? 5 : 0)
 
-                if state.waitForSuggestion {
+                if state.isAwaitingDeterminationResult {
                     CustomProgressView(text: progressText.rawValue)
                 }
             }
@@ -507,17 +507,3 @@ extension Treatments {
         }
     }
 }
-
-// fix iOS 15 bug
-struct ActivityIndicator: UIViewRepresentable {
-    @Binding var isAnimating: Bool
-    let style: UIActivityIndicatorView.Style
-
-    func makeUIView(context _: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
-        UIActivityIndicatorView(style: style)
-    }
-
-    func updateUIView(_ uiView: UIActivityIndicatorView, context _: UIViewRepresentableContext<ActivityIndicator>) {
-        isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
-    }
-}

+ 1 - 1
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -570,7 +570,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     debug(.watchManager, "📱 Bolus cancelled from watch")
 
                     // perform determine basal sync, otherwise you could end up with too much IOB when opening the calculator again
-                    await self?.apsManager.determineBasalSync()
+                    try await self?.apsManager.determineBasalSync()
                 }
             }