Prechádzať zdrojové kódy

Watch boluscalc service (#64)

* bg test

* extract insulin calculation in service

* move logic for fetching to BolusCalculationManager

* add bolus recommendation to watch

* Add animation for insulin calc to BolusInputView

* Use pump bolus increment on watch; round bolus calc using loopkit WIP

* Adjust formatting, clear carbs before entering view; limit when bolus calc runs

* Adjust formatting, clear carbs before entering view; limit when bolus calc runs

* Remove wrong formatting; add userTaskInfo to sendDataToWatch()

* Add dynamic glucose color (respecting thresholds) to BG bobble on watch

---------

Co-authored-by: Deniz Cengiz <d.c.cengiz@googlemail.com>
polscm32 1 rok pred
rodič
commit
3c68429801

+ 96 - 71
Trio Watch App Extension/Views/BolusInputView.swift

@@ -33,93 +33,103 @@ struct BolusInputView: View {
 
     var body: some View {
         VStack {
-            if effectiveBolusLimit == 0 {
-                VStack(spacing: 10) {
-                    Spacer()
-
-                    Text("Bolus limit cannot be fetched from phone!").font(.headline)
-                    Text("Check device settings, connect to phone, and try again.").font(.caption)
-
-                    Spacer()
-                }
-                .foregroundColor(.loopRed)
-                .scenePadding()
+            if state.showBolusCalculationProgress {
+                ProgressView("Calculating Bolus...")
+                Spacer()
             } else {
-                if state.carbsAmount > 0 {
-                    HStack {
-                        Text("Carbs:").bold().font(.subheadline).padding(.leading)
-                        Text("\(state.carbsAmount) g").font(.subheadline).foregroundStyle(Color.orange)
+                if effectiveBolusLimit == 0 {
+                    VStack(spacing: 10) {
                         Spacer()
-                    }
-                }
 
-                Spacer()
+                        Text("Bolus limit cannot be fetched from phone!").font(.headline)
+                        Text("Check device settings, connect to phone, and try again.").font(.caption)
 
-                HStack {
-                    // "-" Button
-                    Button(action: {
-                        if bolusAmount > 0 { bolusAmount -= 1 }
-                    }) {
-                        Image(systemName: "minus.circle.fill")
-                            .font(.title3)
-                            .foregroundColor(Color.insulin)
+                        Spacer()
+                    }
+                    .foregroundColor(.loopRed)
+                    .scenePadding()
+                } else {
+                    if state.carbsAmount > 0 {
+                        HStack {
+                            Text("Carbs:").bold().font(.subheadline).padding(.leading)
+                            Text("\(state.carbsAmount) g").font(.subheadline).foregroundStyle(Color.orange)
+                            Spacer()
+                        }
                     }
-                    .buttonStyle(.borderless)
-                    .disabled(bolusAmount < 1)
 
                     Spacer()
 
-                    // Display the current carb amount
-                    Text(String(format: "%.2f U", bolusAmount))
-                        .fontWeight(.bold)
-                        .font(.system(.title2, design: .rounded))
-                        .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
-                        .focusable(true)
-                        .focused($isCrownFocused)
-                        .digitalCrownRotation(
-                            $bolusAmount,
-                            from: 0,
-                            through: effectiveBolusLimit,
-                            by: 1, // TODO: use pump increment here
-                            sensitivity: .medium,
-                            isContinuous: false,
-                            isHapticFeedbackEnabled: true
-                        )
+                    HStack {
+                        // "-" Button
+                        Button(action: {
+                            if bolusAmount > 0 { bolusAmount -= Double(truncating: state.bolusIncrement as NSNumber) }
+                        }) {
+                            Image(systemName: "minus.circle.fill")
+                                .font(.title3)
+                                .foregroundColor(Color.insulin)
+                        }
+                        .buttonStyle(.borderless)
+                        .disabled(bolusAmount < 1)
+
+                        Spacer()
+
+                        // Display the current carb amount
+                        // TODO: format this properly using state.bolusIncrement
+                        Text(String(format: "%.2f U", bolusAmount * Double(truncating: state.bolusIncrement as NSNumber)))
+                            .fontWeight(.bold)
+                            .font(.system(.title2, design: .rounded))
+                            .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
+                            .focusable(true)
+                            .focused($isCrownFocused)
+                            .digitalCrownRotation(
+                                $bolusAmount,
+                                from: 0,
+                                through: effectiveBolusLimit,
+                                by: Double(truncating: state.bolusIncrement as NSNumber),
+                                sensitivity: .medium,
+                                isContinuous: false,
+                                isHapticFeedbackEnabled: true
+                            )
+
+                        Spacer()
+
+                        // "+" Button
+                        Button(action: {
+                            bolusAmount += Double(truncating: state.bolusIncrement as NSNumber)
+                        }) {
+                            Image(systemName: "plus.circle.fill")
+                                .font(.title3)
+                                .foregroundColor(Color.insulin)
+                        }
+                        .buttonStyle(.borderless)
+                        .disabled(bolusAmount >= effectiveBolusLimit)
+                    }.padding(.horizontal)
+
+                    Text("Insulin")
+                        .font(.subheadline)
+                        .foregroundColor(.secondary)
+                        .padding(.bottom)
 
                     Spacer()
 
-                    // "+" Button
-                    Button(action: {
-                        bolusAmount += 0.5
-                    }) {
-                        Image(systemName: "plus.circle.fill")
-                            .font(.title3)
-                            .foregroundColor(Color.insulin)
+                    if bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit {
+                        Text("Bolus Limit Reached!")
+                            .font(.footnote)
+                            .foregroundColor(.loopRed)
                     }
-                    .buttonStyle(.borderless)
-                    .disabled(bolusAmount >= effectiveBolusLimit)
-                }.padding(.horizontal)
-
-                Text("Insulin")
-                    .font(.subheadline)
-                    .foregroundColor(.secondary)
-                    .padding(.bottom)
 
-                Spacer()
+                    Button("Log Bolus") {
+                        state.bolusAmount = min(bolusAmount, effectiveBolusLimit)
+                        navigationPath.append(NavigationDestinations.bolusConfirm)
+                    }
+                    .buttonStyle(.bordered)
+                    .tint(Color.insulin)
+                    .disabled(!(bolusAmount > 0.0) || bolusAmount >= effectiveBolusLimit)
 
-                if bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit {
-                    Text("Bolus Limit Reached!")
+                    Text(String(format: "Recommended: %.1f U", NSDecimalNumber(decimal: state.recommendedBolus).doubleValue))
                         .font(.footnote)
-                        .foregroundColor(.loopRed)
-                }
-
-                Button("Log Bolus") {
-                    state.bolusAmount = min(bolusAmount, effectiveBolusLimit)
-                    navigationPath.append(NavigationDestinations.bolusConfirm)
+                        .foregroundStyle(.secondary)
                 }
-                .buttonStyle(.bordered)
-                .tint(Color.insulin)
-                .disabled(!(bolusAmount > 0.0) || bolusAmount >= effectiveBolusLimit)
             }
         }
         .background(trioBackgroundColor)
@@ -144,5 +154,20 @@ struct BolusInputView: View {
                 }.transition(.opacity)
             }
         }
+        .onAppear {
+            // Set initial bolus amount to recommended value
+            // Only do this if user has not updated amount previously, e.g., when navigating to next and then back to this view
+            if bolusAmount == 0 {
+                state.requestBolusRecommendation()
+                bolusAmount = Double(truncating: NSDecimalNumber(decimal: state.recommendedBolus))
+            }
+        }
+        // Add onChange to update bolus amount when recommendation changes
+        .onChange(of: state.recommendedBolus) { _, newValue in
+            if bolusAmount == 0 { // Only update if user hasn't modified the value
+                state.showBolusCalculationProgress = true
+                bolusAmount = Double(truncating: NSDecimalNumber(decimal: newValue))
+            }
+        }
     }
 }

+ 1 - 2
Trio Watch App Extension/Views/GlucoseTrendView.swift

@@ -52,6 +52,7 @@ struct GlucoseTrendView: View {
                     Text(state.currentGlucose)
                         .fontWeight(.semibold)
                         .font(.system(is40mm ? .title2 : .title, design: .rounded))
+                        .foregroundStyle(state.currentGlucoseColorString.toColor())
 
                     if let delta = state.delta {
                         Text(delta)
@@ -62,8 +63,6 @@ struct GlucoseTrendView: View {
                 }
             }
 
-//            Spacer()
-
             Text(state.lastLoopTime ?? "--").font(.system(size: is40mm ? 9 : 10))
 
             Spacer()

+ 5 - 1
Trio Watch App Extension/Views/TrioMainWatchView.swift

@@ -60,7 +60,11 @@ struct TrioMainWatchView: View {
                 }
             }
             .onAppear {
-                state.confirmationProgress = 0 // reset auth progress
+                // reset input amounts
+                state.bolusAmount = 0
+                state.carbsAmount = 0
+                // reset auth progress
+                state.confirmationProgress = 0
             }
             .toolbar {
                 ToolbarItem(placement: .topBarLeading) {

+ 41 - 0
Trio Watch App Extension/WatchState.swift

@@ -13,6 +13,7 @@ import WatchConnectivity
     var isReachable = false
 
     var currentGlucose: String = "--"
+    var currentGlucoseColorString: String = "#ffffff"
     var trend: String? = ""
     var delta: String? = "--"
     var glucoseValues: [(date: Date, glucose: Double, color: Color)] = []
@@ -42,6 +43,9 @@ import WatchConnectivity
     var maxIOB: Decimal = 0
     var maxCOB: Decimal = 120
 
+    // Pump specific dosing increment
+    var bolusIncrement: Decimal = 0.05
+
     // acknowlegement handling
     var showCommsAnimation: Bool = false
     var showAcknowledgmentBanner: Bool = false
@@ -49,6 +53,9 @@ import WatchConnectivity
     var acknowledgmentMessage: String = ""
     var shouldNavigateToRoot: Bool = true
 
+    // bolus calculation progress
+    var showBolusCalculationProgress: Bool = false
+
     // Meal bolus-specific properties
     var mealBolusStep: MealBolusStep = .savingCarbs
     var isMealBolusCombo: Bool = false
@@ -58,6 +65,8 @@ import WatchConnectivity
             !isBolusCanceled
     }
 
+    var recommendedBolus: Decimal = 0
+
     override init() {
         super.init()
         setupSession()
@@ -220,6 +229,23 @@ import WatchConnectivity
         showCommsAnimation = true
     }
 
+    func requestBolusRecommendation() {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            "requestBolusRecommendation": true,
+            "carbs": carbsAmount
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("Error requesting bolus recommendation: \(error.localizedDescription)")
+        }
+
+        if bolusAmount == 0 {
+            showBolusCalculationProgress = true
+        }
+    }
+
     // MARK: – Handle Acknowledgement Messages FROM Phone
 
     private func handleAcknowledgment(success: Bool, message: String, isFinal: Bool = true) {
@@ -295,6 +321,10 @@ import WatchConnectivity
                 self.currentGlucose = currentGlucose
             }
 
+            if let currentGlucoseColorString = message["currentGlucoseColorString"] as? String {
+                self.currentGlucoseColorString = currentGlucoseColorString
+            }
+
             if let trend = message["trend"] as? String {
                 self.trend = trend
             }
@@ -399,6 +429,17 @@ import WatchConnectivity
                     self.maxCOB = decimalValue
                 }
             }
+
+            if let bolusIncrement = message["bolusIncrement"] {
+                if let decimalValue = (bolusIncrement as? NSNumber)?.decimalValue {
+                    self.bolusIncrement = decimalValue
+                }
+            }
+
+            if let recommendedBolus = message["recommendedBolus"] as? NSNumber {
+                self.recommendedBolus = recommendedBolus.decimalValue
+                showBolusCalculationProgress = false
+            }
         }
     }
 

+ 12 - 39
Trio Watch Complication/TrioWatchComplication.swift

@@ -55,14 +55,9 @@ struct TrioAccessoryCornerView: View {
     var entry: TrioWatchComplicationProvider.Entry
 
     var body: some View {
-        ZStack {
-            Circle()
-                .fill(Color.white.opacity(0.2))
-            Image("ComplicationIcon")
-                .resizable()
-                .scaledToFit()
-                .padding(5)
-        }
+        Text("Trio")
+            .font(.caption)
+            .foregroundColor(.white)
     }
 }
 
@@ -71,19 +66,9 @@ struct TrioAccessoryCircularView: View {
     var entry: TrioWatchComplicationProvider.Entry
 
     var body: some View {
-        if let uiImage = UIImage(named: "ComplicationIcon") {
-            Image(uiImage: uiImage)
-                .resizable()
-                .scaledToFit()
-                .padding(5)
-        } else {
-            ZStack {
-                Circle().fill(Color.red.opacity(0.2))
-                Text("No Image!")
-                    .font(.caption)
-                    .foregroundColor(.white)
-            }
-        }
+        Text("Trio")
+            .font(.caption)
+            .foregroundColor(.white)
     }
 }
 
@@ -92,15 +77,9 @@ struct TrioAccessoryRectangularView: View {
     var entry: TrioWatchComplicationProvider.Entry
 
     var body: some View {
-        HStack {
-            Image("ComplicationIcon")
-                .resizable()
-                .scaledToFit()
-                .frame(width: 30, height: 30)
-            Text("Trio")
-                .font(.headline)
-                .foregroundColor(.primary)
-        }
+        Text("Trio")
+            .font(.headline)
+            .foregroundColor(.primary)
     }
 }
 
@@ -109,15 +88,9 @@ struct TrioAccessoryInlineView: View {
     var entry: TrioWatchComplicationProvider.Entry
 
     var body: some View {
-        HStack {
-            Image("ComplicationIcon")
-                .resizable()
-                .scaledToFit()
-                .frame(width: 12, height: 12)
-            Text("Trio")
-                .font(.caption)
-                .foregroundColor(.primary)
-        }
+        Text("Trio")
+            .font(.caption)
+            .foregroundColor(.primary)
     }
 }
 

+ 12 - 0
Trio.xcodeproj/project.pbxproj

@@ -307,6 +307,7 @@
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
+		BD7DB88E2D2C4A17003D3155 /* BolusCalculationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */; };
 		BD8207C42D2B42E60023339D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
 		BD8207C52D2B42E60023339D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
 		BD8207CE2D2B42E70023339D /* Trio Watch Complication Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -1029,6 +1030,7 @@
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
+		BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculationManager.swift; sourceTree = "<group>"; };
 		BD8207C32D2B42E50023339D /* Trio Watch Complication Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Trio Watch Complication Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
 		BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchManager.swift; sourceTree = "<group>"; };
 		BDA25EE52D260D5800035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
@@ -1751,6 +1753,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				BD7DB88C2D2C49FF003D3155 /* BolusCalculator */,
 				3811DE9225C9D88200A708ED /* Appearance */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
@@ -2510,6 +2513,14 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BD7DB88C2D2C49FF003D3155 /* BolusCalculator */ = {
+			isa = PBXGroup;
+			children = (
+				BD7DB88D2D2C4A0A003D3155 /* BolusCalculationManager.swift */,
+			);
+			path = BolusCalculator;
+			sourceTree = "<group>";
+		};
 		BDA25F1A2D26BCE800035F34 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -3773,6 +3784,7 @@
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
 				BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */,
+				BD7DB88E2D2C4A17003D3155 /* BolusCalculationManager.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,

+ 1 - 0
Trio/Sources/Assemblies/ServiceAssembly.swift

@@ -19,6 +19,7 @@ final class ServiceAssembly: Assembly {
         container.register(HealthKitManager.self) { r in BaseHealthKitManager(resolver: r) }
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
+        container.register(BolusCalculationManager.self) { r in BaseBolusCalculationManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
         container.register(ContactImageManager.self) { r in BaseContactImageManager(resolver: r) }
         container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }

+ 7 - 1
Trio/Sources/Models/WatchState.swift

@@ -3,6 +3,7 @@ import SwiftUI
 
 struct WatchState: Hashable, Equatable, Sendable {
     var currentGlucose: String?
+    var currentGlucoseColorString: String?
     var trend: String?
     var delta: String?
     var glucoseValues: [(date: Date, glucose: Double, color: String)] = []
@@ -21,6 +22,9 @@ struct WatchState: Hashable, Equatable, Sendable {
     var maxIOB: Decimal = 0
     var maxCOB: Decimal = 120
 
+    // Pump specific dosing increment
+    var bolusIncrement: Decimal = 0.05
+
     static func == (lhs: WatchState, rhs: WatchState) -> Bool {
         lhs.currentGlucose == rhs.currentGlucose &&
             lhs.trend == rhs.trend &&
@@ -40,7 +44,8 @@ struct WatchState: Hashable, Equatable, Sendable {
             lhs.maxFat == rhs.maxFat &&
             lhs.maxProtein == rhs.maxProtein &&
             lhs.maxIOB == rhs.maxIOB &&
-            lhs.maxCOB == rhs.maxCOB
+            lhs.maxCOB == rhs.maxCOB &&
+            lhs.bolusIncrement == rhs.bolusIncrement
     }
 
     func hash(into hasher: inout Hasher) {
@@ -64,5 +69,6 @@ struct WatchState: Hashable, Equatable, Sendable {
         hasher.combine(maxProtein)
         hasher.combine(maxIOB)
         hasher.combine(maxCOB)
+        hasher.combine(bolusIncrement)
     }
 }

+ 55 - 67
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -17,6 +17,7 @@ extension Treatments {
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
         @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
+        @ObservationIgnored @Injected() var bolusCalculationManager: BolusCalculationManager!
 
         var lowGlucose: Decimal = 70
         var highGlucose: Decimal = 180
@@ -376,59 +377,44 @@ extension Treatments {
         // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
 
         /// Calculate insulin recommendation
-        func calculateInsulin() -> Decimal {
-            debug(.bolusState, "calculateInsulin fired")
-            let isfForCalculation = isf
-
-            // insulin needed for the current blood glucose
-            targetDifference = currentBG - target
-            targetDifferenceInsulin = targetDifference / isfForCalculation
-
-            // more or less insulin because of bg trend in the last 15 minutes
-            fifteenMinInsulin = deltaBG / isfForCalculation
-
-            // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
-            wholeCob = Decimal(cob) + carbs
-            wholeCobInsulin = wholeCob / carbRatio
-
-            // determine how much the calculator reduces/ increases the bolus because of IOB
-            iobInsulinReduction = (-1) * iob
-
-            // adding everything together
-            // add a calc for the case that no fifteenMinInsulin is available
-            if deltaBG != 0 {
-                wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin)
-            } else {
-                // add (rare) case that no glucose value is available -> maybe display warning?
-                // if no bg is available, ?? sets its value to 0
-                if currentBG == 0 {
-                    wholeCalc = (iobInsulinReduction + wholeCobInsulin)
-                } else {
-                    wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin)
-                }
-            }
-
-            // apply custom factor at the end of the calculations
-            let result = wholeCalc * fraction
-
-            // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
-            if useFattyMealCorrectionFactor {
-                insulinCalculated = result * fattyMealFactor
-            } else if useSuperBolus {
-                superBolusInsulin = sweetMealFactor * currentBasal
-                insulinCalculated = result + superBolusInsulin
-            } else {
-                insulinCalculated = result
-            }
-            // display no negative insulinCalculated
-            insulinCalculated = max(insulinCalculated, 0)
-            insulinCalculated = min(insulinCalculated, maxBolus)
+        @MainActor func calculateInsulin() async -> Decimal {
+//            let input = CalculationInput(
+//                carbs: carbs,
+//                currentBG: currentBG,
+//                deltaBG: deltaBG,
+//                target: target,
+//                isf: isf,
+//                carbRatio: carbRatio,
+//                iob: iob,
+//                cob: cob,
+//                useFattyMealCorrectionFactor: useFattyMealCorrectionFactor,
+//                fattyMealFactor: fattyMealFactor,
+//                useSuperBolus: useSuperBolus,
+//                sweetMealFactor: sweetMealFactor,
+//                basal: basal,
+//                fraction: fraction,
+//                maxBolus: maxBolus
+//            )
+//
+//            let result = await bolusCalculationManager.calculateInsulin(input: input)
+
+            let result = await bolusCalculationManager.handleBolusCalculation(
+                carbs: carbs,
+                useFattyMealCorrection: useFattyMealCorrectionFactor,
+                useSuperBolus: useSuperBolus
+            )
 
-            guard let apsManager = apsManager else {
-                return insulinCalculated
-            }
+            // Update state properties with calculation results on main thread
+            targetDifference = result.targetDifference
+            targetDifferenceInsulin = result.targetDifferenceInsulin
+            wholeCob = result.wholeCob
+            wholeCobInsulin = result.wholeCobInsulin
+            iobInsulinReduction = result.iobInsulinReduction
+            superBolusInsulin = result.superBolusInsulin
+            wholeCalc = result.wholeCalc
+            fifteenMinInsulin = result.fifteenMinutesInsulin
 
-            return apsManager.roundBolus(amount: insulinCalculated)
+            return apsManager.roundBolus(amount: result.insulinCalculated)
         }
 
         // MARK: - Button tasks
@@ -733,7 +719,7 @@ extension Treatments.StateModel {
         let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
             .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
 
-        await updateDeterminationsArray(with: determinationObjects)
+        updateDeterminationsArray(with: determinationObjects)
     }
 
     private func mapForecastsForChart() async -> Determination? {
@@ -788,21 +774,23 @@ extension Treatments.StateModel {
         }
     }
 
-    @MainActor private func updateDeterminationsArray(with objects: [OrefDetermination]) {
-        guard let mostRecentDetermination = objects.first else { return }
-        determination = objects
-
-        // setup vars for bolus calculation
-        insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
-        evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
-        insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
-        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
-        basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
-        carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
-        insulinCalculated = calculateInsulin()
+    private func updateDeterminationsArray(with objects: [OrefDetermination]) {
+        Task { @MainActor in
+            guard let mostRecentDetermination = objects.first else { return }
+            determination = objects
+
+            // setup vars for bolus calculation
+            insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
+            evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
+            insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
+            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
+            basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
+            carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
+            insulinCalculated = await calculateInsulin()
+        }
     }
 }
 

+ 17 - 13
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -21,7 +21,7 @@ extension Treatments {
 
         @State private var showPresetSheet = false
         @State private var autofocus: Bool = true
-        @State private var calculatorDetent = PresentationDetent.medium
+        @State private var calculatorDetent = PresentationDetent.large
         @State private var pushed: Bool = false
         @State private var debounce: DispatchWorkItem?
 
@@ -66,8 +66,8 @@ extension Treatments {
         func handleDebouncedInput() {
             debounce?.cancel()
             debounce = DispatchWorkItem { [self] in
-                state.insulinCalculated = state.calculateInsulin()
                 Task {
+                    state.insulinCalculated = await state.calculateInsulin()
                     await state.updateForecasts()
                 }
             }
@@ -218,9 +218,11 @@ extension Treatments {
                                         .toggleStyle(CheckboxToggleStyle())
                                         .font(.footnote)
                                         .onChange(of: state.useFattyMealCorrectionFactor) {
-                                            state.insulinCalculated = state.calculateInsulin()
-                                            if state.useFattyMealCorrectionFactor {
-                                                state.useSuperBolus = false
+                                            Task {
+                                                state.insulinCalculated = await state.calculateInsulin()
+                                                if state.useFattyMealCorrectionFactor {
+                                                    state.useSuperBolus = false
+                                                }
                                             }
                                         }
                                     }
@@ -231,9 +233,11 @@ extension Treatments {
                                         .toggleStyle(CheckboxToggleStyle())
                                         .font(.footnote)
                                         .onChange(of: state.useSuperBolus) {
-                                            state.insulinCalculated = state.calculateInsulin()
-                                            if state.useSuperBolus {
-                                                state.useFattyMealCorrectionFactor = false
+                                            Task {
+                                                state.insulinCalculated = await state.calculateInsulin()
+                                                if state.useSuperBolus {
+                                                    state.useFattyMealCorrectionFactor = false
+                                                }
                                             }
                                         }
                                     }
@@ -330,7 +334,9 @@ extension Treatments {
             .onAppear {
                 configureView {
                     state.isActive = true
-                    state.insulinCalculated = state.calculateInsulin()
+                    Task { @MainActor in
+                        state.insulinCalculated = await state.calculateInsulin()
+                    }
                 }
             }
             .onDisappear {
@@ -339,10 +345,8 @@ extension Treatments {
             }
             .sheet(isPresented: $state.showInfo) {
                 PopupView(state: state)
-                    .presentationDetents(
-                        [.fraction(0.9), .large],
-                        selection: $calculatorDetent
-                    )
+                    .presentationDetents([.fraction(0.9), .medium, .large])
+                    .presentationDragIndicator(.visible)
             }
             .sheet(isPresented: $showPresetSheet, onDismiss: {
                 showPresetSheet = false

+ 446 - 0
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -0,0 +1,446 @@
+import CoreData
+import Foundation
+import Swinject
+
+protocol BolusCalculationManager {
+    func calculateInsulin(input: CalculationInput) async -> CalculationResult
+    func handleBolusCalculation(carbs: Decimal, useFattyMealCorrection: Bool, useSuperBolus: Bool) async -> CalculationResult
+}
+
+final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
+    @Injected() private var apsManager: APSManager!
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var fileStorage: FileStorage!
+    @Injected() private var determinationStorage: DeterminationStorage!
+
+    let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
+    let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    // MARK: - Types
+
+    private struct GlucoseVariables {
+        var currentBG: Decimal
+        var deltaBG: Decimal
+    }
+
+    private struct BolusCalculatorVariables {
+        var insulinRequired: Decimal
+        var evBG: Decimal
+        var insulin: Decimal
+        var target: Decimal
+        var isf: Decimal
+        var cob: Int16
+        var iob: Decimal
+        var basal: Decimal
+        var carbRatio: Decimal
+        var insulinCalculated: Decimal
+    }
+
+    private enum SettingType {
+        case basal
+        case carbRatio
+        case bgTarget
+        case isf
+    }
+
+    /// Retrieves current settings from the SettingsManager
+    /// - Returns: Tuple containing units, fraction, fattyMealFactor, sweetMealFactor, and maxCarbs settings
+    private func getSettings() async -> (
+        units: GlucoseUnits,
+        fraction: Decimal,
+        fattyMealFactor: Decimal,
+        sweetMealFactor: Decimal,
+        maxCarbs: Decimal
+    ) {
+        return (
+            units: settingsManager.settings.units,
+            fraction: settingsManager.settings.overrideFactor,
+            fattyMealFactor: settingsManager.settings.fattyMealFactor,
+            sweetMealFactor: settingsManager.settings.sweetMealFactor,
+            maxCarbs: settingsManager.settings.maxCarbs
+        )
+    }
+
+    /// Gets the current setting value for a specific setting type based on the time of day
+    /// - Parameter type: The type of setting to retrieve (basal, carbRatio, bgTarget, or isf)
+    /// - Returns: The current decimal value for the specified setting type
+    private func getCurrentSettingValue(for type: SettingType) async -> Decimal {
+        let now = Date()
+        let calendar = Calendar.current
+        let dateFormatter = DateFormatter()
+        dateFormatter.timeZone = TimeZone.current
+
+        let regexWithSeconds = #"^\d{2}:\d{2}:\d{2}$"#
+
+        let entries: [(start: String, value: Decimal)]
+
+        switch type {
+        case .basal:
+            let basalEntries = await getBasalProfile()
+            entries = basalEntries.map { ($0.start, $0.rate) }
+        case .carbRatio:
+            let carbRatios = await getCarbRatios()
+            entries = carbRatios.schedule.map { ($0.start, $0.ratio) }
+        case .bgTarget:
+            let bgTargets = await getBGTargets()
+            entries = bgTargets.targets.map { ($0.start, $0.low) }
+        case .isf:
+            let isfValues = await getISFValues()
+            entries = isfValues.sensitivities.map { ($0.start, $0.sensitivity) }
+        }
+
+        for (index, entry) in entries.enumerated() {
+            // Dynamically set the format based on whether it matches the regex
+            if entry.start.range(of: regexWithSeconds, options: .regularExpression) != nil {
+                dateFormatter.dateFormat = "HH:mm:ss"
+            } else {
+                dateFormatter.dateFormat = "HH:mm"
+            }
+
+            guard let entryTime = dateFormatter.date(from: entry.start) else {
+                print("Invalid entry start time: \(entry.start)")
+                continue
+            }
+
+            let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+            let entryStartTime = calendar.date(
+                bySettingHour: entryComponents.hour!,
+                minute: entryComponents.minute!,
+                second: entryComponents.second ?? 0, // Set seconds to 0 if not provided
+                of: now
+            )!
+
+            let entryEndTime: Date
+            if index < entries.count - 1 {
+                // Dynamically set the format again for the next element
+                if entries[index + 1].start.range(of: regexWithSeconds, options: .regularExpression) != nil {
+                    dateFormatter.dateFormat = "HH:mm:ss"
+                } else {
+                    dateFormatter.dateFormat = "HH:mm"
+                }
+
+                if let nextEntryTime = dateFormatter.date(from: entries[index + 1].start) {
+                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                    entryEndTime = calendar.date(
+                        bySettingHour: nextEntryComponents.hour!,
+                        minute: nextEntryComponents.minute!,
+                        second: nextEntryComponents.second ?? 0,
+                        of: now
+                    )!
+                } else {
+                    entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+                }
+            } else {
+                entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+            }
+
+            if now >= entryStartTime, now < entryEndTime {
+                return entry.value
+            }
+        }
+        return 0
+    }
+
+    /// Retrieves the pump settings from storage
+    /// - Returns: PumpSettings object containing pump configuration
+    private func getPumpSettings() async -> PumpSettings {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
+            ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+            ?? PumpSettings(insulinActionCurve: 10, maxBolus: 10, maxBasal: 2)
+    }
+
+    /// Retrieves the basal profile from storage
+    /// - Returns: Array of BasalProfileEntry objects
+    private func getBasalProfile() async -> [BasalProfileEntry] {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+            ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
+            ?? []
+    }
+
+    /// Retrieves carb ratios from storage
+    /// - Returns: CarbRatios object containing carb ratio schedule
+    private func getCarbRatios() async -> CarbRatios {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
+            ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
+            ?? CarbRatios(units: .grams, schedule: [])
+    }
+
+    /// Retrieves blood glucose targets from storage
+    /// - Returns: BGTargets object containing target schedule
+    private func getBGTargets() async -> BGTargets {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+            ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+            ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+    }
+
+    /// Retrieves insulin sensitivity factors from storage
+    /// - Returns: InsulinSensitivities object containing sensitivity schedule
+    private func getISFValues() async -> InsulinSensitivities {
+        await fileStorage.retrieveAsync(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self)
+            ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
+            ?? InsulinSensitivities(
+                units: .mgdL,
+                userPreferredUnits: .mgdL,
+                sensitivities: []
+            )
+    }
+
+    /// Fetches recent glucose readings from CoreData
+    /// - Returns: Array of NSManagedObjectIDs for glucose readings
+    private func fetchGlucose() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: glucoseFetchContext,
+            predicate: NSPredicate.glucose,
+            key: "date",
+            ascending: false,
+            fetchLimit: 288
+        )
+
+        return await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    /// Updates glucose-related variables based on recent readings
+    /// - Parameter objects: Array of GlucoseStored objects
+    /// - Returns: GlucoseVariables containing current blood glucose and delta
+    private func updateGlucoseVariables(with objects: [GlucoseStored]) -> GlucoseVariables {
+        let lastGlucose = objects.first?.glucose ?? 0
+        let thirdLastGlucose = objects.dropFirst(2).first?.glucose ?? 0
+        let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
+
+        return GlucoseVariables(currentBG: Decimal(lastGlucose), deltaBG: delta)
+    }
+
+    /// Updates bolus calculator variables based on recent determinations and current settings
+    /// - Parameters:
+    ///   - objects: Array of OrefDetermination objects
+    ///   - currentBGTarget: Current blood glucose target
+    ///   - currentISF: Current insulin sensitivity factor
+    ///   - currentCarbRatio: Current carb ratio
+    ///   - currentBasal: Current basal rate
+    /// - Returns: BolusCalculatorVariables containing updated calculation parameters
+    private func updateBolusCalculatorVariables(
+        with objects: [OrefDetermination],
+        currentBGTarget: Decimal,
+        currentISF: Decimal,
+        currentCarbRatio: Decimal,
+        currentBasal: Decimal
+    ) -> BolusCalculatorVariables {
+        guard let mostRecentDetermination = objects.first else {
+            return BolusCalculatorVariables(
+                insulinRequired: 0,
+                evBG: 0,
+                insulin: 0,
+                target: currentBGTarget,
+                isf: currentISF,
+                cob: 0,
+                iob: 0,
+                basal: currentBasal,
+                carbRatio: currentCarbRatio,
+                insulinCalculated: 0
+            )
+        }
+
+        return BolusCalculatorVariables(
+            insulinRequired: (mostRecentDetermination.insulinReq ?? 0) as Decimal,
+            evBG: (mostRecentDetermination.eventualBG ?? 0) as Decimal,
+            insulin: (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal,
+            target: (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal,
+            isf: (mostRecentDetermination.insulinSensitivity ?? NSDecimalNumber(decimal: currentISF)) as Decimal,
+            cob: mostRecentDetermination.cob as Int16,
+            iob: (mostRecentDetermination.iob ?? 0) as Decimal,
+            basal: (mostRecentDetermination.tempBasal ?? currentBasal as NSDecimalNumber) as Decimal,
+            carbRatio: (mostRecentDetermination.carbRatio ?? NSDecimalNumber(decimal: currentCarbRatio)) as Decimal,
+            insulinCalculated: 0
+        )
+    }
+
+    private func prepareCalculationInput(
+        carbs: Decimal,
+        useFattyMealCorrection: Bool,
+        useSuperBolus: Bool
+    ) async -> CalculationInput {
+        // Get settings
+        let settings = await getSettings()
+
+        // Get max bolus
+        let maxBolus = await getPumpSettings().maxBolus
+
+        // Get current profile values
+        let currentBasal = await getCurrentSettingValue(for: .basal)
+        let currentCarbRatio = await getCurrentSettingValue(for: .carbRatio)
+        let currentBGTarget = await getCurrentSettingValue(for: .bgTarget)
+        let currentISF = await getCurrentSettingValue(for: .isf)
+
+        // Fetch glucose data
+        let glucoseIds = await fetchGlucose()
+        let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(
+            with: glucoseIds,
+            context: glucoseFetchContext
+        )
+        let glucoseVars = await glucoseFetchContext.perform {
+            self.updateGlucoseVariables(with: glucoseObjects)
+        }
+
+        // Fetch determination data
+        let determinationIds = await determinationStorage.fetchLastDeterminationObjectID(
+            predicate: NSPredicate.predicateFor30MinAgoForDetermination
+        )
+        let determinationObjects: [OrefDetermination] = await CoreDataStack.shared.getNSManagedObject(
+            with: determinationIds,
+            context: determinationFetchContext
+        )
+        let bolusVars = await determinationFetchContext.perform {
+            self.updateBolusCalculatorVariables(
+                with: determinationObjects,
+                currentBGTarget: currentBGTarget,
+                currentISF: currentISF,
+                currentCarbRatio: currentCarbRatio,
+                currentBasal: currentBasal
+            )
+        }
+
+        return CalculationInput(
+            carbs: carbs,
+            currentBG: glucoseVars.currentBG,
+            deltaBG: glucoseVars.deltaBG,
+            target: bolusVars.target,
+            isf: bolusVars.isf,
+            carbRatio: bolusVars.carbRatio,
+            iob: bolusVars.iob,
+            cob: bolusVars.cob,
+            useFattyMealCorrectionFactor: useFattyMealCorrection,
+            fattyMealFactor: settings.fattyMealFactor,
+            useSuperBolus: useSuperBolus,
+            sweetMealFactor: settings.sweetMealFactor,
+            basal: bolusVars.basal,
+            fraction: settings.fraction,
+            maxBolus: maxBolus
+        )
+    }
+
+    /// Calculates the recommended insulin dose based on various parameters
+    /// - Parameter input: CalculationInput containing all necessary parameters
+    /// - Returns: CalculationResult with detailed breakdown of the calculation
+    func calculateInsulin(input: CalculationInput) async -> CalculationResult {
+        // insulin needed for the current blood glucose
+        let targetDifference = input.currentBG - input.target
+        let targetDifferenceInsulin = apsManager.roundBolus(amount: targetDifference / input.isf)
+
+        // more or less insulin because of bg trend in the last 15 minutes
+        let fifteenMinutesInsulin = apsManager.roundBolus(amount: input.deltaBG / input.isf)
+
+        // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
+        let wholeCob = Decimal(input.cob) + input.carbs
+        let wholeCobInsulin = apsManager.roundBolus(amount: wholeCob / input.carbRatio)
+
+        // determine how much the calculator reduces/ increases the bolus because of IOB
+        let iobInsulinReduction = (-1) * input.iob
+
+        // adding everything together
+        // add a calc for the case that no fifteenMinInsulin is available
+        let wholeCalc: Decimal
+        if input.deltaBG != 0 {
+            wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinutesInsulin)
+        } else {
+            // add (rare) case that no glucose value is available -> maybe display warning?
+            // if no bg is available, ?? sets its value to 0
+            if input.currentBG == 0 {
+                wholeCalc = (iobInsulinReduction + wholeCobInsulin)
+            } else {
+                wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin)
+            }
+        }
+
+        // apply custom factor at the end of the calculations
+        let result = wholeCalc * input.fraction
+
+        // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
+        var insulinCalculated: Decimal
+        var superBolusInsulin: Decimal = 0
+        if input.useFattyMealCorrectionFactor {
+            insulinCalculated = result * input.fattyMealFactor
+        } else if input.useSuperBolus {
+            superBolusInsulin = input.sweetMealFactor * input.basal
+            insulinCalculated = result + superBolusInsulin
+        } else {
+            insulinCalculated = result
+        }
+
+        // display no negative insulinCalculated
+        insulinCalculated = max(insulinCalculated, 0)
+        insulinCalculated = min(insulinCalculated, input.maxBolus)
+
+        // round calculated recommendation to allowed bolus increment
+        insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+
+        return CalculationResult(
+            insulinCalculated: insulinCalculated,
+            wholeCalc: wholeCalc,
+            correctionInsulin: targetDifferenceInsulin,
+            iobInsulinReduction: iobInsulinReduction,
+            superBolusInsulin: superBolusInsulin,
+            targetDifference: targetDifference,
+            targetDifferenceInsulin: targetDifferenceInsulin,
+            fifteenMinutesInsulin: fifteenMinutesInsulin,
+            wholeCob: wholeCob,
+            wholeCobInsulin: wholeCobInsulin
+        )
+    }
+
+    /// Handles the complete bolus calculation process
+    /// - Parameters:
+    ///   - carbs: Amount of carbohydrates to be consumed
+    ///   - useFattyMealCorrection: Whether to apply fatty meal correction
+    ///   - useSuperBolus: Whether to use super bolus calculation
+    /// - Returns: CalculationResult containing the calculated insulin dose and details
+    func handleBolusCalculation(carbs: Decimal, useFattyMealCorrection: Bool, useSuperBolus: Bool) async -> CalculationResult {
+        let input = await prepareCalculationInput(
+            carbs: carbs,
+            useFattyMealCorrection: useFattyMealCorrection,
+            useSuperBolus: useSuperBolus
+        )
+        let result = await calculateInsulin(input: input)
+        return result
+    }
+}
+
+/// Input parameters required for bolus calculation
+struct CalculationInput: Sendable {
+    let carbs: Decimal // Carbohydrates to be consumed (in grams)
+    let currentBG: Decimal // Current blood glucose level
+    let deltaBG: Decimal // Blood glucose change in last 15 minutes
+    let target: Decimal // Target blood glucose level
+    let isf: Decimal // Insulin Sensitivity Factor
+    let carbRatio: Decimal // Carb to insulin ratio
+    let iob: Decimal // Insulin on Board
+    let cob: Int16 // Carbs on Board
+    let useFattyMealCorrectionFactor: Bool // Whether to apply fatty meal correction
+    let fattyMealFactor: Decimal // Factor for fatty meal adjustment
+    let useSuperBolus: Bool // Whether to use super bolus calculation
+    let sweetMealFactor: Decimal // Factor for sweet meal adjustment
+    let basal: Decimal // Current basal rate
+    let fraction: Decimal // General correction factor
+    let maxBolus: Decimal // Maximum allowed bolus
+}
+
+/// Results of the bolus calculation
+struct CalculationResult: Sendable {
+    let insulinCalculated: Decimal // Final calculated insulin amount
+    let wholeCalc: Decimal // Total calculation before adjustments
+    let correctionInsulin: Decimal // Insulin for BG correction
+    let iobInsulinReduction: Decimal // IOB reduction amount
+    let superBolusInsulin: Decimal // Additional insulin for super bolus
+    let targetDifference: Decimal // Difference from target BG
+    let targetDifferenceInsulin: Decimal // Insulin needed for target difference
+    let fifteenMinutesInsulin: Decimal // Trend-based insulin adjustment
+    let wholeCob: Decimal // Total carbs (COB + new carbs)
+    let wholeCobInsulin: Decimal // Insulin needed for total carbs
+}

+ 125 - 53
Trio/Sources/Services/WatchManager/AppleWatchManager.swift

@@ -2,6 +2,7 @@ import Combine
 import CoreData
 import Foundation
 import Swinject
+import UIKit
 import WatchConnectivity
 
 /// Protocol defining the base functionality for Watch communication
@@ -21,6 +22,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
     @Injected() private var determinationStorage: DeterminationStorage!
     @Injected() private var overrideStorage: OverrideStorage!
     @Injected() private var tempTargetStorage: TempTargetsStorage!
+    @Injected() private var bolusCalculationManager: BolusCalculationManager!
 
     private var units: GlucoseUnits = .mgdL
     private var glucoseColorScheme: GlucoseColorScheme = .staticColor
@@ -66,7 +68,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 guard let self = self else { return }
                 Task {
                     let state = await self.setupWatchState()
-                    self.sendDataToWatch(state)
+                    await self.sendDataToWatch(state)
                 }
             }
             .store(in: &subscriptions)
@@ -80,7 +82,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             guard let self = self else { return }
             Task {
                 let state = await self.setupWatchState()
-                self.sendDataToWatch(state)
+                await self.sendDataToWatch(state)
             }
         }.store(in: &subscriptions)
 
@@ -89,7 +91,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             guard let self = self else { return }
             Task {
                 let state = await self.setupWatchState()
-                self.sendDataToWatch(state)
+                await self.sendDataToWatch(state)
             }
         }.store(in: &subscriptions)
 
@@ -97,7 +99,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             guard let self = self else { return }
             Task {
                 let state = await self.setupWatchState()
-                self.sendDataToWatch(state)
+                await self.sendDataToWatch(state)
             }
         }.store(in: &subscriptions)
 
@@ -105,7 +107,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             guard let self = self else { return }
             Task {
                 let state = await self.setupWatchState()
-                self.sendDataToWatch(state)
+                await self.sendDataToWatch(state)
             }
         }.store(in: &subscriptions)
     }
@@ -188,27 +190,46 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                 return watchState
             }
 
-            // Map glucose values
-            watchState.glucoseValues = glucoseObjects.compactMap { glucose in
+            // Assign currentGlucose and its color
+            /// Set current glucose with proper formatting
+            if self.units == .mgdL {
+                watchState.currentGlucose = "\(latestGlucose.glucose)"
+            } else {
+                let mgdlValue = Decimal(latestGlucose.glucose)
+                let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
+                watchState.currentGlucose = "\(latestGlucoseValue)"
+            }
 
-                var glucoseValue: Double
-                if self.units == .mgdL {
-                    glucoseValue = Double(glucose.glucose)
-                } else {
-                    let mgdlValue = Decimal(glucose.glucose)
-                    glucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
-                }
+            /// Calculate latest color
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
 
-                // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
-                let hardCodedLow = Decimal(55)
-                let hardCodedHigh = Decimal(220)
-                let isDynamicColorScheme = self.glucoseColorScheme == .dynamicColor
+            let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
+            let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
+            let highGlucoseColorValue = self.units == .mgdL ? highGlucoseValue : highGlucoseValue.asMmolL
+            let lowGlucoseColorValue = self.units == .mgdL ? lowGlucoseValue : lowGlucoseValue.asMmolL
+            let targetGlucose = self.units == .mgdL ? self.currentGlucoseTarget : self.currentGlucoseTarget.asMmolL
 
-                let highGlucoseValue = isDynamicColorScheme ? hardCodedHigh : self.highGlucose
-                let lowGlucoseValue = isDynamicColorScheme ? hardCodedLow : self.lowGlucose
-                let highGlucoseColorValue = self.units == .mgdL ? highGlucoseValue : highGlucoseValue.asMmolL
-                let lowGlucoseColorValue = self.units == .mgdL ? lowGlucoseValue : lowGlucoseValue.asMmolL
-                let targetGlucose = self.units == .mgdL ? self.currentGlucoseTarget : self.currentGlucoseTarget.asMmolL
+            let currentGlucoseColor = Trio.getDynamicGlucoseColor(
+                glucoseValue: Decimal(latestGlucose.glucose),
+                highGlucoseColorValue: highGlucoseColorValue,
+                lowGlucoseColorValue: lowGlucoseColorValue,
+                targetGlucose: targetGlucose,
+                glucoseColorScheme: self.glucoseColorScheme
+            )
+
+            if Decimal(latestGlucose.glucose) <= self.lowGlucose || Decimal(latestGlucose.glucose) >= self.highGlucose {
+                watchState.currentGlucoseColorString = currentGlucoseColor.toHexString()
+            } else {
+                watchState.currentGlucoseColorString = "#ffffff" // white when in range; colored when out of range
+            }
+
+            // Map glucose values
+            watchState.glucoseValues = glucoseObjects.compactMap { glucose in
+                let glucoseValue = self.units == .mgdL
+                    ? Double(glucose.glucose)
+                    : Double(truncating: Decimal(glucose.glucose).asMmolL as NSNumber)
 
                 let glucoseColor = Trio.getDynamicGlucoseColor(
                     glucoseValue: Decimal(glucose.glucose),
@@ -218,21 +239,10 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     glucoseColorScheme: self.glucoseColorScheme
                 )
 
-                let colorString = glucoseColor.toHexString()
-
-                return (date: glucose.date ?? Date(), glucose: glucoseValue, color: colorString)
+                return (date: glucose.date ?? Date(), glucose: glucoseValue, color: glucoseColor.toHexString())
             }
             .sorted { $0.date < $1.date }
 
-            // Set current glucose with proper formatting
-            if self.units == .mgdL {
-                watchState.currentGlucose = "\(latestGlucose.glucose)"
-            } else {
-                let mgdlValue = Decimal(latestGlucose.glucose)
-                let latestGlucoseValue = Double(truncating: mgdlValue.asMmolL as NSNumber)
-                watchState.currentGlucose = "\(latestGlucoseValue)"
-            }
-
             // Convert direction to trend string
             watchState.trend = latestGlucose.direction
 
@@ -255,13 +265,14 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             // Set units
             watchState.units = self.units
 
-            // Add settings values
+            // Add limits and pump specific dosing increment settings values
             watchState.maxBolus = self.settingsManager.pumpSettings.maxBolus
             watchState.maxCarbs = self.settingsManager.settings.maxCarbs
             watchState.maxFat = self.settingsManager.settings.maxFat
             watchState.maxProtein = self.settingsManager.settings.maxProtein
             watchState.maxIOB = self.settingsManager.preferences.maxIOB
             watchState.maxCOB = self.settingsManager.preferences.maxCOB
+            watchState.bolusIncrement = self.settingsManager.preferences.bolusIncrement
 
             debug(
                 .watchManager,
@@ -296,14 +307,47 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
 
     /// Sends the state of type WatchState to the connected Watch
     /// - Parameter state: Current WatchState containing glucose data to be sent
-    func sendDataToWatch(_ state: WatchState) {
-        guard let session = session, session.isReachable else {
-            debug(.watchManager, "⌚️ Watch not reachable")
+    @MainActor func sendDataToWatch(_ state: WatchState) async {
+        guard let session = session else { return }
+
+        guard session.isPaired else {
+            debug(.watchManager, "⌚️❌ No Watch is paired")
             return
         }
 
+        guard session.isWatchAppInstalled else {
+            debug(.watchManager, "⌚️❌ Trio Watch app is")
+            return
+        }
+
+        guard session.activationState == .activated else {
+            let activationStateString = "\(session.activationState)"
+            debug(.watchManager, "⌚️ Watch session activationState = \(activationStateString). Reactivating...")
+            session.activate()
+            return
+        }
+
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Watch Data Upload") {
+            guard backgroundTaskID != .invalid else { return }
+            Task {
+                UIApplication.shared.endBackgroundTask(backgroundTaskID)
+            }
+            backgroundTaskID = .invalid
+        }
+
+        defer {
+            if backgroundTaskID != .invalid {
+                Task {
+                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
+                }
+                backgroundTaskID = .invalid
+            }
+        }
+
         let message: [String: Any] = [
-            "currentGlucose": state.currentGlucose ?? "",
+            "currentGlucose": state.currentGlucose ?? "--",
+            "currentGlucoseColorString": state.currentGlucoseColorString ?? "#ffffff",
             "trend": state.trend ?? "",
             "delta": state.delta ?? "",
             "iob": state.iob ?? "",
@@ -333,16 +377,18 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             "maxFat": state.maxFat,
             "maxProtein": state.maxProtein,
             "maxIOB": state.maxIOB,
-            "maxCOB": state.maxCOB
+            "maxCOB": state.maxCOB,
+            "bolusIncrement": state.bolusIncrement
         ]
 
-        debug(.watchManager, "📱 Sending to watch - Message content:")
-        message.forEach { key, value in
-            debug(.watchManager, "📱 \(key): \(value) (type: \(type(of: value)))")
-        }
-
-        session.sendMessage(message, replyHandler: nil) { error in
-            debug(.watchManager, "❌ Error sending data: \(error.localizedDescription)")
+        // if session is reachable, it means watch App is in the foreground -> send watchState as message
+        // if session is not reachable, it means it's in background -> send watchState as userInfo
+        if session.isReachable {
+            session.sendMessage(message, replyHandler: nil, errorHandler: { (error) -> Void in
+                debug(.watchManager, "❌ Error sending watch state: \(error.localizedDescription)")
+            })
+        } else {
+            session.transferUserInfo(message)
         }
     }
 
@@ -376,7 +422,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
         // Try to send initial data after activation
         Task {
             let state = await self.setupWatchState()
-            self.sendDataToWatch(state)
+            await self.sendDataToWatch(state)
         }
     }
 
@@ -442,6 +488,32 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
                     await self?.apsManager.determineBasalSync()
                 }
             }
+
+            if message["requestBolusRecommendation"] as? Bool == true {
+                let carbs = message["carbs"] as? Int ?? 0
+
+                Task { [weak self] in
+                    guard let self = self else { return }
+
+                    // Get recommendation from BolusCalculationManager
+                    let result = await bolusCalculationManager.handleBolusCalculation(
+                        carbs: Decimal(carbs),
+                        useFattyMealCorrection: false,
+                        useSuperBolus: false
+                    )
+
+                    // Send recommendation back to watch
+                    let recommendationMessage: [String: Any] = [
+                        "recommendedBolus": NSDecimalNumber(decimal: result.insulinCalculated)
+                    ]
+
+                    if let session = self.session, session.isReachable {
+                        print("📱 Sending recommendedBolus: \(result.insulinCalculated)")
+                        session.sendMessage(recommendationMessage, replyHandler: nil)
+                    }
+                }
+                return
+            }
         }
     }
 
@@ -459,7 +531,7 @@ final class BaseWatchManager: NSObject, WCSessionDelegate, Injectable, WatchMana
             // Try to send data when connection is established
             Task {
                 let state = await self.setupWatchState()
-                self.sendDataToWatch(state)
+                await self.sendDataToWatch(state)
             }
         } else {
             // Try to reconnect after a short delay
@@ -812,7 +884,7 @@ extension BaseWatchManager: SettingsObserver, PumpSettingsObserver, PreferencesO
     func preferencesDidChange(_: Preferences) {
         Task {
             let state = await self.setupWatchState()
-            self.sendDataToWatch(state)
+            await self.sendDataToWatch(state)
         }
     }
 
@@ -820,7 +892,7 @@ extension BaseWatchManager: SettingsObserver, PumpSettingsObserver, PreferencesO
     func pumpSettingsDidChange(_: PumpSettings) {
         Task {
             let state = await self.setupWatchState()
-            self.sendDataToWatch(state)
+            await self.sendDataToWatch(state)
         }
     }
 
@@ -833,7 +905,7 @@ extension BaseWatchManager: SettingsObserver, PumpSettingsObserver, PreferencesO
 
         Task {
             let state = await self.setupWatchState()
-            self.sendDataToWatch(state)
+            await self.sendDataToWatch(state)
         }
     }
 }

+ 2 - 2
Trio/Sources/Shortcuts/TempPresets/TempPresetsIntentRequest.swift

@@ -77,7 +77,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
                 return try self.coredataContext.fetch(fetchRequest).first?.objectID
             } catch {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Override: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Target: \(error.localizedDescription)"
                 )
                 return nil
             }
@@ -87,7 +87,7 @@ final class TempPresetsIntentRequest: BaseIntentsRequest {
     @MainActor func enactTempTarget(_ preset: TempPreset) async -> Bool {
         // Start background task
         var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
-        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Override Upload") {
+        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TempTarget Upload") {
             guard backgroundTaskID != .invalid else { return }
             Task {
                 // End background task when the time is about to expire