Jonas Björkert 1 год назад
Родитель
Сommit
1b28e0067d

+ 8 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -115,6 +115,8 @@
 		DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */; };
 		DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */; };
 		DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */; };
+		DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */; };
+		DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */; };
 		DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; };
 		DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; };
 		DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; };
@@ -454,6 +456,8 @@
 		DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusAlarmEditor.swift; sourceTree = "<group>"; };
 		DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBCondition.swift; sourceTree = "<group>"; };
 		DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBAlarmEditor.swift; sourceTree = "<group>"; };
+		DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseCondition.swift; sourceTree = "<group>"; };
+		DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseAlarmEditor.swift; sourceTree = "<group>"; };
 		DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = "<group>"; };
 		DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = "<group>"; };
 		DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = "<group>"; };
@@ -722,6 +726,7 @@
 		DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */,
 				DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */,
 				DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */,
 				DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */,
@@ -980,6 +985,7 @@
 		DDC7E53F2DBD8A1600EB1127 /* Editors */ = {
 			isa = PBXGroup;
 			children = (
+				DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */,
 				DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */,
 				DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */,
 				DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */,
@@ -1771,6 +1777,7 @@
 				DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */,
 				DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */,
 				DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */,
+				DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */,
 				DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */,
 				DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */,
 				DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */,
@@ -1865,6 +1872,7 @@
 				DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */,
 				DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
 				FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
+				DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,
 				DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */,
 				DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */,

+ 35 - 0
LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift

@@ -0,0 +1,35 @@
+//
+//  FastRiseCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-15.
+//
+
+import Foundation
+
+struct FastRiseCondition: AlarmCondition {
+    static let type: AlarmType = .fastRise
+    init() {}
+
+    func evaluate(alarm: Alarm, data: AlarmData) -> Bool {
+        guard
+            let risePerReading = alarm.delta, risePerReading > 0,
+            let risesNeeded = alarm.monitoringWindow, risesNeeded > 0,
+            data.bgReadings.count >= risesNeeded + 1
+        else { return false }
+
+        if let limit = alarm.threshold {
+            guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false }
+            guard Double(latest.sgv) > limit else { return false }
+        }
+
+        let recent = data.bgReadings.suffix(risesNeeded + 1)
+        let readings = Array(recent)
+
+        for i in 1 ... risesNeeded {
+            let delta = Double(readings[i].sgv - readings[i - 1].sgv)
+            if delta < risePerReading { return false }
+        }
+        return true
+    }
+}

+ 2 - 0
LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift

@@ -65,6 +65,8 @@ struct AlarmEditor: View {
             RecBolusAlarmEditor(alarm: $alarm)
         case .cob:
             COBAlarmEditor(alarm: $alarm)
+        case .fastRise:
+            FastRiseAlarmEditor(alarm: $alarm)
         /* TODO: add other condition types here */
         default:
             Text("No editor for \(alarm.type.rawValue)")

+ 1 - 1
LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift

@@ -34,7 +34,7 @@ struct FastDropAlarmEditor: View {
             // TODO: In the migration script, use 1 value less than stored since we are switching from readings to drops
             AlarmStepperSection(
                 header: "Consecutive Drops",
-                footer: "Number of back-to-back drops—each meeting the rate above—required before an alert fires.",
+                footer: "Number of drops—each meeting the rate above—required before an alert fires.",
                 title: "Drops in a row",
                 range: 1 ... 3,
                 step: 1,

+ 65 - 0
LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift

@@ -0,0 +1,65 @@
+//
+//  FastRiseAlarmEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-15.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct FastRiseAlarmEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(
+                text: "Alerts when glucose readings rise rapidly. For example, "
+                    + "three straight readings each climbing by at least the amount "
+                    + "you set.  Optionally limit alerts to only fire above a certain BG.",
+                alarmType: alarm.type
+            )
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmBGSection(
+                header: "Rate of Rise",
+                footer: "How much the BG must rise to count as a “fast” rise.",
+                title: "Rise per reading",
+                range: 3 ... 20,
+                value: Binding(
+                    get: { alarm.delta ?? 3 },
+                    set: { alarm.delta = $0 }
+                )
+            )
+
+            AlarmStepperSection(
+                header: "Consecutive Rises",
+                footer: "Number of rises—each meeting the rate above—"
+                    + "required before an alert fires.",
+                title: "Rises in a row",
+                range: 1 ... 3,
+                step: 1,
+                value: Binding(
+                    get: { Double(alarm.monitoringWindow ?? 2) },
+                    set: { alarm.monitoringWindow = Int($0) }
+                )
+            )
+
+            AlarmBGLimitSection(
+                header: "BG Limit",
+                footer: "When enabled, this alert only fires if the glucose is "
+                    + "above the limit you set.",
+                toggleText: "Use BG Limit",
+                pickerTitle: "Rising above",
+                range: 40 ... 300,
+                value: $alarm.threshold
+            )
+
+            AlarmActiveSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+            AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5)
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 1 - 0
LoopFollow/Alarm/AlarmManager.swift

@@ -29,6 +29,7 @@ class AlarmManager {
             RecBolusCondition.self,
             COBCondition.self,
             MissedReadingCondition.self,
+            FastRiseCondition.self,
             // TODO: add other condition types here
         ]
     ) {