Explorar o código

Decouple IoB for display from determination

Sam King hai 1 ano
pai
achega
abf9724157

+ 25 - 9
Trio.xcodeproj/project.pbxproj

@@ -244,6 +244,8 @@
 		3B4BA78F2D8DC0EC0069D5B8 /* TidepoolServiceKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		3B4BA7902D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; };
 		3B4BA7912D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		3B616AD92DE427CD0070C2F7 /* IobResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B616AD82DE427CA0070C2F7 /* IobResult.swift */; };
+		3B616ADB2DE42A740070C2F7 /* IobSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B616ADA2DE42A6F0070C2F7 /* IobSetup.swift */; };
 		3B997DCB2DC00849006B6BB2 /* JSONImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */; };
 		3B997DCF2DC00A3A006B6BB2 /* JSONImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */; };
 		3B997DD32DC02AEF006B6BB2 /* glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B997DD12DC02AEF006B6BB2 /* glucose.json */; };
@@ -1056,6 +1058,8 @@
 		3B4BA7692D8DBD690069D5B8 /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA7882D8DC0EC0069D5B8 /* TidepoolServiceKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		3B4BA7892D8DC0EC0069D5B8 /* TidepoolServiceKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TidepoolServiceKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		3B616AD82DE427CA0070C2F7 /* IobResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobResult.swift; sourceTree = "<group>"; };
+		3B616ADA2DE42A6F0070C2F7 /* IobSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobSetup.swift; sourceTree = "<group>"; };
 		3B997DCA2DC00849006B6BB2 /* JSONImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporter.swift; sourceTree = "<group>"; };
 		3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONImporterTests.swift; sourceTree = "<group>"; };
 		3B997DD12DC02AEF006B6BB2 /* glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = glucose.json; sourceTree = "<group>"; };
@@ -2131,6 +2135,7 @@
 				38192E06261BA9960094D973 /* FetchTreatmentsManager.swift */,
 				3856933F270B57A00002C50D /* CGM */,
 				38A504F625DDA0E200C5B9E8 /* Extensions */,
+				3B616AD72DE427B60070C2F7 /* Models */,
 				388E5A5825B6F0070019842D /* OpenAPS */,
 				38A0362725ECF05300FCBB52 /* Storage */,
 			);
@@ -2579,6 +2584,14 @@
 			path = TrioTests;
 			sourceTree = "<group>";
 		};
+		3B616AD72DE427B60070C2F7 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				3B616AD82DE427CA0070C2F7 /* IobResult.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
 		3B997DD22DC02AEF006B6BB2 /* JSONImporterData */ = {
 			isa = PBXGroup;
 			children = (
@@ -2668,18 +2681,19 @@
 		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
-				3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */,
-				BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */,
-				BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */,
-				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
-				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
+				58645BA22CA2D325008AFCE7 /* BatterySetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
+				58645BA62CA2D390008AFCE7 /* ChartAxisSetup.swift */,
+				3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */,
 				58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */,
-				58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */,
-				58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */,
-				58645BA22CA2D325008AFCE7 /* BatterySetup.swift */,
 				58645BA42CA2D347008AFCE7 /* ForecastSetup.swift */,
-				58645BA62CA2D390008AFCE7 /* ChartAxisSetup.swift */,
+				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
+				BD4E1A792D3681AD00D21626 /* GlucoseTargetSetup.swift */,
+				3B616ADA2DE42A6F0070C2F7 /* IobSetup.swift */,
+				58645BA02CA2D2F8008AFCE7 /* OverrideSetup.swift */,
+				58645B9E2CA2D2BE008AFCE7 /* PumpHistorySetup.swift */,
+				BD4E1A7B2D3686D400D21626 /* StartEndMarkerSetup.swift */,
+				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
 			);
 			path = "HomeStateModel+Setup";
 			sourceTree = "<group>";
@@ -4296,6 +4310,7 @@
 				DDF847E12C5C287F0049BB3B /* LiveActivitySettingsStateModel.swift in Sources */,
 				583684082BD195A700070A60 /* Determination.swift in Sources */,
 				DD17451D2C543C5F00211FAC /* ServicesView.swift in Sources */,
+				3B616ADB2DE42A740070C2F7 /* IobSetup.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
 				3811DEEA25CA063400A708ED /* SyncAccess.swift in Sources */,
 				190EBCC829FF13AA00BA767D /* UserInterfaceSettingsStateModel.swift in Sources */,
@@ -4430,6 +4445,7 @@
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DD17454B2C55C62800211FAC /* AutosensSettingsRootView.swift in Sources */,
 				DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */,
+				3B616AD92DE427CD0070C2F7 /* IobResult.swift in Sources */,
 				DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */,
 				BD54A95B2D28087C00F9C1EE /* OverridePresetWatch.swift in Sources */,
 				3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */,

+ 6 - 0
Trio/Sources/APS/APSManager.swift

@@ -30,6 +30,7 @@ protocol APSManager {
     func roundBolus(amount: Decimal) -> Decimal
     var lastError: CurrentValueSubject<Error?, Never> { get }
     func cancelBolus(_ callback: ((Bool, String) -> Void)?) async
+    func iobForDisplay(at: Date) async throws -> Decimal?
 }
 
 enum APSError: LocalizedError {
@@ -415,6 +416,11 @@ final class BaseAPSManager: APSManager, Injectable {
         await tddStorage.storeTDD(tddResult)
     }
 
+    /// Calculate the iob at a given time using oref
+    func iobForDisplay(at: Date) async throws -> Decimal? {
+        try await openAPS.iobForDisplay(clock: at)
+    }
+
     func determineBasal() async throws {
         debug(.apsManager, "Start determine basal")
 

+ 33 - 0
Trio/Sources/APS/Models/IobResult.swift

@@ -0,0 +1,33 @@
+import Foundation
+
+/// A model to represent IoB results returned from the oref `iob` call via JSON
+struct IobResult: Codable {
+    let iob: Decimal
+    let activity: Decimal
+    let basaliob: Decimal
+    let bolusiob: Decimal
+    let netbasalinsulin: Decimal
+    let bolusinsulin: Decimal
+    let time: Date
+    let iobWithZeroTemp: IobWithZeroTemp
+    var lastBolusTime: UInt64?
+    var lastTemp: LastTemp?
+
+    struct IobWithZeroTemp: Codable {
+        let iob: Decimal
+        let activity: Decimal
+        let basaliob: Decimal
+        let bolusiob: Decimal
+        let netbasalinsulin: Decimal
+        let bolusinsulin: Decimal
+        let time: Date
+    }
+
+    struct LastTemp: Codable {
+        let rate: Decimal?
+        let timestamp: Date?
+        let started_at: Date?
+        let date: UInt64
+        let duration: Decimal?
+    }
+}

+ 35 - 0
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -276,6 +276,41 @@ final class OpenAPS {
         return .bolus(bolusDTO)
     }
 
+    /// Calculate IoB
+    ///
+    /// This function will run through the IoB calculation to get the IoB at the given time. We expect
+    /// it to be used for display purposes outside of the tradiation determination code path
+    /// Parameters:
+    /// - clock: the time to use for the IoB calculation (usually Date())
+    func iobForDisplay(clock: Date) async throws -> Decimal? {
+        // Perform asynchronous calls in parallel
+        async let pumpHistoryObjectIDs = fetchPumpHistoryObjectIDs() ?? []
+        async let profileAsync = loadFileFromStorageAsync(name: Settings.profile)
+        async let autosenseAsync = loadFileFromStorageAsync(name: Settings.autosense)
+
+        // Await the results of asynchronous tasks
+        let (
+            pumpHistoryJSON,
+            profile,
+            autosens
+        ) = await (
+            try parsePumpHistory(await pumpHistoryObjectIDs),
+            profileAsync,
+            autosenseAsync
+        )
+
+        // IOB calculation
+        let iob = try await self.iob(
+            pumphistory: pumpHistoryJSON,
+            profile: profile,
+            clock: clock,
+            autosens: autosens.isEmpty ? .null : autosens
+        )
+        guard let data = iob.data(using: .utf8) else { return nil }
+        let iobResult = try JSONCoding.decoder.decode([IobResult].self, from: data)
+        return iobResult.first?.iob
+    }
+
     func determineBasal(
         currentTemp: TempBasal,
         clock: Date = Date(),

+ 52 - 0
Trio/Sources/Modules/Home/HomeStateModel+Setup/IobSetup.swift

@@ -0,0 +1,52 @@
+import Foundation
+
+extension Home.StateModel {
+    /// Fetch the most recent IoB value using the oref `iob` function. Call this function any time
+    /// you want to update the displayed IoB, like on the arrival of a new pump event or at initialization
+    func setupIobForDisplay() {
+        Task {
+            do {
+                let at = Date()
+                guard let iob = try await apsManager.iobForDisplay(at: at) else {
+                    debug(.default, "Could not get iob for display (oref returned nil)")
+                    await updateIobForDisplay(iob: iobForDisplay, at: at)
+                    return
+                }
+                await updateIobForDisplay(iob: iob, at: at)
+            } catch {
+                debug(
+                    .default,
+                    "\(DebuggingIdentifiers.failed) Error setting up iob for display \(error)"
+                )
+            }
+        }
+    }
+
+    /// Update IoB periodically. This function will update the displayed IoB in response
+    /// to timer events. Internally, it has logic to reduce the frequency of updates as it is
+    /// designed to capture typical decay from elapsed time. If there is something that
+    /// changes IoB, like a new pump event, use `setupIobForDisplay` instead
+    func setupIobForDisplayOnTimer() {
+        Task {
+            let lastRun = iobForDisplayUpdatedAt ?? .distantPast
+            // if we don't have an iob value re-run it more often
+            let period = iobForDisplay == nil ? 1.minutes.timeInterval : 5.minutes.timeInterval
+            if Date().timeIntervalSince(lastRun) > period {
+                setupIobForDisplay()
+            }
+        }
+    }
+
+    /// Update the IoB state values. There is a small amount of logic to try to deal with
+    /// updated values that happen concurrently by using the most recent results
+    @MainActor func updateIobForDisplay(iob: Decimal?, at: Date) {
+        // in case we get two calls at around the same time
+        // make sure to only use the most recent result
+        // unless the iobForDisplay isn't set and we have a result
+        let lastRun = iobForDisplayUpdatedAt ?? .distantPast
+        if at > lastRun || (iob != nil && iobForDisplay == nil) {
+            iobForDisplay = iob
+            iobForDisplayUpdatedAt = at
+        }
+    }
+}

+ 7 - 0
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -106,6 +106,8 @@ extension Home {
         var listOfCGM: [CGMModel] = []
         var cgmCurrent = cgmDefaultModel
         var shouldRunDeleteOnSettingsChange = true
+        var iobForDisplay: Decimal?
+        var iobForDisplayUpdatedAt: Date?
 
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
@@ -215,6 +217,9 @@ extension Home {
                     group.addTask {
                         self.setupTempTargetsRunStored()
                     }
+                    group.addTask {
+                        self.setupIobForDisplay()
+                    }
                 }
             }
         }
@@ -265,6 +270,7 @@ extension Home {
                 self.setupLastBolus()
                 self.displayPumpStatusHighlightMessage()
                 self.displayPumpStatusBadge()
+                self.setupIobForDisplay()
             }.store(in: &subscriptions)
 
             coreDataPublisher?.filteredByEntityName("OpenAPS_Battery").sink { [weak self] _ in
@@ -306,6 +312,7 @@ extension Home {
             timer.eventHandler = {
                 DispatchQueue.main.async { [weak self] in
                     self?.timerDate = Date()
+                    self?.setupIobForDisplayOnTimer()
                 }
             }
             timer.resume()

+ 2 - 2
Trio/Sources/Modules/Home/View/HomeRootView.swift

@@ -431,8 +431,8 @@ extension Home {
                         .foregroundColor(Color.insulin)
                     Text(
                         (
-                            Formatter.decimalFormatterWithTwoFractionDigits
-                                .string(from: (state.enactedAndNonEnactedDeterminations.first?.iob ?? 0) as NSNumber) ?? "0"
+                            state.iobForDisplay.flatMap({ Formatter.decimalFormatterWithTwoFractionDigits
+                                    .string(from: $0 as NSNumber) }) ?? "??"
                         ) +
                             String(localized: " U", comment: "Insulin unit")
                     )

+ 4 - 1
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -910,7 +910,10 @@ extension Treatments.StateModel {
             target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
             isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
             cob = mostRecentDetermination.cob as Int16
-            iob = (mostRecentDetermination.iob ?? 0) as Decimal
+            // FIXME: for now we fall back to determinationIob but we should use
+            // a more formal error state if we can't collect the needed information
+            let determinationIob = (mostRecentDetermination.iob ?? 0) as Decimal
+            iob = (try? await apsManager.iobForDisplay(at: Date())) ?? determinationIob
             basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
             carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
             insulinCalculated = await calculateInsulin()