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

Merge pull request #32 from polscm32/release/2.3.3

Release 2.3.3
Andreas Stokholm 2 лет назад
Родитель
Сommit
bfca31f57f
67 измененных файлов с 1833 добавлено и 1608 удалено
  1. 2 2
      Config.xcconfig
  2. 7 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/Pod.swift
  3. 116 86
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift
  4. 38 18
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodCommsSession.swift
  5. 18 17
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift
  6. 8 8
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewControllers/DashUICoordinator.swift
  7. 16 27
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/InsertCannulaViewModel.swift
  8. 29 22
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/PairPodViewModel.swift
  9. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/PodLifeState.swift
  10. 4 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/InsertCannulaView.swift
  11. 7 3
      Dependencies/OmniKit/OmniKit/OmnipodCommon/Pod.swift
  12. 46 22
      Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift
  13. 38 18
      Dependencies/OmniKit/OmniKit/PumpManager/PodCommsSession.swift
  14. 16 16
      Dependencies/OmniKit/OmniKit/PumpManager/PodState.swift
  15. 7 3
      Dependencies/OmniKit/OmniKitTests/PodCommsSessionTests.swift
  16. 7 7
      Dependencies/OmniKit/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift
  17. 16 27
      Dependencies/OmniKit/OmniKitUI/ViewModels/InsertCannulaViewModel.swift
  18. 28 21
      Dependencies/OmniKit/OmniKitUI/ViewModels/PairPodViewModel.swift
  19. 1 1
      Dependencies/OmniKit/OmniKitUI/ViewModels/PodLifeState.swift
  20. 3 1
      Dependencies/OmniKit/OmniKitUI/Views/InsertCannulaView.swift
  21. 16 16
      FreeAPS.xcodeproj/project.pbxproj
  22. 1 1
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  23. 1 1
      FreeAPS/Resources/Assets.xcassets/Colors/LoopYellow.colorset/Contents.json
  24. 20 0
      FreeAPS/Sources/Helpers/CheckboxToggleStyle.swift
  25. 52 0
      FreeAPS/Sources/Helpers/CustomProgressView.swift
  26. 3 0
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  27. 3 0
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  28. 3 0
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  29. 3 0
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  30. 3 0
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  31. 3 0
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  32. 3 0
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  33. 3 0
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  34. 3 0
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  35. 3 0
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  36. 3 0
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  37. 3 0
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  38. 3 0
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  39. 3 0
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  40. 3 0
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  41. 3 0
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  42. 3 0
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  43. 3 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  44. 3 0
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  45. 3 0
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  46. 3 0
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  47. 3 3
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  48. 121 16
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  49. 290 216
      FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift
  50. 0 11
      FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift
  51. 0 113
      FreeAPS/Sources/Modules/Bolus/View/Predictions.swift
  52. 2 2
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift
  53. 1 1
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  54. 33 45
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  55. 59 139
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  56. 0 7
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  57. 50 5
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  58. 540 526
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  59. 1 1
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  60. 1 1
      FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift
  61. 4 4
      FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift
  62. 64 91
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  63. 1 1
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift
  64. 1 1
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  65. 90 99
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  66. 3 3
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  67. 8 1
      FreeAPS/Sources/Views/DecimalTextField.swift

+ 2 - 2
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.3.2
+APP_VERSION = 2.3.3
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##
@@ -9,4 +9,4 @@ APP_ICON = pod_colorful
 APP_URL_SCHEME = freeaps-x
 
 #include? "ConfigOverride.xcconfig"
-//#include? "../../ConfigOverride.xcconfig"
+#include? "../../ConfigOverride.xcconfig"

+ 7 - 3
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/Pod.swift

@@ -106,16 +106,20 @@ public enum DeliveryStatus: UInt8, CustomStringConvertible {
     case bolusAndTempBasal = 6
     case extendedBolusRunning = 9
     case extendedBolusAndTempBasal = 10
-    
+
+    public var suspended: Bool {
+        return self == .suspended
+    }
+
     public var bolusing: Bool {
         return self == .bolusInProgress || self == .bolusAndTempBasal || self == .extendedBolusRunning || self == .extendedBolusAndTempBasal
     }
-    
+
     public var tempBasalRunning: Bool {
         return self == .tempBasalRunning || self == .bolusAndTempBasal || self == .extendedBolusAndTempBasal
     }
 
-    public var extendedBolusRunninng: Bool {
+    public var extendedBolusRunning: Bool {
         return self == .extendedBolusRunning || self == .extendedBolusAndTempBasal
     }
 

+ 116 - 86
Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift

@@ -32,7 +32,6 @@ public enum OmniBLEPumpManagerError: Error {
     case insulinTypeNotConfigured
     case notReadyForCannulaInsertion
     case invalidSetting
-    case setupNotComplete
     case communication(Error)
     case state(Error)
 }
@@ -45,13 +44,11 @@ extension OmniBLEPumpManagerError: LocalizedError {
         case .podAlreadyPaired:
             return LocalizedString("Pod already paired", comment: "Error message shown when user cannot pair because pod is already paired")
         case .insulinTypeNotConfigured:
-            return LocalizedString("Insulin type not configured", comment: "Error description for OmniBLEPumpManagerError.insulinTypeNotConfigured")
+            return LocalizedString("Insulin type not configured", comment: "Error description for insulin type not configured")
         case .notReadyForCannulaInsertion:
             return LocalizedString("Pod is not in a state ready for cannula insertion.", comment: "Error message when cannula insertion fails because the pod is in an unexpected state")
         case .invalidSetting:
             return LocalizedString("Invalid Setting", comment: "Error description for invalid setting")
-        case .setupNotComplete:
-            return LocalizedString("Pod setup is not complete", comment: "Error description when pod setup is not complete")
         case .communication(let error):
             if let error = error as? LocalizedError {
                 return error.errorDescription
@@ -458,7 +455,7 @@ extension OmniBLEPumpManager {
         } else if !podState.isSetupComplete {
             return .activating
         }
-        return .deactivating
+        return .deactivating // Can't be reached and thus will never be returned
     }
 
     public var podCommState: PodCommState {
@@ -599,7 +596,7 @@ extension OmniBLEPumpManager {
         switch podCommState(for: state) {
         case .activating:
             return PumpStatusHighlight(
-                localizedMessage: LocalizedString("Finish Pairing", comment: "Status highlight that when pod is activating."),
+                localizedMessage: LocalizedString("Finish Setup", comment: "Status highlight that when pod is activating."),
                 imageName: "exclamationmark.circle.fill",
                 state: .warning)
         case .deactivating:
@@ -710,6 +707,25 @@ extension OmniBLEPumpManager {
         }
     }
 
+    // Reset all the per pod state kept in pump manager state which doesn't span pods
+    fileprivate func resetPerPodPumpManagerState() {
+
+        // Reset any residual per pod slot based pump manager alerts
+        // (i.e., all but timeOffsetChangeDetected which isn't actually used)
+        let podAlerts = state.activeAlerts.filter { $0 != .timeOffsetChangeDetected }
+        for alert in podAlerts {
+            self.retractAlert(alert: alert)
+        }
+
+        self.setState { (state) in
+            // Reset alertsWithPendingAcknowledgment which are all pod slot based
+            state.alertsWithPendingAcknowledgment = []
+
+            // Reset other miscellaneous state variables that are actually per pod
+            state.podAttachmentConfirmed = false
+            state.acknowledgedTimeOffsetAlert = false
+        }
+    }
 
     // MARK: - Pod comms
 
@@ -737,13 +753,14 @@ extension OmniBLEPumpManager {
 
         self.podComms.forgetPod()
 
+        self.resetPerPodPumpManagerState()
+
         if let dosesToStore = state.podState?.dosesToStore {
             store(doses: dosesToStore, completion: { error in
                 self.setState({ (state) in
                     if error != nil {
                         state.unstoredDoses.append(contentsOf: dosesToStore)
                     }
-                    state.alertsWithPendingAcknowledgment = []
                 })
                 self.prepForNewPod()
                 completion()
@@ -778,6 +795,8 @@ extension OmniBLEPumpManager {
         self.podComms.delegate = self
         self.podComms.messageLogger = self
 
+        self.resetPerPodPumpManagerState()
+
         setState({ (state) in
             state.updatePodStateFromPodComms(podState)
             state.scheduledExpirationReminderOffset = state.defaultExpirationReminderOffset
@@ -875,6 +894,8 @@ extension OmniBLEPumpManager {
                     self.podComms.pairAndSetupPod(timeZone: .currentFixed, insulinType: insulinType, messageLogger: self)
                     { (result) in
 
+                        self.resetPerPodPumpManagerState()
+
                         // Calls completion
                         primeSession(result)
                     }
@@ -1087,7 +1108,7 @@ extension OmniBLEPumpManager {
 
         guard state.podState?.setupProgress == .completed else {
             // A cancel delivery command before pod setup is complete will fault the pod
-            completion(.state(OmniBLEPumpManagerError.setupNotComplete))
+            completion(.state(PodCommsError.setupNotComplete))
             return
         }
 
@@ -1127,7 +1148,7 @@ extension OmniBLEPumpManager {
 
             guard state.podState?.setupProgress == .completed else {
                 // A cancel delivery command before pod setup is complete will fault the pod
-                return .failure(PumpManagerError.deviceState(OmniBLEPumpManagerError.setupNotComplete))
+                return .failure(PumpManagerError.deviceState(PodCommsError.setupNotComplete))
             }
 
             guard state.podState?.unfinalizedBolus?.isFinished() != false else {
@@ -1822,7 +1843,7 @@ extension OmniBLEPumpManager: PumpManager {
 
         guard state.podState?.setupProgress == .completed else {
             // A cancel delivery command before pod setup is complete will fault the pod
-            completion(.failure(PumpManagerError.deviceState(OmniBLEPumpManagerError.setupNotComplete)))
+            completion(.failure(PumpManagerError.deviceState(PodCommsError.setupNotComplete)))
             return
         }
 
@@ -1883,8 +1904,15 @@ extension OmniBLEPumpManager: PumpManager {
     }
 
     public func runTemporaryBasalProgram(unitsPerHour: Double, for duration: TimeInterval, automatic: Bool, completion: @escaping (PumpManagerError?) -> Void) {
-        guard self.hasActivePod else {
-            completion(.deviceState(OmniBLEPumpManagerError.noPodPaired))
+
+        guard self.hasActivePod, let podState = self.state.podState else {
+            completion(.configuration(OmniBLEPumpManagerError.noPodPaired))
+            return
+        }
+
+        guard state.podState?.setupProgress == .completed else {
+            // A cancel delivery command before pod setup is complete will fault the pod
+            completion(.deviceState(PodCommsError.setupNotComplete))
             return
         }
 
@@ -1917,95 +1945,97 @@ extension OmniBLEPumpManager: PumpManager {
                 return
             }
 
-            do {
-                if case .some(.suspended) = self.state.podState?.suspendState {
-                    self.log.info("Not enacting temp basal because podState indicates pod is suspended.")
-                    throw PodCommsError.podSuspended
-                }
-
-                // A resume scheduled basal delivery request is denoted by a 0 duration that cancels any existing temp basal.
-                let resumingScheduledBasal = duration < .ulpOfOne
+            if case (.suspended) = podState.suspendState {
+                self.log.info("Not enacting temp basal because podState indicates pod is suspended.")
+                completion(.deviceState(PodCommsError.podSuspended))
+                return
+            }
 
-                // If a bolus is not finished, fail if not resuming the scheduled basal
-                guard self.state.podState?.unfinalizedBolus?.isFinished() != false || resumingScheduledBasal else {
-                    self.log.info("Not enacting temp basal because podState indicates unfinalized bolus in progress.")
-                    throw PodCommsError.unfinalizedBolus
-                }
+            // A resume scheduled basal delivery request is denoted by a 0 duration that cancels any existing temp basal.
+            let resumingScheduledBasal = duration < .ulpOfOne
 
-                // Did the last message have comms issues or is the last delivery status not verified correctly?
-                let uncertainDeliveryStatus = self.state.podState?.lastCommsOK == false || self.state.podState?.deliveryStatusVerified == false
+            // If a bolus is not finished, fail if not resuming the scheduled basal
+            guard podState.unfinalizedBolus?.isFinished() != false || resumingScheduledBasal else {
+                self.log.info("Not enacting temp basal because podState indicates unfinalized bolus in progress.")
+                completion(.deviceState(PodCommsError.unfinalizedBolus))
+                return
+            }
 
-                // Do the cancel temp basal command if currently running a temp basal OR
-                // if resuming scheduled basal delivery OR if the delivery status is uncertain.
-                if self.state.podState?.unfinalizedTempBasal != nil || resumingScheduledBasal || uncertainDeliveryStatus {
-                    let status: StatusResponse
+            // Do the safe cancel TB command when resuming scheduled basal delivery OR if unfinalizedTempBasal indicates a
+            // running a temp basal OR if we don't have the last pod delivery status confirming that no temp basal is running.
+            if resumingScheduledBasal || podState.unfinalizedTempBasal != nil ||
+                podState.lastDeliveryStatusReceived == nil || podState.lastDeliveryStatusReceived!.tempBasalRunning
+            {
+                let status: StatusResponse
 
-                    // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
-                    let beepType: BeepType = resumingScheduledBasal && acknowledgementBeep ? .beep : .noBeepCancel
-                    let result = session.cancelDelivery(deliveryType: .tempBasal, beepType: beepType)
-                    switch result {
-                    case .certainFailure(let error):
-                        throw error
-                    case .unacknowledged(let error):
-                        throw error
-                    case .success(let cancelTempStatus, _):
-                        status = cancelTempStatus
-                    }
+                // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
+                let beepType: BeepType = resumingScheduledBasal && acknowledgementBeep ? .beep : .noBeepCancel
+                let result = session.cancelDelivery(deliveryType: .tempBasal, beepType: beepType)
+                switch result {
+                case .certainFailure(let error):
+                    completion(.communication(error))
+                    return
+                case .unacknowledged(let error):
+                    completion(.communication(error))
+                    return
+                case .success(let cancelTempStatus, _):
+                    status = cancelTempStatus
+                }
 
-                    // If pod is bolusing, fail if not resuming the scheduled basal
-                    guard !status.deliveryStatus.bolusing || resumingScheduledBasal else {
-                        throw PodCommsError.unfinalizedBolus
-                    }
+                // If pod is bolusing, fail if not resuming the scheduled basal
+                guard !status.deliveryStatus.bolusing || resumingScheduledBasal else {
+                    self.log.info("Canceling temp basal because status return indicates bolus in progress.")
+                    completion(.communication(PodCommsError.unfinalizedBolus))
+                    return
+                }
 
-                    guard status.deliveryStatus != .suspended else {
-                        self.log.info("Canceling temp basal because status return indicates pod is suspended.")
-                        throw PodCommsError.podSuspended
-                    }
-                } else {
-                    self.log.info("Skipped Cancel TB command before enacting temp basal")
+                guard status.deliveryStatus != .suspended else {
+                    self.log.info("Canceling temp basal because status return indicates pod is suspended!")
+                    completion(.communication(PodCommsError.podSuspended))
+                    return
                 }
+            } else {
+                self.log.info("Skipped Cancel TB command before enacting temp basal")
+            }
 
-                defer {
-                    self.setState({ (state) in
-                        state.tempBasalEngageState = .stable
-                    })
+            defer {
+                self.setState({ (state) in
+                    state.tempBasalEngageState = .stable
+                })
+            }
+
+            if resumingScheduledBasal {
+                self.setState({ (state) in
+                    state.tempBasalEngageState = .disengaging
+                })
+                session.dosesForStorage() { (doses) -> Bool in
+                    return self.store(doses: doses, in: session)
                 }
+                completion(nil)
+            } else {
+                self.setState({ (state) in
+                    state.tempBasalEngageState = .engaging
+                })
 
-                if resumingScheduledBasal {
-                    self.setState({ (state) in
-                        state.tempBasalEngageState = .disengaging
-                    })
+                var calendar = Calendar(identifier: .gregorian)
+                calendar.timeZone = self.state.timeZone
+                let scheduledRate = self.state.basalSchedule.currentRate(using: calendar, at: self.dateGenerator())
+                let isHighTemp = rate > scheduledRate
+
+                let result = session.setTempBasal(rate: rate, duration: duration, isHighTemp: isHighTemp, automatic: automatic, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep)
+                switch result {
+                case .success:
                     session.dosesForStorage() { (doses) -> Bool in
                         return self.store(doses: doses, in: session)
                     }
                     completion(nil)
-                } else {
-                    self.setState({ (state) in
-                        state.tempBasalEngageState = .engaging
-                    })
-
-                    var calendar = Calendar(identifier: .gregorian)
-                    calendar.timeZone = self.state.timeZone
-                    let scheduledRate = self.state.basalSchedule.currentRate(using: calendar, at: self.dateGenerator())
-                    let isHighTemp = rate > scheduledRate
-
-                    let result = session.setTempBasal(rate: rate, duration: duration, isHighTemp: isHighTemp, automatic: automatic, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep)
-
-                    switch result {
-                    case .success:
-                        session.dosesForStorage() { (doses) -> Bool in
-                            return self.store(doses: doses, in: session)
-                        }
-                        completion(nil)
-                    case .unacknowledged(let error):
-                        throw error
-                    case .certainFailure(let error):
-                        throw error
-                    }
+                case .unacknowledged(let error):
+                    self.log.error("Temp basal uncertain error: %@", String(describing: error))
+                    completion(nil)
+                case .certainFailure(let error):
+                    self.log.error("setTempBasal failed: %{public}@", String(describing: error))
+                    completion(.communication(error))
                 }
-            } catch let error {
-                self.log.error("Error during temp basal: %{public}@", String(describing: error))
-                completion(.communication(error as? LocalizedError))
             }
         }
     }

+ 38 - 18
Dependencies/OmniBLE/OmniBLE/PumpManager/PodCommsSession.swift

@@ -38,6 +38,7 @@ public enum PodCommsError: Error {
     case podIncompatible(str: String)
     case noPodsFound
     case tooManyPodsFound
+    case setupNotComplete
 }
 
 extension PodCommsError: LocalizedError {
@@ -96,7 +97,8 @@ extension PodCommsError: LocalizedError {
             return LocalizedString("No pods found", comment: "Error message for PodCommsError.noPodsFound")
         case .tooManyPodsFound:
             return LocalizedString("Too many pods found", comment: "Error message for PodCommsError.tooManyPodsFound")
-
+        case .setupNotComplete:
+            return LocalizedString("Pod setup is not complete", comment: "Error description when pod setup is not complete")
         }
     }
     
@@ -158,6 +160,8 @@ extension PodCommsError: LocalizedError {
             return LocalizedString("Make sure your pod is filled and nearby.", comment: "Recovery suggestion for PodCommsError.noPodsFound")
         case .tooManyPodsFound:
             return LocalizedString("Move to a new area away from any other pods and try again.", comment: "Recovery suggestion for PodCommsError.tooManyPodsFound")
+        case .setupNotComplete:
+            return nil
         }
     }
 
@@ -274,7 +278,9 @@ public class PodCommsSession {
 
             let message = Message(address: podState.address, messageBlocks: blocksToSend, sequenceNum: messageNumber, expectFollowOnMessage: expectFollowOnMessage)
 
-            self.podState.lastCommsOK = false // mark last comms as not OK until we get the expected response
+            // Clear the lastDeliveryStatusReceived variable which is used to guard against possible 0x31 pod faults
+            podState.lastDeliveryStatusReceived = nil
+
             let response = try transport.sendMessage(message)
             
             // Simulate fault
@@ -283,7 +289,6 @@ public class PodCommsSession {
 
             if let responseMessageBlock = response.messageBlocks[0] as? T {
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
-                self.podState.lastCommsOK = true // message successfully sent and expected response received
                 return responseMessageBlock
             }
 
@@ -431,7 +436,6 @@ public class PodCommsSession {
 
     public func insertCannula(optionalAlerts: [PodAlert] = [], silent: Bool) throws -> TimeInterval {
         let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
-        let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
         guard podState.activatedAt != nil else {
             throw PodCommsError.noPodPaired
@@ -443,7 +447,8 @@ public class PodCommsSession {
             if status.podProgressStatus == .insertingCannula {
                 podState.setupProgress = .cannulaInserting
                 podState.updateFromStatusResponse(status, at: currentDate)
-                return insertionWait // Not sure when it started, wait full time to be sure
+                // return a non-zero wait time based on the bolus not yet delivered
+                return (status.bolusNotDelivered / Pod.primeDeliveryRate) + 1
             }
             if status.podProgressStatus.readyForDelivery {
                 markSetupProgressCompleted(statusResponse: status)
@@ -472,7 +477,7 @@ public class PodCommsSession {
         podState.updateFromStatusResponse(status2, at: currentDate)
         
         podState.setupProgress = .cannulaInserting
-        return insertionWait
+        return status2.bolusNotDelivered / Pod.primeDeliveryRate // seconds for the cannula insert bolus to finish
     }
 
     public func checkInsertionCompleted() throws {
@@ -508,16 +513,18 @@ public class PodCommsSession {
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, units: units, timeBetweenPulses: timeBetweenPulses, extendedUnits: extendedUnits, extendedDuration: extendedDuration)
         
-        // Do a getstatus to verify that there isn't an on-going bolus in progress if the last bolus command is still
-        // finalized, if the last delivery status wasn't successfully verified or the last comms attempt wasn't OK
-        if podState.unfinalizedBolus != nil || !podState.deliveryStatusVerified || !podState.lastCommsOK {
-            var ongoingBolus = true
+        // Do a get status here to verify that there isn't an on-going bolus in progress if the last bolus command
+        // is still not finalized OR we don't have the last pod delivery status confirming that no bolus is active.
+        if podState.unfinalizedBolus != nil || podState.lastDeliveryStatusReceived == nil || podState.lastDeliveryStatusReceived!.bolusing {
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
                 podState.updateFromStatusResponse(statusResponse, at: currentDate)
-                ongoingBolus = podState.unfinalizedBolus != nil
-            }
-            guard !ongoingBolus else {
-                return DeliveryCommandResult.certainFailure(error: .unfinalizedBolus)
+                guard podState.unfinalizedBolus == nil else {
+                    log.default("bolus: pod is still bolusing")
+                    return DeliveryCommandResult.certainFailure(error: .unfinalizedBolus)
+                }
+            } else {
+                log.default("bolus: failed to read pod status to verify there is no bolus running")
+                return DeliveryCommandResult.certainFailure(error: .noResponse)
             }
         }
 
@@ -619,6 +626,11 @@ public class PodCommsSession {
             return .certainFailure(error: .unacknowledgedCommandPending)
         }
 
+        guard podState.setupProgress == .completed else {
+            // A cancel delivery command before pod setup is complete will fault the pod
+            return .certainFailure(error: PodCommsError.setupNotComplete)
+        }
+
         do {
             var alertConfigurations: [AlertConfiguration] = []
             var podSuspendedReminderAlert: PodAlert? = nil
@@ -699,6 +711,11 @@ public class PodCommsSession {
             return .certainFailure(error: .unacknowledgedCommandPending)
         }
 
+        guard podState.setupProgress == .completed else {
+            // A cancel delivery command before pod setup is complete will fault the pod
+            return .certainFailure(error: PodCommsError.setupNotComplete)
+        }
+
         do {
             podState.unacknowledgedCommand = PendingCommand.stopProgram(deliveryType, transport.messageNumber, currentDate)
             let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: deliveryType, beepType: beepType)
@@ -747,10 +764,13 @@ public class PodCommsSession {
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
 
         do {
-            if podState.setupProgress == .completed && !(podState.lastCommsOK && podState.deliveryStatusVerified) {
-                // The pod setup is complete and the current delivery state can't be trusted so
-                // do a cancel all to be sure that setting the basal program won't fault the pod.
-                let _: StatusResponse = try send([CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)])
+            if !podState.isSuspended || podState.lastDeliveryStatusReceived == nil || !podState.lastDeliveryStatusReceived!.suspended {
+                // The podState or the last pod delivery status return indicates that the pod is not currently suspended.
+                // So execute a cancel all command here before setting the basal to prevent a possible 0x31 pod fault,
+                // but only when the pod startup is complete as a cancel command during pod setup also fault the pod!
+                if podState.setupProgress == .completed  {
+                    let _: StatusResponse = try send([CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)])
+                }
             }
             var status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
             let now = currentDate

+ 18 - 17
Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift

@@ -42,6 +42,10 @@ public enum SetupProgress: Int {
     public var needsCannulaInsertion: Bool {
         return self.rawValue < SetupProgress.completed.rawValue
     }
+
+    public var cannulaInsertionSuccessfullyStarted: Bool {
+        return self.rawValue > SetupProgress.startingInsertCannula.rawValue
+    }
 }
 
 // TODO: Mutating functions aren't guaranteed to synchronize read/write calls.
@@ -111,11 +115,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         return false
     }
 
-    // the following two vars are not persistent across app restarts
-    public var deliveryStatusVerified: Bool
-    public var lastCommsOK: Bool
+    var lastDeliveryStatusReceived: DeliveryStatus? // this variable is not persistent across app restarts
+
 
-    public init(address: UInt32, ltk: Data, firmwareVersion: String, bleFirmwareVersion: String, lotNo: UInt32, lotSeq: UInt32, productId: UInt8, messageTransportState: MessageTransportState? = nil, bleIdentifier: String, insulinType: InsulinType) {
+    public init(address: UInt32, ltk: Data, firmwareVersion: String, bleFirmwareVersion: String, lotNo: UInt32, lotSeq: UInt32, productId: UInt8,
+        messageTransportState: MessageTransportState? = nil, bleIdentifier: String, insulinType: InsulinType, initialDeliveryStatus: DeliveryStatus? = nil)
+    {
         self.address = address
         self.ltk = ltk
         self.firmwareVersion = firmwareVersion
@@ -134,9 +139,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder]
         self.bleIdentifier = bleIdentifier
         self.insulinType = insulinType
-        self.deliveryStatusVerified = false
-        self.lastCommsOK = false
         self.podTime = 0
+        self.lastDeliveryStatusReceived = initialDeliveryStatus // can be non-nil when testing
     }
     
     public var unfinishedSetup: Bool {
@@ -293,24 +297,22 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.unacknowledgedCommand = nil
     }
 
-    
     private mutating func updateDeliveryStatus(deliveryStatus: DeliveryStatus, podProgressStatus: PodProgressStatus, bolusNotDelivered: Double, at date: Date) {
 
-        deliveryStatusVerified = true
-        // See if the pod deliveryStatus indicates an active bolus or temp basal that the PodState isn't tracking (possible Loop restart)
-        if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that Loop doesn't know about?
+        // save the current pod delivery state for verification before any insulin delivery command
+        self.lastDeliveryStatusReceived = deliveryStatus
+
+        // See if the pod's deliveryStatus indicates some insulin delivery that podState isn't tracking
+        if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that we aren't tracking
             if podProgressStatus.readyForDelivery {
-                deliveryStatusVerified = false // remember that we had inconsistent (bolus) delivery status
                 // Create an unfinalizedBolus with the remaining bolus amount to capture what we can.
                 unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusNotDelivered, startTime: date, scheduledCertainty: .certain, insulinType: insulinType, automatic: false)
             }
         }
-        if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that app isn't tracking
-            deliveryStatusVerified = false // remember that we had inconsistent (temp basal) delivery status
+        if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that we aren't tracking
             // unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: 0, startTime: Date(), duration: .minutes(30), isHighTemp: false, scheduledCertainty: .certain, insulinType: insulinType)
         }
-        if deliveryStatus != .suspended && isSuspended { // active basal that app isn't tracking
-            deliveryStatusVerified = false // remember that we had inconsistent (basal) delivery status
+        if deliveryStatus != .suspended && isSuspended { // active basal that we aren't tracking
             let resumeStartTime = Date()
             suspendState = .resumed(resumeStartTime)
             unfinalizedResume = UnfinalizedDose(resumeStartTime: resumeStartTime, scheduledCertainty: .certain, insulinType: insulinType)
@@ -511,8 +513,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             self.insulinType = .novolog
         }
 
-        self.deliveryStatusVerified = false
-        self.lastCommsOK = false
+        self.lastDeliveryStatusReceived = nil
     }
     
     public var rawValue: RawValue {

+ 8 - 8
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewControllers/DashUICoordinator.swift

@@ -19,7 +19,7 @@ enum DashUIScreen {
     case expirationReminderSetup
     case lowReservoirReminderSetup
     case insulinTypeSelection
-    case pairPod
+    case pairAndPrime
     case insertCannula
     case confirmAttachment
     case checkInsertedCannula
@@ -38,8 +38,8 @@ enum DashUIScreen {
         case .lowReservoirReminderSetup:
             return .insulinTypeSelection
         case .insulinTypeSelection:
-            return .pairPod
-        case .pairPod:
+            return .pairAndPrime
+        case .pairAndPrime:
             return .confirmAttachment
         case .confirmAttachment:
             return .insertCannula
@@ -54,7 +54,7 @@ enum DashUIScreen {
         case .uncertaintyRecovered:
             return nil
         case .deactivate:
-            return .pairPod
+            return .pairAndPrime
         case .settings:
             return nil
         }
@@ -171,7 +171,7 @@ class DashUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
             }
             let view = OmniBLESettingsView(viewModel: viewModel, supportedInsulinTypes: allowedInsulinTypes)
             return hostingController(rootView: view)
-        case .pairPod:
+        case .pairAndPrime:
             pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManager)
 
             let viewModel = PairPodViewModel(podPairer: pumpManager)
@@ -185,7 +185,7 @@ class DashUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
             viewModel.didRequestDeactivation = { [weak self] in
                 self?.navigateTo(.deactivate)
             }
-            
+
             let view = hostingController(rootView: PairPodView(viewModel: viewModel))
             view.navigationItem.title = LocalizedString("Pair Pod", comment: "Title for pod pairing screen")
             view.navigationItem.backButtonDisplayMode = .generic
@@ -350,13 +350,13 @@ class DashUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
             if pumpManager.podAttachmentConfirmed {
                 return .insertCannula
             } else {
-                return .confirmAttachment
+                return .pairAndPrime // need to finish the priming
             }
         } else if !pumpManager.isOnboarded {
             if !pumpManager.initialConfigurationCompleted {
                 return .firstRunScreen
             }
-            return .pairPod
+            return .pairAndPrime // pair and prime a new pod
         } else {
             return .settings
         }

+ 16 - 27
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/InsertCannulaViewModel.swift

@@ -12,9 +12,14 @@ import LoopKitUI
 public protocol CannulaInserter {
     func insertCannula(completion: @escaping (Result<TimeInterval,OmniBLEPumpManagerError>) -> ())
     func checkCannulaInsertionFinished(completion: @escaping (OmniBLEPumpManagerError?) -> Void)
+    var cannulaInsertionSuccessfullyStarted: Bool { get }
 }
 
-extension OmniBLEPumpManager: CannulaInserter { }
+extension OmniBLEPumpManager: CannulaInserter {
+    public var cannulaInsertionSuccessfullyStarted: Bool {
+        return state.podState?.setupProgress.cannulaInsertionSuccessfullyStarted == true
+    }
+}
 
 class InsertCannulaViewModel: ObservableObject, Identifiable {
 
@@ -28,9 +33,9 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
         
         var actionButtonAccessibilityLabel: String {
             switch self {
-            case .ready, .startingInsertion:
+            case .ready:
                 return LocalizedString("Slide Button to insert Cannula", comment: "Insert cannula slider button accessibility label while ready to pair")
-            case .inserting:
+            case .inserting, .startingInsertion:
                 return LocalizedString("Inserting. Please wait.", comment: "Insert cannula action button accessibility label while pairing")
             case .checkingInsertion:
                 return LocalizedString("Checking Insertion", comment: "Insert cannula action button accessibility label checking insertion")
@@ -142,22 +147,15 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
     
     init(cannulaInserter: CannulaInserter) {
         self.cannulaInserter = cannulaInserter
+
+        // If resuming, don't wait for the button action
+        if cannulaInserter.cannulaInsertionSuccessfullyStarted {
+            insertCannula()
+        }
     }
-    
-//    private func handleEvent(_ event: ActivationStep2Event) {
-//        switch event {
-//        case .insertingCannula:
-//            let finishTime = TimeInterval(Pod.estimatedCannulaInsertionDuration)
-//            state = .inserting(finishTime: CACurrentMediaTime() + finishTime)
-//        case .step2Completed:
-//            state = .finished
-//        default:
-//            break
-//        }
-//    }
-    
+
     private func checkCannulaInsertionFinished() {
-        state = .startingInsertion
+        state = .checkingInsertion
         cannulaInserter.checkCannulaInsertionFinished() { (error) in
             DispatchQueue.main.async {
                 if let error = error {
@@ -171,7 +169,7 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
     
     private func insertCannula() {
         state = .startingInsertion
-        
+
         cannulaInserter.insertCannula { (result) in
             DispatchQueue.main.async {
                 switch(result) {
@@ -189,14 +187,6 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
                     self.state = .error(error)
                 }
             }
-
-            
-//            switch status {
-//            case .error(let error):
-//                self.state = .error(error)
-//            case .event(let event):
-//                self.handleEvent(event)
-//            }
         }
     }
     
@@ -214,7 +204,6 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
             insertCannula()
         }
     }
-    
 }
 
 public extension OmniBLEPumpManagerError {

+ 29 - 22
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/PairPodViewModel.swift

@@ -38,7 +38,7 @@ class PairPodViewModel: ObservableObject, Identifiable {
     enum PairPodViewModelState {
         case ready
         case pairing
-        case priming(finishTime: CFTimeInterval)
+        case priming(finishTime: CFTimeInterval?)
         case error(DashPairingError)
         case finished
         
@@ -84,14 +84,6 @@ class PairPodViewModel: ObservableObject, Identifiable {
         }
         
         var navBarButtonAction: NavBarButtonAction {
-//            switch self {
-//            case .error(_, let podCommState):
-//                if podCommState == .activating {
-//                    return .discard
-//                }
-//            default:
-//                break
-//            }
             return .cancel
         }
         
@@ -118,7 +110,11 @@ class PairPodViewModel: ObservableObject, Identifiable {
             case .pairing:
                 return .indeterminantProgress
             case .priming(let finishTime):
-                return .timedProgress(finishTime: finishTime)
+                if let finishTime {
+                    return .timedProgress(finishTime: finishTime)
+                } else {
+                    return .indeterminantProgress
+                }
             case .finished:
                 return .completed
             }
@@ -151,9 +147,9 @@ class PairPodViewModel: ObservableObject, Identifiable {
     @Published var state: PairPodViewModelState = .ready
     
     var podIsActivated: Bool {
-        return false // podPairer.podCommState != .noPod
+        return podPairer.podCommState != .noPod
     }
-    
+
     var backButtonHidden: Bool {
         if case .pairing = state {
             return true
@@ -174,12 +170,22 @@ class PairPodViewModel: ObservableObject, Identifiable {
 
     init(podPairer: PodPairer) {
         self.podPairer = podPairer
+
+        // If resuming, don't wait for the button action
+        if podPairer.podCommState == .activating {
+            pairAndPrime()
+        }
     }
-        
-    private func pair() {
-        state = .pairing
-        
-        podPairer.pair { (status) in
+
+    private func pairAndPrime() {
+        if podPairer.podCommState == .noPod {
+            state = .pairing
+        } else {
+            // Already paired, so resume with the prime
+            state = .priming(finishTime: nil)
+        }
+
+        podPairer.pairAndPrimePod { (status) in
             DispatchQueue.main.async {
                 switch status {
                 case .failure(let error):
@@ -207,14 +213,14 @@ class PairPodViewModel: ObservableObject, Identifiable {
                 self.didRequestDeactivation?()
             } else {
                 // Retry
-                pair()
+                pairAndPrime()
             }
         case .finished:
             didFinish?()
         default:
-            pair()
+            pairAndPrime()
         }
-    }    
+    }
 }
 
 // Pairing recovery suggestions
@@ -245,15 +251,16 @@ enum DashPairingError : LocalizedError {
 }
 
 public protocol PodPairer {
-    func pair(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void)
+    func pairAndPrimePod(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void)
     func discardPod(completion: @escaping (Bool) -> ())
+    var podCommState: PodCommState { get }
 }
 
 extension OmniBLEPumpManager: PodPairer {
     public func discardPod(completion: @escaping (Bool) -> ()) {
     }
     
-    public func pair(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void) {
+    public func pairAndPrimePod(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void) {
         pairAndPrime(completion: completion)
     }
 }

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/PodLifeState.swift

@@ -72,7 +72,7 @@ enum PodLifeState {
     var nextPodLifecycleAction: DashUIScreen {
         switch self {
         case .podActivating, .noPod:
-            return .pairPod
+            return .pairAndPrime
         default:
             return .deactivate
         }

+ 4 - 1
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/InsertCannulaView.swift

@@ -120,10 +120,11 @@ struct InsertCannulaView: View {
             secondaryButton: .default(FrameworkLocalText("No, Continue With Pod", comment: "Continue pairing button title of in pairing cancel modal"))
         )
     }
+
 }
 
 class MockCannulaInserter: CannulaInserter {
-    public func insertCannula(completion: @escaping (Result<TimeInterval,OmniBLEPumpManagerError>) -> Void) {
+    func insertCannula(completion: @escaping (Result<TimeInterval,OmniBLEPumpManagerError>) -> Void) {
         let mockDelay = TimeInterval(seconds: 3)
         let result :Result<TimeInterval, OmniBLEPumpManagerError> = .success(mockDelay)
         completion(result)
@@ -132,6 +133,8 @@ class MockCannulaInserter: CannulaInserter {
     func checkCannulaInsertionFinished(completion: @escaping (OmniBLEPumpManagerError?) -> Void) {
         completion(nil)
     }
+
+    var cannulaInsertionSuccessfullyStarted: Bool = false
 }
 
 struct InsertCannulaView_Previews: PreviewProvider {

+ 7 - 3
Dependencies/OmniKit/OmniKit/OmnipodCommon/Pod.swift

@@ -108,16 +108,20 @@ public enum DeliveryStatus: UInt8, CustomStringConvertible {
     case bolusAndTempBasal = 6
     case extendedBolusRunning = 9
     case extendedBolusAndTempBasal = 10
-    
+
+    public var suspended: Bool {
+        return self == .suspended
+    }
+
     public var bolusing: Bool {
         return self == .bolusInProgress || self == .bolusAndTempBasal || self == .extendedBolusRunning || self == .extendedBolusAndTempBasal
     }
-    
+
     public var tempBasalRunning: Bool {
         return self == .tempBasalRunning || self == .bolusAndTempBasal || self == .extendedBolusAndTempBasal
     }
 
-    public var extendedBolusRunninng: Bool {
+    public var extendedBolusRunning: Bool {
         return self == .extendedBolusRunning || self == .extendedBolusAndTempBasal
     }
 

+ 46 - 22
Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -44,7 +44,6 @@ public enum OmnipodPumpManagerError: Error {
     case insulinTypeNotConfigured
     case notReadyForCannulaInsertion
     case invalidSetting
-    case setupNotComplete
     case communication(Error)
     case state(Error)
 }
@@ -57,13 +56,11 @@ extension OmnipodPumpManagerError: LocalizedError {
         case .podAlreadyPaired:
             return LocalizedString("Pod already paired", comment: "Error message shown when user cannot pair because pod is already paired")
         case .insulinTypeNotConfigured:
-            return LocalizedString("Insulin type not configured", comment: "Error description for OmniBLEPumpManagerError.insulinTypeNotConfigured")
+            return LocalizedString("Insulin type not configured", comment: "Error description for insulin type not configured")
         case .notReadyForCannulaInsertion:
             return LocalizedString("Pod is not in a state ready for cannula insertion.", comment: "Error message when cannula insertion fails because the pod is in an unexpected state")
         case .invalidSetting:
             return LocalizedString("Invalid Setting", comment: "Error description for invalid setting")
-        case .setupNotComplete:
-            return LocalizedString("Pod setup is not complete", comment: "Error description when pod setup is not complete")
         case .communication(let error):
             if let error = error as? LocalizedError {
                 return error.errorDescription
@@ -323,7 +320,7 @@ extension OmnipodPumpManager {
         switch podCommState(for: state) {
         case .activating:
             return PumpStatusHighlight(
-                localizedMessage: LocalizedString("Finish Pairing", comment: "Status highlight that when pod is activating."),
+                localizedMessage: LocalizedString("Finish Setup", comment: "Status highlight that when pod is activating."),
                 imageName: "exclamationmark.circle.fill",
                 state: .warning)
         case .deactivating:
@@ -586,7 +583,7 @@ extension OmnipodPumpManager {
         } else if !podState.isSetupComplete {
             return .activating
         }
-        return .deactivating
+        return .deactivating // Can't be reached and thus will never be returned
     }
 
     public var podCommState: PodCommState {
@@ -669,6 +666,25 @@ extension OmnipodPumpManager {
         return date
     }
 
+    // Reset all the per pod state kept in pump manager state which doesn't span pods
+    fileprivate func resetPerPodPumpManagerState() {
+
+        // Reset any residual per pod slot based pump manager alerts
+        // (i.e., all but timeOffsetChangeDetected which isn't actually used)
+        let podAlerts = state.activeAlerts.filter { $0 != .timeOffsetChangeDetected }
+        for alert in podAlerts {
+            self.retractAlert(alert: alert)
+        }
+
+        self.setState { (state) in
+            // Reset alertsWithPendingAcknowledgment which are all pod slot based
+            state.alertsWithPendingAcknowledgment = []
+
+            // Reset other miscellaneous state variables that are actually per pod
+            state.podAttachmentConfirmed = false
+            state.acknowledgedTimeOffsetAlert = false
+        }
+    }
 
     // MARK: - Pod comms
 
@@ -686,6 +702,8 @@ extension OmnipodPumpManager {
 
         podComms.forgetPod()
 
+        self.resetPerPodPumpManagerState()
+
         if let dosesToStore = self.state.podState?.dosesToStore {
             self.store(doses: dosesToStore, completion: { error in
                 self.setState({ (state) in
@@ -704,7 +722,7 @@ extension OmnipodPumpManager {
             completion()
         }
     }
-    
+
     // MARK: Testing
     #if targetEnvironment(simulator)
     private func jumpStartPod(address: UInt32, lot: UInt32, tid: UInt32, fault: DetailedStatus? = nil, startDate: Date? = nil, mockFault: Bool) {
@@ -719,8 +737,14 @@ extension OmnipodPumpManager {
 
         podComms = PodComms(podState: podState)
 
+        self.podComms.delegate = self
+        self.podComms.messageLogger = self
+
+        self.resetPerPodPumpManagerState()
+
         setState({ (state) in
             state.updatePodStateFromPodComms(podState)
+            state.scheduledExpirationReminderOffset = state.defaultExpirationReminderOffset
         })
     }
     #endif
@@ -814,7 +838,9 @@ extension OmnipodPumpManager {
                         state.pairingAttemptAddress = nil
                     }
                 }
-                
+
+                self.resetPerPodPumpManagerState()
+
                 // Calls completion
                 primeSession(result)
             }
@@ -1026,7 +1052,7 @@ extension OmnipodPumpManager {
 
         guard state.podState?.setupProgress == .completed else {
             // A cancel delivery command before pod setup is complete will fault the pod
-            completion(.state(OmnipodPumpManagerError.setupNotComplete))
+            completion(.state(PodCommsError.setupNotComplete))
             return
         }
 
@@ -1066,7 +1092,7 @@ extension OmnipodPumpManager {
 
             guard state.podState?.setupProgress == .completed else {
                 // A cancel delivery command before pod setup is complete will fault the pod
-                return .failure(PumpManagerError.deviceState(OmnipodPumpManagerError.setupNotComplete))
+                return .failure(PumpManagerError.deviceState(PodCommsError.setupNotComplete))
             }
 
             guard state.podState?.unfinalizedBolus?.isFinished() != false else {
@@ -1815,7 +1841,7 @@ extension OmnipodPumpManager: PumpManager {
 
         guard state.podState?.setupProgress == .completed else {
             // A cancel delivery command before pod setup is complete will fault the pod
-            completion(.failure(PumpManagerError.deviceState(OmnipodPumpManagerError.setupNotComplete)))
+            completion(.failure(PumpManagerError.deviceState(PodCommsError.setupNotComplete)))
             return
         }
 
@@ -1879,14 +1905,14 @@ extension OmnipodPumpManager: PumpManager {
 
     public func runTemporaryBasalProgram(unitsPerHour: Double, for duration: TimeInterval, automatic: Bool, completion: @escaping (PumpManagerError?) -> Void) {
 
-        guard self.hasActivePod else {
+        guard self.hasActivePod, let podState = self.state.podState else {
             completion(.configuration(OmnipodPumpManagerError.noPodPaired))
             return
         }
 
         guard state.podState?.setupProgress == .completed else {
             // A cancel delivery command before pod setup is complete will fault the pod
-            completion(.deviceState(OmnipodPumpManagerError.setupNotComplete))
+            completion(.deviceState(PodCommsError.setupNotComplete))
             return
         }
 
@@ -1920,7 +1946,7 @@ extension OmnipodPumpManager: PumpManager {
                 return
             }
 
-            if case .some(.suspended) = self.state.podState?.suspendState {
+            if case (.suspended) = podState.suspendState {
                 self.log.info("Not enacting temp basal because podState indicates pod is suspended.")
                 completion(.deviceState(PodCommsError.podSuspended))
                 return
@@ -1930,18 +1956,17 @@ extension OmnipodPumpManager: PumpManager {
             let resumingScheduledBasal = duration < .ulpOfOne
 
             // If a bolus is not finished, fail if not resuming the scheduled basal
-            guard self.state.podState?.unfinalizedBolus?.isFinished() != false || resumingScheduledBasal else {
+            guard podState.unfinalizedBolus?.isFinished() != false || resumingScheduledBasal else {
                 self.log.info("Not enacting temp basal because podState indicates unfinalized bolus in progress.")
                 completion(.deviceState(PodCommsError.unfinalizedBolus))
                 return
             }
 
-            // Did the last message have comms issues or is the last delivery status not verified correctly?
-            let uncertainDeliveryStatus = self.state.podState?.lastCommsOK == false || self.state.podState?.deliveryStatusVerified == false
-
-            // Do the cancel temp basal command if currently running a temp basal OR
-            // if resuming scheduled basal delivery OR if the delivery status is uncertain.
-            if self.state.podState?.unfinalizedTempBasal != nil || resumingScheduledBasal || uncertainDeliveryStatus {
+            // Do the safe cancel TB command when resuming scheduled basal delivery OR if unfinalizedTempBasal indicates a
+            // running a temp basal OR if we don't have the last pod delivery status confirming that no temp basal is running.
+            if resumingScheduledBasal || podState.unfinalizedTempBasal != nil ||
+                podState.lastDeliveryStatusReceived == nil || podState.lastDeliveryStatusReceived!.tempBasalRunning
+            {
                 let status: StatusResponse
 
                 // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
@@ -1952,7 +1977,6 @@ extension OmnipodPumpManager: PumpManager {
                     completion(.communication(error))
                     return
                 case .unacknowledged(let error):
-                    // TODO: Return PumpManagerError.uncertainDelivery and implement recovery
                     completion(.communication(error))
                     return
                 case .success(let cancelTempStatus, _):

+ 38 - 18
Dependencies/OmniKit/OmniKit/PumpManager/PodCommsSession.swift

@@ -38,6 +38,7 @@ public enum PodCommsError: Error {
     case podIncompatible(str: String)
     case noPodsFound
     case tooManyPodsFound
+    case setupNotComplete
 }
 
 extension PodCommsError: LocalizedError {
@@ -98,7 +99,8 @@ extension PodCommsError: LocalizedError {
             return LocalizedString("No pods found", comment: "Error message for PodCommsError.noPodsFound")
         case .tooManyPodsFound:
             return LocalizedString("Too many pods found", comment: "Error message for PodCommsError.tooManyPodsFound")
-
+        case .setupNotComplete:
+            return LocalizedString("Pod setup is not complete", comment: "Error description when pod setup is not complete")
         }
     }
 
@@ -162,6 +164,8 @@ extension PodCommsError: LocalizedError {
             return LocalizedString("Make sure your pod is filled and nearby.", comment: "Recovery suggestion for PodCommsError.noPodsFound")
         case .tooManyPodsFound:
             return LocalizedString("Move to a new area away from any other pods and try again.", comment: "Recovery suggestion for PodCommsError.tooManyPodsFound")
+        case .setupNotComplete:
+            return nil
         }
     }
 
@@ -279,7 +283,9 @@ public class PodCommsSession {
 
             let message = Message(address: podState.address, messageBlocks: blocksToSend, sequenceNum: messageNumber, expectFollowOnMessage: expectFollowOnMessage)
 
-            self.podState.lastCommsOK = false // mark last comms as not OK until we get the expected response
+            // Clear the lastDeliveryStatusReceived variable which is used to guard against possible 0x31 pod faults
+            podState.lastDeliveryStatusReceived = nil
+
             let response = try transport.sendMessage(message)
 
             // Simulate fault
@@ -288,7 +294,6 @@ public class PodCommsSession {
 
             if let responseMessageBlock = response.messageBlocks[0] as? T {
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
-                self.podState.lastCommsOK = true // message successfully sent and expected response received
                 return responseMessageBlock
             }
 
@@ -436,7 +441,6 @@ public class PodCommsSession {
 
     public func insertCannula(optionalAlerts: [PodAlert] = [], silent: Bool) throws -> TimeInterval {
         let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
-        let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
         guard podState.activatedAt != nil else {
             throw PodCommsError.noPodPaired
@@ -448,7 +452,8 @@ public class PodCommsSession {
             if status.podProgressStatus == .insertingCannula {
                 podState.setupProgress = .cannulaInserting
                 podState.updateFromStatusResponse(status, at: currentDate)
-                return insertionWait // Not sure when it started, wait full time to be sure
+                // return a non-zero wait time based on the bolus not yet delivered
+                return (status.bolusNotDelivered / Pod.primeDeliveryRate) + 1
             }
             if status.podProgressStatus.readyForDelivery {
                 markSetupProgressCompleted(statusResponse: status)
@@ -477,7 +482,7 @@ public class PodCommsSession {
         podState.updateFromStatusResponse(status2, at: currentDate)
 
         podState.setupProgress = .cannulaInserting
-        return insertionWait
+        return status2.bolusNotDelivered / Pod.primeDeliveryRate // seconds for the cannula insert bolus to finish
     }
 
     public func checkInsertionCompleted() throws {
@@ -513,16 +518,18 @@ public class PodCommsSession {
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, units: units, timeBetweenPulses: timeBetweenPulses, extendedUnits: extendedUnits, extendedDuration: extendedDuration)
 
-        // Do a getstatus to verify that there isn't an on-going bolus in progress if the last bolus command is still
-        // finalized, if the last delivery status wasn't successfully verified or the last comms attempt wasn't OK
-        if podState.unfinalizedBolus != nil || !podState.deliveryStatusVerified || !podState.lastCommsOK {
-            var ongoingBolus = true
+        // Do a get status here to verify that there isn't an on-going bolus in progress if the last bolus command
+        // is still not finalized OR we don't have the last pod delivery status confirming that no bolus is active.
+        if podState.unfinalizedBolus != nil || podState.lastDeliveryStatusReceived == nil || podState.lastDeliveryStatusReceived!.bolusing {
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
                 podState.updateFromStatusResponse(statusResponse, at: currentDate)
-                ongoingBolus = podState.unfinalizedBolus != nil
-            }
-            guard !ongoingBolus else {
-                return DeliveryCommandResult.certainFailure(error: .unfinalizedBolus)
+                guard podState.unfinalizedBolus == nil else {
+                    log.default("bolus: pod is still bolusing")
+                    return DeliveryCommandResult.certainFailure(error: .unfinalizedBolus)
+                }
+            } else {
+                log.default("bolus: failed to read pod status to verify there is no bolus running")
+                return DeliveryCommandResult.certainFailure(error: .noResponse)
             }
         }
 
@@ -624,6 +631,11 @@ public class PodCommsSession {
             return .certainFailure(error: .unacknowledgedCommandPending)
         }
 
+        guard podState.setupProgress == .completed else {
+            // A cancel delivery command before pod setup is complete will fault the pod
+            return .certainFailure(error: PodCommsError.setupNotComplete)
+        }
+
         do {
             var alertConfigurations: [AlertConfiguration] = []
             var podSuspendedReminderAlert: PodAlert? = nil
@@ -704,6 +716,11 @@ public class PodCommsSession {
             return .certainFailure(error: .unacknowledgedCommandPending)
         }
 
+        guard podState.setupProgress == .completed else {
+            // A cancel delivery command before pod setup is complete will fault the pod
+            return .certainFailure(error: PodCommsError.setupNotComplete)
+        }
+
         do {
             podState.unacknowledgedCommand = PendingCommand.stopProgram(deliveryType, transport.messageNumber, currentDate)
             let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: deliveryType, beepType: beepType)
@@ -752,10 +769,13 @@ public class PodCommsSession {
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
 
         do {
-            if podState.setupProgress == .completed && !(podState.lastCommsOK && podState.deliveryStatusVerified) {
-                // The pod setup is complete and the current delivery state can't be trusted so
-                // do a cancel all to be sure that setting the basal program won't fault the pod.
-                let _: StatusResponse = try send([CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)])
+            if !podState.isSuspended || podState.lastDeliveryStatusReceived == nil || !podState.lastDeliveryStatusReceived!.suspended {
+                // The podState or the last pod delivery status return indicates that the pod is not currently suspended.
+                // So execute a cancel all command here before setting the basal to prevent a possible 0x31 pod fault,
+                // but only when the pod startup is complete as a cancel command during pod setup also fault the pod!
+                if podState.setupProgress == .completed  {
+                    let _: StatusResponse = try send([CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)])
+                }
             }
             var status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
             let now = currentDate

+ 16 - 16
Dependencies/OmniKit/OmniKit/PumpManager/PodState.swift

@@ -41,6 +41,10 @@ public enum SetupProgress: Int {
     public var needsCannulaInsertion: Bool {
         return self.rawValue < SetupProgress.completed.rawValue
     }
+
+    public var cannulaInsertionSuccessfullyStarted: Bool {
+        return self.rawValue > SetupProgress.startingInsertCannula.rawValue
+    }
 }
 
 // TODO: Mutating functions aren't guaranteed to synchronize read/write calls.
@@ -107,11 +111,10 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         return false
     }
 
-    // the following two vars are not persistent across app restarts
-    public var deliveryStatusVerified: Bool
-    public var lastCommsOK: Bool
+    var lastDeliveryStatusReceived: DeliveryStatus? // this variable is not persistent across app restarts
 
-    public init(address: UInt32, pmVersion: String, piVersion: String, lot: UInt32, tid: UInt32, packetNumber: Int = 0, messageNumber: Int = 0, insulinType: InsulinType) {
+    public init(address: UInt32, pmVersion: String, piVersion: String, lot: UInt32, tid: UInt32, packetNumber: Int = 0, messageNumber: Int = 0, insulinType: InsulinType, initialDeliveryStatus: DeliveryStatus? = nil)
+    {
         self.address = address
         self.nonceState = NonceState(lot: lot, tid: tid)
         self.pmVersion = pmVersion
@@ -128,9 +131,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.setupProgress = .addressAssigned
         self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder]
         self.insulinType = insulinType
-        self.deliveryStatusVerified = false
-        self.lastCommsOK = false
         self.podTime = 0
+        self.lastDeliveryStatusReceived = initialDeliveryStatus // can be non-nil when testing
     }
     
     public var unfinishedSetup: Bool {
@@ -285,21 +287,20 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     
     private mutating func updateDeliveryStatus(deliveryStatus: DeliveryStatus, podProgressStatus: PodProgressStatus, bolusNotDelivered: Double, at date: Date) {
 
-        deliveryStatusVerified = true
-        // See if the pod deliveryStatus indicates an active bolus or temp basal that the PodState isn't tracking (possible Loop restart)
-        if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that Loop doesn't know about?
+        // save the current pod delivery state for verification before any insulin delivery command
+        self.lastDeliveryStatusReceived = deliveryStatus
+
+        // See if the pod's deliveryStatus indicates some insulin delivery that podState isn't tracking
+        if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that we aren't tracking
             if podProgressStatus.readyForDelivery {
-                deliveryStatusVerified = false // remember that we had inconsistent (bolus) delivery status
                 // Create an unfinalizedBolus with the remaining bolus amount to capture what we can.
                 unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusNotDelivered, startTime: date, scheduledCertainty: .certain, insulinType: insulinType, automatic: false)
             }
         }
-        if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that app isn't tracking
-            deliveryStatusVerified = false // remember that we had inconsistent (temp basal) delivery status
+        if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that we aren't tracking
             // unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: 0, startTime: Date(), duration: .minutes(30), isHighTemp: false, scheduledCertainty: .certain, insulinType: insulinType)
         }
-        if deliveryStatus != .suspended && isSuspended { // active basal that app isn't tracking
-            deliveryStatusVerified = false // remember that we had inconsistent (basal) delivery status
+        if deliveryStatus != .suspended && isSuspended { // active basal that we aren't tracking
             let resumeStartTime = Date()
             suspendState = .resumed(resumeStartTime)
             unfinalizedResume = UnfinalizedDose(resumeStartTime: resumeStartTime, scheduledCertainty: .certain, insulinType: insulinType)
@@ -492,8 +493,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             self.insulinType = .novolog
         }
 
-        self.deliveryStatusVerified = false
-        self.lastCommsOK = false
+        self.lastDeliveryStatusReceived = nil
     }
     
     public var rawValue: RawValue {

+ 7 - 3
Dependencies/OmniKit/OmniKitTests/PodCommsSessionTests.swift

@@ -20,7 +20,7 @@ class PodCommsSessionTests: XCTestCase {
 
 
     override func setUp() {
-        podState = PodState(address: address, pmVersion: "2.7.0", piVersion: "2.7.0", lot: 43620, tid: 560313, insulinType: .novolog)
+        podState = PodState(address: address, pmVersion: "2.7.0", piVersion: "2.7.0", lot: 43620, tid: 560313, insulinType: .novolog, initialDeliveryStatus: .scheduledBasal)
         mockTransport = MockMessageTransport(address: podState.address, messageNumber: 5)
     }
 
@@ -34,6 +34,9 @@ class PodCommsSessionTests: XCTestCase {
             // 2018-05-26T09:11:08.580347 pod Message(1f16b11e seq:06 [OmniKitPacketParser.ErrorResponse(blockType: OmniKitPacketParser.MessageBlockType.errorResponse, errorReponseType: OmniKitPacketParser.ErrorResponse.ErrorReponseType.badNonce, nonceSearchKey: 43492, data: 5 bytes)])
             mockTransport.addResponse(try ErrorResponse(encodedData: Data(hexadecimalString: "060314a9e403f5")!))
             mockTransport.addResponse(try StatusResponse(encodedData: Data(hexadecimalString: "1d5800d1a8140012e3ff8018")!))
+            // These responses are for session.bolus() which verifies that the pod is not bolusing before sending a bolus command
+            mockTransport.addResponse(try StatusResponse(encodedData: Data(hexadecimalString: "1d180160a800001cd3ff001e")!)) // not bolusing
+            mockTransport.addResponse(try StatusResponse(encodedData: Data(hexadecimalString: "1d580160e014001cd7ff81ce")!)) // bolus successfully started
         } catch (let error) {
             XCTFail("message decoding threw error: \(error)")
             return
@@ -61,11 +64,12 @@ class PodCommsSessionTests: XCTestCase {
             XCTFail("message sending error: \(error)")
         }
 
-        // Try sending another bolus command: nonce should be 676940027
+        // Try sending another bolus command: nonce should be 545302454
         XCTAssertEqual(545302454, lastPodStateUpdate!.currentNonce)
 
         let _ = session.bolus(units: 2, automatic: false)
-        let bolusTry3 = mockTransport.sentMessages[2].messageBlocks[0] as! SetInsulinScheduleCommand
+        let lastSentMessageIndex = mockTransport.sentMessages.endIndex - 1
+        let bolusTry3 = mockTransport.sentMessages[lastSentMessageIndex].messageBlocks[0] as! SetInsulinScheduleCommand
         XCTAssertEqual(545302454, bolusTry3.nonce)
 
     }

+ 7 - 7
Dependencies/OmniKit/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift

@@ -24,7 +24,7 @@ enum OmnipodUIScreen {
     case lowReservoirReminderSetup
     case insulinTypeSelection
     case rileyLinkSetup
-    case pairPod
+    case pairAndPrime
     case insertCannula
     case confirmAttachment
     case checkInsertedCannula
@@ -45,8 +45,8 @@ enum OmnipodUIScreen {
         case .insulinTypeSelection:
             return .rileyLinkSetup
         case .rileyLinkSetup:
-            return .pairPod
-        case .pairPod:
+            return .pairAndPrime
+        case .pairAndPrime:
             return .confirmAttachment
         case .confirmAttachment:
             return .insertCannula
@@ -61,7 +61,7 @@ enum OmnipodUIScreen {
         case .uncertaintyRecovered:
             return nil
         case .deactivate:
-            return .pairPod
+            return .pairAndPrime
         case .settings:
             return nil
         }
@@ -199,7 +199,7 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
 
             let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection, supportedInsulinTypes: allowedInsulinTypes)
             return hostingController(rootView: view)
-        case .pairPod:
+        case .pairAndPrime:
             pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManager)
 
             let viewModel = PairPodViewModel(podPairer: pumpManager)
@@ -410,13 +410,13 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
             if pumpManager.podAttachmentConfirmed {
                 return .insertCannula
             } else {
-                return .confirmAttachment
+                return .pairAndPrime // need to finish the priming
             }
         } else if !pumpManager.isOnboarded {
             if !pumpManager.initialConfigurationCompleted {
                 return .firstRunScreen
             }
-            return .pairPod
+            return .pairAndPrime // pair and prime a new pod
         } else {
             return .settings
         }

+ 16 - 27
Dependencies/OmniKit/OmniKitUI/ViewModels/InsertCannulaViewModel.swift

@@ -13,9 +13,14 @@ import OmniKit
 public protocol CannulaInserter {
     func insertCannula(completion: @escaping (Result<TimeInterval,OmnipodPumpManagerError>) -> ())
     func checkCannulaInsertionFinished(completion: @escaping (OmnipodPumpManagerError?) -> Void)
+    var cannulaInsertionSuccessfullyStarted: Bool { get }
 }
 
-extension OmnipodPumpManager: CannulaInserter {}
+extension OmnipodPumpManager: CannulaInserter {
+    public var cannulaInsertionSuccessfullyStarted: Bool {
+        return state.podState?.setupProgress.cannulaInsertionSuccessfullyStarted == true
+    }
+}
 
 class InsertCannulaViewModel: ObservableObject, Identifiable {
 
@@ -29,9 +34,9 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
         
         var actionButtonAccessibilityLabel: String {
             switch self {
-            case .ready, .startingInsertion:
+            case .ready:
                 return LocalizedString("Slide Button to insert Cannula", comment: "Insert cannula slider button accessibility label while ready to pair")
-            case .inserting:
+            case .inserting, .startingInsertion:
                 return LocalizedString("Inserting. Please wait.", comment: "Insert cannula action button accessibility label while pairing")
             case .checkingInsertion:
                 return LocalizedString("Checking Insertion", comment: "Insert cannula action button accessibility label checking insertion")
@@ -142,22 +147,15 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
     
     init(cannulaInserter: CannulaInserter) {
         self.cannulaInserter = cannulaInserter
+
+        // If resuming, don't wait for the button action
+        if cannulaInserter.cannulaInsertionSuccessfullyStarted {
+            insertCannula()
+        }
     }
-    
-//    private func handleEvent(_ event: ActivationStep2Event) {
-//        switch event {
-//        case .insertingCannula:
-//            let finishTime = TimeInterval(Pod.estimatedCannulaInsertionDuration)
-//            state = .inserting(finishTime: CACurrentMediaTime() + finishTime)
-//        case .step2Completed:
-//            state = .finished
-//        default:
-//            break
-//        }
-//    }
-    
+
     private func checkCannulaInsertionFinished() {
-        state = .startingInsertion
+        state = .checkingInsertion
         cannulaInserter.checkCannulaInsertionFinished() { (error) in
             DispatchQueue.main.async {
                 if let error = error {
@@ -171,7 +169,7 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
     
     private func insertCannula() {
         state = .startingInsertion
-        
+
         cannulaInserter.insertCannula { (result) in
             DispatchQueue.main.async {
                 switch(result) {
@@ -189,14 +187,6 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
                     self.state = .error(error)
                 }
             }
-
-            
-//            switch status {
-//            case .error(let error):
-//                self.state = .error(error)
-//            case .event(let event):
-//                self.handleEvent(event)
-//            }
         }
     }
     
@@ -214,7 +204,6 @@ class InsertCannulaViewModel: ObservableObject, Identifiable {
             insertCannula()
         }
     }
-    
 }
 
 public extension OmnipodPumpManagerError {

+ 28 - 21
Dependencies/OmniKit/OmniKitUI/ViewModels/PairPodViewModel.swift

@@ -39,7 +39,7 @@ class PairPodViewModel: ObservableObject, Identifiable {
     enum PairPodViewModelState {
         case ready
         case pairing
-        case priming(finishTime: CFTimeInterval)
+        case priming(finishTime: CFTimeInterval?)
         case error(OmnipodPairingError)
         case finished
         
@@ -85,14 +85,6 @@ class PairPodViewModel: ObservableObject, Identifiable {
         }
         
         var navBarButtonAction: NavBarButtonAction {
-//            switch self {
-//            case .error(_, let podCommState):
-//                if podCommState == .activating {
-//                    return .discard
-//                }
-//            default:
-//                break
-//            }
             return .cancel
         }
         
@@ -119,7 +111,11 @@ class PairPodViewModel: ObservableObject, Identifiable {
             case .pairing:
                 return .indeterminantProgress
             case .priming(let finishTime):
-                return .timedProgress(finishTime: finishTime)
+                if let finishTime {
+                    return .timedProgress(finishTime: finishTime)
+                } else {
+                    return .indeterminantProgress
+                }
             case .finished:
                 return .completed
             }
@@ -152,7 +148,7 @@ class PairPodViewModel: ObservableObject, Identifiable {
     @Published var state: PairPodViewModelState = .ready
     
     var podIsActivated: Bool {
-        return false // podPairer.podCommState != .noPod
+        return podPairer.podCommState != .noPod
     }
     
     var backButtonHidden: Bool {
@@ -175,12 +171,22 @@ class PairPodViewModel: ObservableObject, Identifiable {
 
     init(podPairer: PodPairer) {
         self.podPairer = podPairer
+
+        // If resuming, don't wait for the button action
+        if podPairer.podCommState == .activating {
+            pairAndPrime()
+        }
     }
-        
-    private func pair() {
-        state = .pairing
-        
-        podPairer.pair { (status) in
+
+    private func pairAndPrime() {
+        if podPairer.podCommState == .noPod {
+            state = .pairing
+        } else {
+            // Already paired, so resume with the prime
+            state = .priming(finishTime: nil)
+        }
+
+        podPairer.pairAndPrimePod { (status) in
             DispatchQueue.main.async {
                 switch status {
                 case .failure(let error):
@@ -208,14 +214,14 @@ class PairPodViewModel: ObservableObject, Identifiable {
                 self.didRequestDeactivation?()
             } else {
                 // Retry
-                pair()
+                pairAndPrime()
             }
         case .finished:
             didFinish?()
         default:
-            pair()
+            pairAndPrime()
         }
-    }    
+    }
 }
 
 // Pairing recovery suggestions
@@ -246,15 +252,16 @@ enum OmnipodPairingError : LocalizedError {
 }
 
 public protocol PodPairer {
-    func pair(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void)
+    func pairAndPrimePod(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void)
     func discardPod(completion: @escaping (Bool) -> ())
+    var podCommState: PodCommState { get }
 }
 
 extension OmnipodPumpManager: PodPairer {
     public func discardPod(completion: @escaping (Bool) -> ()) {
     }
     
-    public func pair(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void) {
+    public func pairAndPrimePod(completion: @escaping (PumpManagerResult<TimeInterval>) -> Void) {
         pairAndPrime(completion: completion)
     }
 }

+ 1 - 1
Dependencies/OmniKit/OmniKitUI/ViewModels/PodLifeState.swift

@@ -72,7 +72,7 @@ enum PodLifeState {
     var nextPodLifecycleAction: OmnipodUIScreen {
         switch self {
         case .podActivating, .noPod:
-            return .pairPod
+            return .pairAndPrime
         default:
             return .deactivate
         }

+ 3 - 1
Dependencies/OmniKit/OmniKitUI/Views/InsertCannulaView.swift

@@ -124,7 +124,7 @@ struct InsertCannulaView: View {
 }
 
 class MockCannulaInserter: CannulaInserter {
-    public func insertCannula(completion: @escaping (Result<TimeInterval,OmnipodPumpManagerError>) -> Void) {
+    func insertCannula(completion: @escaping (Result<TimeInterval,OmnipodPumpManagerError>) -> Void) {
         let mockDelay = TimeInterval(seconds: 3)
         let result :Result<TimeInterval, OmnipodPumpManagerError> = .success(mockDelay)
         completion(result)
@@ -133,6 +133,8 @@ class MockCannulaInserter: CannulaInserter {
     func checkCannulaInsertionFinished(completion: @escaping (OmnipodPumpManagerError?) -> Void) {
         completion(nil)
     }
+
+    var cannulaInsertionSuccessfullyStarted: Bool = false
 }
 
 struct InsertCannulaView_Previews: PreviewProvider {

+ 16 - 16
FreeAPS.xcodeproj/project.pbxproj

@@ -19,7 +19,6 @@
 		190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC729FF13AA00BA767D /* StatConfigStateModel.swift */; };
 		190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */; };
 		191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */; };
-		19229B962AFBB84800CD91CA /* Predictions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19229B952AFBB84800CD91CA /* Predictions.swift */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
 		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
@@ -315,6 +314,7 @@
 		B9CAAEFC2AE70836000F68BC /* branch.txt in Resources */ = {isa = PBXBuildFile; fileRef = B9CAAEFB2AE70836000F68BC /* branch.txt */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
+		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
 		BD188BEC2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */; };
 		BD188BED2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
@@ -543,7 +543,6 @@
 		190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigRootView.swift; sourceTree = "<group>"; };
 		1918333A26ADA46800F45722 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
 		191F62672AD6B05A004D7911 /* NightscoutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettings.swift; sourceTree = "<group>"; };
-		19229B952AFBB84800CD91CA /* Predictions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predictions.swift; sourceTree = "<group>"; };
 		1927C8E92744611700347C69 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EA2744611800347C69 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EB2744611900347C69 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -881,6 +880,7 @@
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
+		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBobble.swift; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
@@ -1762,6 +1762,7 @@
 				CEA4F62229BE10F70011ADF7 /* SavitzkyGolayFilter.swift */,
 				587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */,
 				BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */,
+				BD1661302B82ADAB00256551 /* CustomProgressView.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -2177,7 +2178,6 @@
 				10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */,
 				BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */,
 				BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */,
-				19229B952AFBB84800CD91CA /* Predictions.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2949,7 +2949,6 @@
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				38E8754F275556FA00975559 /* WatchManager.swift in Sources */,
 				A228DF96647338139F152B15 /* PreferencesEditorDataFlow.swift in Sources */,
-				19229B962AFBB84800CD91CA /* Predictions.swift in Sources */,
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
 				E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */,
@@ -2990,6 +2989,7 @@
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,
+				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				FE41E4D429463C660047FD55 /* NightscoutStatistics.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
@@ -3298,14 +3298,14 @@
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
 				DEVELOPMENT_ASSET_PATHS = "";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Dependencies/ios-armv7_arm64",
 				);
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -3340,14 +3340,14 @@
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
 				DEVELOPMENT_ASSET_PATHS = "";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/Dependencies/ios-armv7_arm64",
 				);
 				INFOPLIST_FILE = FreeAPS/Resources/Info.plist;
-				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 16.2;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -3385,7 +3385,7 @@
 				CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				GENERATE_INFOPLIST_FILE = YES;
 				IBSC_MODULE = FreeAPSWatch_WatchKit_Extension;
 				INFOPLIST_FILE = FreeAPSWatch/Info.plist;
@@ -3420,7 +3420,7 @@
 				CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				GENERATE_INFOPLIST_FILE = YES;
 				IBSC_MODULE = FreeAPSWatch_WatchKit_Extension;
 				INFOPLIST_FILE = FreeAPSWatch/Info.plist;
@@ -3450,7 +3450,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
 				DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\"";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = "FreeAPSWatch WatchKit Extension/Info.plist";
@@ -3490,7 +3490,7 @@
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = $APP_BUILD_NUMBER;
 				DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\"";
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = "FreeAPSWatch WatchKit Extension/Info.plist";
@@ -3524,7 +3524,7 @@
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CODE_SIGN_STYLE = Automatic;
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.4;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -3545,7 +3545,7 @@
 			buildSettings = {
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				CODE_SIGN_STYLE = Automatic;
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				INFOPLIST_FILE = FreeAPSTests/Info.plist;
 				IPHONEOS_DEPLOYMENT_TARGET = 14.4;
 				LD_RUNPATH_SEARCH_PATHS = (
@@ -3570,7 +3570,7 @@
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -3604,7 +3604,7 @@
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
-				DEVELOPMENT_TEAM = "";
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
 				GENERATE_INFOPLIST_FILE = YES;

+ 1 - 1
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -39,7 +39,7 @@
       },
       {
         "package": "SwiftCharts",
-        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts",
+        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts.git",
         "state": {
           "branch": "master",
           "revision": "c354c1945bb35a1f01b665b22474f6db28cba4a2",

+ 1 - 1
FreeAPS/Resources/Assets.xcassets/Colors/LoopYellow.colorset/Contents.json

@@ -22,7 +22,7 @@
       "color" : {
         "color-space" : "srgb",
         "components" : {
-          "alpha" : "1.000",
+          "alpha" : "0.950",
           "blue" : "0.271",
           "green" : "0.757",
           "red" : "1.000"

+ 20 - 0
FreeAPS/Sources/Helpers/CheckboxToggleStyle.swift

@@ -21,3 +21,23 @@ struct CheckboxToggleStyle: ToggleStyle {
         }
     }
 }
+
+struct Checkbox: ToggleStyle {
+    func makeBody(configuration: Self.Configuration) -> some View {
+        HStack {
+            RoundedRectangle(cornerRadius: 5)
+                .stroke(lineWidth: 2)
+                .foregroundColor(.secondary)
+                .frame(width: 20, height: 20)
+                .overlay {
+                    if configuration.isOn {
+                        Image(systemName: "checkmark").font(.body).fontWeight(.bold)
+                    }
+                }
+                .onTapGesture {
+                    configuration.isOn.toggle()
+                }
+            configuration.label
+        }
+    }
+}

+ 52 - 0
FreeAPS/Sources/Helpers/CustomProgressView.swift

@@ -0,0 +1,52 @@
+import SwiftUI
+
+struct CustomProgressView: View {
+    @State var animate = false
+
+    let text: String
+
+    @Environment(\.colorScheme) var colorScheme
+
+    var body: some View {
+        ZStack {
+            Text(text)
+                .font(.system(.body, design: .rounded))
+                .bold()
+                .offset(x: 0, y: -25)
+
+            RoundedRectangle(cornerRadius: 3)
+                .stroke(Color(.systemGray5), lineWidth: 3)
+                .frame(width: 250, height: 3)
+
+            RoundedRectangle(cornerRadius: 3)
+                .stroke(LinearGradient(colors: [
+                    Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
+                    Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
+                    Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
+                    Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
+                    Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
+                ], startPoint: .leading, endPoint: .trailing), lineWidth: 3)
+                .frame(width: 250, height: 3)
+                .mask(
+                    RoundedRectangle(cornerRadius: 3)
+                        .frame(width: 80, height: 3)
+                        .offset(x: self.animate ? 180 : -180, y: 0)
+                        .animation(
+                            Animation.linear(duration: 2)
+                                .repeatForever(autoreverses: false), value: UUID()
+                        )
+                )
+        }
+        .onAppear {
+            self.animate.toggle()
+        }
+    }
+}
+
+enum ProgressText: String {
+    case updatingIOB = "Updating IOB ..."
+    case updatingCOB = "Updating COB ..."
+    case updatingHistory = "Updating History ..."
+    case updatingTreatments = "Updating Treatments ..."
+    case updatingIOBandCOB = "Updating IOB and COB ..."
+}

+ 3 - 0
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings

@@ -1891,3 +1891,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Korrekturfaktor";
+
+/* Menu */
+"Menu" = "Menü";

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

@@ -2232,3 +2232,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings

@@ -2217,3 +2217,6 @@ Un 1.0 de valor permite un ajuste completo con el nuevo factor de sensibilidad d
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menú";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Facteur appliqué";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings

@@ -2214,3 +2214,6 @@ Regola la costante ISF dinamica.";
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Fattore Regolazione";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Justeringsfaktor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings

@@ -2216,3 +2216,6 @@ Eenvoudig gezegd, de Dynamische Carb Ratio past de koolhydraatverhouding aan op
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Aanpassingsfactor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings

@@ -2214,3 +2214,6 @@ Połączono z Nightscout!";
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Коэффициент регулировки";
+
+/* Menu */
+"Menu" = "меню";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Faktor úpravy";
+
+/* Menu */
+"Menu" = "Menu";

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

@@ -2224,3 +2224,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Justeringskonstant";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings

@@ -2216,3 +2216,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Düzeltme Katsayısı";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings

@@ -2212,3 +2212,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Коефіцієнт Регулювання";
+
+/* Menu */
+"Menu" = "Menu";

+ 3 - 0
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings

@@ -2214,3 +2214,6 @@ Enact a temp Basal or a temp target */
 
 /* Headline "Adjustment Factor" */
 "Adjustment Factor" = "Adjustment Factor";
+
+/* Menu */
+"Menu" = "菜单";

+ 3 - 3
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -53,7 +53,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var fattyMealFactor: Decimal = 0.7
     var sweetMeals: Bool = false
     var sweetMealFactor: Decimal = 2
-    var displayPredictions: Bool = true
+    var displayPresets: Bool = true
     var useLiveActivity: Bool = false
     var historyLayout: HistoryLayout = .twoTabs
     var lockScreenView: LockScreenView = .simple
@@ -278,8 +278,8 @@ extension FreeAPSSettings: Decodable {
             settings.onlyAutotuneBasals = onlyAutotuneBasals
         }
 
-        if let displayPredictions = try? container.decode(Bool.self, forKey: .displayPredictions) {
-            settings.displayPredictions = displayPredictions
+        if let displayPresets = try? container.decode(Bool.self, forKey: .displayPresets) {
+            settings.displayPresets = displayPresets
         }
 
         if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {

+ 121 - 16
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -14,6 +14,7 @@ extension Bolus {
         @Injected() var settings: SettingsManager!
         @Injected() var nsManager: NightscoutManager!
         @Injected() var carbsStorage: CarbsStorage!
+        @Injected() var glucoseStorage: GlucoseStorage!
 
         @Published var suggestion: Suggestion?
         @Published var predictions: Predictions?
@@ -36,6 +37,8 @@ extension Bolus {
         @Published var waitForSuggestion: Bool = false
         @Published var carbRatio: Decimal = 0
 
+        @Published var addButtonPressed: Bool = false
+
         var waitForSuggestionInitial: Bool = false
 
         // added for bolus calculator
@@ -60,7 +63,7 @@ extension Bolus {
         @Published var fattyMeals: Bool = false
         @Published var fattyMealFactor: Decimal = 0
         @Published var useFattyMealCorrectionFactor: Bool = false
-        @Published var displayPredictions: Bool = true
+        @Published var displayPresets: Bool = true
 
         @Published var currentBasal: Decimal = 0
         @Published var sweetMeals: Bool = false
@@ -75,19 +78,20 @@ extension Bolus {
         @Published var note: String = ""
 
         @Published var date = Date()
-        // @Published var protein: Decimal = 0
-        // @Published var fat: Decimal = 0
+
         @Published var carbsRequired: Decimal?
         @Published var useFPUconversion: Bool = false
         @Published var dish: String = ""
         @Published var selection: Presets?
         @Published var summation: [String] = []
         @Published var maxCarbs: Decimal = 0
-        // @Published var note: String = ""
+
         @Published var id_: String = ""
         @Published var summary: String = ""
         @Published var skipBolus: Bool = false
 
+        @Published var externalInsulin: Bool = false
+
         let now = Date.now
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
@@ -95,6 +99,7 @@ extension Bolus {
         override func subscribe() {
             setupInsulinRequired()
             broadcaster.register(SuggestionObserver.self, observer: self)
+            broadcaster.register(BolusFailureObserver.self, observer: self)
             units = settingsManager.settings.units
             percentage = settingsManager.settings.insulinReqPercentage
             threshold = provider.suggestion?.threshold ?? 0
@@ -106,7 +111,7 @@ extension Bolus {
             fattyMealFactor = settings.settings.fattyMealFactor
             sweetMeals = settings.settings.sweetMeals
             sweetMealFactor = settings.settings.sweetMealFactor
-            displayPredictions = settings.settings.displayPredictions
+            displayPresets = settings.settings.displayPresets
 
             carbsRequired = provider.suggestion?.carbsReq
             maxCarbs = settings.settings.maxCarbs
@@ -178,7 +183,9 @@ extension Bolus {
             deltaBG = delta
         }
 
-        // CALCULATIONS FOR THE BOLUS CALCULATOR
+        // MARK: CALCULATIONS FOR THE BOLUS CALCULATOR
+
+        /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
             // ensure that isf is in mg/dL
             var conversion: Decimal {
@@ -234,7 +241,37 @@ extension Bolus {
                 .roundBolus(amount: max(insulinCalculated, 0))
         }
 
-        func add() async {
+        @MainActor func invokeTreatmentsTask() {
+            Task {
+                if amount > 0 {
+                    if !externalInsulin {
+                        await add()
+                    } else {
+                        do {
+                            await addExternalInsulin()
+                        }
+                    }
+                    waitForSuggestion = true
+                } else {
+                    if carbs > 0 {
+                        waitForSuggestion = true
+                    } else {
+                        hideModal()
+                    }
+                }
+                addCarbs()
+                addButtonPressed = true
+
+                // if glucose data is stale end the custom loading animation by hiding the modal
+                // alternatively only set waitforSuggestion to false...
+                let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
+                guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
+                    return hideModal()
+                }
+            }
+        }
+
+        @MainActor func add() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
@@ -250,7 +287,13 @@ extension Bolus {
                     print("authentication failed")
                 }
             } catch {
-                print("authentication error: \(error.localizedDescription)")
+                print("authentication error for pump bolus: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    self.waitForSuggestion = false
+                    if self.addButtonPressed {
+                        self.hideModal()
+                    }
+                }
             }
         }
 
@@ -315,14 +358,13 @@ extension Bolus {
             )]
             carbsStorage.storeCarbs(carbsToStore)
 
-            if skipBolus {
-                apsManager.determineBasalSync()
-                showModal(for: nil)
-            } else if carbs > 0 {
+            if carbs > 0 {
                 saveToCoreData(carbsToStore)
-                apsManager.determineBasalSync()
-            } else {
-                hideModal()
+
+                // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
+                if amount <= 0 {
+                    apsManager.determineBasalSync()
+                }
             }
         }
 
@@ -468,14 +510,77 @@ extension Bolus {
                 print("meals 1: ID: " + (save.id ?? "").description + " FPU ID: " + (save.fpuID ?? "").description)
             }
         }
+
+        // MARK: EXTERNAL INSULIN
+
+        @MainActor func addExternalInsulin() async {
+            guard amount > 0 else {
+                showModal(for: nil)
+                return
+            }
+
+            amount = min(amount, maxBolus * 3)
+
+            do {
+                let authenticated = try await unlockmanager.unlock()
+                if authenticated {
+                    storeExternalInsulinEvent()
+                } else {
+                    print("authentication failed")
+                }
+            } catch {
+                print("authentication error for external insulin: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    self.waitForSuggestion = false
+                    if self.addButtonPressed {
+                        self.hideModal()
+                    }
+                }
+            }
+        }
+
+        private func storeExternalInsulinEvent() {
+            pumpHistoryStorage.storeEvents(
+                [
+                    PumpHistoryEvent(
+                        id: UUID().uuidString,
+                        type: .bolus,
+                        timestamp: date,
+                        amount: amount,
+                        duration: nil,
+                        durationMin: nil,
+                        rate: nil,
+                        temp: nil,
+                        carbInput: nil,
+                        isExternal: true
+                    )
+                ]
+            )
+            debug(.default, "External insulin saved to pumphistory.json")
+
+            // perform determine basal sync
+            apsManager.determineBasalSync()
+        }
     }
 }
 
-extension Bolus.StateModel: SuggestionObserver {
+extension Bolus.StateModel: SuggestionObserver, BolusFailureObserver {
     func suggestionDidUpdate(_: Suggestion) {
         DispatchQueue.main.async {
             self.waitForSuggestion = false
+            if self.addButtonPressed {
+                self.hideModal()
+            }
         }
         setupInsulinRequired()
     }
+
+    func bolusDidFail() {
+        DispatchQueue.main.async {
+            self.waitForSuggestion = false
+            if self.addButtonPressed {
+                self.hideModal()
+            }
+        }
+    }
 }

+ 290 - 216
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift

@@ -1,5 +1,6 @@
 import Charts
 import CoreData
+import LoopKitUI
 import SwiftUI
 import Swinject
 
@@ -9,16 +10,15 @@ extension Bolus {
 
         @StateObject var state: StateModel
 
-        @State private var showInfo = false
+        @State private var showInfo: Bool = false
         @State private var showAlert = false
-        @State private var exceededMaxBolus = false
         @State private var autofocus: Bool = true
         @State private var calculatorDetent = PresentationDetent.medium
-        @State var pushed = false
-        @State var isPromptPresented = false
-        @State var dish: String = ""
-        @State var saved = false
-        @State var isCalculating: Bool = false
+        @State private var pushed: Bool = false
+        @State private var isPromptPresented: Bool = false
+        @State private var dish: String = ""
+        @State private var saved: Bool = false
+        @State private var debounce: DispatchWorkItem?
 
         @Environment(\.managedObjectContext) var moc
 
@@ -81,7 +81,18 @@ extension Bolus {
         }
 
         private var empty: Bool {
-            state.carbs <= 0 && state.fat <= 0 && state.protein <= 0
+            state.useFPUconversion ? (state.carbs <= 0 && state.fat <= 0 && state.protein <= 0) : (state.carbs <= 0)
+        }
+
+        /// Handles macro input (carb, fat, protein) in a debounced fashion.
+        func handleDebouncedInput() {
+            debounce?.cancel()
+            debounce = DispatchWorkItem { [self] in
+                state.insulinCalculated = state.calculateInsulin()
+            }
+            if let debounce = debounce {
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: debounce)
+            }
         }
 
         private var presetPopover: some View {
@@ -264,233 +275,243 @@ extension Bolus {
         }
 
         var body: some View {
-            Form {
-                // MARK: ADDED
-
-                Section {
-                    HStack {
-                        Text("Carbs").fontWeight(.semibold)
-                        Spacer()
-                        DecimalTextField(
-                            "0",
-                            value: $state.carbs,
-                            formatter: formatter,
-                            autofocus: false,
-                            cleanInput: true
-                        )
-                        Text("g").foregroundColor(.secondary)
-                    }
-
-                    if state.useFPUconversion {
-                        proteinAndFat()
-                    }
-
-                    // Summary when combining presets
-                    if state.waitersNotepad() != "" {
-                        HStack {
-                            Text("Total")
-                            let test = state.waitersNotepad().components(separatedBy: ", ").removeDublicates()
-                            HStack(spacing: 0) {
-                                ForEach(test, id: \.self) {
-                                    Text($0).foregroundStyle(Color.randomGreen()).font(.footnote)
-                                    Text($0 == test[test.count - 1] ? "" : ", ")
+            ZStack(alignment: .center) {
+                VStack {
+                    Form {
+                        Section {
+                            HStack {
+                                Text("Carbs").fontWeight(.semibold)
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.carbs,
+                                    formatter: formatter,
+                                    autofocus: false,
+                                    cleanInput: true
+                                ).onChange(of: state.carbs) { _ in
+                                    if state.carbs > 0 {
+                                        handleDebouncedInput()
+                                    }
                                 }
-                            }.frame(maxWidth: .infinity, alignment: .trailing)
-                        }
-                    }
-
-                    // Time
-                    HStack {
-                        Text("Time").foregroundStyle(Color.secondary)
-                        Spacer()
-                        if !pushed {
-                            Button {
-                                pushed = true
-                            } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary).padding(.trailing, 5)
-                        } else {
-                            Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
-                            label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
-                            DatePicker(
-                                "Time",
-                                selection: $state.date,
-                                displayedComponents: [.hourAndMinute]
-                            ).controlSize(.mini)
-                                .labelsHidden()
-                            Button {
-                                state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                Text("g").foregroundColor(.secondary)
                             }
-                            label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
-                        }
-                    }
-
-                    .popover(isPresented: $isPromptPresented) {
-                        presetPopover
-                    }
-
-                    HStack {
-                        Spacer()
-                        Button {
-                            isCalculating = true
-                            state.insulinCalculated = state.calculateInsulin()
 
-                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
-                                isCalculating = false
+                            if state.useFPUconversion {
+                                proteinAndFat()
                             }
-                        }
-                        label: {
-                            if !isCalculating {
-                                Text("Calculate")
-                            } else {
-                                ProgressView().progressViewStyle(CircularProgressViewStyle())
-                            }
-                        }.disabled(empty)
-
-                        Spacer()
-                    }
-                } header: { Text("Carbs") }.listRowBackground(Color.chart)
-
-                Section {
-                    mealPresets
-                }.listRowBackground(Color.chart)
 
-                Section {
-                    HStack {
-                        Button(action: {
-                            showInfo.toggle()
-                        }, label: {
-                            Image(systemName: "info.circle")
-                            Text("Calculations")
-                        })
-                            .foregroundStyle(.blue)
-                            .font(.footnote)
-                            .buttonStyle(PlainButtonStyle())
-                            .frame(maxWidth: .infinity, alignment: .leading)
-
-                        if state.fattyMeals {
-                            Spacer()
-                            Toggle(isOn: $state.useFattyMealCorrectionFactor) {
-                                Text("Fatty Meal")
-                            }
-                            .toggleStyle(CheckboxToggleStyle())
-                            .font(.footnote)
-                            .onChange(of: state.useFattyMealCorrectionFactor) { _ in
-                                state.insulinCalculated = state.calculateInsulin()
-                                if state.useFattyMealCorrectionFactor {
-                                    state.useSuperBolus = false
+                            // Summary when combining presets
+                            if state.waitersNotepad() != "" {
+                                HStack {
+                                    Text("Total")
+                                    let test = state.waitersNotepad().components(separatedBy: ", ").removeDublicates()
+                                    HStack(spacing: 0) {
+                                        ForEach(test, id: \.self) {
+                                            Text($0).foregroundStyle(Color.randomGreen()).font(.footnote)
+                                            Text($0 == test[test.count - 1] ? "" : ", ")
+                                        }
+                                    }.frame(maxWidth: .infinity, alignment: .trailing)
                                 }
                             }
-                        }
-                        if state.sweetMeals {
-                            Spacer()
-                            Toggle(isOn: $state.useSuperBolus) {
-                                Text("Super Bolus")
-                            }
-                            .toggleStyle(CheckboxToggleStyle())
-                            .font(.footnote)
-                            .onChange(of: state.useSuperBolus) { _ in
-                                state.insulinCalculated = state.calculateInsulin()
-                                if state.useSuperBolus {
-                                    state.useFattyMealCorrectionFactor = false
+
+                            // Time
+                            HStack {
+                                Text("Time").foregroundStyle(Color.secondary)
+                                Spacer()
+                                if !pushed {
+                                    Button {
+                                        pushed = true
+                                    } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
+                                        .padding(.trailing, 5)
+                                } else {
+                                    Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
+                                    label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+                                    DatePicker(
+                                        "Time",
+                                        selection: $state.date,
+                                        displayedComponents: [.hourAndMinute]
+                                    ).controlSize(.mini)
+                                        .labelsHidden()
+                                    Button {
+                                        state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                    }
+                                    label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                 }
                             }
-                        }
-                    }
 
-                    HStack {
-                        Text("Recommended Bolus")
-                        Spacer()
-                        Text(
-                            formatter
-                                .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
-                        )
-                        Text(
-                            NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
-                        ).foregroundColor(.secondary)
-                    }.contentShape(Rectangle())
-                        .onTapGesture { state.amount = state.insulinCalculated }
-
-                    HStack {
-                        Text("Bolus")
-                        Spacer()
-                        DecimalTextField(
-                            "0",
-                            value: $state.amount,
-                            formatter: formatter,
-                            autofocus: false,
-                            cleanInput: true
-                        )
-                        Text(exceededMaxBolus ? "😵" : " U").foregroundColor(.secondary)
-                    }
-                    .onChange(of: state.amount) { newValue in
-                        if newValue > state.maxBolus {
-                            exceededMaxBolus = true
-                        } else {
-                            exceededMaxBolus = false
+                            .popover(isPresented: $isPromptPresented) {
+                                presetPopover
+                            }
+                        }.listRowBackground(Color.chart)
+
+                        if state.displayPresets {
+                            Section {
+                                mealPresets
+                            }.listRowBackground(Color.chart)
                         }
-                    }
 
-                } header: { Text("Bolus") }.listRowBackground(Color.chart)
+                        Section {
+                            HStack {
+                                Button(action: {
+                                    showInfo.toggle()
+                                }, label: {
+                                    Image(systemName: "info.circle")
+                                    Text("Calculations")
+                                })
+                                    .foregroundStyle(.blue)
+                                    .font(.footnote)
+                                    .buttonStyle(PlainButtonStyle())
+                                    .frame(maxWidth: .infinity, alignment: .leading)
+
+                                if state.fattyMeals {
+                                    Spacer()
+                                    Toggle(isOn: $state.useFattyMealCorrectionFactor) {
+                                        Text("Fatty Meal")
+                                    }
+                                    .toggleStyle(CheckboxToggleStyle())
+                                    .font(.footnote)
+                                    .onChange(of: state.useFattyMealCorrectionFactor) { _ in
+                                        state.insulinCalculated = state.calculateInsulin()
+                                        if state.useFattyMealCorrectionFactor {
+                                            state.useSuperBolus = false
+                                        }
+                                    }
+                                }
+                                if state.sweetMeals {
+                                    Spacer()
+                                    Toggle(isOn: $state.useSuperBolus) {
+                                        Text("Super Bolus")
+                                    }
+                                    .toggleStyle(CheckboxToggleStyle())
+                                    .font(.footnote)
+                                    .onChange(of: state.useSuperBolus) { _ in
+                                        state.insulinCalculated = state.calculateInsulin()
+                                        if state.useSuperBolus {
+                                            state.useFattyMealCorrectionFactor = false
+                                        }
+                                    }
+                                }
+                            }
 
-                if state.amount > 0 {
-                    Section {
-                        Button {
-                            Task {
-                                await state.add()
-                                state.hideModal()
-                                state.addCarbs()
+                            HStack {
+                                Text("Recommended Bolus")
+                                Spacer()
+                                Text(
+                                    formatter
+                                        .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
+                                )
+                                Text(
+                                    NSLocalizedString(
+                                        " U",
+                                        comment: "Unit in number of units delivered (keep the space character!)"
+                                    )
+                                ).foregroundColor(.secondary)
+                            }.contentShape(Rectangle())
+                                .onTapGesture { state.amount = state.insulinCalculated }
+
+                            HStack {
+                                Text("Bolus")
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.amount,
+                                    formatter: formatter,
+                                    autofocus: false,
+                                    cleanInput: true,
+                                    textColor: .systemBlue
+                                )
+                                Text(" U").foregroundColor(.secondary)
                             }
-                        }
 
-                        label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .disabled(disabled)
-                            .listRowBackground(!disabled ? Color(.systemBlue) : Color(.systemGray4))
-                            .tint(.white)
+                            if state.amount > 0 {
+                                HStack {
+                                    Text("External insulin")
+                                    Spacer()
+                                    Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
+                                }
+                            }
+                        }.listRowBackground(Color.chart)
                     }
+                }.safeAreaInset(edge: .bottom, spacing: 0) {
+                    stickyButton
+                }.blur(radius: state.waitForSuggestion ? 5 : 0)
+
+                if state.waitForSuggestion {
+                    CustomProgressView(text: progressText.rawValue)
                 }
-                if state.amount <= 0 {
-                    Section {
-                        Button {
-                            state.hideModal()
-                            state.addCarbs()
-                        }
-                        label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
-                    }.listRowBackground(Color.chart)
-                }
-            }.scrollContentBackground(.hidden).background(color)
-                .blur(radius: showInfo ? 3 : 0)
-                .navigationTitle("Treatments")
-                .navigationBarTitleDisplayMode(.large)
-                .toolbar(content: {
-                    ToolbarItem(placement: .topBarLeading) {
-                        Button {
-                            state.hideModal()
-                        } label: {
-                            Text("Close")
-                        }
-                    }
-                })
-                .onAppear {
-                    configureView {
-                        state.insulinCalculated = state.calculateInsulin()
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .blur(radius: showInfo ? 3 : 0)
+            .navigationTitle("Treatments")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button {
+                        state.hideModal()
+                    } label: {
+                        Text("Close")
                     }
                 }
-
-                .sheet(isPresented: $showInfo) {
-                    calculationsDetailView
-                        .presentationDetents(
-                            [.fraction(0.9), .large],
-                            selection: $calculatorDetent
-                        )
+            })
+            .onAppear {
+                configureView {
+                    state.insulinCalculated = state.calculateInsulin()
                 }
+            }
+            .onDisappear {
+                state.addButtonPressed = false
+            }
+            .sheet(isPresented: $showInfo) {
+                calculationsDetailView
+                    .presentationDetents(
+                        [.fraction(0.9), .large],
+                        selection: $calculatorDetent
+                    )
+            }
+        }
+
+        var progressText: ProgressText {
+            switch (state.amount > 0, state.carbs > 0) {
+            case (true, true):
+                return .updatingIOBandCOB
+            case (false, true):
+                return .updatingCOB
+            case (true, false):
+                return .updatingIOB
+            default:
+                return .updatingTreatments
+            }
         }
 
-        var predictionChart: some View {
+        var stickyButton: some View {
             ZStack {
-                PredictionView(
-                    predictions: $state.predictions, units: $state.units, eventualBG: $state.evBG, target: $state.target,
-                    displayPredictions: $state.displayPredictions
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 120).offset(y: 40)
+                    .shadow(
+                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
+                            Color.black.opacity(0.33),
+                        radius: 3
+                    )
+                    .foregroundStyle(Color.chart)
+
+                Button {
+                    state.invokeTreatmentsTask()
+                } label: {
+                    taskButtonLabel
+                        .font(.headline)
+                        .foregroundStyle(Color.white)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .frame(minHeight: 50)
+                }
+                .disabled(disableTaskButton)
+                .background(
+                    (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) ? Color(.systemRed) :
+                        Color(.systemBlue)
                 )
+                .shadow(radius: 3)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding()
+                .offset(y: 20)
             }
         }
 
@@ -581,7 +602,7 @@ extension Bolus {
                     + " / " +
                     state.isf.formatted()
                     + " ≈ " +
-                self.insulinRounder(state.targetDifferenceInsulin).formatted()
+                    self.insulinRounder(state.targetDifferenceInsulin).formatted()
 
                 Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
 
@@ -677,7 +698,7 @@ extension Bolus {
                         + " / " +
                         state.isf.formatted()
                         + " ≈ " +
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
+                        self.insulinRounder(state.fifteenMinInsulin).formatted()
                 )
                 .foregroundColor(.secondary)
                 .gridColumnAlignment(.leading)
@@ -927,7 +948,7 @@ extension Bolus {
                         .padding(.top)
                 }
                 .padding([.horizontal, .bottom])
-                .font(.system(size: 15))
+                .font(.subheadline)
             }
         }
 
@@ -936,8 +957,61 @@ extension Bolus {
             return Decimal(floor(100 * toRound) / 100)
         }
 
-        private var disabled: Bool {
-            state.amount <= 0 || state.amount > state.maxBolus
+        private var taskButtonLabel: some View {
+            let hasInsulin = state.amount > 0
+            let hasCarbs = state.carbs > 0
+            let hasFatOrProtein = state.fat > 0 || state.protein > 0
+
+            switch (hasInsulin, hasCarbs, hasFatOrProtein) {
+            case (true, true, true):
+                return Text(
+                    state
+                        .externalInsulin ? (
+                            externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log meal and external insulin"
+                        ) :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log meal and enact bolus")
+                )
+            case (true, true, false):
+                return Text(
+                    state
+                        .externalInsulin ?
+                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log carbs and external insulin") :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log carbs and enact bolus")
+                )
+            case (true, false, true):
+                return Text(
+                    state
+                        .externalInsulin ?
+                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log FPUs and external insulin") :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log FPUs and enact bolus")
+                )
+            case (true, false, false):
+                return Text(
+                    state
+                        .externalInsulin ? (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log external insulin") :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Enact bolus")
+                )
+            case (false, true, true):
+                return Text("Log meal")
+            case (false, true, false):
+                return Text("Log carbs")
+            case (false, false, true):
+                return Text("Log FPUs")
+            default:
+                return Text("Continue without treatment")
+            }
+        }
+
+        private var pumpBolusLimit: Bool {
+            state.amount > state.maxBolus
+        }
+
+        private var externalBolusLimit: Bool {
+            state.amount > state.maxBolus * 3
+        }
+
+        private var disableTaskButton: Bool {
+            state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false
         }
     }
 

+ 0 - 11
FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift

@@ -57,8 +57,6 @@ extension Bolus {
                 Section {
                     if state.waitForSuggestion {
                         Text("Please wait")
-                    } else {
-                        predictionChart
                     }
                 } header: { Text("Predictions") }.listRowBackground(Color.chart)
 
@@ -180,15 +178,6 @@ extension Bolus {
             state.amount <= 0 || state.amount > state.maxBolus
         }
 
-        var predictionChart: some View {
-            ZStack {
-                PredictionView(
-                    predictions: $state.predictions, units: $state.units, eventualBG: $state.evBG, target: $state.target,
-                    displayPredictions: $state.displayPredictions
-                )
-            }
-        }
-
         var changed: Bool {
             ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
         }

+ 0 - 113
FreeAPS/Sources/Modules/Bolus/View/Predictions.swift

@@ -1,113 +0,0 @@
-import Charts
-import CoreData
-import SwiftUI
-import Swinject
-
-struct PredictionView: View {
-    @Binding var predictions: Predictions?
-    @Binding var units: GlucoseUnits
-    @Binding var eventualBG: Int
-    @Binding var target: Decimal
-    @Binding var displayPredictions: Bool
-
-    private enum Config {
-        static let height: CGFloat = 160
-        static let lineWidth: CGFloat = 2
-    }
-
-    var body: some View {
-        VStack {
-            if displayPredictions {
-                chart()
-            }
-            HStack {
-                let conversion = units == .mmolL ? 0.0555 : 1
-                Text("Eventual Glucose")
-                Spacer()
-                Text(
-                    (Double(eventualBG) * conversion)
-                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(units == .mmolL ? 1 : 0)))
-                )
-                Text(units.rawValue).foregroundStyle(.secondary)
-                Divider()
-            }.font(.callout)
-        }
-    }
-
-    func chart() -> some View {
-        // Data Source
-        let iob = predictions?.iob ?? [Int]()
-        let cob = predictions?.cob ?? [Int]()
-        let uam = predictions?.uam ?? [Int]()
-        let zt = predictions?.zt ?? [Int]()
-        let count = max(iob.count, cob.count, uam.count, zt.count)
-        var now = Date.now
-        var startIndex = 0
-        let conversion = units == .mmolL ? 0.0555 : 1
-        // Organize the data needed for prediction chart.
-        var data = [ChartData]()
-        repeat {
-            now = now.addingTimeInterval(5.minutes.timeInterval)
-            if startIndex < count {
-                let addedData = ChartData(
-                    date: now,
-                    iob: startIndex < iob.count ? Double(iob[startIndex]) * conversion : 0,
-                    zt: startIndex < zt.count ? Double(zt[startIndex]) * conversion : 0,
-                    cob: startIndex < cob.count ? Double(cob[startIndex]) * conversion : 0,
-                    uam: startIndex < uam.count ? Double(uam[startIndex]) * conversion : 0,
-                    id: UUID()
-                )
-                data.append(addedData)
-            }
-            startIndex += 1
-        } while startIndex < count
-        // Chart
-        return Chart(data) {
-            // Remove 0 (empty) values
-            if $0.iob != 0 {
-                LineMark(
-                    x: .value("Time", $0.date),
-                    y: .value("IOB", $0.iob),
-                    series: .value("IOB", "A")
-                )
-                .foregroundStyle(Color(.insulin))
-                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
-            }
-            if $0.uam != 0 {
-                LineMark(
-                    x: .value("Time", $0.date),
-                    y: .value("UAM", $0.uam),
-                    series: .value("UAM", "B")
-                )
-                .foregroundStyle(Color(.UAM))
-                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
-            }
-            if $0.cob != 0 {
-                LineMark(
-                    x: .value("Time", $0.date),
-                    y: .value("COB", $0.cob),
-                    series: .value("COB", "C")
-                )
-                .foregroundStyle(Color(.loopYellow))
-                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
-            }
-            if $0.zt != 0 {
-                LineMark(
-                    x: .value("Time", $0.date),
-                    y: .value("ZT", $0.zt),
-                    series: .value("ZT", "D")
-                )
-                .foregroundStyle(Color(.ZT))
-                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
-            }
-        }
-        .frame(minHeight: Config.height)
-        .chartForegroundStyleScale([
-            "IOB": Color(.insulin),
-            "UAM": .uam,
-            "COB": Color(.loopYellow),
-            "ZT": .zt
-        ])
-        .chartYAxisLabel(NSLocalizedString("Glucose, ", comment: "") + units.rawValue, alignment: .center)
-    }
-}

+ 2 - 2
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -9,7 +9,7 @@ extension BolusCalculatorConfig {
         @Published var sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var insulinReqPercentage: Decimal = 70
-        @Published var displayPredictions: Bool = true
+        @Published var displayPresets: Bool = true
 
         override func subscribe() {
             subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
@@ -20,7 +20,7 @@ extension BolusCalculatorConfig {
             })
             subscribeSetting(\.useCalc, on: $useCalc) { useCalc = $0 }
             subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
-            subscribeSetting(\.displayPredictions, on: $displayPredictions) { displayPredictions = $0 }
+            subscribeSetting(\.displayPresets, on: $displayPresets) { displayPresets = $0 }
             subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
                 let value = max(min($0, 1.2), 0.1)
                 fattyMealFactor = value

+ 1 - 1
FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -62,7 +62,7 @@ extension BolusCalculatorConfig {
                 } header: { Text("Calculator settings") }
 
                 Section {
-                    Toggle("Display Predictions", isOn: $state.displayPredictions)
+                    Toggle("Display Presets", isOn: $state.displayPresets)
 
                 } header: { Text("Smaller iPhone Screens") }
 

+ 33 - 45
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -4,6 +4,7 @@ import SwiftUI
 extension DataTable {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var broadcaster: Broadcaster!
+        @Injected() var apsManager: APSManager!
         @Injected() var unlockmanager: UnlockManager!
         @Injected() private var storage: FileStorage!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
@@ -17,8 +18,10 @@ extension DataTable {
         @Published var meals: [Treatment] = []
         @Published var manualGlucose: Decimal = 0
         @Published var maxBolus: Decimal = 0
-        @Published var externalInsulinAmount: Decimal = 0
-        @Published var externalInsulinDate = Date()
+        @Published var waitForSuggestion: Bool = false
+
+        @Published var insulinEntryDeleted: Bool = false
+        @Published var carbEntryDeleted: Bool = false
 
         var units: GlucoseUnits = .mmolL
         var historyLayout: HistoryLayout = .twoTabs
@@ -34,6 +37,7 @@ extension DataTable {
             broadcaster.register(TempTargetsObserver.self, observer: self)
             broadcaster.register(CarbsObserver.self, observer: self)
             broadcaster.register(GlucoseObserver.self, observer: self)
+            broadcaster.register(SuggestionObserver.self, observer: self)
         }
 
         private func setupTreatments() {
@@ -156,8 +160,25 @@ extension DataTable {
             }
         }
 
+        func invokeCarbDeletionTask(_ treatment: Treatment) {
+            carbEntryDeleted = true
+            waitForSuggestion = true
+            deleteCarbs(treatment)
+        }
+
         func deleteCarbs(_ treatment: Treatment) {
             provider.deleteCarbs(treatment)
+            apsManager.determineBasalSync()
+        }
+
+        @MainActor func invokeInsulinDeletionTask(_ treatment: Treatment) {
+            Task {
+                do {
+                    await deleteInsulin(treatment)
+                    insulinEntryDeleted = true
+                    waitForSuggestion = true
+                }
+            }
         }
 
         func deleteInsulin(_ treatment: Treatment) async {
@@ -165,6 +186,7 @@ extension DataTable {
                 let authenticated = try await unlockmanager.unlock()
                 if authenticated {
                     provider.deleteInsulin(treatment)
+                    apsManager.determineBasalSync()
                 } else {
                     print("authentication failed")
                 }
@@ -222,49 +244,6 @@ extension DataTable {
             var saveToHealth = [BloodGlucose]()
             saveToHealth.append(saveToJSON)
         }
-
-        func addExternalInsulin() async {
-            guard externalInsulinAmount > 0 else {
-                showModal(for: nil)
-                return
-            }
-
-            externalInsulinAmount = min(externalInsulinAmount, maxBolus * 3)
-
-            do {
-                let authenticated = try await unlockmanager.unlock()
-                if authenticated {
-                    storeExternalInsulinEvent()
-                } else {
-                    print("authentication failed")
-                }
-            } catch {
-                print("authentication error: \(error.localizedDescription)")
-            }
-        }
-
-        private func storeExternalInsulinEvent() {
-            pumpHistoryStorage.storeEvents(
-                [
-                    PumpHistoryEvent(
-                        id: UUID().uuidString,
-                        type: .bolus,
-                        timestamp: externalInsulinDate,
-                        amount: externalInsulinAmount,
-                        duration: nil,
-                        durationMin: nil,
-                        rate: nil,
-                        temp: nil,
-                        carbInput: nil,
-                        isExternal: true
-                    )
-                ]
-            )
-            debug(.default, "External insulin saved to pumphistory.json")
-
-            // Reset amount to 0 for next entry.
-            externalInsulinAmount = 0
-        }
     }
 }
 
@@ -276,6 +255,7 @@ extension DataTable.StateModel:
     GlucoseObserver
 {
     func settingsDidChange(_: FreeAPSSettings) {
+        historyLayout = settingsManager.settings.historyLayout
         setupTreatments()
     }
 
@@ -295,3 +275,11 @@ extension DataTable.StateModel:
         setupGlucose()
     }
 }
+
+extension DataTable.StateModel: SuggestionObserver {
+    func suggestionDidUpdate(_: Suggestion) {
+        DispatchQueue.main.async {
+            self.waitForSuggestion = false
+        }
+    }
+}

+ 59 - 139
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -12,7 +12,6 @@ extension DataTable {
         @State private var alertTreatmentToDelete: Treatment?
         @State private var alertGlucoseToDelete: Glucose?
 
-        @State private var showExternalInsulin: Bool = false
         @State private var showFutureEntries: Bool = false // default to hide future entries
         @State private var showManualGlucose: Bool = false
         @State private var isAmountUnconfirmed: Bool = true
@@ -75,75 +74,89 @@ extension DataTable {
         }
 
         var body: some View {
-            VStack {
-                Picker("Mode", selection: $state.mode) {
-                    ForEach(
-                        Mode.allCases.filter({ state.historyLayout == .twoTabs ? $0 != .meals : true }).indexed(),
-                        id: \.1
-                    ) { index, item in
-                        if state.historyLayout == .threeTabs && item == .treatments {
-                            Text("Insulin")
-                                .tag(index)
-                        } else {
-                            Text(item.name)
-                                .tag(index)
+            ZStack(alignment: .center, content: {
+                VStack {
+                    Picker("Mode", selection: $state.mode) {
+                        ForEach(
+                            Mode.allCases.filter({ state.historyLayout == .twoTabs ? $0 != .meals : true }).indexed(),
+                            id: \.1
+                        ) { index, item in
+                            if state.historyLayout == .threeTabs && item == .treatments {
+                                Text("Insulin")
+                                    .tag(index)
+                            } else {
+                                Text(item.name)
+                                    .tag(index)
+                            }
                         }
                     }
+                    .pickerStyle(SegmentedPickerStyle())
+                    .padding(.horizontal)
+
+                    Form {
+                        switch state.mode {
+                        case .treatments: treatmentsList
+                        case .glucose: glucoseList
+                        case .meals: state.historyLayout == .threeTabs ? AnyView(mealsList) : AnyView(EmptyView())
+                        }
+                    }.scrollContentBackground(.hidden)
+                        .background(color)
+                }.blur(radius: state.waitForSuggestion ? 8 : 0)
+
+                if state.waitForSuggestion {
+                    CustomProgressView(text: progressText.rawValue)
                 }
-                .pickerStyle(SegmentedPickerStyle())
-                .padding(.horizontal)
-
-                Form {
-                    switch state.mode {
-                    case .treatments: treatmentsList
-                    case .glucose: glucoseList
-                    case .meals: state.historyLayout == .threeTabs ? AnyView(mealsList) : AnyView(EmptyView())
-                    }
-                }.scrollContentBackground(.hidden)
-                    .background(color)
-            }.background(color)
+            })
+                .background(color)
                 .onAppear(perform: configureView)
+                .onDisappear {
+                    state.carbEntryDeleted = false
+                    state.insulinEntryDeleted = false
+                }
                 .navigationTitle("History")
                 .navigationBarTitleDisplayMode(.large)
                 .toolbar {
                     ToolbarItem(placement: .topBarTrailing) {
-                        switch state.mode {
-                        case .treatments: addButton({
-                                showExternalInsulin = true
-                                state.externalInsulinDate = Date()
-                            })
-                        case .meals: EmptyView()
-                        case .glucose: addButton({
-                                showManualGlucose = true
-                                state.manualGlucose = 0
-                            })
-                        }
+                        addButton({
+                            showManualGlucose = true
+                            state.manualGlucose = 0
+                        })
                     }
                 }
                 .sheet(isPresented: $showManualGlucose) {
                     addGlucoseView()
                 }
-                .sheet(isPresented: $showExternalInsulin, onDismiss: { if isAmountUnconfirmed { state.externalInsulinAmount = 0
-                    state.externalInsulinDate = Date() } }) {
-                    addExternalInsulinView()
-                }
         }
 
         @ViewBuilder func addButton(_ action: @escaping () -> Void) -> some View {
             Button(
                 action: action,
                 label: {
-                    Image(systemName: "plus")
-                        .font(.system(size: 20))
+                    HStack {
+                        Text("Add Glucose")
+                        Image(systemName: "plus")
+                            .font(.system(size: 20))
+                    }
                 }
             )
         }
 
+        private var progressText: ProgressText {
+            switch (state.carbEntryDeleted, state.insulinEntryDeleted) {
+            case (true, false):
+                return .updatingCOB
+            case(false, true):
+                return .updatingIOB
+            default:
+                return .updatingHistory
+            }
+        }
+
         private var treatmentsList: some View {
             List {
                 HStack {
                     if state.historyLayout == .twoTabs {
-                        Text("Insulin").foregroundStyle(.secondary)
+                        Text("Treatments").foregroundStyle(.secondary)
                         Spacer()
                         filterEntriesButton
                     } else {
@@ -320,13 +333,9 @@ extension DataTable {
                     }
 
                     if state.historyLayout == .twoTabs, treatmentToDelete.type == .carbs || treatmentToDelete.type == .fpus {
-                        state.deleteCarbs(treatmentToDelete)
+                        state.invokeCarbDeletionTask(treatmentToDelete)
                     } else {
-                        Task {
-                            do {
-                                await state.deleteInsulin(treatmentToDelete)
-                            }
-                        }
+                        state.invokeInsulinDeletionTask(treatmentToDelete)
                     }
                 }
             } message: {
@@ -382,102 +391,13 @@ extension DataTable {
                         debug(.default, "Cannot gracefully unwrap alertTreatmentToDelete!")
                         return
                     }
-
-                    state.deleteCarbs(treatmentToDelete)
+                    state.invokeCarbDeletionTask(treatmentToDelete)
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))
             }
         }
 
-        @ViewBuilder func addExternalInsulinView() -> some View {
-            NavigationView {
-                VStack {
-                    Form {
-                        Section {
-                            HStack {
-                                Text("Amount")
-                                Spacer()
-                                DecimalTextField(
-                                    "0",
-                                    value: $state.externalInsulinAmount,
-                                    formatter: insulinFormatter,
-                                    autofocus: true,
-                                    cleanInput: true
-                                )
-                                Text("U").foregroundColor(.secondary)
-                            }
-                        }.listRowBackground(Color.chart)
-
-                        Section {
-                            DatePicker("Date", selection: $state.externalInsulinDate, in: ...Date())
-                        }.listRowBackground(Color.chart)
-
-                        let amountWarningCondition = (state.externalInsulinAmount > state.maxBolus)
-
-                        var listBackgroundColor: Color {
-                            if amountWarningCondition {
-                                return Color.red
-                            } else if state.externalInsulinAmount <= 0 || state.externalInsulinAmount > state.maxBolus * 3 {
-                                return Color(.systemGray4)
-                            } else {
-                                return Color(.systemBlue)
-                            }
-                        }
-
-                        var foregroundColor: Color {
-                            if amountWarningCondition {
-                                return Color.white
-                            } else if state.externalInsulinAmount <= 0 || state.externalInsulinAmount > state.maxBolus * 3 {
-                                return Color.secondary
-                            } else {
-                                return Color.white
-                            }
-                        }
-
-                        Section {
-                            HStack {
-                                Button {
-                                    Task {
-                                        do {
-                                            await state.addExternalInsulin()
-                                            isAmountUnconfirmed = false
-                                            showExternalInsulin = false
-                                        }
-                                    }
-                                } label: {
-                                    Text("Log external insulin")
-                                }
-                                .foregroundStyle(foregroundColor)
-                                .frame(maxWidth: .infinity, alignment: .center)
-                                .disabled(
-                                    state.externalInsulinAmount <= 0 || state.externalInsulinAmount > state.maxBolus * 3
-                                )
-                            }
-                        }
-                        header: {
-                            if amountWarningCondition
-                            {
-                                Text("⚠️ Warning! The entered insulin amount is greater than your Max Bolus setting!")
-                            }
-                        }
-                        .listRowBackground(listBackgroundColor).tint(.white)
-                    }.scrollContentBackground(.hidden).background(color)
-                }
-                .onAppear(perform: configureView)
-                .navigationTitle("External Insulin")
-                .navigationBarTitleDisplayMode(.inline)
-                .toolbar {
-                    ToolbarItem(placement: .topBarLeading) {
-                        Button("Close") {
-                            showExternalInsulin = false
-                            state.externalInsulinAmount = 0
-                        }
-                    }
-                }
-            }
-        }
-
         @ViewBuilder private func glucoseView(_ item: Glucose, isManual: BloodGlucose) -> some View {
             HStack {
                 Text(item.glucose.glucose.map {

+ 0 - 7
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -33,13 +33,6 @@ extension Home {
             }
         }
 
-        func manualGlucose(hours: Int) -> [BloodGlucose] {
-            glucoseStorage.recent().filter {
-                $0.type == GlucoseType.manual.rawValue &&
-                    $0.dateString.addingTimeInterval(hours.hours.timeInterval) > Date()
-            }
-        }
-
         func pumpHistory(hours: Int) -> [PumpHistoryEvent] {
             pumpHistoryStorage.recent().filter {
                 $0.timestamp.addingTimeInterval(hours.hours.timeInterval) > Date()

+ 50 - 5
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -12,7 +12,7 @@ extension Home {
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         @Published var glucose: [BloodGlucose] = []
-        @Published var isManual: [BloodGlucose] = []
+        @Published var manualGlucose: [BloodGlucose] = []
         @Published var announcement: [Announcement] = []
         @Published var suggestion: Suggestion?
         @Published var uploadStats = false
@@ -72,6 +72,11 @@ extension Home {
 
         @Published var selectedTab: Int = 0
 
+        @Published var waitForSuggestion: Bool = false
+
+        @Published var carbsForChart: [CarbsEntry] = []
+        @Published var fpusForChart: [CarbsEntry] = []
+
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
@@ -87,6 +92,8 @@ extension Home {
             setupReservoir()
             setupAnnouncements()
             setupCurrentPumpTimezone()
+            filterCarbs()
+            filterFpus()
 
             suggestion = provider.suggestion
             uploadStats = settingsManager.settings.uploadStats
@@ -209,12 +216,37 @@ extension Home {
                 .store(in: &lifetime)
         }
 
+        func filterCarbs() {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                let allCarbs = self.provider.carbs(hours: self.filteredHours)
+                let filteredCarbs = allCarbs.filter { !($0.isFPU ?? false) }
+
+                self.carbsForChart.removeAll()
+                self.carbsForChart.append(contentsOf: filteredCarbs)
+            }
+        }
+
+        func filterFpus() {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                let allCarbs = self.provider.carbs(hours: self.filteredHours)
+                let filteredFpus = allCarbs.filter { $0.isFPU ?? false }
+
+                self.fpusForChart.removeAll()
+                self.fpusForChart.append(contentsOf: filteredFpus)
+            }
+        }
+
         func runLoop() {
             provider.heartbeatNow()
         }
 
         func cancelBolus() {
             apsManager.cancelBolus()
+
+            // perform determine basal sync, otherwise you have could end up with too much iob when opening the calculator again
+            apsManager.determineBasalSync()
         }
 
         func cancelProfile() {
@@ -229,14 +261,21 @@ extension Home {
         private func setupGlucose() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
-                self.isManual = self.provider.manualGlucose(hours: self.filteredHours)
-                self.glucose = self.provider.filteredGlucose(hours: self.filteredHours)
+                let filteredGlucose = self.provider.filteredGlucose(hours: self.filteredHours)
+
+                self.glucose = filteredGlucose
+                self.manualGlucose = filteredGlucose.filter { $0.type == GlucoseType.manual.rawValue }
+
                 self.recentGlucose = self.glucose.last
+
                 if self.glucose.count >= 2 {
-                    self.glucoseDelta = (self.recentGlucose?.glucose ?? 0) - (self.glucose[self.glucose.count - 2].glucose ?? 0)
+                    self
+                        .glucoseDelta = (self.recentGlucose?.glucose ?? 0) -
+                        (self.glucose[self.glucose.count - 2].glucose ?? 0)
                 } else {
                     self.glucoseDelta = nil
                 }
+
                 self.alarm = self.provider.glucoseStorage.alarm
             }
         }
@@ -387,7 +426,10 @@ extension Home {
         }
 
         private func setupCurrentPumpTimezone() {
-            timeZone = provider.pumpTimeZone()
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                self.timeZone = self.provider.pumpTimeZone()
+            }
         }
 
         func openCGM() {
@@ -438,6 +480,7 @@ extension Home.StateModel:
         self.suggestion = suggestion
         carbsRequired = suggestion.carbsReq
         setStatusTitle()
+        waitForSuggestion = false
     }
 
     func settingsDidChange(_ settings: FreeAPSSettings) {
@@ -480,6 +523,8 @@ extension Home.StateModel:
 
     func carbsDidUpdate(_: [CarbsEntry]) {
         setupCarbs()
+        filterFpus()
+        filterCarbs()
     }
 
     func enactedSuggestionDidUpdate(_ suggestion: Suggestion) {

Разница между файлами не показана из-за своего большого размера
+ 540 - 526
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift


+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -77,7 +77,7 @@ struct CurrentGlucoseView: View {
                                     .string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)! }
                             ?? "--"
                     )
-                    .font(.system(size: 40, weight: .bold))
+                    .font(.system(size: 40, weight: .bold, design: .rounded))
                     .foregroundColor(alarm == nil ? colourGlucoseText : .loopRed)
                 }
                 HStack {

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -43,7 +43,7 @@ struct LoopView: View {
             }
         }
         .strikethrough(!closedLoop || manualTempBasal, pattern: .solid, color: color)
-        .font(.system(size: 16, weight: .bold))
+        .font(.system(size: 16, weight: .bold, design: .rounded))
         .foregroundColor(color)
     }
 

+ 4 - 4
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -46,13 +46,13 @@ struct PumpView: View {
                         .font(.system(size: 16))
                         .foregroundColor(reservoirColor)
                     if reservoir == 0xDEAD_BEEF {
-                        Text("50+ " + NSLocalizedString("U", comment: "Insulin unit")).font(.system(size: 15))
+                        Text("50+ " + NSLocalizedString("U", comment: "Insulin unit")).font(.system(size: 15, design: .rounded))
                     } else {
                         Text(
                             reservoirFormatter
                                 .string(from: reservoir as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
                         )
-                        .font(.system(size: 16))
+                        .font(.system(size: 16, design: .rounded))
                     }
                 }
 
@@ -69,7 +69,7 @@ struct PumpView: View {
                     Image(systemName: "battery.100")
                         .font(.system(size: 16))
                         .foregroundColor(batteryColor)
-                    Text("\(Int(battery.percent ?? 100)) %").font(.system(size: 16))
+                    Text("\(Int(battery.percent ?? 100)) %").font(.system(size: 16, design: .rounded))
                 }
             }
 
@@ -79,7 +79,7 @@ struct PumpView: View {
                         .font(.system(size: 16))
                         .foregroundColor(timerColor)
 
-                    Text(remainingTimeString(time: date.timeIntervalSince(timerDate))).font(.system(size: 16))
+                    Text(remainingTimeString(time: date.timeIntervalSince(timerDate))).font(.system(size: 16, design: .rounded))
                 }
             }
         }

+ 64 - 91
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -358,6 +358,9 @@ extension Home {
 
                 MainChartView(
                     glucose: $state.glucose,
+                    manualGlucose: $state.manualGlucose,
+                    carbsForChart: $state.carbsForChart,
+                    fpusForChart: $state.fpusForChart,
                     units: $state.units,
                     eventualBG: $state.eventualBG,
                     suggestion: $state.suggestion,
@@ -370,7 +373,6 @@ extension Home {
                     autotunedBasalProfile: $state.autotunedBasalProfile,
                     basalProfile: $state.basalProfile,
                     tempTargets: $state.tempTargets,
-                    carbs: $state.carbs,
                     smooth: $state.smooth,
                     highGlucose: $state.highGlucose,
                     lowGlucose: $state.lowGlucose,
@@ -463,7 +465,7 @@ extension Home {
                         (numberFormatter.string(from: (state.suggestion?.iob ?? 0) as NSNumber) ?? "0") +
                             NSLocalizedString(" U", comment: "Insulin unit")
                     )
-                    .font(.system(size: 16, weight: .bold))
+                    .font(.system(size: 16, weight: .bold, design: .rounded))
                 }
 
                 Spacer()
@@ -476,7 +478,7 @@ extension Home {
                         (numberFormatter.string(from: (state.suggestion?.cob ?? 0) as NSNumber) ?? "0") +
                             NSLocalizedString(" g", comment: "gram of carbs")
                     )
-                    .font(.system(size: 16, weight: .bold))
+                    .font(.system(size: 16, weight: .bold, design: .rounded))
                 }
 
                 Spacer()
@@ -484,13 +486,13 @@ extension Home {
                 HStack {
                     if state.pumpSuspended {
                         Text("Pump suspended")
-                            .font(.system(size: 12, weight: .bold)).foregroundColor(.loopGray)
+                            .font(.system(size: 12, weight: .bold, design: .rounded)).foregroundColor(.loopGray)
                     } else if let tempBasalString = tempBasalString {
                         Image(systemName: "drop.circle")
                             .font(.system(size: 16))
                             .foregroundColor(.insulinTintColor)
                         Text(tempBasalString)
-                            .font(.system(size: 16, weight: .bold))
+                            .font(.system(size: 16, weight: .bold, design: .rounded))
                     }
                 }
                 if !state.tins {
@@ -499,7 +501,7 @@ extension Home {
                         "TDD: " + (numberFormatter.string(from: (state.suggestion?.tdd ?? 0) as NSNumber) ?? "0") +
                             NSLocalizedString(" U", comment: "Insulin unit")
                     )
-                    .font(.system(size: 16, weight: .bold))
+                    .font(.system(size: 16, weight: .bold, design: .rounded))
                 } else {
                     Spacer()
                     HStack {
@@ -507,7 +509,7 @@ extension Home {
                             "TINS: \(state.roundedTotalBolus)" +
                                 NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
                         )
-                        .font(.system(size: 16, weight: .bold))
+                        .font(.system(size: 16, weight: .bold, design: .rounded))
                         .onChange(of: state.hours) { _ in
                             state.roundedTotalBolus = state.calculateTINS()
                         }
@@ -690,6 +692,7 @@ extension Home {
                     Spacer()
 
                     Button {
+                        state.waitForSuggestion = true
                         state.cancelBolus()
                     } label: {
                         Image(systemName: "xmark.app")
@@ -707,8 +710,8 @@ extension Home {
         @ViewBuilder func mainView() -> some View {
             GeometryReader { geo in
                 VStack(spacing: 0) {
-                    Spacer()
-                        .frame(height: UIScreen.main.bounds.height / 16)
+//                    Spacer()
+//                        .frame(height: UIScreen.main.bounds.height / 40)
 
                     ZStack {
                         /// glucose bobble
@@ -729,29 +732,17 @@ extension Home {
 
                     mealPanel(geo).padding(.top, 30).padding(.bottom, 20)
 
-                    RoundedRectangle(cornerRadius: 15)
-                        .fill(Color("Chart"))
-                        .overlay(mainChart)
-                        .clipShape(RoundedRectangle(cornerRadius: 15))
-                        .shadow(
-                            color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
-                                Color.black.opacity(0.33),
-                            radius: 3
-                        )
-                        .padding(.horizontal, 10)
-                        .frame(maxHeight: UIScreen.main.bounds.height / 2.2)
+                    mainChart
 
-                    timeInterval.padding(.top, 15).padding(.bottom, 30)
+                    timeInterval.padding(.top, 20).padding(.bottom, 40)
 
                     if let progress = state.bolusProgress {
-                        bolusView(geo, progress)
+                        bolusView(geo, progress).padding(.bottom, 10)
                     } else {
-                        profileView(geo)
+                        profileView(geo).padding(.bottom, 10)
                     }
                 }
-
                 .background(color)
-                .edgesIgnoringSafeArea(.all)
             }
             .onChange(of: state.hours) { _ in
                 highlightButtons()
@@ -787,80 +778,62 @@ extension Home {
             }
         }
 
-        @ViewBuilder func tabBarButton(index: Int, systemName: String, label: String) -> some View {
-            Button(action: {
-                selectedTab = index
-            }) {
-                ZStack(alignment: .bottom, content: {
-                    VStack {
-                        Image(systemName: systemName)
-                            .font(.system(size: 22))
-                            .foregroundStyle(selectedTab == index ? Color(.label) : Color.gray)
-                        Text(label)
-                            .font(.caption2)
-                            .foregroundStyle(selectedTab == index ? Color(.label) : Color.gray)
-                            .padding(.top, 1)
-                    }
-                    if selectedTab == index {
-                        Capsule()
-                            .frame(width: 25, height: 5)
-                            .foregroundStyle(Color(.label))
-                            .offset(y: 10)
-                    }
-                })
-            }
-        }
+        @ViewBuilder func tabBar() -> some View {
+            ZStack(alignment: .bottom) {
+                TabView {
+                    let carbsRequiredBadge: String? = {
+                        guard let carbsRequired = state.carbsRequired else { return nil }
+                        return carbsRequired > 0 ? "\(numberFormatter.string(from: carbsRequired as NSNumber) ?? "") " +
+                            NSLocalizedString("g", comment: "Short representation of grams") : nil
+                    }()
+
+                    NavigationStack { mainView() }
+                        .tabItem { Label("Main", systemImage: "chart.xyaxis.line") }
+                        .badge(carbsRequiredBadge)
+
+                    NavigationStack { DataTable.RootView(resolver: resolver) }
+                        .tabItem { Label("History", systemImage: historySFSymbol) }
 
-        @ViewBuilder func customTabBar() -> some View {
-            VStack {
-                ZStack {
-                    switch selectedTab {
-                    case 0:
-                        mainView()
-                    case 1:
-                        NavigationStack { DataTable.RootView(resolver: resolver) }
-                    case 2:
-                        NavigationStack { OverrideProfilesConfig.RootView(resolver: resolver) }
-                    case 3:
-                        NavigationStack { Settings.RootView(resolver: resolver) }
-                    default:
-                        mainView()
-                    }
-                }
-                HStack {
-                    tabBarButton(
-                        index: 0,
-                        systemName: selectedTab == 0 ? "house.fill" : "house",
-                        label: "Home"
-                    )
-                    Spacer()
-                    tabBarButton(index: 1, systemName: historySFSymbol, label: "History")
                     Spacer()
-                    Button(action: {
-                        state.showModal(for: .bolus(waitForSuggestion: false, fetch: false))
-                    }) {
+
+                    NavigationStack { OverrideProfilesConfig.RootView(resolver: resolver) }
+                        .tabItem {
+                            Label(
+                                "Profile",
+                                systemImage: "person.fill"
+                            ) }
+
+                    NavigationStack { Settings.RootView(resolver: resolver) }
+                        .tabItem {
+                            Label(
+                                "Menu",
+                                systemImage: "text.justify"
+                            ) }
+                }
+                .tint(Color.tabBar)
+
+                Button(
+                    action: {
+                        state.showModal(for: .bolus(waitForSuggestion: false, fetch: false)) },
+                    label: {
                         Image(systemName: "plus.circle.fill")
-                            .font(.system(size: 45))
+                            .font(.system(size: 40))
                             .foregroundStyle(Color.tabBar)
-                            .padding(.bottom, 2)
+                            .padding(.bottom, 1)
+                            .padding(.horizontal, 20)
                     }
-                    Spacer()
-                    tabBarButton(
-                        index: 2,
-                        systemName: selectedTab == 2 ? "person.fill" : "person",
-                        label: "Profile"
-                    )
-                    Spacer()
-                    tabBarButton(index: 3, systemName: "text.justify", label: "Menu")
-                }
-                .padding(.horizontal, 20)
-            }
-            .ignoresSafeArea(.keyboard, edges: .bottom)
-            .blur(radius: isMenuPresented ? 5 : 0)
+                )
+            }.ignoresSafeArea(.keyboard, edges: .bottom).blur(radius: state.waitForSuggestion ? 8 : 0)
         }
 
         var body: some View {
-            customTabBar()
+            ZStack(alignment: .center) {
+                tabBar()
+
+                if state.waitForSuggestion {
+                    CustomProgressView(text: "Updating IOB...")
+                }
+            }
         }
 
         private var popup: some View {

+ 1 - 1
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -127,7 +127,7 @@ extension OverrideProfilesConfig {
                         .foregroundColor(
                             state
                                 .percentageProfiles >= 130 ? .red :
-                                (isEditing ? .orange : Color.blue)
+                                (isEditing ? .orange : Color.tabBar)
                         )
                         .font(.largeTitle)
                     Slider(

+ 1 - 1
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -176,7 +176,7 @@ extension Settings {
                     ShareSheet(activityItems: state.logItems())
                 }
                 .onAppear(perform: configureView)
-                .navigationTitle("Menü")
+                .navigationTitle("Menu")
                 .navigationBarTitleDisplayMode(.large)
                 .onDisappear(perform: { state.uploadProfileAndSettings(false) })
         }

+ 90 - 99
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -111,8 +111,6 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
         injectServices(resolver)
         guard isAvailableOnCurrentDevice,
               Config.healthBGObject != nil else { return }
-        createBGObserver()
-        enableBackgroundDelivery()
 
         broadcaster.register(CarbsObserver.self, observer: self)
         broadcaster.register(PumpHistoryObserver.self, observer: self)
@@ -184,10 +182,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             }
         }
 
-        loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id))
-            .receive(on: processQueue)
-            .sink(receiveValue: save)
-            .store(in: &lifetime)
+        loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id), completion: { samples in
+            save(samples: samples)
+        })
     }
 
     func saveIfNeeded(carbs: [CarbsEntry]) {
@@ -237,10 +234,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             }
         }
 
-        loadSamplesFromHealth(sampleType: sampleType)
-            .receive(on: processQueue)
-            .sink(receiveValue: save)
-            .store(in: &lifetime)
+        loadSamplesFromHealth(sampleType: sampleType, completion: { samples in
+            save(samples: samples)
+        })
     }
 
     func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
@@ -261,7 +257,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
                 )
                 self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
                     guard let error = error else { return }
-                    warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
+                    warning(.service, "Cannot delete sample with syncID: \(syncID.id)", error: error)
                 }
             }
             let bolusTotal = bolus + bolusToModify
@@ -314,69 +310,66 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             }
         }
 
-        loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
-            .receive(on: processQueue)
-            .compactMap { samples -> ([InsulinBolus], [InsulinBolus], [InsulinBasal]) in
-                let sampleIDs = samples.compactMap(\.syncIdentifier)
-                let bolusToModify = events
-                    .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
-                    .compactMap { event -> InsulinBolus? in
-                        guard let amount = event.amount else { return nil }
-                        guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
-                        else { return nil }
-                        if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
-                            return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
-                        } else { return nil }
-                    }
+        loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id), completion: { samples in
+            let sampleIDs = samples.compactMap(\.syncIdentifier)
+            let bolusToModify = events
+                .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
+                .compactMap { event -> InsulinBolus? in
+                    guard let amount = event.amount else { return nil }
+                    guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
+                    else { return nil }
+                    if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
+                        return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
+                    } else { return nil }
+                }
 
-                let bolus = events
-                    .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
-                    .compactMap { event -> InsulinBolus? in
-                        guard let amount = event.amount else { return nil }
-                        return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
+            let bolus = events
+                .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
+                .compactMap { event -> InsulinBolus? in
+                    guard let amount = event.amount else { return nil }
+                    return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
+                }
+            let basalEvents = events
+                .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
+                .sorted(by: { $0.timestamp < $1.timestamp })
+            let basal = basalEvents.enumerated()
+                .compactMap { item -> InsulinBasal? in
+                    let nextElementEventIndex = item.offset + 1
+                    guard basalEvents.count > nextElementEventIndex else { return nil }
+
+                    var minimalDose = self.settingsManager.preferences.bolusIncrement
+                    if (minimalDose != 0.05) || (minimalDose != 0.025) {
+                        minimalDose = Decimal(0.05)
                     }
-                let basalEvents = events
-                    .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
-                    .sorted(by: { $0.timestamp < $1.timestamp })
-                let basal = basalEvents.enumerated()
-                    .compactMap { item -> InsulinBasal? in
-                        let nextElementEventIndex = item.offset + 1
-                        guard basalEvents.count > nextElementEventIndex else { return nil }
-
-                        var minimalDose = self.settingsManager.preferences.bolusIncrement
-                        if (minimalDose != 0.05) || (minimalDose != 0.025) {
-                            minimalDose = Decimal(0.05)
-                        }
-
-                        let nextBasalEvent = basalEvents[nextElementEventIndex]
-                        let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
-                        let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
-                        let incrementsRaw = amount / minimalDose
-
-                        var amountRounded: Decimal
-                        if incrementsRaw >= 1 {
-                            let incrementsRounded = floor(Double(incrementsRaw))
-                            amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
-                        } else {
-                            amountRounded = 0
-                        }
-
-                        let id = String(item.element.id.dropFirst())
-                        guard amountRounded > 0,
-                              id != ""
-                        else { return nil }
-
-                        return InsulinBasal(
-                            id: id,
-                            amount: amountRounded,
-                            startDelivery: item.element.timestamp,
-                            endDelivery: nextBasalEvent.timestamp
-                        )
+
+                    let nextBasalEvent = basalEvents[nextElementEventIndex]
+                    let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
+                    let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
+                    let incrementsRaw = amount / minimalDose
+
+                    var amountRounded: Decimal
+                    if incrementsRaw >= 1 {
+                        let incrementsRounded = floor(Double(incrementsRaw))
+                        amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
+                    } else {
+                        amountRounded = 0
                     }
-                return (bolusToModify, bolus, basal)
-            }
-            .sink(receiveValue: save)
-            .store(in: &lifetime)
+
+                    let id = String(item.element.id.dropFirst())
+                    guard amountRounded > 0,
+                          id != ""
+                    else { return nil }
+
+                    return InsulinBasal(
+                        id: id,
+                        amount: amountRounded,
+                        startDelivery: item.element.timestamp,
+                        endDelivery: nextBasalEvent.timestamp
+                    )
+                }
+
+            save(bolusToModify: bolusToModify, bolus: bolus, basal: basal)
+        })
     }
 
     func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent]) {
@@ -433,43 +426,41 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
     /// Try to load samples from Health store
     private func loadSamplesFromHealth(
         sampleType: HKQuantityType,
-        limit: Int = 100
-    ) -> Future<[HKSample], Never> {
-        Future { promise in
-            let query = HKSampleQuery(
-                sampleType: sampleType,
-                predicate: nil,
-                limit: limit,
-                sortDescriptors: nil
-            ) { _, results, _ in
-                promise(.success((results as? [HKQuantitySample]) ?? []))
-            }
-            self.healthKitStore.execute(query)
+        limit: Int = 100,
+        completion: @escaping (_ samples: [HKSample]) -> Void
+    ) {
+        let query = HKSampleQuery(
+            sampleType: sampleType,
+            predicate: nil,
+            limit: limit,
+            sortDescriptors: nil
+        ) { _, results, _ in
+            completion(results as? [HKQuantitySample] ?? [])
         }
+        healthKitStore.execute(query)
     }
 
     /// Try to load samples from Health store with id and do some work
     private func loadSamplesFromHealth(
         sampleType: HKQuantityType,
         withIDs ids: [String],
-        limit: Int = 100
-    ) -> Future<[HKSample], Never> {
-        Future { promise in
-            let predicate = HKQuery.predicateForObjects(
-                withMetadataKey: HKMetadataKeySyncIdentifier,
-                allowedValues: ids
-            )
+        limit: Int = 100,
+        completion: @escaping (_ samples: [HKSample]) -> Void
+    ) {
+        let predicate = HKQuery.predicateForObjects(
+            withMetadataKey: HKMetadataKeySyncIdentifier,
+            allowedValues: ids
+        )
 
-            let query = HKSampleQuery(
-                sampleType: sampleType,
-                predicate: predicate,
-                limit: limit,
-                sortDescriptors: nil
-            ) { _, results, _ in
-                promise(.success((results as? [HKQuantitySample]) ?? []))
-            }
-            self.healthKitStore.execute(query)
+        let query = HKSampleQuery(
+            sampleType: sampleType,
+            predicate: predicate,
+            limit: limit,
+            sortDescriptors: nil
+        ) { _, results, _ in
+            completion(results as? [HKQuantitySample] ?? [])
         }
+        healthKitStore.execute(query)
     }
 
     private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {

+ 3 - 3
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -72,9 +72,9 @@ extension LiveActivityAttributes.ContentState {
 
         let chartDate = chart.map(\.date)
 
-        /// glucose limits from settings
-        let highGlucose = settings.highGlucose / Decimal(conversionFactor)
-        let lowGlucose = settings.lowGlucose / Decimal(conversionFactor)
+        /// glucose limits from UI settings, not from notifications settings
+        let highGlucose = settings.high / Decimal(conversionFactor)
+        let lowGlucose = settings.low / Decimal(conversionFactor)
 
         let cob = suggestion.cob ?? 0
         let iob = suggestion.iob ?? 0

+ 8 - 1
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -8,6 +8,7 @@ struct DecimalTextField: UIViewRepresentable {
     private var autofocus: Bool
     private var cleanInput: Bool
     private var useButtons: Bool
+    private var textColor: UIColor?
 
     init(
         _ placeholder: String,
@@ -15,7 +16,8 @@ struct DecimalTextField: UIViewRepresentable {
         formatter: NumberFormatter,
         autofocus: Bool = false,
         cleanInput: Bool = false,
-        useButtons: Bool = true
+        useButtons: Bool = true,
+        textColor: UIColor? = nil
     ) {
         self.placeholder = placeholder
         _value = value
@@ -23,6 +25,7 @@ struct DecimalTextField: UIViewRepresentable {
         self.autofocus = autofocus
         self.cleanInput = cleanInput
         self.useButtons = useButtons
+        self.textColor = textColor
     }
 
     func makeUIView(context: Context) -> UITextField {
@@ -33,6 +36,10 @@ struct DecimalTextField: UIViewRepresentable {
         textfield.text = cleanInput ? "" : formatter.string(for: value) ?? placeholder
         textfield.textAlignment = .right
 
+        if let textColor = textColor {
+            textfield.textColor = textColor
+        }
+
         lazy var toolBar: UIToolbar = {
             let tool: UIToolbar = .init(frame: .init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 35))
             tool.barStyle = .default