Просмотр исходного кода

Implement PR feedback and fix hint in CGMRootView

Marvin Polscheit 2 месяцев назад
Родитель
Сommit
4407aa1784

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

@@ -240,18 +240,35 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
         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,
             onContext: context,
             // 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),
             // this fetch window must be expanded accordingly.
-            predicate: NSPredicate.predicateForOneDayAgoInMinutes,
+            predicate: compoundPredicate,
             key: "date",
-            ascending: true, // fetch newest -> oldest
+            ascending: true, // the first element is the oldest
             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 {
@@ -383,42 +400,43 @@ extension BaseFetchGlucoseManager {
     func exponentialSmoothingGlucose(context: NSManagedObjectContext) async {
         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()
-            } 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],
         minimumWindowSize: Int,
         maximumAllowedGapMinutes: Int,

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

@@ -103,7 +103,8 @@ final class OpenAPS {
         shouldSmoothGlucose: Bool,
         fetchLimit: Int?
     ) 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,
             onContext: context,
             predicate: NSPredicate.predicateForOneDayAgoInMinutes,
@@ -113,22 +114,25 @@ final class OpenAPS {
             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 {
                 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
                 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
                 } else {
                     glucoseValue = glucose.glucose
@@ -141,8 +145,9 @@ final class OpenAPS {
                     isManual: glucose.isManual
                 )
             }
-            return self.jsonConverter.convertToJSON(algorithmGlucose)
         }
+
+        return jsonConverter.convertToJSON(algorithmGlucose)
     }
 
     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" : {
-      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "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" : {
       "localizations" : {
         "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." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "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." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "stringUnit" : {
@@ -213482,7 +213486,11 @@
     "SMBs Off%@" : {
 
     },
+    "Smooth CGM readings using exponential smoothing." : {
+
+    },
     "Smooth CGM readings using Savitzky-Golay filtering." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "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." : {
       "localizations" : {
         "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." : {
+      "extractionState" : "stale",
       "localizations" : {
         "bg" : {
           "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." : {
       "localizations" : {
         "bg" : {

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

@@ -113,17 +113,17 @@ extension CGMSettings {
                         units: state.units,
                         type: .boolean,
                         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) {
                             Text("Default: OFF").bold()
                             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(
-                                "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(
-                                "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,
                         shouldDisplayHint: $shouldDisplayHint,
                         hintLabel: hintLabel ?? "",
-                        hintText: AnyView(
+                        hintText: selectedVerboseHint ?? AnyView(
                             VStack(alignment: .leading, spacing: 10) {
                                 Text(
                                     "Current CGM Models Supported:"