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

Merge branch 'tabbar-dev' into tabbar-dev

Andreas Stokholm 2 лет назад
Родитель
Сommit
7f82d7562a
30 измененных файлов с 719 добавлено и 631 удалено
  1. 1 1
      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. 10 10
      FreeAPS.xcodeproj/project.pbxproj
  22. 8 0
      FreeAPS/Sources/Helpers/CustomProgressView.swift
  23. 39 4
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  24. 53 86
      FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift
  25. 19 5
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  26. 49 32
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  27. 44 15
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  28. 69 168
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  29. 13 7
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  30. 8 1
      FreeAPS/Sources/Views/DecimalTextField.swift

+ 1 - 1
Config.xcconfig

@@ -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 {

+ 10 - 10
FreeAPS.xcodeproj/project.pbxproj

@@ -3298,7 +3298,7 @@
 				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)",
@@ -3340,7 +3340,7 @@
 				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)",
@@ -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;

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

@@ -42,3 +42,11 @@ struct CustomProgressView: View {
         }
     }
 }
+
+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 ..."
+}

+ 39 - 4
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -238,7 +238,30 @@ 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
+            }
+        }
+
+        @MainActor func add() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
@@ -254,7 +277,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()
+                    }
+                }
             }
         }
 
@@ -474,7 +503,7 @@ extension Bolus {
 
         // MARK: EXTERNAL INSULIN
 
-        func addExternalInsulin() async {
+        @MainActor func addExternalInsulin() async {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
@@ -490,7 +519,13 @@ extension Bolus {
                     print("authentication failed")
                 }
             } catch {
-                print("authentication error: \(error.localizedDescription)")
+                print("authentication error for external insulin: \(error.localizedDescription)")
+                DispatchQueue.main.async {
+                    self.waitForSuggestion = false
+                    if self.addButtonPressed {
+                        self.hideModal()
+                    }
+                }
             }
         }
 

+ 53 - 86
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift

@@ -1,5 +1,6 @@
 import Charts
 import CoreData
+import LoopKitUI
 import SwiftUI
 import Swinject
 
@@ -11,7 +12,6 @@ extension Bolus {
 
         @State private var showInfo = 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
@@ -423,17 +423,11 @@ extension Bolus {
                                     value: $state.amount,
                                     formatter: formatter,
                                     autofocus: false,
-                                    cleanInput: true
+                                    cleanInput: true,
+                                    textColor: .systemBlue
                                 )
                                 Text(" U").foregroundColor(.secondary)
                             }
-                            .onChange(of: state.amount) { newValue in
-                                if newValue > state.maxBolus {
-                                    exceededMaxBolus = true
-                                } else {
-                                    exceededMaxBolus = false
-                                }
-                            }
                         }.listRowBackground(Color.chart)
 
                         if state.amount > 0 {
@@ -451,7 +445,7 @@ extension Bolus {
                 }.blur(radius: state.waitForSuggestion ? 5 : 0)
 
                 if state.waitForSuggestion {
-                    CustomProgressView(text: progressText)
+                    CustomProgressView(text: progressText.rawValue)
                 }
             }
             .scrollContentBackground(.hidden).background(color)
@@ -484,70 +478,50 @@ extension Bolus {
             }
         }
 
-        var progressText: String {
+        var progressText: ProgressText {
             switch (state.amount > 0, state.carbs > 0) {
             case (true, true):
-                return "Updating COB and IOB..."
+                return .updatingIOBandCOB
             case (false, true):
-                return "Updating COB..."
+                return .updatingCOB
             case (true, false):
-                return "Updating IOB..."
+                return .updatingIOB
             default:
-                return "Updating Treatments..."
+                return .updatingTreatments
             }
         }
 
         var stickyButton: some View {
-            Section {
-                Button {
-                    if state.amount > 0 {
-                        if !state.externalInsulin {
-                            Task {
-                                await state.add()
-                                state.waitForSuggestion = true
-                            }
-                        } else {
-                            Task {
-                                do {
-                                    await state.addExternalInsulin()
-                                    state.waitForSuggestion = true
-                                }
-                            }
-                        }
-                        state.addCarbs()
-                        state.addButtonPressed = true
-                    } else {
-                        // show loading bar only when carbs are actually added
-                        if state.carbs > 0 {
-                            state.addCarbs()
-                            state.waitForSuggestion = true
-                        } else {
-                            // hide modal because its otherwise only hidden after a suggestion update, see StateModal
-                            state.hideModal()
-                        }
-                        state.addButtonPressed = true
-                    }
-                } label: {
-                    if state.amount > 0 {
-                        Text(
-                            !state
-                                .externalInsulin ? (exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") :
-                                (exceededMaxBolus ? "Max Bolus exceeded!" : "Log external insulin")
-                        ).font(.system(size: 17, design: .rounded))
-                    } else {
-                        Text("Continue without bolus").font(.system(size: 17, design: .rounded))
+            ZStack {
+                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)
+
+                Section {
+                    Button {
+                        state.invokeTreatmentsTask()
+                    } label: {
+                        taskButtonLabel
                     }
-                }
-                .frame(maxWidth: .infinity, alignment: .center)
-                .frame(minHeight: 50)
-                .disabled(state.amount > 0 ? (state.externalInsulin ? limitManualBolus : limitPumpBolus) : false)
-                .background(state.amount > 0 ? logExternalInsulinBackground : Color(.systemBlue))
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .foregroundStyle(state.amount > 0 ? logExternalInsulinForeground : .white)
-                .padding()
+                    .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))
+                    .foregroundStyle(Color.white)
+                    .padding()
+                }.offset(y: 20)
+                    .listRowBackground(Color.chart)
             }
-            .listRowBackground(Color.chart)
         }
 
         var calcSettingsFirstRow: some View {
@@ -983,7 +957,7 @@ extension Bolus {
                         .padding(.top)
                 }
                 .padding([.horizontal, .bottom])
-                .font(.system(size: 15))
+                .font(.subheadline)
             }
         }
 
@@ -992,34 +966,27 @@ extension Bolus {
             return Decimal(floor(100 * toRound) / 100)
         }
 
-        private var limitPumpBolus: Bool {
-            state.amount <= 0 || state.amount > state.maxBolus
+        private var taskButtonLabel: some View {
+            if state.amount > 0 {
+                Text(
+                    !state.externalInsulin ? (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Enact bolus") :
+                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log external insulin")
+                ).font(.headline)
+            } else {
+                Text("Continue without bolus").font(.headline)
+            }
         }
 
-        // MARK: DEFINITIONS FOR ADDING EXTERNAL INSULIN
-
-        private var limitManualBolus: Bool {
-            state.amount <= 0 || state.amount > state.maxBolus * 3
+        private var pumpBolusLimit: Bool {
+            state.amount > state.maxBolus
         }
 
-        private var logExternalInsulinBackground: Color {
-            if state.amount > state.maxBolus {
-                return Color.red
-            } else if state.amount <= 0 || state.amount > state.maxBolus * 3 {
-                return Color(.systemGray4)
-            } else {
-                return Color(.systemBlue)
-            }
+        private var externalBolusLimit: Bool {
+            state.amount > state.maxBolus * 3
         }
 
-        private var logExternalInsulinForeground: Color {
-            if state.amount > state.maxBolus {
-                return Color.white
-            } else if state.amount <= 0 || state.amount > state.maxBolus * 3 {
-                return Color.secondary
-            } else {
-                return Color.white
-            }
+        private var disableTaskButton: Bool {
+            state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false
         }
     }
 

+ 19 - 5
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -19,8 +19,9 @@ extension DataTable {
         @Published var manualGlucose: Decimal = 0
         @Published var maxBolus: Decimal = 0
         @Published var waitForSuggestion: Bool = false
-        @Published var showExternalInsulin: Bool = false
-        @Published var addButtonPressed: Bool = false
+
+        @Published var insulinEntryDeleted: Bool = false
+        @Published var carbEntryDeleted: Bool = false
 
         var units: GlucoseUnits = .mmolL
         var historyLayout: HistoryLayout = .twoTabs
@@ -159,11 +160,27 @@ 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 {
             do {
                 let authenticated = try await unlockmanager.unlock()
@@ -263,9 +280,6 @@ extension DataTable.StateModel: SuggestionObserver {
     func suggestionDidUpdate(_: Suggestion) {
         DispatchQueue.main.async {
             self.waitForSuggestion = false
-            if self.addButtonPressed {
-                self.showExternalInsulin = false
-            }
         }
     }
 }

+ 49 - 32
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -74,34 +74,45 @@ 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 {
@@ -134,6 +145,17 @@ extension DataTable {
             )
         }
 
+        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 {
@@ -315,13 +337,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: {
@@ -377,8 +395,7 @@ extension DataTable {
                         debug(.default, "Cannot gracefully unwrap alertTreatmentToDelete!")
                         return
                     }
-
-                    state.deleteCarbs(treatmentToDelete)
+                    state.invokeCarbDeletionTask(treatmentToDelete)
                 }
             } message: {
                 Text("\n" + NSLocalizedString(alertMessage, comment: ""))

+ 44 - 15
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -74,6 +74,9 @@ extension Home {
 
         @Published var waitForSuggestion: Bool = false
 
+        @Published var carbsForChart: [CarbsEntry] = []
+        @Published var fpusForChart: [CarbsEntry] = []
+
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
@@ -89,6 +92,8 @@ extension Home {
             setupReservoir()
             setupAnnouncements()
             setupCurrentPumpTimezone()
+            filterCarbs()
+            filterFpus()
 
             suggestion = provider.suggestion
             uploadStats = settingsManager.settings.uploadStats
@@ -211,6 +216,28 @@ 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()
         }
@@ -232,25 +259,25 @@ extension Home {
         }
 
         private func setupGlucose() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                let filteredGlucose = self.provider.filteredGlucose(hours: self.filteredHours)
+                  DispatchQueue.main.async { [weak self] in
+                      guard let self = self else { return }
+                      let filteredGlucose = self.provider.filteredGlucose(hours: self.filteredHours)
 
-                self.glucose += filteredGlucose
-                self.manualGlucose += filteredGlucose.filter { $0.type == GlucoseType.manual.rawValue }
+                      self.glucose = filteredGlucose
+                      self.manualGlucose = filteredGlucose.filter { $0.type == GlucoseType.manual.rawValue }
 
-                self.recentGlucose = self.glucose.last
+                      self.recentGlucose = self.glucose.last
 
-                if self.glucose.count >= 2 {
-                    self.glucoseDelta = (self.recentGlucose?.glucose ?? 0) - (self.glucose[self.glucose.count - 2].glucose ?? 0)
-                } else {
-                    self.glucoseDelta = nil
-                }
-
-                self.alarm = self.provider.glucoseStorage.alarm
-            }
-        }
+                      if self.glucose.count >= 2 {
+                          self.glucoseDelta = (self.recentGlucose?.glucose ?? 0) - (self.glucose[self.glucose.count - 2].glucose ?? 0)
+                      } else {
+                          self.glucoseDelta = nil
+                      }
 
+                      self.alarm = self.provider.glucoseStorage.alarm
+                  }
+              }
+        
         private func setupBasals() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -491,6 +518,8 @@ extension Home.StateModel:
 
     func carbsDidUpdate(_: [CarbsEntry]) {
         setupCarbs()
+        filterFpus()
+        filterCarbs()
     }
 
     func enactedSuggestionDidUpdate(_ suggestion: Suggestion) {

+ 69 - 168
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -23,18 +23,6 @@ private struct Prediction: Hashable {
     let type: PredictionType
 }
 
-private struct Carb: Hashable {
-    let amount: Decimal
-    let timestamp: Date
-}
-
-private struct ChartBolus: Hashable {
-    let amount: Decimal
-    let timestamp: Date
-    let nearestGlucose: BloodGlucose
-    let yPosition: Decimal
-}
-
 private struct ChartTempTarget: Hashable {
     let amount: Decimal
     let start: Date
@@ -61,6 +49,8 @@ struct MainChartView: View {
 
     @Binding var glucose: [BloodGlucose]
     @Binding var manualGlucose: [BloodGlucose]
+    @Binding var carbsForChart: [CarbsEntry]
+    @Binding var fpusForChart: [CarbsEntry]
     @Binding var units: GlucoseUnits
     @Binding var eventualBG: Int?
     @Binding var suggestion: Suggestion?
@@ -73,7 +63,7 @@ struct MainChartView: View {
     @Binding var autotunedBasalProfile: [BasalProfileEntry]
     @Binding var basalProfile: [BasalProfileEntry]
     @Binding var tempTargets: [TempTarget]
-    @Binding var carbs: [CarbsEntry]
+//    @Binding var carbs: [CarbsEntry]
     @Binding var smooth: Bool
     @Binding var highGlucose: Decimal
     @Binding var lowGlucose: Decimal
@@ -90,9 +80,6 @@ struct MainChartView: View {
     @State private var TempBasals: [PumpHistoryEvent] = []
     @State private var ChartTempTargets: [ChartTempTarget] = []
     @State private var Predictions: [Prediction] = []
-    @State private var ChartCarbs: [Carb] = []
-    @State private var ChartFpus: [Carb] = []
-    @State private var ChartBoluses: [ChartBolus] = []
     @State private var count: Decimal = 1
     @State private var startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
     @State private var endMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 + 10800))
@@ -189,7 +176,7 @@ extension MainChartView {
                         Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
                         unit: .second
                     )
-                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color.insulin)
+                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
                 RuleMark(
                     x: .value(
                         "",
@@ -205,47 +192,51 @@ extension MainChartView {
                     )
                 ).foregroundStyle(Color.clear)
                 /// carbs
-                ForEach(ChartCarbs, id: \.self) { carb in
-                    let carbAmount = carb.amount
+                ForEach(carbsForChart) { carb in
+                    let carbAmount = carb.carbs
                     let yPosition = units == .mgdL ? 60 : 3.33
 
                     PointMark(
-                        x: .value("Time", carb.timestamp, unit: .second),
+                        x: .value("Time", carb.actualDate ?? Date(), unit: .second),
                         y: .value("Value", yPosition)
                     )
                     .symbolSize((Config.carbsSize + CGFloat(carbAmount) * Config.carbsScale) * 10)
                     .foregroundStyle(Color.orange)
                     .annotation(position: .bottom) {
-                        Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2).foregroundStyle(Color.orange)
+                        Text(carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
+                            .foregroundStyle(Color.orange)
                     }
                 }
                 /// fpus
-                ForEach(ChartFpus, id: \.self) { fpu in
-                    let fpuAmount = fpu.amount
+                ForEach(fpusForChart) { fpu in
+                    let fpuAmount = fpu.carbs
                     let size = (Config.fpuSize + CGFloat(fpuAmount) * Config.carbsScale) * 1.8
                     let yPosition = units == .mgdL ? 60 : 3.33
 
                     PointMark(
-                        x: .value("Time", fpu.timestamp, unit: .second),
+                        x: .value("Time", fpu.actualDate ?? Date(), unit: .second),
                         y: .value("Value", yPosition)
                     )
                     .symbolSize(size)
                     .foregroundStyle(Color.brown)
                 }
                 /// smbs in triangle form
-                ForEach(ChartBoluses, id: \.self) { bolus in
-                    let bolusAmount = bolus.amount
+                ForEach(boluses) { bolus in
+                    let bolusAmount = bolus.amount ?? 0
+                    let glucose = timeToNearestGlucose(time: bolus.timestamp.timeIntervalSince1970)
+                    let yPosition = (Decimal(glucose.sgv ?? defaultBolusPosition) * conversionFactor) + bolusOffset
                     let size = (Config.bolusSize + CGFloat(bolusAmount) * Config.bolusScale) * 1.8
 
                     PointMark(
                         x: .value("Time", bolus.timestamp, unit: .second),
-                        y: .value("Value", bolus.yPosition)
+                        y: .value("Value", yPosition)
                     )
                     .symbol {
                         Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.insulin)
                     }
                     .annotation(position: .top) {
-                        Text(bolusFormatter.string(from: bolusAmount as NSNumber)!).font(.caption2).foregroundStyle(Color.insulin)
+                        Text(bolusFormatter.string(from: bolusAmount as NSNumber)!).font(.caption2)
+                            .foregroundStyle(Color.insulin)
                     }
                 }
                 /// temp targets
@@ -351,14 +342,8 @@ extension MainChartView {
             }.id("MainChart")
                 .onChange(of: glucose) { _ in
                     calculatePredictions()
-                    calculateFpus()
-                }
-                .onChange(of: carbs) { _ in
-                    calculateCarbs()
-                    calculateFpus()
                 }
                 .onChange(of: boluses) { _ in
-                    calculateBoluses()
                     state.roundedTotalBolus = state.calculateTINS()
                 }
                 .onChange(of: tempTargets) { _ in
@@ -388,6 +373,7 @@ extension MainChartView {
                             AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
                         }
                         AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                            .font(.footnote)
                     }
                 }
                 .chartYAxis {
@@ -405,7 +391,7 @@ extension MainChartView {
                             if units == .mmolL {
                                 AxisTick(length: 7, stroke: .init(lineWidth: 7)).foregroundStyle(Color.clear)
                             }
-                            AxisValueLabel()
+                            AxisValueLabel().font(.footnote)
                         }
                     }
                 }
@@ -421,7 +407,7 @@ extension MainChartView {
                         Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
                         unit: .second
                     )
-                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color.insulin)
+                ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
                 RuleMark(
                     x: .value(
                         "",
@@ -552,51 +538,27 @@ extension MainChartView {
             }
             .chartYAxis {
                 AxisMarks(position: .trailing) { _ in
-                    AxisTick(length: 25, stroke: .init(lineWidth: 4))
-                        .foregroundStyle(Color.clear)
+                    AxisTick(length: units == .mmolL ? 25 : 27, stroke: .init(lineWidth: 4))
+                        .foregroundStyle(Color.clear).font(.footnote)
                 }
             }
         }
     }
 
     var legendPanel: some View {
-        ZStack {
-            HStack(alignment: .center) {
-                Spacer()
-
-                Group {
-                    Circle().fill(Color.loopGreen).frame(width: 8, height: 8)
-                    Text("BG")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.loopGreen)
-                }
-                Group {
-                    Circle().fill(Color.insulin).frame(width: 8, height: 8)
-                        .padding(.leading, 8)
-                    Text("IOB")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.insulin)
-                }
-                Group {
-                    Circle().fill(Color.zt).frame(width: 8, height: 8)
-                        .padding(.leading, 8)
-                    Text("ZT")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.zt)
-                }
-                Group {
-                    Circle().fill(Color.orange).frame(width: 8, height: 8).padding(.leading, 8)
-                    Text("COB")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(Color.orange)
-                }
-                Group {
-                    Circle().fill(Color.uam).frame(width: 8, height: 8)
-                        .padding(.leading, 8)
-                    Text("UAM")
-                        .font(.system(size: 10, weight: .bold)).foregroundColor(.uam)
-                }
-                Spacer()
-            }
-            .padding(.horizontal, 10)
-            .frame(maxWidth: .infinity)
+        HStack(spacing: 10) {
+            Spacer()
+
+            LegendItem(color: .loopGreen, label: "BG")
+            LegendItem(color: .insulin, label: "IOB")
+            LegendItem(color: .zt, label: "ZT")
+            LegendItem(color: Color.orange, label: "COB")
+            LegendItem(color: .uam, label: "UAM")
+
+            Spacer()
         }
+        .padding(.horizontal, 10)
+        .frame(maxWidth: .infinity)
     }
 }
 
@@ -636,48 +598,6 @@ extension MainChartView {
         viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
     }
 
-    private func calculateCarbs() {
-        var calculatedCarbs: [Carb] = []
-
-        /// check if carbs are not fpus before adding them to the chart
-        /// this solves the problem of a first CARB entry with the amount of the single fpu entries that was made at current time when adding ONLY fpus
-        let realCarbs = carbs.filter { !($0.isFPU ?? false) }
-
-        realCarbs.forEach { carb in
-            calculatedCarbs.append(Carb(amount: carb.carbs, timestamp: carb.actualDate ?? carb.createdAt))
-        }
-        ChartCarbs = calculatedCarbs
-    }
-
-    private func calculateFpus() {
-        var calculatedFpus: [Carb] = []
-
-        /// check for only fpus
-        let fpus = carbs.filter { $0.isFPU ?? false }
-
-        fpus.forEach { fpu in
-            calculatedFpus
-                .append(Carb(amount: fpu.carbs, timestamp: fpu.actualDate ?? Date()))
-        }
-        ChartFpus = calculatedFpus
-    }
-
-    private func calculateBoluses() {
-        var calculatedBoluses: [ChartBolus] = []
-        boluses.forEach { bolus in
-            let bg = timeToNearestGlucose(time: bolus.timestamp.timeIntervalSince1970)
-            let yPosition = (Decimal(bg.sgv ?? defaultBolusPosition) * conversionFactor) + bolusOffset
-            calculatedBoluses
-                .append(ChartBolus(
-                    amount: bolus.amount ?? 0,
-                    timestamp: bolus.timestamp,
-                    nearestGlucose: bg,
-                    yPosition: yPosition
-                ))
-        }
-        ChartBoluses = calculatedBoluses
-    }
-
     /// calculations for temp target bar mark
     private func calculateTTs() {
         var groupedPackages: [[TempTarget]] = []
@@ -737,69 +657,38 @@ extension MainChartView {
         ChartTempTargets = calculatedTTs
     }
 
-    private func calculatePredictions() {
+    private func addPredictions(_ predictions: [Int], type: PredictionType, deliveredAt: Date, endMarker: Date) -> [Prediction] {
         var calculatedPredictions: [Prediction] = []
-        let uam = suggestion?.predictions?.uam ?? []
-        let iob = suggestion?.predictions?.iob ?? []
-        let cob = suggestion?.predictions?.cob ?? []
-        let zt = suggestion?.predictions?.zt ?? []
-        guard let deliveredAt = suggestion?.deliverAt else {
-            return
-        }
-        uam.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: uam[index], timestamp: predTime, type: .uam)
-                )
-            }
-        }
-        iob.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: iob[index], timestamp: predTime, type: .iob)
-                )
-            }
-        }
-        cob.indices.forEach { index in
+        predictions.indices.forEach { index in
             let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
+                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes.timeInterval
             )
             if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
                 calculatedPredictions.append(
-                    Prediction(amount: cob[index], timestamp: predTime, type: .cob)
+                    Prediction(amount: predictions[index], timestamp: predTime, type: type)
                 )
             }
         }
-        zt.indices.forEach { index in
-            let predTime = Date(
-                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
-                    .timeInterval
-            )
-            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
-                calculatedPredictions.append(
-                    Prediction(amount: zt[index], timestamp: predTime, type: .zt)
-                )
-            }
-        }
-        Predictions = calculatedPredictions
+        return calculatedPredictions
     }
 
-    private func getLastUam() -> Int {
-        let uam = suggestion?.predictions?.uam ?? []
-        return uam.last ?? 0
+    private func calculatePredictions() {
+        guard let suggestion = suggestion, let deliveredAt = suggestion.deliverAt else { return }
+        let uamPredictions = suggestion.predictions?.uam ?? []
+        let iobPredictions = suggestion.predictions?.iob ?? []
+        let cobPredictions = suggestion.predictions?.cob ?? []
+        let ztPredictions = suggestion.predictions?.zt ?? []
+
+        let uam = addPredictions(uamPredictions, type: .uam, deliveredAt: deliveredAt, endMarker: endMarker)
+        let iob = addPredictions(iobPredictions, type: .iob, deliveredAt: deliveredAt, endMarker: endMarker)
+        let cob = addPredictions(cobPredictions, type: .cob, deliveredAt: deliveredAt, endMarker: endMarker)
+        let zt = addPredictions(ztPredictions, type: .zt, deliveredAt: deliveredAt, endMarker: endMarker)
+
+        Predictions = uam + iob + cob + zt
     }
 
     private func calculateTempBasals() {
-        var basals = tempBasals
+        let basals = tempBasals
         var returnTempBasalRates: [PumpHistoryEvent] = []
         var finished: [Int: Bool] = [:]
         basals.indices.forEach { i in
@@ -890,8 +779,6 @@ extension MainChartView {
 
     private func calculateBasals() {
         let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
-        let firstTempTime = (tempBasals.first?.timestamp ?? Date()).timeIntervalSince1970
-
         let regularPoints = findRegularBasalPoints(
             timeBegin: dayAgoTime,
             timeEnd: endMarker.timeIntervalSince1970,
@@ -926,3 +813,17 @@ extension MainChartView {
         BasalProfiles = basals
     }
 }
+
+struct LegendItem: View {
+    var color: Color
+    var label: String
+
+    var body: some View {
+        Group {
+            Circle().fill(color).frame(width: 8, height: 8)
+            Text(label)
+                .font(.system(size: 10, weight: .bold))
+                .foregroundColor(color)
+        }
+    }
+}

+ 13 - 7
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -359,6 +359,8 @@ extension Home {
                 MainChartView(
                     glucose: $state.glucose,
                     manualGlucose: $state.manualGlucose,
+                    carbsForChart: $state.carbsForChart,
+                    fpusForChart: $state.fpusForChart,
                     units: $state.units,
                     eventualBG: $state.eventualBG,
                     suggestion: $state.suggestion,
@@ -371,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,
@@ -744,12 +745,12 @@ extension Home {
                         .frame(maxHeight: UIScreen.main.bounds.height * 0.45)
                         .frame(minHeight: UIScreen.main.bounds.height * 0.4)
 
-                    timeInterval.padding(.top, 20).padding(.bottom, 20)
+                    timeInterval.padding(.top, 20).padding(.bottom, 40)
 
                     if let progress = state.bolusProgress {
-                        bolusView(geo, progress).padding(.bottom, 30)
+                        bolusView(geo, progress).padding(.bottom, 10)
                     } else {
-                        profileView(geo).padding(.bottom, 30)
+                        profileView(geo).padding(.bottom, 10)
                     }
                 }
                 .background(color)
@@ -791,10 +792,15 @@ extension Home {
         @ViewBuilder func tabBar() -> some View {
             ZStack(alignment: .bottom) {
                 TabView {
-                    mainView()
+                    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("Home", systemImage: "house") }
-                        .toolbarBackground(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white, for: .tabBar)
-                        .toolbarBackground(.visible, for: .tabBar)
+                        .badge(carbsRequiredBadge)
 
                     NavigationStack { DataTable.RootView(resolver: resolver) }
                         .tabItem { Label("History", systemImage: historySFSymbol) }

+ 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