فهرست منبع

Implement PR feedback and fix hint in CGMRootView

Marvin Polscheit 2 ماه پیش
والد
کامیت
4407aa1784

+ 53 - 35
Trio/Sources/APS/FetchGlucoseManager.swift

@@ -240,18 +240,35 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         return Manager.init(rawState: rawState)
         return Manager.init(rawState: rawState)
     }
     }
 
 
-    func fetchGlucose(context: NSManagedObjectContext) async throws -> [GlucoseStored]? {
-        try await CoreDataStack.shared.fetchEntitiesAsync(
+    func fetchGlucose(context: NSManagedObjectContext) async throws -> [NSManagedObjectID] {
+        // Compound predicate: time window + non-manual + valid date
+        let timePredicate = NSPredicate.predicateForOneDayAgoInMinutes
+        let manualPredicate = NSPredicate(format: "isManual == NO")
+        let datePredicate = NSPredicate(format: "date != nil")
+
+        let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
+            timePredicate,
+            manualPredicate,
+            datePredicate
+        ])
+
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             // Predicate must cover at least the full glucose horizon used by downstream algorithm consumers.
             // Predicate must cover at least the full glucose horizon used by downstream algorithm consumers.
             // If autosens / oref / smoothing logic ever starts looking back further (e.g. 36h),
             // If autosens / oref / smoothing logic ever starts looking back further (e.g. 36h),
             // this fetch window must be expanded accordingly.
             // this fetch window must be expanded accordingly.
-            predicate: NSPredicate.predicateForOneDayAgoInMinutes,
+            predicate: compoundPredicate,
             key: "date",
             key: "date",
-            ascending: true, // fetch newest -> oldest
+            ascending: true, // the first element is the oldest
             fetchLimit: 350
             fetchLimit: 350
-        ) as? [GlucoseStored]
+        )
+
+        guard let glucoseArray = results as? [GlucoseStored] else {
+            throw CoreDataError.fetchError(function: #function, file: #file)
+        }
+
+        return glucoseArray.map(\.objectID)
     }
     }
 
 
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
     private func glucoseStoreAndHeartDecision(syncDate: Date, glucose: [BloodGlucose]) async throws {
@@ -383,42 +400,43 @@ extension BaseFetchGlucoseManager {
     func exponentialSmoothingGlucose(context: NSManagedObjectContext) async {
     func exponentialSmoothingGlucose(context: NSManagedObjectContext) async {
         let startTime = Date()
         let startTime = Date()
 
 
-        guard let glucoseStored = try? await fetchGlucose(context: context) else { return }
-
-        await context.perform {
-            // Only smooth CGM values; ignore manually entered glucose.
-            // `fetchGlucose(context:)` returns chronological order (oldest -> newest),
-            // which matches the natural order required by the smoothing algorithm.
-            let glucoseReadings: [GlucoseStored] = glucoseStored
-                .filter { !$0.isManual }
-                .filter { $0.date != nil }
-
-            guard !glucoseReadings.isEmpty else { return }
-
-            self.applyExponentialSmoothingAndStore(
-                glucoseReadings: glucoseReadings,
-                minimumWindowSize: 4,
-                maximumAllowedGapMinutes: 12,
-                xDripErrorGlucose: 38,
-                minimumSmoothedGlucose: 39,
-                firstOrderWeight: 0.4,
-                firstOrderAlpha: 0.5,
-                secondOrderAlpha: 0.4,
-                secondOrderBeta: 1.0
-            )
+        do {
+            // get objectIDs
+            let objectIDs = try await fetchGlucose(context: context)
+
+            try await context.perform {
+                // Load managed objects from object IDs
+                // Filtering (isManual, date) already done at DB level in fetchGlucose
+                let glucoseReadings = objectIDs.compactMap {
+                    context.object(with: $0) as? GlucoseStored
+                }
+
+                guard !glucoseReadings.isEmpty else { return }
+
+                // Static method call to avoid self-capture
+                Self.applyExponentialSmoothingAndStore(
+                    glucoseReadings: glucoseReadings,
+                    minimumWindowSize: 4,
+                    maximumAllowedGapMinutes: 12,
+                    xDripErrorGlucose: 38,
+                    minimumSmoothedGlucose: 39,
+                    firstOrderWeight: 0.4,
+                    firstOrderAlpha: 0.5,
+                    secondOrderAlpha: 0.4,
+                    secondOrderBeta: 1.0
+                )
 
 
-            do {
                 try context.save()
                 try context.save()
-            } catch {
-                debugPrint("Failed to save context after smoothing: \(error)")
             }
             }
-        }
 
 
-        let duration = Date().timeIntervalSince(startTime)
-        debugPrint(String(format: "Exponential smoothing duration: %0.04fs", duration))
+            let duration = Date().timeIntervalSince(startTime)
+            debugPrint(String(format: "Exponential smoothing duration: %0.04fs", duration))
+        } catch {
+            debug(.deviceManager, "Failed to smooth glucose: \(error)")
+        }
     }
     }
 
 
-    private func applyExponentialSmoothingAndStore(
+    private static func applyExponentialSmoothingAndStore(
         glucoseReadings data: [GlucoseStored],
         glucoseReadings data: [GlucoseStored],
         minimumWindowSize: Int,
         minimumWindowSize: Int,
         maximumAllowedGapMinutes: Int,
         maximumAllowedGapMinutes: Int,

+ 17 - 12
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -103,7 +103,8 @@ final class OpenAPS {
         shouldSmoothGlucose: Bool,
         shouldSmoothGlucose: Bool,
         fetchLimit: Int?
         fetchLimit: Int?
     ) async throws -> String {
     ) async throws -> String {
-        let results = try CoreDataStack.shared.fetchEntities(
+        // make it async and await it
+        let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
             onContext: context,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
@@ -113,22 +114,25 @@ final class OpenAPS {
             batchSize: 48
             batchSize: 48
         )
         )
 
 
-        return try await context.perform {
+        // mapping within the context closure, JSON conversion outside
+        let algorithmGlucose = try await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
             guard let glucoseResults = results as? [GlucoseStored] else {
                 throw CoreDataError.fetchError(function: #function, file: #file)
                 throw CoreDataError.fetchError(function: #function, file: #file)
             }
             }
 
 
-            let algorithmGlucose = glucoseResults.map { glucose -> AlgorithmGlucose in
+            // extracting handler to only create it 1x
+            let roundingBehavior = NSDecimalNumberHandler(
+                roundingMode: .plain,
+                scale: 0,
+                raiseOnExactness: false,
+                raiseOnOverflow: false,
+                raiseOnUnderflow: false,
+                raiseOnDivideByZero: false
+            )
+
+            return glucoseResults.map { glucose -> AlgorithmGlucose in
                 let glucoseValue: Int16
                 let glucoseValue: Int16
                 if shouldSmoothGlucose, !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose {
                 if shouldSmoothGlucose, !glucose.isManual, let smoothedGlucose = glucose.smoothedGlucose {
-                    let roundingBehavior = NSDecimalNumberHandler(
-                        roundingMode: .plain,
-                        scale: 0,
-                        raiseOnExactness: false,
-                        raiseOnOverflow: false,
-                        raiseOnUnderflow: false,
-                        raiseOnDivideByZero: false
-                    )
                     glucoseValue = smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value
                     glucoseValue = smoothedGlucose.rounding(accordingToBehavior: roundingBehavior).int16Value
                 } else {
                 } else {
                     glucoseValue = glucose.glucose
                     glucoseValue = glucose.glucose
@@ -141,8 +145,9 @@ final class OpenAPS {
                     isManual: glucose.isManual
                     isManual: glucose.isManual
                 )
                 )
             }
             }
-            return self.jsonConverter.convertToJSON(algorithmGlucose)
         }
         }
+
+        return jsonConverter.convertToJSON(algorithmGlucose)
     }
     }
 
 
     private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {
     private func fetchAndProcessCarbs(additionalCarbs: Decimal? = nil, carbsDate: Date? = nil) async throws -> String {

+ 16 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -10182,7 +10182,6 @@
       }
       }
     },
     },
     "%lld h" : {
     "%lld h" : {
-      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -44682,6 +44681,9 @@
         }
         }
       }
       }
     },
     },
+    "Applies a dual-stage exponential smoothing algorithm (inspired by AndroidAPS) to reduce noise in CGM readings. The algorithm combines two smoothing approaches: a fast-responding filter for recent trends and a slower filter that considers momentum, then blends them for optimal results." : {
+
+    },
     "Apply a Temporary Target" : {
     "Apply a Temporary Target" : {
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
@@ -139165,6 +139167,7 @@
       }
       }
     },
     },
     "It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed. This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels." : {
     "It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed. This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -170619,6 +170622,7 @@
       }
       }
     },
     },
     "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app." : {
     "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -213482,7 +213486,11 @@
     "SMBs Off%@" : {
     "SMBs Off%@" : {
 
 
     },
     },
+    "Smooth CGM readings using exponential smoothing." : {
+
+    },
     "Smooth CGM readings using Savitzky-Golay filtering." : {
     "Smooth CGM readings using Savitzky-Golay filtering." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -231524,6 +231532,9 @@
         }
         }
       }
       }
     },
     },
+    "The smoothing intelligently handles data gaps and excludes sensor error values. It requires at least 4 consecutive readings within a 12-minute window to operate, ensuring reliability. Only CGM readings are smoothed—manual entries remain unchanged." : {
+
+    },
     "The source of the glucose reading will be added to the notification." : {
     "The source of the glucose reading will be added to the notification." : {
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
@@ -235126,6 +235137,7 @@
       }
       }
     },
     },
     "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows." : {
     "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -235479,6 +235491,9 @@
         }
         }
       }
       }
     },
     },
+    "This helps Trio make more stable dosing decisions by reducing over-reactions to sensor noise or brief fluctuations that don't reflect your true glucose trend. The algorithm preserves important patterns while filtering out unreliable variations." : {
+
+    },
     "This is a limit on the size of a single SMB. One SMB can only be as large as this many minutes of your current profile basal rate." : {
     "This is a limit on the size of a single SMB. One SMB can only be as large as this many minutes of your current profile basal rate." : {
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {

+ 5 - 5
Trio/Sources/Modules/CGMSettings/View/CGMRootView.swift

@@ -113,17 +113,17 @@ extension CGMSettings {
                         units: state.units,
                         units: state.units,
                         type: .boolean,
                         type: .boolean,
                         label: String(localized: "Smooth Glucose Value"),
                         label: String(localized: "Smooth Glucose Value"),
-                        miniHint: String(localized: "Smooth CGM readings using Savitzky-Golay filtering."),
+                        miniHint: String(localized: "Smooth CGM readings using exponential smoothing."),
                         verboseHint: VStack(alignment: .leading, spacing: 10) {
                         verboseHint: VStack(alignment: .leading, spacing: 10) {
                             Text("Default: OFF").bold()
                             Text("Default: OFF").bold()
                             Text(
                             Text(
-                                "This filter looks at small groups of nearby readings and fits them to a simple mathematical curve. This process doesn't change the overall pattern of your glucose data but helps smooth out the \"noise\" or irregular fluctuations that could lead to false highs or lows."
+                                "Applies a dual-stage exponential smoothing algorithm (inspired by AndroidAPS) to reduce noise in CGM readings. The algorithm combines two smoothing approaches: a fast-responding filter for recent trends and a slower filter that considers momentum, then blends them for optimal results."
                             )
                             )
                             Text(
                             Text(
-                                "It's designed to keep the important trends in your data while minimizing those small, misleading variations, giving you and Trio a clearer sense of where your blood sugar is really headed. This type of filtering is useful in Trio, as it can help prevent over-corrections based on inaccurate glucose readings. This can help reduce the impact of sudden spikes or dips that might not reflect your true blood glucose levels."
+                                "The smoothing intelligently handles data gaps and excludes sensor error values. It requires at least 4 consecutive readings within a 12-minute window to operate, ensuring reliability. Only CGM readings are smoothed—manual entries remain unchanged."
                             )
                             )
                             Text(
                             Text(
-                                "Note: If enabled, the smoothed values you see in Trio may differ from what is shown in your CGM app."
+                                "This helps Trio make more stable dosing decisions by reducing over-reactions to sensor noise or brief fluctuations that don't reflect your true glucose trend. The algorithm preserves important patterns while filtering out unreliable variations."
                             )
                             )
                         }
                         }
                     )
                     )
@@ -181,7 +181,7 @@ extension CGMSettings {
                         hintDetent: $hintDetent,
                         hintDetent: $hintDetent,
                         shouldDisplayHint: $shouldDisplayHint,
                         shouldDisplayHint: $shouldDisplayHint,
                         hintLabel: hintLabel ?? "",
                         hintLabel: hintLabel ?? "",
-                        hintText: AnyView(
+                        hintText: selectedVerboseHint ?? AnyView(
                             VStack(alignment: .leading, spacing: 10) {
                             VStack(alignment: .leading, spacing: 10) {
                                 Text(
                                 Text(
                                     "Current CGM Models Supported:"
                                     "Current CGM Models Supported:"