Browse Source

Backport of various rileylink_ios PR's for Omnipod fixes and improvements (#96)

List of included previous PR's from Loop v2.2.5 and dev:
+ #652 Additional Omnipod automatic bolus support
+ #654 Support for suspend reminder and suspend time expired alerts
+ #655 Verify and use pod constants returned in the SetupPod response
+ #660 Bulletproof prime() to prevent 049 pod fault when incorrectly invoked
+ #662 Fix issue #661 "Pod already primed" and other errors after a cancel
+ #663 Update DetailedStatus & StatusResponse to match updated wiki info
+ #664 Move Pod Details to bottom, confirmationBeepsTapped() simplifications
+ #665 Round Pulse Count for Read Pod Status to avoid possible truncation
+ #673 Various Omnipod Active Time display improvements
+ #675 Use alternate method to prevent infinite loops w/o breaking tests

Currently pending Loop dev PR's:
+ #680 Update RileyLinkDeviceError.swift
+ #683 Reworked suspendDelivery funcs for improved optional reminder beeps
+ #684 Improved error checking and commenting in deactivatePod
+ #685 Add new Automatic Bolus Beeps button

Yet to be submitting Loop dev PR's:
+ Add Omnipod comms optimization when starting a temp basal from normal basal
+ Use consistent PumpManagerError type for mock fault during pairing

Additional updates:
+ Added missing calls in PumpManagers to check the RileyLink battery status
+ Update deprecated `class` keyword use to remove all rileylink_ios warnings
Joe Moran 4 năm trước cách đây
mục cha
commit
340c4f6369
25 tập tin đã thay đổi với 778 bổ sung404 xóa
  1. 1 1
      Dependencies/rileylink_ios/Common/IdentifiableClass.swift
  2. 11 1
      Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManager.swift
  3. 1 1
      Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpOps.swift
  4. 9 12
      Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift
  5. 3 3
      Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift
  6. 68 29
      Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageBlocks/VersionResponse.swift
  7. 2 2
      Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageTransport.swift
  8. 83 5
      Dependencies/rileylink_ios/OmniKit/Model/AlertSlot.swift
  9. 12 6
      Dependencies/rileylink_ios/OmniKit/Model/Pod.swift
  10. 1 5
      Dependencies/rileylink_ios/OmniKit/Model/PodProgressStatus.swift
  11. 80 69
      Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManager.swift
  12. 4 15
      Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManagerState.swift
  13. 57 12
      Dependencies/rileylink_ios/OmniKit/PumpManager/PodComms.swift
  14. 112 19
      Dependencies/rileylink_ios/OmniKit/PumpManager/PodCommsSession.swift
  15. 15 4
      Dependencies/rileylink_ios/OmniKit/PumpManager/PodState.swift
  16. 40 4
      Dependencies/rileylink_ios/OmniKitTests/MessageTests.swift
  17. 16 16
      Dependencies/rileylink_ios/OmniKitTests/PodInfoTests.swift
  18. 1 1
      Dependencies/rileylink_ios/OmniKitTests/StatusTests.swift
  19. 4 4
      Dependencies/rileylink_ios/OmniKitUI/ViewControllers/CommandResponseViewController.swift
  20. 14 6
      Dependencies/rileylink_ios/OmniKitUI/ViewControllers/InsertCannulaSetupViewController.swift
  21. 219 180
      Dependencies/rileylink_ios/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift
  22. 15 6
      Dependencies/rileylink_ios/OmniKitUI/ViewControllers/PairPodSetupViewController.swift
  23. 7 0
      Dependencies/rileylink_ios/OmniKitUI/ViewControllers/ReplacePodViewController.swift
  24. 2 2
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkConnectionManager.swift
  25. 1 1
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDeviceError.swift

+ 1 - 1
Dependencies/rileylink_ios/Common/IdentifiableClass.swift

@@ -9,7 +9,7 @@
 import Foundation
 
 
-protocol IdentifiableClass: class {
+protocol IdentifiableClass: AnyObject {
     static var className: String { get }
 }
 

+ 11 - 1
Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManager.swift

@@ -11,7 +11,7 @@ import RileyLinkKit
 import RileyLinkBLEKit
 import os.log
 
-public protocol MinimedPumpManagerStateObserver: class {
+public protocol MinimedPumpManagerStateObserver: AnyObject {
     func didUpdatePumpManagerState(_ state: MinimedPumpManagerState)
 }
 
@@ -382,6 +382,8 @@ extension MinimedPumpManager {
         pumpDateComponents.timeZone = timeZone
         glucoseDateComponents?.timeZone = timeZone
 
+        checkRileyLinkBattery()
+
         // The pump sends the same message 3x, so ignore it if we've already seen it.
         guard status != recents.latestPumpStatusFromMySentry, let pumpDate = pumpDateComponents.date else {
             return
@@ -431,6 +433,14 @@ extension MinimedPumpManager {
         }
     }
 
+    private func checkRileyLinkBattery() {
+        rileyLinkDeviceProvider.getDevices { devices in
+            for device in devices {
+                device.updateBatteryLevel()
+            }
+        }
+    }
+
     /**
      Store a new reservoir volume and notify observers of new pump data.
 

+ 1 - 1
Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpOps.swift

@@ -13,7 +13,7 @@ import os.log
 import LoopKit
 
 
-public protocol PumpOpsDelegate: class {
+public protocol PumpOpsDelegate: AnyObject {
     // TODO: Audit clients of this as its called on the session queue
     func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState)
 }

+ 9 - 12
Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift

@@ -19,7 +19,7 @@ public struct DetailedStatus : PodInfo, Equatable {
     public let podProgressStatus: PodProgressStatus
     public let deliveryStatus: DeliveryStatus
     public let bolusNotDelivered: Double
-    public let podMessageCounter: UInt8
+    public let lastProgrammingMessageSeqNum: UInt8 // updated by pod for 03, 08, $11, $19, $1A, $1C, $1E & $1F command messages
     public let totalInsulinDelivered: Double
     public let faultEventCode: FaultEventCode
     public let faultEventTimeSinceActivation: TimeInterval?
@@ -31,7 +31,7 @@ public struct DetailedStatus : PodInfo, Equatable {
     public let receiverLowGain: UInt8
     public let radioRSSI: UInt8
     public let previousPodProgressStatus: PodProgressStatus?
-    public let unknownValue: Data
+    // YYYY is uninitialized data for Eros
     public let data: Data
     
     public init(encodedData: Data) throws {
@@ -48,7 +48,7 @@ public struct DetailedStatus : PodInfo, Equatable {
         
         self.bolusNotDelivered = Pod.pulseSize * Double((Int(encodedData[3] & 0x3) << 8) | Int(encodedData[4]))
         
-        self.podMessageCounter = encodedData[5]
+        self.lastProgrammingMessageSeqNum = encodedData[5]
         
         self.totalInsulinDelivered = Pod.pulseSize * Double(encodedData[6...7].toBigEndian(UInt16.self))
         
@@ -73,7 +73,7 @@ public struct DetailedStatus : PodInfo, Equatable {
         
         self.unacknowledgedAlerts =  AlertSet(rawValue: encodedData[15])
         
-        self.faultAccessingTables = encodedData[16] == 2
+        self.faultAccessingTables = (encodedData[16] & 2) != 0
         
         if encodedData[17] == 0x00 {
            self.errorEventInfo = nil // this byte is not valid (no fault has occurred)
@@ -90,8 +90,6 @@ public struct DetailedStatus : PodInfo, Equatable {
             self.previousPodProgressStatus = PodProgressStatus(rawValue: encodedData[19] & 0xF)!
         }
         
-        self.unknownValue = encodedData[20...21]
-        
         self.data = Data(encodedData)
     }
 
@@ -145,7 +143,7 @@ extension DetailedStatus: CustomDebugStringConvertible {
             "* podProgressStatus: \(podProgressStatus)",
             "* deliveryStatus: \(deliveryStatus.description)",
             "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U",
-            "* podMessageCounter: \(podMessageCounter)",
+            "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U",
             "* faultEventCode: \(faultEventCode.description)",
             "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "none")",
@@ -157,7 +155,6 @@ extension DetailedStatus: CustomDebugStringConvertible {
             "* receiverLowGain: \(receiverLowGain)",
             "* radioRSSI: \(radioRSSI)",
             "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
-            "* unknownValue: 0x\(unknownValue.hexadecimalString)",
             "",
             ].joined(separator: "\n")
     }
@@ -205,14 +202,14 @@ extension Double {
 
 // Type for the ErrorEventInfo VV byte if valid
 //    a: insulin state table corruption found during error logging
-//   bb: internal 2-bit variable set and manipulated in main loop routines
+//   bb: internal 2-bit occlusion type
 //    c: immediate bolus in progress during error
 // dddd: Pod Progress at time of first logged fault event
 //
 public struct ErrorEventInfo: CustomStringConvertible, Equatable {
     let rawValue: UInt8
     let insulinStateTableCorruption: Bool // 'a' bit
-    let internalVariable: Int // 'bb' 2-bit internal variable
+    let occlusionType: Int // 'bb' 2-bit occlusion type
     let immediateBolusInProgress: Bool // 'c' bit
     let podProgressStatus: PodProgressStatus // 'dddd' bits
 
@@ -225,7 +222,7 @@ public struct ErrorEventInfo: CustomStringConvertible, Equatable {
         return [
             "rawValue: 0x\(hexString)",
             "insulinStateTableCorruption: \(insulinStateTableCorruption)",
-            "internalVariable: \(internalVariable)",
+            "occlusionType: \(occlusionType)",
             "immediateBolusInProgress: \(immediateBolusInProgress)",
             "podProgressStatus: \(podProgressStatus)",
             ].joined(separator: ", ")
@@ -234,7 +231,7 @@ public struct ErrorEventInfo: CustomStringConvertible, Equatable {
     init(rawValue: UInt8)  {
         self.rawValue = rawValue
         self.insulinStateTableCorruption = (rawValue & 0x80) != 0
-        self.internalVariable = Int((rawValue & 0x60) >> 5)
+        self.occlusionType = Int((rawValue & 0x60) >> 5)
         self.immediateBolusInProgress = (rawValue & 0x10) != 0
         self.podProgressStatus = PodProgressStatus(rawValue: rawValue & 0xF)!
     }

+ 3 - 3
Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift

@@ -17,7 +17,7 @@ public struct StatusResponse : MessageBlock {
     public let reservoirLevel: Double?
     public let insulin: Double
     public let bolusNotDelivered: Double
-    public let podMessageCounter: UInt8
+    public let lastProgrammingMessageSeqNum: UInt8 // updated by pod for 03, 08, $11, $19, $1A, $1C, $1E & $1F command messages
     public let alerts: AlertSet
     
     
@@ -48,7 +48,7 @@ public struct StatusResponse : MessageBlock {
         let lowInsulinBits = Int(encodedData[4] >> 7)
         self.insulin = Double(highInsulinBits | midInsulinBits | lowInsulinBits) / Pod.pulsesPerUnit
         
-        self.podMessageCounter = (encodedData[4] >> 3) & 0xf
+        self.lastProgrammingMessageSeqNum = (encodedData[4] >> 3) & 0xf
         
         self.bolusNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit
 
@@ -65,7 +65,7 @@ public struct StatusResponse : MessageBlock {
 
 extension StatusResponse: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulin), bolusNotDelivered:\(bolusNotDelivered), seq:\(podMessageCounter), alerts:\(alerts))"
+        return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulin), bolusNotDelivered:\(bolusNotDelivered), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))"
     }
 }
 

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 68 - 29
Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageBlocks/VersionResponse.swift


+ 2 - 2
Dependencies/rileylink_ios/OmniKit/MessageTransport/MessageTransport.swift

@@ -11,7 +11,7 @@ import os.log
 
 import RileyLinkBLEKit
 
-protocol MessageLogger: class {
+protocol MessageLogger: AnyObject {
     // Comms logging
     func didSend(_ message: Data)
     func didReceive(_ message: Data)
@@ -49,7 +49,7 @@ public struct MessageTransportState: Equatable, RawRepresentable {
 
 }
 
-protocol MessageTransportDelegate: class {
+protocol MessageTransportDelegate: AnyObject {
     func messageTransport(_ messageTransport: MessageTransport, didUpdate state: MessageTransportState)
 }
 

+ 83 - 5
Dependencies/rileylink_ios/OmniKit/Model/AlertSlot.swift

@@ -81,23 +81,33 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
     // auto-off timer; requires user input every x minutes
     case autoOffAlarm(active: Bool, countdownDuration: TimeInterval)
 
+    // pod suspended reminder, before suspendTime; short beep every 15 minutes if >= 30 min, else every 5 minutes
+    case podSuspendedReminder(active: Bool, suspendTime: TimeInterval)
+
+    // pod suspend time expired alarm, after suspendTime; 2 sets of beeps every min for 3 minutes repeated every 15 minutes
+    case suspendTimeExpired(suspendTime: TimeInterval)
+
     public var description: String {
         var alertName: String
         switch self {
         case .waitingForPairingReminder:
             return LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder")
         case .finishSetupReminder:
-            return LocalizedString("Finish setup ", comment: "Description for finish setup")
+            return LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder")
         case .expirationAlert:
             alertName = LocalizedString("Expiration alert", comment: "Description for expiration alert")
         case .expirationAdvisoryAlarm:
-            alertName = LocalizedString("Pod expiration advisory alarm", comment: "Description for expiration advisory alarm")
+            alertName = LocalizedString("Expiration advisory", comment: "Description for expiration advisory")
         case .shutdownImminentAlarm:
-            alertName = LocalizedString("Shutdown imminent alarm", comment: "Description for shutdown imminent alarm")
+            alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent")
         case .lowReservoirAlarm:
-            alertName = LocalizedString("Low reservoir advisory alarm", comment: "Description for low reservoir alarm")
+            alertName = LocalizedString("Low reservoir advisory", comment: "Description for low reservoir advisory")
         case .autoOffAlarm:
-            alertName = LocalizedString("Auto-off alarm", comment: "Description for auto-off alarm")
+            alertName = LocalizedString("Auto-off", comment: "Description for auto-off")
+        case .podSuspendedReminder:
+            alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder")
+        case .suspendTimeExpired:
+            alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired")
         }
         if self.configuration.active == false {
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
@@ -125,6 +135,53 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
             return AlertConfiguration(alertType: .slot4, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
         case .autoOffAlarm(let active, let countdownDuration):
             return AlertConfiguration(alertType: .slot0, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
+        case .podSuspendedReminder(let active, let suspendTime):
+            // A suspendTime of 0 is an untimed suspend
+            let reminderInterval, duration: TimeInterval
+            let trigger: AlertTrigger
+            let beepRepeat: BeepRepeat
+            let beepType: BeepType
+            if active {
+                if suspendTime >= TimeInterval(minutes :30) {
+                    // Use 15-minute pod suspended reminder beeps for longer scheduled suspend times as per PDM.
+                    reminderInterval = TimeInterval(minutes: 15)
+                    beepRepeat = .every15Minutes
+                } else {
+                    // Use 5-minute pod suspended reminder beeps for untimed & shorter scheduled suspend times.
+                    reminderInterval = TimeInterval(minutes: 5)
+                    beepRepeat = .every5Minutes
+                }
+                if suspendTime == 0 {
+                    duration = 0 // Untimed suspend, no duration
+                } else if suspendTime > reminderInterval {
+                    duration = suspendTime - reminderInterval // End after suspendTime total time
+                } else {
+                    duration = .minutes(1) // Degenerate case, end ASAP
+                }
+                trigger = .timeUntilAlert(reminderInterval) // Start after reminderInterval has passed
+                beepType = .beep
+            } else {
+                duration = 0
+                trigger = .timeUntilAlert(.minutes(0))
+                beepRepeat = .once
+                beepType = .noBeep
+            }
+            return AlertConfiguration(alertType: .slot5, active: active, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType)
+        case .suspendTimeExpired(let suspendTime):
+            let active = suspendTime != 0 // disable if suspendTime is 0
+            let trigger: AlertTrigger
+            let beepRepeat: BeepRepeat
+            let beepType: BeepType
+            if active {
+                trigger = .timeUntilAlert(suspendTime)
+                beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes
+                beepType = .bipBeepBipBeepBipBeepBipBeep
+            } else {
+                trigger = .timeUntilAlert(.minutes(0))
+                beepRepeat = .once
+                beepType = .noBeep
+            }
+            return AlertConfiguration(alertType: .slot6, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType)
         }
     }
 
@@ -170,6 +227,18 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
                 return nil
             }
             self = .autoOffAlarm(active: active, countdownDuration: TimeInterval(countdownDuration))
+        case "podSuspendedReminder":
+            guard let active = rawValue["active"] as? Bool,
+                let suspendTime = rawValue["suspendTime"] as? Double else
+            {
+                return nil
+            }
+            self = .podSuspendedReminder(active: active, suspendTime: suspendTime)
+        case "suspendTimeExpired":
+            guard let suspendTime = rawValue["suspendTime"] as? Double else {
+                return nil
+            }
+            self = .suspendTimeExpired(suspendTime: suspendTime)
         default:
             return nil
         }
@@ -193,6 +262,10 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
                 return "lowReservoirAlarm"
             case .autoOffAlarm:
                 return "autoOffAlarm"
+            case .podSuspendedReminder:
+                return "podSuspendedReminder"
+            case .suspendTimeExpired:
+                return "suspendTimeExpired"
             }
         }()
 
@@ -214,6 +287,11 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
         case .autoOffAlarm(let active, let countdownDuration):
             rawValue["active"] = active
             rawValue["countdownDuration"] = countdownDuration
+        case .podSuspendedReminder(let active, let suspendTime):
+            rawValue["active"] = active
+            rawValue["suspendTime"] = suspendTime
+        case .suspendTimeExpired(let suspendTime):
+            rawValue["suspendTime"] = suspendTime
         default:
             break
         }

+ 12 - 6
Dependencies/rileylink_ios/OmniKit/Model/Pod.swift

@@ -9,19 +9,22 @@
 import Foundation
 
 public struct Pod {
-    // Volume of insulin in one motor pulse
+    // Volume of U100 insulin in one motor pulse
+    // Must agree with value returned by pod during the pairing process.
     public static let pulseSize: Double = 0.05
 
-    // Number of pulses required to deliver one unit of insulin
+    // Number of pulses required to deliver one unit of U100 insulin
     public static let pulsesPerUnit: Double = 1 / Pod.pulseSize
 
     // Seconds per pulse for boluses
+    // Checked to verify it agrees with value returned by pod during the pairing process.
     public static let secondsPerBolusPulse: Double = 2
 
     // Units per second for boluses
     public static let bolusDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerBolusPulse
 
     // Seconds per pulse for priming/cannula insertion
+    // Checked to verify it agrees with value returned by pod during the pairing process.
     public static let secondsPerPrimePulse: Double = 1
 
     // Units per second for priming/cannula insertion
@@ -37,6 +40,7 @@ public struct Pod {
     public static let endOfServiceImminentWindow = TimeInterval(hours: 1)
 
     // Total pod service time. A fault is triggered if this time is reached before pod deactivation.
+    // Checked to verify it agrees with value returned by pod during the pairing process.
     public static let serviceDuration = TimeInterval(hours: 80)
 
     // Nomimal pod life (72 hours)
@@ -57,13 +61,15 @@ public struct Pod {
     // Minimum duration of a single basal schedule entry
     public static let minimumBasalScheduleEntryDuration = TimeInterval.minutes(30)
 
-    // Amount of insulin delivered with 1 second between pulses for priming
+    // Default amount for priming bolus using secondsPerPrimePulse timing.
+    // Checked to verify it agrees with value returned by pod during the pairing process.
     public static let primeUnits = 2.6
 
-    // Amount of insulin delivered with 1 second between pulses for cannula insertion
-    public static let cannulaInsertionUnitsBase = 0.5
+    // Default amount for cannula insertion bolus using secondsPerPrimePulse timing.
+    // Checked to verify it agrees with value returned by pod during the pairing process.
+    public static let cannulaInsertionUnits = 0.5
+
     public static let cannulaInsertionUnitsExtra = 0.0 // edit to add a fixed additional amount of insulin during cannula insertion
-    public static let cannulaInsertionUnits = cannulaInsertionUnitsBase + cannulaInsertionUnitsExtra
 
     // Default and limits for expiration reminder alerts
     public static let expirationReminderAlertDefaultTimeBeforeExpiration = TimeInterval.hours(2)

+ 1 - 5
Dependencies/rileylink_ios/OmniKit/Model/PodProgressStatus.swift

@@ -30,10 +30,6 @@ public enum PodProgressStatus: UInt8, CustomStringConvertible, Equatable {
         return self == .fiftyOrLessUnits || self == .aboveFiftyUnits
     }
     
-    public var unfinishedPairing: Bool {
-        return self.rawValue < PodProgressStatus.aboveFiftyUnits.rawValue
-    }
-
     public var description: String {
         switch self {
         case .initialized:
@@ -43,7 +39,7 @@ public enum PodProgressStatus: UInt8, CustomStringConvertible, Equatable {
         case .reminderInitialized:
             return LocalizedString("Reminder initialized", comment: "Pod pairing reminder initialized")
         case .pairingCompleted:
-            return LocalizedString("Paired completed", comment: "Pod status when pairing completed")
+            return LocalizedString("Pairing completed", comment: "Pod status when pairing completed")
         case .priming:
             return LocalizedString("Priming", comment: "Pod status when priming")
         case .primingCompleted:

+ 80 - 69
Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -27,8 +27,6 @@ public protocol PodStateObserver: AnyObject {
 public enum OmnipodPumpManagerError: Error {
     case noPodPaired
     case podAlreadyPaired
-    case podAlreadyPrimed
-    case notReadyForPrime
     case notReadyForCannulaInsertion
 }
 
@@ -37,12 +35,8 @@ extension OmnipodPumpManagerError: LocalizedError {
         switch self {
         case .noPodPaired:
             return LocalizedString("No pod paired", comment: "Error message shown when no pod is paired")
-        case .podAlreadyPrimed:
-            return LocalizedString("Pod already primed", comment: "Error message shown when prime is attempted, but pod is already primed")
         case .podAlreadyPaired:
             return LocalizedString("Pod already paired", comment: "Error message shown when user cannot pair because pod is already paired")
-        case .notReadyForPrime:
-            return LocalizedString("Pod is not in a state ready for priming.", comment: "Error message when prime fails because the pod is in an unexpected state")
         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")
         }
@@ -52,12 +46,8 @@ extension OmnipodPumpManagerError: LocalizedError {
         switch self {
         case .noPodPaired:
             return nil
-        case .podAlreadyPrimed:
-            return nil
         case .podAlreadyPaired:
             return nil
-        case .notReadyForPrime:
-            return nil
         case .notReadyForCannulaInsertion:
             return nil
         }
@@ -67,12 +57,8 @@ extension OmnipodPumpManagerError: LocalizedError {
         switch self {
         case .noPodPaired:
             return LocalizedString("Please pair a new pod", comment: "Recover suggestion shown when no pod is paired")
-        case .podAlreadyPrimed:
-            return nil
         case .podAlreadyPaired:
             return nil
-        case .notReadyForPrime:
-            return nil
         case .notReadyForCannulaInsertion:
             return nil
         }
@@ -443,7 +429,7 @@ extension OmnipodPumpManager {
         
         return nil
     }
-        
+
 
     // Thread-safe
     public var hasActivePod: Bool {
@@ -581,7 +567,7 @@ extension OmnipodPumpManager {
     private func jumpStartPod(address: UInt32, lot: UInt32, tid: UInt32, fault: DetailedStatus? = nil, startDate: Date? = nil, mockFault: Bool) {
         let start = startDate ?? Date()
         var podState = PodState(address: address, piVersion: "jumpstarted", pmVersion: "jumpstarted", lot: lot, tid: tid, insulinType: .novolog)
-        podState.setupProgress = .podConfigured
+        podState.setupProgress = .completed
         podState.activatedAt = start
         podState.expiresAt = start + .hours(72)
         
@@ -618,7 +604,8 @@ extension OmnipodPumpManager {
                 return state.podState?.fault
             })
             if mockFaultDuringPairing {
-                completion(.failure(PumpManagerError.deviceState(PodCommsError.podFault(fault: fault!))))
+                // needs to be PumpManagerError.communication for OmniKitUI error checking to work correctly
+                completion(.failure(PumpManagerError.communication(PodCommsError.podFault(fault: fault!))))
             } else if mockCommsErrorDuringPairing {
                 completion(.failure(PumpManagerError.communication(PodCommsError.noResponse)))
             } else {
@@ -628,13 +615,13 @@ extension OmnipodPumpManager {
         }
         #else
         let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
-        let configureAndPrimeSession = { (result: PodComms.SessionRunResult) in
+        let primeSession = { (result: PodComms.SessionRunResult) in
             switch result {
             case .success(let session):
                 // We're on the session queue
                 session.assertOnSessionQueue()
 
-                self.log.default("Beginning pod configuration and prime")
+                self.log.default("Beginning pod prime")
 
                 // Clean up any previously un-stored doses if needed
                 let unstoredDoses = self.state.unstoredDoses
@@ -655,21 +642,16 @@ extension OmnipodPumpManager {
             }
         }
 
-        let needsPairing = setStateWithResult({ (state) -> PumpManagerResult<Bool> in
+        let needsPairing = setStateWithResult({ (state) -> Bool in
             guard let podState = state.podState else {
-                return .success(true) // Needs pairing
+                return true // Needs pairing
             }
 
-            guard podState.setupProgress.primingNeeded else {
-                return .failure(PumpManagerError.deviceState(OmnipodPumpManagerError.podAlreadyPrimed))
-            }
-
-            // If still need configuring, run pair()
-            return .success(podState.setupProgress == .addressAssigned)
+            // Return true if not yet paired
+            return podState.setupProgress.isPaired == false
         })
 
-        switch needsPairing {
-        case .success(true):
+        if needsPairing {
             self.log.default("Pairing pod before priming")
             
             // Create random address with 20 bits to match PDM, could easily use 24 bits instead
@@ -694,17 +676,15 @@ extension OmnipodPumpManager {
                 }
                 
                 // Calls completion
-                configureAndPrimeSession(result)
+                primeSession(result)
             }
-        case .success(false):
+        } else {
             self.log.default("Pod already paired. Continuing.")
 
-            self.podComms.runSession(withName: "Configure and prime pod", using: deviceSelector) { (result) in
+            self.podComms.runSession(withName: "Prime pod", using: deviceSelector) { (result) in
                 // Calls completion
-                configureAndPrimeSession(result)
+                primeSession(result)
             }
-        case .failure(let error):
-            completion(.failure(error))
         }
         #endif
     }
@@ -719,7 +699,7 @@ extension OmnipodPumpManager {
                 // Mock fault
                 //            let fault = try! DetailedStatus(encodedData: Data(hexadecimalString: "020d0000000e00c36a020703ff020900002899080082")!)
                 //            self.state.podState?.fault = fault
-                //            return .failure(PodCommsError.podFault(fault: fault))
+                //            return .failure(PumpManagerError.communication(PodCommsError.podFault(fault: fault)))
 
                 // Mock success
                 state.podState?.setupProgress = .completed
@@ -796,7 +776,7 @@ extension OmnipodPumpManager {
         }
     }
 
-    public func refreshStatus(emitConfirmationBeep: Bool, completion: ((_ result: PumpManagerResult<StatusResponse>) -> Void)? = nil) {
+    public func refreshStatus(emitConfirmationBeep: Bool = false, completion: ((_ result: PumpManagerResult<StatusResponse>) -> Void)? = nil) {
         guard self.hasActivePod else {
             completion?(.failure(.deviceState(OmnipodPumpManagerError.noPodPaired)))
             return
@@ -1014,7 +994,7 @@ extension OmnipodPumpManager {
     public func readPodStatus(completion: @escaping (Result<DetailedStatus, Error>) -> Void) {
         // use hasSetupPod to be able to read pod info from a faulted Pod
         guard self.hasSetupPod else {
-            completion(.failure(PodCommsError.noPodPaired))
+            completion(.failure(OmnipodPumpManagerError.noPodPaired))
             return
         }
 
@@ -1079,7 +1059,6 @@ extension OmnipodPumpManager {
             case .success(let session):
                 let beep = self.confirmationBeeps
                 let result = session.beepConfig(beepConfigType: .bipBeepBipBeepBipBeepBipBeep, basalCompletionBeep: beep, tempBasalCompletionBeep: false, bolusCompletionBeep: beep)
-                
                 switch result {
                 case .success:
                     completion(nil)
@@ -1095,7 +1074,7 @@ extension OmnipodPumpManager {
     public func readPulseLog(completion: @escaping (Result<String, Error>) -> Void) {
         // use hasSetupPod to be able to read the pulse log from a faulted Pod
         guard self.hasSetupPod else {
-            completion(.failure(PodCommsError.noPodPaired))
+            completion(.failure(OmnipodPumpManagerError.noPodPaired))
             return
         }
         guard state.podState?.isFaulted == true || state.podState?.unfinalizedBolus?.scheduledCertainty == .uncertain || state.podState?.unfinalizedBolus?.isFinished != false else
@@ -1259,6 +1238,12 @@ extension OmnipodPumpManager: PumpManager {
     // MARK: Methods
 
     public func suspendDelivery(completion: @escaping (Error?) -> Void) {
+        suspendDelivery(withSuspendReminders: 0, completion: completion) // untimed with suspend reminder beeps
+    }
+
+    // A nil suspendReminder is untimed with no reminders beeps, a suspendReminder of 0 is untimed using reminders beeps, otherwise it
+    // specifies a suspend duration implemented using an appropriate combination of suspended reminder and suspend time expired beeps.
+    public func suspendDelivery(withSuspendReminders suspendReminder: TimeInterval? = nil, completion: @escaping (Error?) -> Void) {
         guard self.hasActivePod else {
             completion(OmnipodPumpManagerError.noPodPaired)
             return
@@ -1285,9 +1270,8 @@ extension OmnipodPumpManager: PumpManager {
                 state.suspendEngageState = .engaging
             })
 
-            // use confirmationBeepType here for confirmation beeps to avoid getting 3 beeps!
             let beepType: BeepConfigType? = self.confirmationBeeps ? .beeeeeep : nil
-            let result = session.cancelDelivery(deliveryType: .all, confirmationBeepType: beepType)
+            let result = session.suspendDelivery(suspendReminder: suspendReminder, confirmationBeepType: beepType)
             switch result {
             case .certainFailure(let error):
                 completion(error)
@@ -1333,6 +1317,7 @@ extension OmnipodPumpManager: PumpManager {
                 let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
                 let beep = self.confirmationBeeps
                 let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep, completionBeep: beep)
+                try session.cancelSuspendAlerts()
                 session.dosesForStorage() { (doses) -> Bool in
                     return self.store(doses: doses, in: session)
                 }
@@ -1372,6 +1357,8 @@ extension OmnipodPumpManager: PumpManager {
             return state.isPumpDataStale
         }
 
+        checkRileyLinkBattery()
+
         switch shouldFetchStatus {
         case .none:
             completion?()
@@ -1399,6 +1386,14 @@ extension OmnipodPumpManager: PumpManager {
         }
     }
 
+    private func checkRileyLinkBattery() {
+        rileyLinkDeviceProvider.getDevices { devices in
+            for device in devices {
+                device.updateBatteryLevel()
+            }
+        }
+    }
+
     public func enactBolus(units: Double, automatic: Bool, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
         guard self.hasActivePod else {
             completion(.failure(PumpManagerError.configuration(OmnipodPumpManagerError.noPodPaired)))
@@ -1465,7 +1460,7 @@ extension OmnipodPumpManager: PumpManager {
                     completion(.failure(PumpManagerError.deviceState(PodCommsError.unfinalizedBolus)))
                     return
                 } else if unfinalizedBolus.isBolusPositivelyFinished == false {
-                    self.log.info("enactBolus: doing getStatus to verify bolus completion")
+                    self.log.info("enactBolus: doing getStatus to verify if bolus completed")
                     getStatusNeeded = true
                 } else {
                     finalizeFinishedDosesNeeded = true // call finalizeFinishDoses() to clean up the certain & positively finalized bolus
@@ -1495,7 +1490,7 @@ extension OmnipodPumpManager: PumpManager {
             let acknowledgementBeep = self.confirmationBeeps && (!automatic || self.automaticBolusBeeps)
             let completionBeep = self.confirmationBeeps && !automatic
 
-            // Use an alternate 0x3F TimeInterval for denote an automatic bolus in the Omnipod Communications Log
+            // Use a maximum programReminderInterval value of 0x3F to denote an automatic bolus in the communication log
             let programReminderInterval: TimeInterval = automatic ? TimeInterval(minutes: 0x3F) : 0
 
             let result = session.bolus(units: enactUnits, automatic: automatic, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep, programReminderInterval: programReminderInterval)
@@ -1608,32 +1603,48 @@ extension OmnipodPumpManager: PumpManager {
                 return
             }
 
-            let status: StatusResponse
-            let canceledDose: UnfinalizedDose?
+            // resuming a normal basal is denoted by a 0 duration temp basal which simply cancels any existing temp basal
+            let resumingNormalBasal = duration < .ulpOfOne
 
-            let result = session.cancelDelivery(deliveryType: .tempBasal)
-            switch result {
-            case .certainFailure(let error):
-                completion(.failure(PumpManagerError.communication(error)))
-                return
-            case .uncertainFailure(let error):
-                // TODO: Return PumpManagerError.uncertainDelivery and implement recovery
-                completion(.failure(PumpManagerError.communication(error)))
-                return
-            case .success(let cancelTempStatus, let dose):
-                status = cancelTempStatus
-                canceledDose = dose
-            }
+            // Skip the Cancel TB comms optimization if the last message had any
+            // comms issues or if the last delivery status hasn't been verified OK
+            let skipCancelTBCommsOptimization = self.state.podState?.lastCommsOK == false ||
+                self.state.podState?.deliveryStatusVerified == false
 
-            guard !status.deliveryStatus.bolusing else {
-                completion(.failure(PumpManagerError.communication(PodCommsError.unfinalizedBolus)))
-                return
-            }
+            // Do the cancel TB command if we are resuming a normal basal,
+            // we currently have a temp basal running,
+            // or we are skipping the cancel TB comms optimization
+            var canceledDose: UnfinalizedDose? = nil
+            if resumingNormalBasal || self.state.podState?.unfinalizedTempBasal != nil || skipCancelTBCommsOptimization {
+                let status: StatusResponse
 
-            guard status.deliveryStatus != .suspended else {
-                self.log.info("Canceling temp basal because status return indicates pod is suspended.")
-                completion(.failure(PumpManagerError.communication(PodCommsError.podSuspended)))
-                return
+                let result = session.cancelDelivery(deliveryType: .tempBasal, beepType: .noBeep)
+                switch result {
+                case .certainFailure(let error):
+                    completion(.failure(PumpManagerError.deviceState(error)))
+                    return
+                case .uncertainFailure(let error):
+                    // TODO: Return PumpManagerError.uncertainDelivery and implement recovery
+                    completion(.failure(PumpManagerError.deviceState(error)))
+                    return
+                case .success(let cancelTempStatus, let dose):
+                    status = cancelTempStatus
+                    canceledDose = dose
+                }
+
+                guard !status.deliveryStatus.bolusing else {
+                    self.log.info("Canceling temp basal because status return indicates bolus in progress.")
+                    completion(.failure(PumpManagerError.deviceState(PodCommsError.unfinalizedBolus)))
+                    return
+                }
+
+                guard status.deliveryStatus != .suspended else {
+                    self.log.info("Canceling temp basal because status return indicates pod is suspended!")
+                    completion(.failure(PumpManagerError.deviceState(PodCommsError.podSuspended)))
+                    return
+                }
+            } else {
+                self.log.info("Skipped Cancel TB command before enacting temp basal")
             }
 
             defer {
@@ -1642,8 +1653,7 @@ extension OmnipodPumpManager: PumpManager {
                 })
             }
 
-            if duration < .ulpOfOne {
-                // 0 duration temp basals are used to cancel any existing temp basal
+            if resumingNormalBasal {
                 self.setState({ (state) in
                     state.tempBasalEngageState = .disengaging
                 })
@@ -1668,6 +1678,7 @@ extension OmnipodPumpManager: PumpManager {
                 case .success:
                     completion(.success(dose))
                 case .uncertainFailure(let error):
+                    // TODO: Return PumpManagerError.uncertainDelivery and implement recovery
                     self.log.error("Temp basal uncertain error: %@", String(describing: error))
                     completion(.success(dose))
                 case .certainFailure(let error):

+ 4 - 15
Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManagerState.swift

@@ -166,21 +166,10 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
             "insulinType": insulinType.rawValue,
         ]
         
-        if let podState = podState {
-            value["podState"] = podState.rawValue
-        }
-
-        if let expirationReminderDate = expirationReminderDate {
-            value["expirationReminderDate"] = expirationReminderDate
-        }
-        
-        if let rileyLinkConnectionManagerState = rileyLinkConnectionManagerState {
-            value["rileyLinkConnectionManagerState"] = rileyLinkConnectionManagerState.rawValue
-        }
-        
-        if let pairingAttemptAddress = pairingAttemptAddress {
-            value["pairingAttemptAddress"] = pairingAttemptAddress
-        }
+        value["podState"] = podState?.rawValue
+        value["expirationReminderDate"] = expirationReminderDate
+        value["rileyLinkConnectionManagerState"] = rileyLinkConnectionManagerState?.rawValue
+        value["pairingAttemptAddress"] = pairingAttemptAddress
         value["rileyLinkBatteryAlertLevel"] = rileyLinkBatteryAlertLevel
         value["lastRileyLinkBatteryAlertDate"] = lastRileyLinkBatteryAlertDate
 

+ 57 - 12
Dependencies/rileylink_ios/OmniKit/PumpManager/PodComms.swift

@@ -11,8 +11,9 @@ import RileyLinkBLEKit
 import LoopKit
 import os.log
 
+fileprivate var diagnosePairingRssi = false
 
-protocol PodCommsDelegate: class {
+protocol PodCommsDelegate: AnyObject {
     func podComms(_ podComms: PodComms, didChange podState: PodState)
 }
 
@@ -69,9 +70,11 @@ class PodComms: CustomDebugStringConvertible {
     ///     - PodCommsError.emptyResponse
     ///     - PodCommsError.unexpectedResponse
     ///     - PodCommsError.podChange
+    ///     - PodCommsError.activationTimeExceeded
     ///     - PodCommsError.rssiTooLow
     ///     - PodCommsError.rssiTooHigh
-    ///     - PodCommsError.activationTimeExceeded
+    ///     - PodCommsError.diagnosticMessage
+    ///     - PodCommsError.podIncompatible
     ///     - MessageError.invalidCrc
     ///     - MessageError.invalidSequence
     ///     - MessageError.invalidAddress
@@ -140,12 +143,16 @@ class PodComms: CustomDebugStringConvertible {
                 throw PodCommsError.podChange
             }
 
-            // Checking RSSI
+            // Check the pod RSSI
             let maxRssiAllowed = 59         // maximum RSSI limit allowed
             let minRssiAllowed = 30         // minimum RSSI limit allowed
             if let rssi = config.rssi, let gain = config.gain {
-                let rssiStr = String(format: "Receiver Low Gain: %d.\nReceived Signal Strength Indicator: %d", gain, rssi)
-                log.default("%s", rssiStr)
+                let rssiStr = String(format: "RSSI: %u.\nReceiver Low Gain: %u", rssi, gain)
+                log.default("%@", rssiStr)
+                if diagnosePairingRssi {
+                    throw PodCommsError.diagnosticMessage(str: rssiStr)
+                }
+
                 rssiRetries -= 1
                 if rssi < minRssiAllowed {
                     log.default("RSSI value %d is less than minimum allowed value of %d, %d retries left", rssi, minRssiAllowed, rssiRetries)
@@ -185,9 +192,44 @@ class PodComms: CustomDebugStringConvertible {
                 throw PodCommsError.activationTimeExceeded
             }
 
-            if config.podProgressStatus == .pairingCompleted {
-                log.info("Version Response %{public}@ indicates pairing is complete, moving pod to configured state", String(describing: config))
-                self.podState?.setupProgress = .podConfigured
+            // It's unlikely that Insulet will release an updated Eros pod using any different fundemental values,
+            // so just verify that the fundemental pod constants returned match the expected constant values in the Pod struct.
+            // To actually be able to handle different fundemental values in Loop things would need to be reworked to save
+            // these values in some persistent PodState and then make sure that everything properly works using these values.
+            var errorStrings: [String] = []
+            if let pulseSize = config.pulseSize, pulseSize != Pod.pulseSize  {
+                errorStrings.append(String(format: "Pod reported pulse size of %.3fU different than expected %.3fU", pulseSize, Pod.pulseSize))
+            }
+            if let secondsPerBolusPulse = config.secondsPerBolusPulse, secondsPerBolusPulse != Pod.secondsPerBolusPulse  {
+                errorStrings.append(String(format: "Pod reported seconds per pulse rate of %.1f different than expected %.1f", secondsPerBolusPulse, Pod.secondsPerBolusPulse))
+            }
+            if let secondsPerPrimePulse = config.secondsPerPrimePulse, secondsPerPrimePulse != Pod.secondsPerPrimePulse  {
+                errorStrings.append(String(format: "Pod reported seconds per prime pulse rate of %.1f different than expected %.1f", secondsPerPrimePulse, Pod.secondsPerPrimePulse))
+            }
+            if let primeUnits = config.primeUnits, primeUnits != Pod.primeUnits {
+                errorStrings.append(String(format: "Pod reported prime bolus of %.2fU different than expected %.2fU", primeUnits, Pod.primeUnits))
+            }
+            if let cannulaInsertionUnits = config.cannulaInsertionUnits, Pod.cannulaInsertionUnits != cannulaInsertionUnits {
+                errorStrings.append(String(format: "Pod reported cannula insertion bolus of %.2fU different than expected %.2fU", cannulaInsertionUnits, Pod.cannulaInsertionUnits))
+            }
+            if let serviceDuration = config.serviceDuration {
+                if serviceDuration < Pod.serviceDuration {
+                    errorStrings.append(String(format: "Pod reported service duration of %.0f hours shorter than expected %.0f", serviceDuration.hours, Pod.serviceDuration.hours))
+                } else if serviceDuration > Pod.serviceDuration {
+                    log.info("Pod reported service duration of %.0f hours limited to expected %.0f", serviceDuration.hours, Pod.serviceDuration.hours)
+                }
+            }
+
+            let errMess = errorStrings.joined(separator: ".\n")
+            if errMess.isEmpty == false {
+                log.error("%@", errMess)
+                self.podState?.setupProgress = .podIncompatible
+                throw PodCommsError.podIncompatible(str: errMess)
+            }
+
+            if config.podProgressStatus == .pairingCompleted && self.podState?.setupProgress.isPaired == false {
+                log.info("Version Response %{public}@ indicates pairing is now complete", String(describing: config))
+                self.podState?.setupProgress = .podPaired
             }
 
             return config
@@ -234,8 +276,11 @@ class PodComms: CustomDebugStringConvertible {
             versionResponse = try sendPairMessage(address: podState.address, transport: transport, message: message, insulinType: insulinType)
         } catch let error {
             if case PodCommsError.podAckedInsteadOfReturningResponse = error {
-                log.default("SetupPod acked instead of returning response. Moving pod to configured state.")
-                self.podState?.setupProgress = .podConfigured
+                log.default("SetupPod acked instead of returning response.")
+                if self.podState?.setupProgress.isPaired == false {
+                    log.default("Moving pod to paired state.")
+                    self.podState?.setupProgress = .podPaired
+                }
                 return
             }
             log.error("SetupPod returns error %{public}@", String(describing: error))
@@ -275,11 +320,11 @@ class PodComms: CustomDebugStringConvertible {
                         return
                     }
 
-                    if self.podState!.setupProgress != .podConfigured {
+                    if self.podState!.setupProgress.isPaired == false {
                         try self.setupPod(podState: self.podState!, timeZone: timeZone, commandSession: commandSession, insulinType: insulinType)
                     }
 
-                    guard self.podState!.setupProgress == .podConfigured else {
+                    guard self.podState!.setupProgress.isPaired else {
                         self.log.error("Unexpected podStatus setupProgress value of %{public}@", String(describing: self.podState!.setupProgress))
                         throw PodCommsError.invalidData
                     }

+ 112 - 19
Dependencies/rileylink_ios/OmniKit/PumpManager/PodCommsSession.swift

@@ -33,6 +33,8 @@ public enum PodCommsError: Error {
     case activationTimeExceeded
     case rssiTooLow
     case rssiTooHigh
+    case diagnosticMessage(str: String)
+    case podIncompatible(str: String)
 }
 
 extension PodCommsError: LocalizedError {
@@ -81,6 +83,10 @@ extension PodCommsError: LocalizedError {
             return LocalizedString("Poor signal strength", comment: "Format string for poor pod signal strength")
         case .rssiTooHigh: // only occurs when RileyLink is too close to the pod for reliable pairing
             return LocalizedString("Signal strength too high", comment: "Format string for pod signal strength too high")
+        case .diagnosticMessage(let str):
+            return str
+        case .podIncompatible(let str):
+            return str
         }
     }
     
@@ -132,11 +138,24 @@ extension PodCommsError: LocalizedError {
             return LocalizedString("Please reposition the RileyLink relative to the pod", comment: "Recovery suggestion when pairing signal strength is too low")
         case .rssiTooHigh:
             return LocalizedString("Please reposition the RileyLink further from the pod", comment: "Recovery suggestion when pairing signal strength is too high")
+        case .diagnosticMessage:
+            return nil
+        case .podIncompatible:
+            return nil
+        }
+    }
+
+    public var isFaulted: Bool {
+        switch self {
+        case .podFault, .activationTimeExceeded, .podIncompatible:
+            return true
+        default:
+            return false
         }
     }
 }
 
-public protocol PodCommsSessionDelegate: class {
+public protocol PodCommsSessionDelegate: AnyObject {
     func podCommsSession(_ podCommsSession: PodCommsSession, didChange state: PodState)
 }
 
@@ -279,19 +298,19 @@ public class PodCommsSession {
 
     // Returns time at which prime is expected to finish.
     public func prime() throws -> TimeInterval {
-        //4c00 00c8 0102
-
-        let primeDuration = TimeInterval(seconds: 55)   // a bit more than (Pod.primeUnits / Pod.primeDeliveryRate)
+        let primeDuration: TimeInterval = .seconds(Pod.primeUnits / Pod.primeDeliveryRate) + 3 // as per PDM
         
-        // Skip following alerts if we've already done them before
-        if podState.setupProgress != .startingPrime {
-            
-            // The following will set Tab5[$16] to 0 during pairing, which disables $6x faults.
+        // If priming has never been attempted on this pod, handle the pre-prime setup tasks.
+        // A FaultConfig can only be done before the prime bolus or the pod will generate an 049 fault.
+        if podState.setupProgress.primingNeverAttempted {
+            // This FaultConfig command will set Tab5[$16] to 0 during pairing, which disables $6x faults
             let _: StatusResponse = try send([FaultConfigCommand(nonce: podState.currentNonce, tab5Sub16: 0, tab5Sub17: 0)])
+
+            // Set up the finish pod setup reminder alert which beeps every 5 minutes for 1 hour
             let finishSetupReminder = PodAlert.finishSetupReminder
             try configureAlerts([finishSetupReminder])
         } else {
-            // We started prime, but didn't get confirmation somehow, so check status
+            // Not the first time through, check to see if prime bolus was successfully started
             let status: StatusResponse = try send([GetStatusCommand()])
             podState.updateFromStatusResponse(status)
             if status.podProgressStatus == .priming || status.podProgressStatus == .primingCompleted {
@@ -300,7 +319,7 @@ public class PodCommsSession {
             }
         }
 
-        // Mark 2.6U delivery with 1 second between pulses for prime
+        // Mark Pod.primeUnits (2.6U) bolus delivery with Pod.primeDeliveryRate (1) between pulses for prime
         
         let primeFinishTime = Date() + primeDuration
         podState.primeFinishTime = primeFinishTime
@@ -323,6 +342,7 @@ public class PodCommsSession {
             podState.updateFromStatusResponse(status)
             if status.podProgressStatus == .basalInitialized {
                 podState.setupProgress = .initialBasalScheduleSet
+                podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: Date(), scheduledCertainty: .certain, insulinType: podState.insulinType))
                 return
             }
         }
@@ -335,10 +355,10 @@ public class PodCommsSession {
     }
 
     @discardableResult
-    private func configureAlerts(_ alerts: [PodAlert]) throws -> StatusResponse {
+    private func configureAlerts(_ alerts: [PodAlert], confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse {
         let configurations = alerts.map { $0.configuration }
         let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations)
-        let status: StatusResponse = try send([configureAlerts])
+        let status: StatusResponse = try send([configureAlerts], confirmationBeepType: confirmationBeepType)
         for alert in alerts {
             podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
         }
@@ -372,7 +392,8 @@ public class PodCommsSession {
     }
 
     public func insertCannula() throws -> TimeInterval {
-        let insertionWait: TimeInterval = .seconds(Pod.cannulaInsertionUnits / Pod.primeDeliveryRate)
+        let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
+        let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
         guard let activatedAt = podState.activatedAt else {
             throw PodCommsError.noPodPaired
@@ -402,14 +423,14 @@ public class PodCommsSession {
             try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm])
         }
         
-        // Mark 0.5U delivery with 1 second between pulses for cannula insertion
+        // Mark cannulaInsertionUnits (0.5U) bolus delivery with Pod.secondsPerPrimePulse (1) between pulses for cannula insertion
 
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerPrimePulse)
-        let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: Pod.cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses)
+        let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, deliverySchedule: bolusSchedule)
         
         podState.setupProgress = .startingInsertCannula
-        let bolusExtraCommand = BolusExtraCommand(units: Pod.cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses)
+        let bolusExtraCommand = BolusExtraCommand(units: cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses)
         let status2: StatusResponse = try send([bolusScheduleCommand, bolusExtraCommand])
         podState.updateFromStatusResponse(status2)
         
@@ -544,6 +565,77 @@ public class PodCommsSession {
         return canceledDose
     }
     
+    // Suspends insulin delivery and sets appropriate podSuspendedReminder & suspendTimeExpired alerts.
+    // A nil suspendReminder is an untimed suspend with no suspend reminders.
+    // A suspendReminder of 0 is an untimed suspend which only uses podSuspendedReminder alert beeps.
+    // A suspendReminder of 1-5 minutes will only use suspendTimeExpired alert beeps.
+    // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts.
+    public func suspendDelivery(suspendReminder: TimeInterval? = nil, confirmationBeepType: BeepConfigType? = nil) -> CancelDeliveryResult {
+        do {
+            var alertConfigurations: [AlertConfiguration] = []
+            var podSuspendedReminderAlert: PodAlert? = nil
+            var suspendTimeExpiredAlert: PodAlert? = nil
+            let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0
+
+            let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeep)
+            var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
+
+            // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
+            if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
+                // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders
+                podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, suspendTime: suspendTime)
+                alertConfigurations += [podSuspendedReminderAlert!.configuration]
+            }
+
+            // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
+            if suspendTime > 0 {
+                // a timed suspend using a suspend time expired alert
+                suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(suspendTime: suspendTime)
+                alertConfigurations += [suspendTimeExpiredAlert!.configuration]
+            }
+
+            // append a ConfigureAlert command if we have any reminder alerts for this suspend
+            if alertConfigurations.count != 0 {
+                let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: alertConfigurations)
+                commandsToSend += [configureAlerts]
+            }
+
+            let status: StatusResponse = try send(commandsToSend, confirmationBeepType: confirmationBeepType)
+            let canceledDose = handleCancelDosing(deliveryType: .all, bolusNotDelivered: status.bolusNotDelivered)
+            podState.updateFromStatusResponse(status)
+
+            if let alert = podSuspendedReminderAlert {
+                podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
+            }
+            if let alert = suspendTimeExpiredAlert {
+                podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
+            }
+
+            return CancelDeliveryResult.success(statusResponse: status, canceledDose: canceledDose)
+        } catch PodCommsError.nonceResyncFailed {
+            return CancelDeliveryResult.certainFailure(error: PodCommsError.nonceResyncFailed)
+        } catch PodCommsError.rejectedMessage(let errorCode) {
+            return CancelDeliveryResult.certainFailure(error: PodCommsError.rejectedMessage(errorCode: errorCode))
+        } catch let error {
+            podState.unfinalizedSuspend = UnfinalizedDose(suspendStartTime: Date(), scheduledCertainty: .uncertain)
+            return CancelDeliveryResult.uncertainFailure(error: error as? PodCommsError ?? PodCommsError.commsError(error: error))
+        }
+    }
+
+    // Cancels any suspend related alerts, should be called when resuming after using suspendDelivery()
+    @discardableResult
+    public func cancelSuspendAlerts() throws -> StatusResponse {
+        do {
+            let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, suspendTime: 0)
+            let suspendTimeExpired = PodAlert.suspendTimeExpired(suspendTime: 0) // A suspendTime of 0 deactivates this alert
+
+            let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired])
+            return status
+        } catch let error {
+            throw error
+        }
+    }
+
     // Cancel beeping can be done implemented using beepType (for a single delivery type) or a separate confirmation beep message block (for cancel all).
     // N.B., Using the built-in cancel delivery command beepType method when cancelling all insulin delivery will emit 3 different sets of cancel beeps!!!
     public func cancelDelivery(deliveryType: CancelDeliveryCommand.DeliveryType, beepType: BeepType = .noBeep, confirmationBeepType: BeepConfigType? = nil) -> CancelDeliveryResult {
@@ -672,6 +764,7 @@ public class PodCommsSession {
         return podInfoResponse
     }
 
+    // Can be called a second time to deactivate a given pod
     public func deactivatePod() throws {
 
         // Don't try to cancel if the pod hasn't completed its setup as it will either receive no response
@@ -688,6 +781,7 @@ public class PodCommsSession {
             }
         }
 
+        // if faulted read the most recent pulse log entries
         if podState.fault != nil {
             // All the dosing cleanup from the fault should have already been
             // handled in handlePodFault() when podState.fault was initialized.
@@ -699,13 +793,12 @@ public class PodCommsSession {
             }
         }
 
-        let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce)
-
         do {
+            let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce)
             let _: StatusResponse = try send([deactivatePod])
         } catch let error as PodCommsError {
             switch error {
-            case .podFault, .unexpectedResponse:
+            case .podFault, .activationTimeExceeded, .unexpectedResponse:
                 break
             default:
                 throw error

+ 15 - 4
Dependencies/rileylink_ios/OmniKit/PumpManager/PodState.swift

@@ -11,7 +11,7 @@ import LoopKit
 
 public enum SetupProgress: Int {
     case addressAssigned = 0
-    case podConfigured
+    case podPaired
     case startingPrime
     case priming
     case settingInitialBasalSchedule
@@ -20,6 +20,15 @@ public enum SetupProgress: Int {
     case cannulaInserting
     case completed
     case activationTimeout
+    case podIncompatible
+
+    public var isPaired: Bool {
+        return self.rawValue >= SetupProgress.podPaired.rawValue
+    }
+
+    public var primingNeverAttempted: Bool {
+        return self.rawValue < SetupProgress.startingPrime.rawValue
+    }
     
     public var primingNeeded: Bool {
         return self.rawValue < SetupProgress.priming.rawValue
@@ -120,7 +129,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.lastCommsOK = false
     }
     
-    public var unfinishedPairing: Bool {
+    public var unfinishedSetup: Bool {
         return setupProgress != .completed
     }
     
@@ -141,7 +150,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     }
 
     public var isFaulted: Bool {
-        return fault != nil || setupProgress == .activationTimeout
+        return fault != nil || setupProgress == .activationTimeout || setupProgress == .podIncompatible
     }
 
     public mutating func advanceToNextNonce() {
@@ -400,6 +409,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
                 .slot2: .shutdownImminentAlarm(0),
                 .slot3: .expirationAlert(0),
                 .slot4: .lowReservoirAlarm(0),
+                .slot5: .podSuspendedReminder(active: false, suspendTime: 0),
+                .slot6: .suspendTimeExpired(suspendTime: 0),
                 .slot7: .expirationAdvisoryAlarm(alarmTime: 0, duration: 0)
             ]
         }
@@ -409,7 +420,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
             self.insulinType = insulinType
         } else {
-            insulinType = .humalog
+            insulinType = .novolog
         }
 
         self.deliveryStatusVerified = false

+ 40 - 4
Dependencies/rileylink_ios/OmniKitTests/MessageTests.swift

@@ -39,7 +39,7 @@ class MessageTests: XCTestCase {
             XCTAssertEqual(.aboveFiftyUnits, statusResponse.podProgressStatus)
             XCTAssertEqual(6.3, statusResponse.insulin, accuracy: 0.01)
             XCTAssertEqual(0, statusResponse.bolusNotDelivered)
-            XCTAssertEqual(3, statusResponse.podMessageCounter)
+            XCTAssertEqual(3, statusResponse.lastProgrammingMessageSeqNum)
             XCTAssert(statusResponse.alerts.isEmpty)
 
             XCTAssertEqual("1f00ee84300a1d18003f1800004297ff8128", msg.encoded().hexadecimalString)
@@ -78,9 +78,21 @@ class MessageTests: XCTestCase {
         do {
             let config = try VersionResponse(encodedData: Data(hexadecimalString: "011502070002070002020000a64000097c279c1f08ced2")!)
             XCTAssertEqual(23, config.data.count)
+            XCTAssertEqual("2.7.0", String(describing: config.piVersion))
+            XCTAssertEqual("2.7.0", String(describing: config.pmVersion))
             XCTAssertEqual(0x1f08ced2, config.address)
             XCTAssertEqual(42560, config.lot)
             XCTAssertEqual(621607, config.tid)
+            XCTAssertEqual(2, config.productID)
+            XCTAssertEqual(.reminderInitialized, config.podProgressStatus)
+            XCTAssertEqual(2, config.gain)
+            XCTAssertEqual(0x1c, config.rssi)
+            XCTAssertNil(config.pulseSize)
+            XCTAssertNil(config.secondsPerBolusPulse)
+            XCTAssertNil(config.secondsPerPrimePulse)
+            XCTAssertNil(config.primeUnits)
+            XCTAssertNil(config.cannulaInsertionUnits)
+            XCTAssertNil(config.serviceDuration)
         } catch (let error) {
             XCTFail("message decoding threw error: \(error)")
         }
@@ -91,11 +103,21 @@ class MessageTests: XCTestCase {
             let message = try Message(encodedData: Data(hexadecimalString: "ffffffff041d011b13881008340a5002070002070002030000a62b000447941f00ee878352")!)
             let config = message.messageBlocks[0] as! VersionResponse
             XCTAssertEqual(29, config.data.count)
-            XCTAssertEqual(0x1f00ee87, config.address)
-            XCTAssertEqual(42539, config.lot)
-            XCTAssertEqual(280468, config.tid)
             XCTAssertEqual("2.7.0", String(describing: config.piVersion))
             XCTAssertEqual("2.7.0", String(describing: config.pmVersion))
+            XCTAssertEqual(42539, config.lot)
+            XCTAssertEqual(280468, config.tid)
+            XCTAssertEqual(0x1f00ee87, config.address)
+            XCTAssertEqual(2, config.productID)
+            XCTAssertEqual(.pairingCompleted, config.podProgressStatus)
+            XCTAssertNil(config.rssi)
+            XCTAssertNil(config.gain)
+            XCTAssertEqual(Pod.pulseSize, config.pulseSize)
+            XCTAssertEqual(Pod.secondsPerBolusPulse, config.secondsPerBolusPulse)
+            XCTAssertEqual(Pod.secondsPerPrimePulse, config.secondsPerPrimePulse)
+            XCTAssertEqual(Pod.primeUnits, config.primeUnits)
+            XCTAssertEqual(Pod.cannulaInsertionUnits, config.cannulaInsertionUnits)
+            XCTAssertEqual(Pod.serviceDuration, config.serviceDuration)
         } catch (let error) {
             XCTFail("message decoding threw error: \(error)")
         }
@@ -105,7 +127,21 @@ class MessageTests: XCTestCase {
         do {
             let message = try Message(encodedData: Data(hexadecimalString: "ffffffff04170115020700020700020e0000a5ad00053030971f08686301fd")!)
             let config = message.messageBlocks[0] as! VersionResponse
+            XCTAssertEqual("2.7.0", String(describing: config.piVersion))
+            XCTAssertEqual("2.7.0", String(describing: config.pmVersion))
+            XCTAssertEqual(0x0000a5ad, config.lot)
+            XCTAssertEqual(0x00053030, config.tid)
+            XCTAssertEqual(0x1f086863, config.address)
+            XCTAssertEqual(2, config.productID)
             XCTAssertEqual(.activationTimeExceeded, config.podProgressStatus)
+            XCTAssertEqual(2, config.gain)
+            XCTAssertEqual(0x17, config.rssi)
+            XCTAssertNil(config.pulseSize)
+            XCTAssertNil(config.secondsPerBolusPulse)
+            XCTAssertNil(config.secondsPerPrimePulse)
+            XCTAssertNil(config.primeUnits)
+            XCTAssertNil(config.cannulaInsertionUnits)
+            XCTAssertNil(config.serviceDuration)
         } catch (let error) {
             XCTFail("message decoding threw error: \(error)")
         }

+ 16 - 16
Dependencies/rileylink_ios/OmniKitTests/PodInfoTests.swift

@@ -21,7 +21,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(faultEvent.faultAccessingTables, false)
             XCTAssertEqual(faultEvent.podProgressStatus, .faultEventOccurred)
             XCTAssertEqual(faultEvent.errorEventInfo?.insulinStateTableCorruption, false)
-            XCTAssertEqual(faultEvent.errorEventInfo?.internalVariable, 1)
+            XCTAssertEqual(faultEvent.errorEventInfo?.occlusionType, 1)
             XCTAssertEqual(faultEvent.errorEventInfo?.immediateBolusInProgress, false)
             XCTAssertEqual(faultEvent.errorEventInfo?.podProgressStatus, .aboveFiftyUnits)
         } catch (let error) {
@@ -127,7 +127,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.aboveFiftyUnits, decoded.podProgressStatus)
             XCTAssertEqual(.scheduledBasal, decoded.deliveryStatus)
             XCTAssertEqual(0000, decoded.bolusNotDelivered)
-            XCTAssertEqual(0x0a, decoded.podMessageCounter)
+            XCTAssertEqual(0x0a, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(.noFaults, decoded.faultEventCode.faultType)
             XCTAssertEqual(TimeInterval(minutes: 0x0000), decoded.faultEventTimeSinceActivation)
             XCTAssertNil(decoded.reservoirLevel)
@@ -156,7 +156,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.inactive, decoded.podProgressStatus)
             XCTAssertEqual(.suspended, decoded.deliveryStatus)
             XCTAssertEqual(0000, decoded.bolusNotDelivered)
-            XCTAssertEqual(9, decoded.podMessageCounter)
+            XCTAssertEqual(9, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(.primeOpenCountTooLow, decoded.faultEventCode.faultType)
             XCTAssertEqual(TimeInterval(minutes: 0x0001), decoded.faultEventTimeSinceActivation)
             XCTAssertNil(decoded.reservoirLevel)
@@ -165,7 +165,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(00, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable)
+            XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType)
             XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress)
             XCTAssertEqual(.primingCompleted, decoded.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b10, decoded.receiverLowGain)
@@ -188,7 +188,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus)
             XCTAssertEqual(.suspended, decoded.deliveryStatus)
             XCTAssertEqual(0, decoded.bolusNotDelivered, accuracy: 0.01)
-            XCTAssertEqual(6, decoded.podMessageCounter)
+            XCTAssertEqual(6, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(.command1AParseUnexpectedFailed, decoded.faultEventCode.faultType)
             XCTAssertEqual(TimeInterval(minutes: 0x0000), decoded.faultEventTimeSinceActivation)
             XCTAssertNil(decoded.reservoirLevel)
@@ -196,7 +196,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable)
+            XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType)
             XCTAssertEqual(PodProgressStatus.pairingCompleted, decoded.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b10, decoded.receiverLowGain)
             XCTAssertEqual(0x22, decoded.radioRSSI)
@@ -218,7 +218,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus)
             XCTAssertEqual(.suspended, decoded.deliveryStatus)
             XCTAssertEqual(0, decoded.bolusNotDelivered)
-            XCTAssertEqual(4, decoded.podMessageCounter)
+            XCTAssertEqual(4, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(101.7, decoded.totalInsulinDelivered, accuracy: 0.01)
             XCTAssertEqual(.basalOverInfusionPulse, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
@@ -228,7 +228,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(TimeInterval(minutes: 0x0a02), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable)
+            XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType)
             XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress)
             XCTAssertEqual(.aboveFiftyUnits, decoded.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b00, decoded.receiverLowGain)
@@ -250,7 +250,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus)
             XCTAssertEqual(.suspended, decoded.deliveryStatus)
             XCTAssertEqual(0, decoded.bolusNotDelivered)
-            XCTAssertEqual(4, decoded.podMessageCounter)
+            XCTAssertEqual(4, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(101.35, decoded.totalInsulinDelivered, accuracy: 0.01)
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
@@ -260,7 +260,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(TimeInterval(minutes: 0x0e14), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(1, decoded.errorEventInfo?.internalVariable)
+            XCTAssertEqual(1, decoded.errorEventInfo?.occlusionType)
             XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress)
             XCTAssertEqual(.aboveFiftyUnits, decoded.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b00, decoded.receiverLowGain)
@@ -282,7 +282,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.inactive, decoded.podProgressStatus)
             XCTAssertEqual(.suspended, decoded.deliveryStatus)
             XCTAssertEqual(0.05, decoded.bolusNotDelivered)
-            XCTAssertEqual(2, decoded.podMessageCounter)
+            XCTAssertEqual(2, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(11.8, decoded.totalInsulinDelivered, accuracy: 0.01)
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
@@ -292,7 +292,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(TimeInterval(minutes: 0x026b), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(1, decoded.errorEventInfo?.internalVariable)
+            XCTAssertEqual(1, decoded.errorEventInfo?.occlusionType)
             XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress)
             XCTAssertEqual(.aboveFiftyUnits, decoded.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b10, decoded.receiverLowGain)
@@ -314,7 +314,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus)
             XCTAssertEqual(.suspended, decoded.deliveryStatus)
             XCTAssertEqual(0.00, decoded.bolusNotDelivered)
-            XCTAssertEqual(0, decoded.podMessageCounter)
+            XCTAssertEqual(0, decoded.lastProgrammingMessageSeqNum)
             XCTAssertEqual(0.00, decoded.totalInsulinDelivered, accuracy: 0.01)
             XCTAssertEqual(.resetDueToLVD, decoded.faultEventCode.faultType)
             XCTAssertNil(decoded.faultEventTimeSinceActivation)
@@ -323,7 +323,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(true, decoded.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable)
+            XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType)
             XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress)
             XCTAssertEqual(.insertingCannula, decoded.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b10, decoded.receiverLowGain)
@@ -453,7 +453,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.faultEventOccurred, faultEvent.podProgressStatus)
             XCTAssertEqual(.suspended, faultEvent.deliveryStatus)
             XCTAssertEqual(0.00, faultEvent.bolusNotDelivered)
-            XCTAssertEqual(0, faultEvent.podMessageCounter)
+            XCTAssertEqual(0, faultEvent.lastProgrammingMessageSeqNum)
             XCTAssertEqual(0.00, faultEvent.totalInsulinDelivered, accuracy: 0.01)
             XCTAssertEqual(.resetDueToLVD, faultEvent.faultEventCode.faultType)
             XCTAssertNil(faultEvent.faultEventTimeSinceActivation)
@@ -462,7 +462,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(0, faultEvent.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(false, faultEvent.faultAccessingTables)
             XCTAssertEqual(true, faultEvent.errorEventInfo?.insulinStateTableCorruption)
-            XCTAssertEqual(0, faultEvent.errorEventInfo?.internalVariable)
+            XCTAssertEqual(0, faultEvent.errorEventInfo?.occlusionType)
             XCTAssertEqual(false, faultEvent.errorEventInfo?.immediateBolusInProgress)
             XCTAssertEqual(.insertingCannula, faultEvent.errorEventInfo?.podProgressStatus)
             XCTAssertEqual(0b10, faultEvent.receiverLowGain)

+ 1 - 1
Dependencies/rileylink_ios/OmniKitTests/StatusTests.swift

@@ -38,7 +38,7 @@ class StatusTests: XCTestCase {
             XCTAssertEqual(129.45, decoded.insulin, accuracy: 0.01)
             XCTAssertEqual(46.00, decoded.reservoirLevel)
             XCTAssertEqual(2.2, decoded.bolusNotDelivered)
-            XCTAssertEqual(9, decoded.podMessageCounter)
+            XCTAssertEqual(9, decoded.lastProgrammingMessageSeqNum)
             //XCTAssert(,decoded.alarms)
         } catch (let error) {
             XCTFail("message decoding threw error: \(error)")

+ 4 - 4
Dependencies/rileylink_ios/OmniKitUI/ViewControllers/CommandResponseViewController.swift

@@ -49,18 +49,18 @@ extension CommandResponseViewController {
         var result, str: String
 
         let formatter = DateComponentsFormatter()
-        formatter.unitsStyle = .full
-        formatter.allowedUnits = [.day, .hour, .minute]
+        formatter.allowedUnits = [.hour, .minute]
+        formatter.unitsStyle = .short
         if let timeStr = formatter.string(from: status.timeActive) {
             str = timeStr
         } else {
             str = String(format: LocalizedString("%1$@ minutes", comment: "The format string for minutes (1: number of minutes string)"), String(describing: Int(status.timeActive / 60)))
         }
-        result = String(format: LocalizedString("Pod Active: %1$@\n", comment: "The format string for Pod Active: (1: Pod active time string)"), str)
+        result = String(format: LocalizedString("Pod Active Time: %1$@\n", comment: "The format string for Pod Active Time: (1: formatted time)"), str)
 
         result += String(format: LocalizedString("Delivery Status: %1$@\n", comment: "The format string for Delivery Status: (1: delivery status string)"), String(describing: status.deliveryStatus))
 
-        result += String(format: LocalizedString("Pulse Count: %1$d\n", comment: "The format string for Pulse Count (1: pulse count)"), Int(status.totalInsulinDelivered / Pod.pulseSize))
+        result += String(format: LocalizedString("Pulse Count: %1$d\n", comment: "The format string for Pulse Count (1: pulse count)"), Int(round(status.totalInsulinDelivered / Pod.pulseSize)))
 
         result += String(format: LocalizedString("Reservoir Level: %1$@ U\n", comment: "The format string for Reservoir Level: (1: reservoir level string)"), status.reservoirLevel?.twoDecimals ?? "50+")
 

+ 14 - 6
Dependencies/rileylink_ios/OmniKitUI/ViewControllers/InsertCannulaSetupViewController.swift

@@ -11,7 +11,6 @@ import LoopKit
 import LoopKitUI
 import RileyLinkKit
 import OmniKit
-import SwiftUI
 
 class InsertCannulaSetupViewController: SetupTableViewController {
     
@@ -119,12 +118,21 @@ class InsertCannulaSetupViewController: SetupTableViewController {
             }
             loadingText = errorText
             
-            // If we have an error, update the continue state depending on whether the cannula insertion was started
-            if let podCommsError = lastError as? PodCommsError {
-                switch podCommsError {
-                case .podFault, .activationTimeExceeded:
-                    continueState = .fault
+            var podCommsError: PodCommsError? = nil
+            if let pumpManagerError = lastError as? PumpManagerError {
+                switch pumpManagerError {
+                case .communication(let error):
+                    podCommsError = error as? PodCommsError
                 default:
+                    break
+                }
+            }
+
+            // If we have an error, update the continue state depending on whether it's fatal or if the cannula insertion was started or not
+            if let podCommsError = podCommsError {
+                if podCommsError.isFaulted {
+                    continueState = .fault
+                } else {
                     continueState = initialOrNeedsCannulaInsertionCheck
                 }
             } else if lastError != nil {

+ 219 - 180
Dependencies/rileylink_ios/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift

@@ -27,6 +27,17 @@ public class ConfirmationBeepsTableViewCell: TextButtonTableViewCell {
     }
 }
 
+public class AutoBolusBeepsTableViewCell: TextButtonTableViewCell {
+
+    public func updateTextLabel(enabled: Bool) {
+        if enabled {
+            self.textLabel?.text = LocalizedString("Disable Automatic Bolus Beeps", comment: "Title text for button to disable automatic bolus beeps")
+        } else {
+            self.textLabel?.text = LocalizedString("Enable Automatic Bolus Beeps", comment: "Title text for button to enable automatic bolus beeps")
+        }
+    }
+}
+
 class OmnipodSettingsViewController: RileyLinkSettingsViewController {
 
     let pumpManager: OmnipodPumpManager
@@ -76,6 +87,13 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
         return cell
     }()
     
+    lazy var autoBolusBeepsTableViewCell: AutoBolusBeepsTableViewCell = {
+        let cell = AutoBolusBeepsTableViewCell(style: .default, reuseIdentifier: nil)
+        cell.updateTextLabel(enabled: pumpManager.automaticBolusBeeps)
+        cell.isEnabled = self.pumpManager.confirmationBeeps
+        return cell
+    }()
+
     var activityIndicator: UIActivityIndicatorView!
     var refreshButton: UIButton!
 
@@ -213,19 +231,19 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     
     private enum Section: Int, CaseIterable {
         case status = 0
-        case podDetails
-        case diagnostics
         case configuration
         case rileyLinks
+        case diagnostics
+        case podDetails
         case deletePumpManager
     }
     
     private class func sectionList(_ podState: PodState?) -> [Section] {
         if let podState = podState {
-            if podState.unfinishedPairing {
+            if podState.unfinishedSetup {
                 return [.configuration, .rileyLinks]
             } else {
-                return [.status, .configuration, .rileyLinks, .podDetails, .diagnostics]
+                return [.status, .configuration, .rileyLinks, .diagnostics, .podDetails]
             }
         } else {
             return [.configuration, .rileyLinks, .deletePumpManager]
@@ -252,7 +270,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     }
     
     private var configurationRows: [ConfigurationRow] {
-        if podState == nil || podState?.unfinishedPairing == true {
+        if podState == nil || podState?.unfinishedSetup == true {
             return [.replacePod]
         } else {
             return ConfigurationRow.allCases
@@ -262,6 +280,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     private enum ConfigurationRow: Int, CaseIterable {
         case suspendResume = 0
         case enableDisableConfirmationBeeps
+        case enableDisableAutoBolusBeeps
         case reminder
         case timeZoneOffset
         case insulinType
@@ -269,8 +288,8 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     }
     
     fileprivate enum StatusRow: Int, CaseIterable {
-        case activatedAt = 0
-        case expiresAt
+        case expiresAt = 0
+        case podActiveClock
         case bolus
         case basal
         case alarms
@@ -303,16 +322,16 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     
     override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
         switch sections[section] {
-        case .podDetails:
-            return LocalizedString("Pod Details", comment: "The title of the device information section in settings")
-        case .diagnostics:
-            return LocalizedString("Diagnostics", comment: "The title of the configuration section in settings")
-        case .configuration:
-            return nil
         case .status:
-            return nil
+            return nil  // No title, appears below a pod picture
+        case .configuration:
+            return LocalizedString("Configuration", comment: "The title of the configuration section in settings")
         case .rileyLinks:
             return super.tableView(tableView, titleForHeaderInSection: section)
+        case .diagnostics:
+            return LocalizedString("Diagnostics", comment: "The title of the diagnostics section in settings")
+        case .podDetails:
+            return LocalizedString("Pod Details", comment: "The title of the pod details section in settings")
         case .deletePumpManager:
             return " "  // Use an empty string for more dramatic spacing
         }
@@ -329,66 +348,73 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         switch sections[indexPath.section] {
-        case .podDetails:
+        case .status:
             let podState = self.podState!
-            switch PodDetailsRow(rawValue: indexPath.row)! {
-            case .podAddress:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("Assigned Address", comment: "The title text for the address assigned to the pod")
-                cell.detailTextLabel?.text = String(format:"%04X", podState.address)
-                return cell
-            case .podLot:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("Lot", comment: "The title of the cell showing the pod lot id")
-                cell.detailTextLabel?.text = String(format:"L%d", podState.lot)
-                return cell
-            case .podTid:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("TID", comment: "The title of the cell showing the pod TID")
-                cell.detailTextLabel?.text = String(format:"%07d", podState.tid)
-                return cell
-            case .piVersion:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("PI Version", comment: "The title of the cell showing the pod pi version")
-                cell.detailTextLabel?.text = podState.piVersion
-                return cell
-            case .pmVersion:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("PM Version", comment: "The title of the cell showing the pod pm version")
-                cell.detailTextLabel?.text = podState.pmVersion
+            let statusRow = StatusRow(rawValue: indexPath.row)!
+            if statusRow == .alarms {
+                let cell = tableView.dequeueReusableCell(withIdentifier: AlarmsTableViewCell.className, for: indexPath) as! AlarmsTableViewCell
+                cell.textLabel?.text = LocalizedString("Alarms", comment: "The title of the cell showing alarm status")
+                cell.alerts = podState.activeAlerts
                 return cell
             }
-        case .diagnostics:
-            
-            switch Diagnostics(rawValue: indexPath.row)! {
-            case .readPodStatus:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("Read Pod Status", comment: "The title of the command to read the pod status")
-                cell.accessoryType = .disclosureIndicator
-                return cell
-            case .playTestBeeps:
-                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("Play Test Beeps", comment: "The title of the command to play test beeps")
-                cell.accessoryType = .disclosureIndicator
-                return cell
-            case .readPulseLog:
+            let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+
+            switch statusRow {
+            case .expiresAt:
                 let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("Read Pulse Log", comment: "The title of the command to read the pulse log")
-                cell.accessoryType = .disclosureIndicator
+                if let expiresAt = podState.expiresAt {
+                    if expiresAt.timeIntervalSinceNow > 0 {
+                        cell.textLabel?.text = LocalizedString("Expires", comment: "The title of the cell showing the pod expiration")
+                    } else {
+                        cell.textLabel?.text = LocalizedString("Expired", comment: "The title of the cell showing the pod expiration after expiry")
+                    }
+                }
+                cell.setDetailDate(podState.expiresAt, formatter: dateFormatter)
                 return cell
-            case .testCommand:
+            case .podActiveClock:
                 let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                cell.textLabel?.text = LocalizedString("Test Command", comment: "The title of the command to run the test command")
-                cell.accessoryType = .disclosureIndicator
+                cell.textLabel?.text = LocalizedString("Pod Active Clock", comment: "The title of the cell showing the pod active clock")
+                cell.setDetailAge(podState.expiresAt?.addingTimeInterval(-Pod.nominalPodLife).timeIntervalSinceNow)
                 return cell
+            case .bolus:
+                cell.textLabel?.text = LocalizedString("Bolus Delivery", comment: "The title of the cell showing pod bolus status")
+
+                let deliveredUnits: Double?
+                if let dose = podState.unfinalizedBolus {
+                    deliveredUnits = pumpManager.roundToSupportedBolusVolume(units: dose.progress * dose.units)
+                } else {
+                    deliveredUnits = nil
+                }
+
+                cell.setDetailBolus(suspended: podState.isSuspended, dose: podState.unfinalizedBolus, deliveredUnits: deliveredUnits)
+                // TODO: This timer is in the wrong context; should be part of a custom bolus progress cell
+//              if bolusProgressTimer == nil {
+//                  bolusProgressTimer = Timer.scheduledTimer(withTimeInterval: .seconds(2), repeats: true) { [weak self] (_) in
+//                      self?.tableView.reloadRows(at: [indexPath], with: .none)
+//                  }
+//              }
+            case .basal:
+                cell.textLabel?.text = LocalizedString("Basal Delivery", comment: "The title of the cell showing pod basal status")
+                cell.setDetailBasal(suspended: podState.isSuspended, dose: podState.unfinalizedTempBasal)
+            case .reservoirLevel:
+                cell.textLabel?.text = LocalizedString("Reservoir", comment: "The title of the cell showing reservoir status")
+                cell.setReservoirDetail(podState.lastInsulinMeasurements)
+            case .deliveredInsulin:
+                cell.textLabel?.text = LocalizedString("Insulin Delivered", comment: "The title of the cell showing delivered insulin")
+                cell.setDeliveredInsulinDetail(podState.lastInsulinMeasurements)
+            default:
+                break
             }
-        case .configuration:
+            return cell
 
+        case .configuration:
             switch configurationRows[indexPath.row] {
             case .suspendResume:
                 return suspendResumeTableViewCell
             case .enableDisableConfirmationBeeps:
                 return confirmationBeepsTableViewCell
+            case .enableDisableAutoBolusBeeps:
+                return autoBolusBeepsTableViewCell
             case .reminder:
                 let cell = tableView.dequeueReusableCell(withIdentifier: ExpirationReminderDateTableViewCell.className, for: indexPath) as! ExpirationReminderDateTableViewCell
                 if let podState = podState, let reminderDate = pumpManager.expirationReminderDate {
@@ -436,7 +462,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
                     cell.textLabel?.text = LocalizedString("Pair New Pod", comment: "The title of the command to pair new pod")
                 } else if let podState = podState, podState.isFaulted {
                     cell.textLabel?.text = LocalizedString("Replace Pod Now", comment: "The title of the command to replace pod when there is a pod fault")
-                } else if let podState = podState, podState.unfinishedPairing {
+                } else if let podState = podState, podState.unfinishedSetup {
                     cell.textLabel?.text = LocalizedString("Finish pod setup", comment: "The title of the command to finish pod setup")
                 } else {
                     cell.textLabel?.text = LocalizedString("Replace Pod", comment: "The title of the command to replace pod")
@@ -447,67 +473,63 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
                 return cell
             }
             
-        case .status:
-            let podState = self.podState!
-            let statusRow = StatusRow(rawValue: indexPath.row)!
-            if statusRow == .alarms {
-                let cell = tableView.dequeueReusableCell(withIdentifier: AlarmsTableViewCell.className, for: indexPath) as! AlarmsTableViewCell
-                cell.textLabel?.text = LocalizedString("Alarms", comment: "The title of the cell showing alarm status")
-                cell.alerts = podState.activeAlerts
+        case .rileyLinks:
+            return super.tableView(tableView, cellForRowAt: indexPath)
+
+        case .diagnostics:
+            switch Diagnostics(rawValue: indexPath.row)! {
+            case .readPodStatus:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("Read Pod Status", comment: "The title of the command to read the pod status")
+                cell.accessoryType = .disclosureIndicator
                 return cell
-            } else {
+            case .playTestBeeps:
                 let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                
-                switch statusRow {
-                case .activatedAt:
-                    let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                    cell.textLabel?.text = LocalizedString("Active Time", comment: "The title of the cell showing the pod activated at time")
-                    cell.setDetailAge(podState.activatedAt?.timeIntervalSinceNow)
-                    return cell
-                case .expiresAt:
-                    let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
-                    if let expiresAt = podState.expiresAt {
-                        if expiresAt.timeIntervalSinceNow > 0 {
-                            cell.textLabel?.text = LocalizedString("Expires", comment: "The title of the cell showing the pod expiration")
-                        } else {
-                            cell.textLabel?.text = LocalizedString("Expired", comment: "The title of the cell showing the pod expiration after expiry")
-                        }
-                    }
-                    cell.setDetailDate(podState.expiresAt, formatter: dateFormatter)
-                    return cell
-                case .bolus:
-                    cell.textLabel?.text = LocalizedString("Bolus Delivery", comment: "The title of the cell showing pod bolus status")
-
-                    let deliveredUnits: Double?
-                    if let dose = podState.unfinalizedBolus {
-                        deliveredUnits = pumpManager.roundToSupportedBolusVolume(units: dose.progress * dose.units)
-                    } else {
-                        deliveredUnits = nil
-                    }
+                cell.textLabel?.text = LocalizedString("Play Test Beeps", comment: "The title of the command to play test beeps")
+                cell.accessoryType = .disclosureIndicator
+                return cell
+            case .readPulseLog:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("Read Pulse Log", comment: "The title of the command to read the pulse log")
+                cell.accessoryType = .disclosureIndicator
+                return cell
+            case .testCommand:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("Test Command", comment: "The title of the command to run the test command")
+                cell.accessoryType = .disclosureIndicator
+                return cell
+            }
 
-                    cell.setDetailBolus(suspended: podState.isSuspended, dose: podState.unfinalizedBolus, deliveredUnits: deliveredUnits)
-                    // TODO: This timer is in the wrong context; should be part of a custom bolus progress cell
-//                    if bolusProgressTimer == nil {
-//                        bolusProgressTimer = Timer.scheduledTimer(withTimeInterval: .seconds(2), repeats: true) { [weak self] (_) in
-//                            self?.tableView.reloadRows(at: [indexPath], with: .none)
-//                        }
-//                    }
-                case .basal:
-                    cell.textLabel?.text = LocalizedString("Basal Delivery", comment: "The title of the cell showing pod basal status")
-                    cell.setDetailBasal(suspended: podState.isSuspended, dose: podState.unfinalizedTempBasal)
-                case .reservoirLevel:
-                    cell.textLabel?.text = LocalizedString("Reservoir", comment: "The title of the cell showing reservoir status")
-                    cell.setReservoirDetail(podState.lastInsulinMeasurements)
-                case .deliveredInsulin:
-                    cell.textLabel?.text = LocalizedString("Insulin Delivered", comment: "The title of the cell showing delivered insulin")
-                    cell.setDeliveredInsulinDetail(podState.lastInsulinMeasurements)
-                default:
-                    break
-                }
+        case .podDetails:
+            let podState = self.podState!
+            switch PodDetailsRow(rawValue: indexPath.row)! {
+            case .podAddress:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("Assigned Address", comment: "The title text for the address assigned to the pod")
+                cell.detailTextLabel?.text = String(format:"%04X", podState.address)
+                return cell
+            case .podLot:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("Lot", comment: "The title of the cell showing the pod lot id")
+                cell.detailTextLabel?.text = String(format:"L%d", podState.lot)
+                return cell
+            case .podTid:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("TID", comment: "The title of the cell showing the pod TID")
+                cell.detailTextLabel?.text = String(format:"%07d", podState.tid)
+                return cell
+            case .piVersion:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("PI Version", comment: "The title of the cell showing the pod pi version")
+                cell.detailTextLabel?.text = podState.piVersion
+                return cell
+            case .pmVersion:
+                let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath)
+                cell.textLabel?.text = LocalizedString("PM Version", comment: "The title of the cell showing the pod pm version")
+                cell.detailTextLabel?.text = podState.pmVersion
                 return cell
             }
-        case .rileyLinks:
-            return super.tableView(tableView, cellForRowAt: indexPath)
+
         case .deletePumpManager:
             let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
             
@@ -521,8 +543,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     
     override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
         switch sections[indexPath.section] {
-        case .podDetails:
-            return false
         case .status:
             switch StatusRow(rawValue: indexPath.row)! {
             case .alarms:
@@ -530,12 +550,13 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
             default:
                 return false
             }
-        case .diagnostics, .configuration, .rileyLinks, .deletePumpManager:
+        case .configuration, .rileyLinks, .diagnostics, .deletePumpManager:
             return true
+        case .podDetails:
+            return false
         }
     }
 
-
     override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
         if indexPath == IndexPath(row: ConfigurationRow.reminder.rawValue, section: Section.configuration.rawValue) {
             tableView.beginUpdates()
@@ -547,27 +568,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
         let sender = tableView.cellForRow(at: indexPath)
         
         switch sections[indexPath.section] {
-        case .podDetails:
-            break
-        case .diagnostics:
-            switch Diagnostics(rawValue: indexPath.row)! {
-            case .readPodStatus:
-                let vc = CommandResponseViewController.readPodStatus(pumpManager: pumpManager)
-                vc.title = sender?.textLabel?.text
-                show(vc, sender: indexPath)
-            case .playTestBeeps:
-                let vc = CommandResponseViewController.playTestBeeps(pumpManager: pumpManager)
-                vc.title = sender?.textLabel?.text
-                show(vc, sender: indexPath)
-            case .readPulseLog:
-                let vc = CommandResponseViewController.readPulseLog(pumpManager: pumpManager)
-                vc.title = sender?.textLabel?.text
-                show(vc, sender: indexPath)
-            case .testCommand:
-                let vc = CommandResponseViewController.testingCommands(pumpManager: pumpManager)
-                vc.title = sender?.textLabel?.text
-                show(vc, sender: indexPath)
-            }
         case .status:
             switch StatusRow(rawValue: indexPath.row)! {
             case .alarms:
@@ -599,10 +599,12 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
             case .enableDisableConfirmationBeeps:
                 confirmationBeepsTapped()
                 tableView.deselectRow(at: indexPath, animated: true)
+            case .enableDisableAutoBolusBeeps:
+                autoBolusBeepsTapped()
+                tableView.deselectRow(at: indexPath, animated: true)
             case .reminder:
                 tableView.deselectRow(at: indexPath, animated: true)
                 tableView.endUpdates()
-                break
             case .timeZoneOffset:
                 let vc = CommandResponseViewController.changeTime(pumpManager: pumpManager)
                 vc.title = sender?.textLabel?.text
@@ -616,11 +618,11 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
                 show(vc, sender: sender)
             case .replacePod:
                 let vc: UIViewController
-                if podState == nil || podState!.setupProgress.primingNeeded {
-                    vc = PodReplacementNavigationController.instantiateNewPodFlow(pumpManager)
-                } else if let podState = podState, podState.isFaulted {
+                if let podState = podState, podState.isFaulted {
                     vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager)
-                } else if let podState = podState, podState.unfinishedPairing {
+                } else if podState == nil || podState!.setupProgress.primingNeeded {
+                    vc = PodReplacementNavigationController.instantiateNewPodFlow(pumpManager)
+                } else if let podState = podState, podState.unfinishedSetup {
                     vc = PodReplacementNavigationController.instantiateInsertCannulaFlow(pumpManager)
                 } else {
                     vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager)
@@ -647,6 +649,27 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
             )
             
             self.show(vc, sender: sender)
+        case .diagnostics:
+            switch Diagnostics(rawValue: indexPath.row)! {
+            case .readPodStatus:
+                let vc = CommandResponseViewController.readPodStatus(pumpManager: pumpManager)
+                vc.title = sender?.textLabel?.text
+                show(vc, sender: indexPath)
+            case .playTestBeeps:
+                let vc = CommandResponseViewController.playTestBeeps(pumpManager: pumpManager)
+                vc.title = sender?.textLabel?.text
+                show(vc, sender: indexPath)
+            case .readPulseLog:
+                let vc = CommandResponseViewController.readPulseLog(pumpManager: pumpManager)
+                vc.title = sender?.textLabel?.text
+                show(vc, sender: indexPath)
+            case .testCommand:
+                let vc = CommandResponseViewController.testingCommands(pumpManager: pumpManager)
+                vc.title = sender?.textLabel?.text
+                show(vc, sender: indexPath)
+            }
+        case .podDetails:
+            break
         case .deletePumpManager:
             let confirmVC = UIAlertController(pumpManagerDeletionHandler: {
                 self.pumpManager.notifyDelegateOfDeactivation {
@@ -664,23 +687,20 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
     
     override func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
         switch sections[indexPath.section] {
-        case .podDetails, .status:
+        case .status:
             break
-        case .diagnostics:
-            switch Diagnostics(rawValue: indexPath.row)! {
-            case .readPodStatus, .playTestBeeps, .readPulseLog, .testCommand:
-                tableView.reloadRows(at: [indexPath], with: .fade)
-            }
         case .configuration:
             switch configurationRows[indexPath.row] {
-            case .suspendResume, .enableDisableConfirmationBeeps, .reminder:
+            case .suspendResume, .enableDisableConfirmationBeeps, .enableDisableAutoBolusBeeps, .reminder:
                 break
             case .timeZoneOffset, .replacePod, .insulinType:
                 tableView.reloadRows(at: [indexPath], with: .fade)
             }
         case .rileyLinks:
             break
-        case .deletePumpManager:
+        case .diagnostics:
+            tableView.reloadRows(at: [indexPath], with: .fade)
+        case .podDetails, .deletePumpManager:
             break
         }
         
@@ -712,39 +732,59 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController {
         }
     }
 
-    private func confirmationBeepsTapped() {
-        let confirmationBeeps: Bool = pumpManager.confirmationBeeps
-        
+    private func setConfirmationBeeps(confirmationBeeps: Bool) {
         func done() {
             DispatchQueue.main.async { [weak self] in
                 if let self = self {
                     self.confirmationBeepsTableViewCell.updateTextLabel(enabled: self.pumpManager.confirmationBeeps)
                     self.confirmationBeepsTableViewCell.isLoading = false
+                    self.autoBolusBeepsTableViewCell.isEnabled = self.pumpManager.confirmationBeeps
                 }
             }
         }
 
         confirmationBeepsTableViewCell.isLoading = true
-        if confirmationBeeps {
-            pumpManager.setConfirmationBeeps(enabled: false, completion: { (error) in
-                if let error = error {
-                    DispatchQueue.main.async {
-                        let title = LocalizedString("Error disabling confirmation beeps", comment: "The alert title for disable confirmation beeps error")
-                        self.present(UIAlertController(with: error, title: title), animated: true)
+        pumpManager.setConfirmationBeeps(enabled: confirmationBeeps, completion: { (error) in
+            if let error = error {
+                DispatchQueue.main.async {
+                    let title: String
+                    if confirmationBeeps {
+                        title = LocalizedString("Error enabling confirmation beeps", comment: "The alert title for enable confirmation beeps error")
+                    } else {
+                        title = LocalizedString("Error disabling confirmation beeps", comment: "The alert title for disable confirmation beeps error")
                     }
+                    self.present(UIAlertController(with: error, title: title), animated: true)
                 }
-                done()
-            })
-        } else {
-            pumpManager.setConfirmationBeeps(enabled: true, completion: { (error) in
-                if let error = error {
-                    DispatchQueue.main.async {
-                        let title = LocalizedString("Error enabling confirmation beeps", comment: "The alert title for enable confirmation beeps error")
-                        self.present(UIAlertController(with: error, title: title), animated: true)
-                    }
+            }
+            done()
+        })
+    }
+
+    private func confirmationBeepsTapped() {
+        setConfirmationBeeps(confirmationBeeps: !pumpManager.confirmationBeeps)
+    }
+
+    private func autoBolusBeepsTapped() {
+        let newValue = !pumpManager.automaticBolusBeeps
+        pumpManager.automaticBolusBeeps = newValue
+
+        func done() {
+            DispatchQueue.main.async { [weak self] in
+                if let self = self {
+                    self.autoBolusBeepsTableViewCell.updateTextLabel(enabled: newValue)
+                    self.autoBolusBeepsTableViewCell.isLoading = false
                 }
-                done()
+            }
+        }
+
+        // Beep if confirmation beeps are enabled else just update the value displayed
+        if pumpManager.confirmationBeeps {
+            self.autoBolusBeepsTableViewCell.isLoading = true
+            pumpManager.setConfirmationBeeps(enabled: true, completion: { (error) in
+                done() // no worries if confirmation beep fails for any reason
             })
+        } else {
+            self.autoBolusBeepsTableViewCell.updateTextLabel(enabled: newValue)
         }
     }
 }
@@ -799,7 +839,7 @@ extension OmnipodSettingsViewController: PodStateObserver {
             return
         }
 
-        let reloadRows: [StatusRow] = [.bolus, .basal, .reservoirLevel, .deliveredInsulin]
+        let reloadRows: [StatusRow] = [.podActiveClock, .bolus, .basal, .reservoirLevel, .deliveredInsulin]
         self.tableView.reloadRows(at: reloadRows.map({ IndexPath(row: $0.rawValue, section: statusIdx) }), with: .none)
 
         if oldState?.activeAlerts != state?.activeAlerts,
@@ -852,14 +892,15 @@ private extension TimeInterval {
     func format(using units: NSCalendar.Unit) -> String? {
         let formatter = DateComponentsFormatter()
         formatter.allowedUnits = units
-        formatter.unitsStyle = .full
+        formatter.unitsStyle = .short
         formatter.zeroFormattingBehavior = .dropLeading
         formatter.maximumUnitCount = 2
-        
+
         return formatter.string(from: self)
     }
 }
 
+
 class AlarmsTableViewCell: LoadingTableViewCell {
     
     private var defaultDetailColor: UIColor?
@@ -943,7 +984,7 @@ private extension UITableViewCell {
     
     func setDetailAge(_ age: TimeInterval?) {
         if let age = age {
-            detailTextLabel?.text = fabs(age).format(using: [.day, .hour, .minute])
+            detailTextLabel?.text = fabs(age).format(using: [.hour, .minute])
         } else {
             detailTextLabel?.text = ""
         }
@@ -977,8 +1018,6 @@ private extension UITableViewCell {
                 self.detailTextLabel?.text = String(format: LocalizedString("%@ U of %@ U (%@)", comment: "Format string for bolus progress. (1: The delivered amount) (2: The programmed amount) (3: the percent progress)"), deliveredUnits, units, progressStr)
             }
         }
-
-
     }
     
     func setDeliveredInsulinDetail(_ measurements: PodInsulinMeasurements?) {

+ 15 - 6
Dependencies/rileylink_ios/OmniKitUI/ViewControllers/PairPodSetupViewController.swift

@@ -134,7 +134,17 @@ class PairPodSetupViewController: SetupTableViewController {
                 errorStrings = [lastError?.localizedDescription].compactMap { $0 }
             }
             
-            if let commsError = lastError as? PodCommsError, commsError.possibleWeakCommsCause {
+            var podCommsError: PodCommsError? = nil
+            if let pumpManagerError = lastError as? PumpManagerError {
+                switch pumpManagerError {
+                case .communication(let error):
+                    podCommsError = error as? PodCommsError
+                default:
+                    break
+                }
+            }
+
+            if let podCommsError = podCommsError, podCommsError.possibleWeakCommsCause {
                 if previouslyEncounteredWeakComms {
                     errorStrings.append(LocalizedString("If the problem persists, move to a new area and try again", comment: "Additional pairing recovery suggestion on multiple pairing failures"))
                 } else {
@@ -152,12 +162,11 @@ class PairPodSetupViewController: SetupTableViewController {
             }
             loadingText = errorText
             
-            // If we have an error, update the continue state
-            if let podCommsError = lastError as? PodCommsError {
-                switch podCommsError {
-                case .podFault, .activationTimeExceeded:
+            // If we have an error, update the continue state appropriately
+            if let podCommsError = podCommsError {
+                if podCommsError.isFaulted {
                     continueState = .fault
-                default:
+                } else {
                     continueState = .initial
                 }
             } else if lastError != nil {

+ 7 - 0
Dependencies/rileylink_ios/OmniKitUI/ViewControllers/ReplacePodViewController.swift

@@ -16,6 +16,7 @@ class ReplacePodViewController: SetupTableViewController {
     enum PodReplacementReason {
         case normal
         case activationTimeout
+        case podIncompatible
         case fault(_ podFault: DetailedStatus)
         case canceledPairingBeforeApplication
         case canceledPairing
@@ -29,6 +30,8 @@ class ReplacePodViewController: SetupTableViewController {
                 break // Text set in interface builder
             case .activationTimeout:
                 instructionsLabel.text = LocalizedString("Activation time exceeded. The pod must be deactivated before pairing with a new one. Please deactivate and discard pod.", comment: "Instructions when deactivating pod that didn't complete activation in time.")
+            case .podIncompatible:
+                instructionsLabel.text = LocalizedString("Unable to use incompatible pod. The pod must be deactivated before pairing with a new one. Please deactivate and discard pod.", comment: "Instructions when deactivating an incompatible pod")
             case .fault(let podFault):
                 var faultDescription = podFault.faultEventCode.localizedDescription
                 if let refString = podFault.pdmRef {
@@ -55,6 +58,10 @@ class ReplacePodViewController: SetupTableViewController {
                 } else {
                     self.replacementReason = .fault(podFault)
                 }
+            } else if podState?.setupProgress == .activationTimeout {
+                self.replacementReason = .activationTimeout
+            } else if podState?.setupProgress == .podIncompatible {
+                self.replacementReason = .podIncompatible
             } else if podState?.setupProgress.primingNeeded == true {
                 self.replacementReason = .canceledPairingBeforeApplication
             } else if podState?.setupProgress.needsCannulaInsertion == true {

+ 2 - 2
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkConnectionManager.swift

@@ -8,7 +8,7 @@
 
 import Foundation
 
-public protocol RileyLinkConnectionManagerDelegate : class {
+public protocol RileyLinkConnectionManagerDelegate : AnyObject {
     func rileyLinkConnectionManager(_ rileyLinkConnectionManager: RileyLinkConnectionManager, didChange state: RileyLinkConnectionManagerState)
 }
 
@@ -85,7 +85,7 @@ public class RileyLinkConnectionManager {
     }
 }
 
-public protocol RileyLinkDeviceProvider: class {
+public protocol RileyLinkDeviceProvider: AnyObject {
     func getDevices(_ completion: @escaping (_ devices: [RileyLinkDevice]) -> Void)
     var idleListeningEnabled: Bool { get }
     var timerTickEnabled: Bool { get set }

+ 1 - 1
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDeviceError.swift

@@ -51,7 +51,7 @@ extension RileyLinkDeviceError: LocalizedError {
         case .peripheralManagerError(let error):
             return error.recoverySuggestion
         case .commandsBlocked:
-            return LocalizedString("RileyLink may need to be turned off and back on.", comment: "commandsBlocked recovery suggestion")
+            return LocalizedString("RileyLink may need to be turned off and back on", comment: "commandsBlocked recovery suggestion")
         default:
             return nil
         }