Przeglądaj źródła

Bolus calculator (#101)

Refactored the new bolus calculator.
Now just using one formula (using two was too confusing).
Removed many of the restrictions ad replaced those with warnings and alerts instead, making it possible to bolus even when BG is falling or glucose predicted below target. The Eventual BG still needs to be over target for a recommendations. This limit is not removed.
Added logging and error strings from oref0 to iAPS.
New bolus pop-up and new bolus alerts.
To do: UI polish.
Jon B Mårtensson 2 lat temu
rodzic
commit
ac066ccfd9

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.1.8
+APP_VERSION = 2.1.9
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##

Plik diff jest za duży
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


+ 29 - 0
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -1278,6 +1278,35 @@ Enact a temp Basal or a temp target */
 /* */
 "Save as Preset" = "Save as Preset";
 
+/* ----------------------- New Bolus Calculator ---------------------------*/
+
+/* Warning about bolus recommendation. Title */
+"Warning!" = "Warning!";
+
+/* Alert to confirm bolus amount to add */
+"\n\nTap 'Add' to continue with selected amount." = "\n\nTap 'Add' to continue with selected amount.";
+
+/* */
+"Eventual Glucose" = "Eventual Glucose";
+
+/* */
+"Target Glucose" = "Target Glucose";
+
+/* */
+"Percentage setting" = "Percentage setting";
+
+/* */
+"Insulin Sensitivity" = "Insulin Sensitivity";
+
+/* Formuala displayed in Bolus info pop-up*/
+"(Eventual Glucose - Target) / ISF =" = "(Eventual Glucose - Target) / ISF =";
+
+/* Bolus pop-up footer */
+"Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended." = "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended.";
+
+/* Hide pop-up */
+"Hide" = "Hide";
+
 /* -------------------------------------------------------------------------------------------
   DASH strings
 */

+ 29 - 0
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings

@@ -1278,6 +1278,35 @@ Enact a temp Basal or a temp target */
 /* */
 "Save as Preset" = "Spara som förval";
 
+/* ----------------------- New Bolus Calculator ---------------------------*/
+
+/* Warning about bolus recommendation. Title */
+"Warning!" = "Varning!";
+
+/* Alert to confirm bolus amount to add */
+"\n\nTap 'Add' to continue with selected amount." = "\n\nTryck 'Lägg till' för att fylla i bolusmängd.";
+
+/* */
+"Eventual Glucose" = "Blodsockerprognos";
+
+/* */
+"Target Glucose" = "Målvärde";
+
+/* */
+"Percentage setting" = "Bolusprocentinställning";
+
+/* */
+"Insulin Sensitivity" = "Insulinkänslighet";
+
+/* Formuala displayed in Bolus info pop-up. Make translation short! */
+"(Eventual Glucose - Target) / ISF =" = "(Blodsockerprognos - Målvärde) / ISF =";
+
+/* Bolus pop-up footer */
+"Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended." = "Kolhydrater och tidigare insulin ingår i prognos, men om prognos är lägre än målvärde kommer ingen bolus att föreslås.";
+
+/* Hide pop-up */
+"Hide" = "Göm";
+
 /* -------------------------------------------------------------------------------------------
   DASH strings
 */

+ 2 - 0
FreeAPS/Sources/Models/Suggestion.swift

@@ -23,6 +23,7 @@ struct Suggestion: JSON, Equatable {
     let insulin: Insulin?
     let current_target: Decimal?
     let insulinForManualBolus: Decimal?
+    let manualBolusErrorString: String?
 }
 
 struct Predictions: JSON, Equatable {
@@ -63,6 +64,7 @@ extension Suggestion {
         case insulin
         case current_target
         case insulinForManualBolus
+        case manualBolusErrorString
     }
 }
 

+ 24 - 12
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -12,12 +12,24 @@ extension Bolus {
         @Published var insulinRequired: Decimal = 0
         @Published var waitForSuggestion: Bool = false
         @Published var manual: Bool = false
+        @Published var error: Bool = false
+        @Published var errorString: String = ""
+
+        @Published var evBG: Int = 0
+        @Published var insulin: Decimal = 0
+        @Published var target: Decimal = 0
+        @Published var isf: Decimal = 0
+        @Published var percentage: Decimal = 0
+
+        @Published var units: GlucoseUnits = .mmolL
 
         var waitForSuggestionInitial: Bool = false
 
         override func subscribe() {
             setupInsulinRequired()
             broadcaster.register(SuggestionObserver.self, observer: self)
+            units = settingsManager.settings.units
+            percentage = settingsManager.settings.insulinReqPercentage
 
             if waitForSuggestionInitial {
                 apsManager.determineBasal()
@@ -76,21 +88,21 @@ extension Bolus {
 
         func setupInsulinRequired() {
             DispatchQueue.main.async {
-                // Manual Bolus recommendation screen after a carb entry (normally) yields a higher amount than the insulin reqiured amount computed for SMBs (auto boluses). Carbs combined with a manual bolus threfore now (test) uses the Eventual BG for glucose prediction, whereas the insulinReg for SMBs (and for manual boluses not combined with a carb entry) uses the minPredBG for glucose prediction (typically lower than Eventual BG).
+                self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
 
-                if self.manual {
-                    self.insulinRequired = self.provider.suggestion?.insulinForManualBolus ?? 0
+                // Manual Bolus recommendation screen after a carb entry (normally) yields a higher amount than the insulin reqiured amount computed for SMBs (auto boluses). Carbs combined with a manual bolus threfore now (test) uses the Eventual BG for glucose prediction, whereas the insulinReg for SMBs uses the minPredBG for glucose prediction (typically lower than Eventual BG).
 
-                    if self.settingsManager.settings.insulinReqPercentage != 100 {
-                        self.insulinRecommended = self
-                            .insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100)
-                    } else { self.insulinRecommended = self.insulinRequired }
+                self.evBG = self.provider.suggestion?.eventualBG ?? 0
+                self.insulin = self.provider.suggestion?.insulinForManualBolus ?? 0
+                self.target = self.provider.suggestion?.current_target ?? 0
+                self.isf = self.provider.suggestion?.isf ?? 0
 
-                } else {
-                    self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
-                    self.insulinRecommended = self
-                        .insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * 2
-                }
+                if self.settingsManager.settings.insulinReqPercentage != 100 {
+                    self.insulinRecommended = self.insulin * (self.settingsManager.settings.insulinReqPercentage / 100)
+                } else { self.insulinRecommended = self.insulin }
+
+                self.errorString = self.provider.suggestion?.manualBolusErrorString ?? ""
+                if self.errorString.count > 8 { self.error = true }
 
                 self.insulinRecommended = self.apsManager
                     .roundBolus(amount: max(self.insulinRecommended, 0))

+ 136 - 39
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -8,6 +8,10 @@ extension Bolus {
         let manualBolus: Bool
         @StateObject var state = StateModel()
         @State private var isAddInsulinAlertPresented = false
+        @State private var presentInfo = false
+        @State private var displayError = false
+
+        @Environment(\.colorScheme) var colorScheme
 
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -26,45 +30,25 @@ extension Bolus {
                             ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
                         }
                     } else {
-                        if state.manual {
-                            HStack {
-                                Text("Insulin recommended")
-                                Spacer()
-                                Text(
-                                    formatter
-                                        .string(from: state.insulinRecommended as NSNumber)! +
-                                        NSLocalizedString(" U", comment: "Insulin unit")
-                                ).foregroundColor(.secondary)
-                            }.contentShape(Rectangle())
-                                .onTapGesture {
-                                    state.amount = state.insulinRecommended
-                                }
-                        } else {
-                            HStack {
-                                Text("Insulin required").foregroundColor(.secondary)
-                                Spacer()
-                                Text(
-                                    formatter
-                                        .string(from: state.insulinRequired as NSNumber)! +
-                                        NSLocalizedString(" U", comment: "Insulin unit")
-                                ).foregroundColor(.secondary)
-                            }.contentShape(Rectangle())
-                                .onTapGesture {
-                                    state.amount = state.insulinRequired
-                                }
-
-                            HStack {
-                                Text("Insulin recommended")
-                                Spacer()
-                                Text(
-                                    formatter
-                                        .string(from: state.insulinRecommended as NSNumber)! +
-                                        NSLocalizedString(" U", comment: "Insulin unit")
-                                ).foregroundColor(.secondary)
-                            }.contentShape(Rectangle())
-                                .onTapGesture {
-                                    state.amount = state.insulinRecommended
-                                }
+                        HStack {
+                            Text("Insulin recommended")
+                            Spacer()
+                            Text(
+                                formatter
+                                    .string(from: state.insulinRecommended as NSNumber)! +
+                                    NSLocalizedString(" U", comment: "Insulin unit")
+                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
+                        }.contentShape(Rectangle())
+                            .onTapGesture {
+                                if state.error, state.insulinRecommended > 0 { displayError = true }
+                                else { state.amount = state.insulinRecommended }
+                            }
+                        HStack {
+                            Image(systemName: "info.bubble").symbolRenderingMode(.palette).foregroundStyle(
+                                .primary, .blue
+                            )
+                        }.onTapGesture {
+                            presentInfo.toggle()
                         }
                     }
                 }
@@ -118,6 +102,23 @@ extension Bolus {
                     secondaryButton: .cancel()
                 )
             }
+            .alert(isPresented: $displayError) {
+                Alert(
+                    title: Text("Warning!"),
+                    message: Text("\n" + state.errorString + NSLocalizedString(
+                        "\n\nTap 'Add' to continue with selected amount.",
+                        comment: "Alert text to confirm bolus amount to add"
+                    )),
+                    primaryButton: .destructive(
+                        Text("Add"),
+                        action: {
+                            state.amount = state.insulinRecommended
+                            displayError = false
+                        }
+                    ),
+                    secondaryButton: .cancel()
+                )
+            }
             .onAppear {
                 configureView {
                     state.waitForSuggestionInitial = waitForSuggestion
@@ -128,6 +129,102 @@ extension Bolus {
             .navigationTitle("Enact Bolus")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
+                bolusInfo
+            }
+        }
+
+        var bolusInfo: some View {
+            VStack {
+                // Variables
+                VStack(spacing: 3) {
+                    HStack {
+                        Text("Eventual Glucose").foregroundColor(.secondary)
+                        let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
+                        let fractionDigit = state.units == .mmolL ? 1 : 0
+                        Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigit))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("Target Glucose").foregroundColor(.secondary)
+                        let target = state.units == .mmolL ? state.target.asMmolL : state.target
+                        let fractionDigit = state.units == .mmolL ? 1 : 0
+                        Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigit))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF").foregroundColor(.secondary)
+                        let isf = state.isf
+                        Text(isf.formatted())
+                        Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
+                            .foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF:")
+                        Text("Insulin Sensitivity")
+                    }.foregroundColor(.secondary).italic()
+                    if state.percentage != 100 {
+                        HStack {
+                            Text("Percentage setting").foregroundColor(.secondary)
+                            let percentage = state.percentage
+                            Text(percentage.formatted())
+                            Text("%").foregroundColor(.secondary)
+                        }
+                    }
+                }
+                .font(.footnote)
+                .padding(.top, 10)
+                Divider()
+                // Formula
+                VStack(spacing: 5) {
+                    let unit = NSLocalizedString(
+                        " U",
+                        comment: "Unit in number of units delivered (keep the space character!)"
+                    )
+                    Text("(Eventual Glucose - Target) / ISF =").font(.callout).italic()
+                    let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
+                    let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
+                    HStack {
+                        Text(" = ").font(.callout)
+                        Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
+                    }
+                    if state.percentage != 100, state.insulin > 0 {
+                        Divider()
+                        HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
+                            Text(
+                                state.insulinRecommended.formatted() + unit
+                            ).font(.callout).foregroundColor(.blue).bold()
+                        }
+                    }
+                }
+                // Warning
+                VStack {
+                    Divider()
+                    if state.error, state.insulinRecommended > 0 {
+                        Text("Warning!").font(.callout).foregroundColor(.orange).bold()
+                        Text(state.errorString).font(.caption)
+                        Divider()
+                    }
+                }.padding(.horizontal, 10)
+                // Footer. Warning string .
+                if !(state.error && state.insulinRecommended > 0) {
+                    VStack {
+                        Text(
+                            "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
+                        ).font(.caption2).foregroundColor(.secondary)
+                    }.padding(20)
+                }
+                // Hide button
+                VStack {
+                    Button { presentInfo = false }
+                    label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
+                        .foregroundColor(.blue)
+                }.padding(.bottom, 10)
+            }
+            .background(
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
+            )
         }
     }
 }