Przeglądaj źródła

Update all frameworks Loop

avouspierre 3 lat temu
rodzic
commit
857f78db5f
83 zmienionych plików z 2399 dodań i 1256 usunięć
  1. 12 0
      Dependencies/LoopKit/LoopKit.xcodeproj/project.pbxproj
  2. 8 3
      Dependencies/LoopKit/LoopKit/DeviceManager/PumpManager.swift
  3. 3 3
      Dependencies/LoopKit/LoopKit/Insulin/ExponentialInsulinModelPreset.swift
  4. 5 2
      Dependencies/LoopKit/LoopKit/InsulinKit/DoseStore.swift
  5. 7 25
      Dependencies/LoopKit/LoopKit/InsulinKit/InsulinMath.swift
  6. 1 1
      Dependencies/LoopKit/LoopKit/QuantityFormatter.swift
  7. 1 2
      Dependencies/LoopKit/LoopKit/TherapySettings.swift
  8. 63 6
      Dependencies/LoopKit/LoopKitTests/DoseStoreTests.swift
  9. 282 0
      Dependencies/LoopKit/LoopKitTests/Fixtures/InsulinKit/reservoir_iob_test.json
  10. 2 2
      Dependencies/LoopKit/LoopKitTests/HKUnitTests.swift
  11. 13 12
      Dependencies/LoopKit/LoopKitUI/CarbKit/FoodEmojiDataSource.swift
  12. 5 0
      Dependencies/LoopKit/LoopKitUI/View Controllers/EmojiInputController.swift
  13. 3 0
      Dependencies/LoopKit/LoopKitUI/ViewModels/TherapySettingsViewModel.swift
  14. 5 1
      Dependencies/LoopKit/LoopKitUI/Views/Information Screens/InformationView.swift
  15. 16 2
      Dependencies/LoopKit/LoopKitUI/Views/ScheduleEditor.swift
  16. 8 1
      Dependencies/LoopKit/LoopKitUI/Views/Settings Editors/CorrectionRangeOverridesEditor.swift
  17. 8 2
      Dependencies/LoopKit/LoopKitUI/Views/Settings Editors/TherapySettingsView.swift
  18. 1 5
      Dependencies/LoopKit/MockKit/MockCGMManager.swift
  19. 1 1
      Dependencies/LoopKit/MockKit/MockPumpManager.swift
  20. 5 1
      Dependencies/LoopKit/MockKitUI/Views/DeliveryUncertaintyRecoveryView.swift
  21. 14 2
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/UnfinalizedDose.swift
  22. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift
  23. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift
  24. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewControllers/DashUICoordinator.swift
  25. 1 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ExpirationReminderSetupView.swift
  26. 7 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift
  27. 53 27
      Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManager.swift
  28. 5 0
      Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManagerError.swift
  29. 14 12
      Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManagerState.swift
  30. 164 0
      Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpMessageSender.swift
  31. 11 98
      Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpMessageSender.swift
  32. 34 29
      Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpOps.swift
  33. 2 31
      Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpOpsError.swift
  34. 1 1
      Dependencies/rileylink_ios/MinimedKit/PumpManager/RileyLinkDevice.swift
  35. 7 7
      Dependencies/rileylink_ios/MinimedKit/PumpManager/UnfinalizedDose.swift
  36. 34 100
      Dependencies/rileylink_ios/MinimedKitTests/MinimedPumpManagerTests.swift
  37. 10 10
      Dependencies/rileylink_ios/MinimedKitTests/PumpOpsSynchronousBuildFromFramesTests.swift
  38. 86 11
      Dependencies/rileylink_ios/MinimedKitTests/PumpOpsSynchronousTests.swift
  39. 118 0
      Dependencies/rileylink_ios/MinimedKitTests/ReconciliationTests.swift
  40. 5 0
      Dependencies/rileylink_ios/MinimedKitTests/TimestampedHistoryEventTests.swift
  41. 1 1
      Dependencies/rileylink_ios/MinimedKitUI/CommandResponseViewController.swift
  42. 3 2
      Dependencies/rileylink_ios/MinimedKitUI/MinimedPumpManager+UI.swift
  43. 9 6
      Dependencies/rileylink_ios/MinimedKitUI/MinimedPumpSettingsViewController.swift
  44. 14 8
      Dependencies/rileylink_ios/MinimedKitUI/Setup/MinimedPumpIDSetupViewController.swift
  45. 3 1
      Dependencies/rileylink_ios/MinimedKitUI/Setup/MinimedPumpManagerSetupViewController.swift
  46. 1 1
      Dependencies/rileylink_ios/MinimedKitUI/Setup/MinimedPumpSentrySetupViewController.swift
  47. 3 3
      Dependencies/rileylink_ios/OmniKit/OmnipodCommon/MessageBlocks/ErrorResponse.swift
  48. 8 8
      Dependencies/rileylink_ios/OmniKit/OmnipodCommon/MessageBlocks/VersionResponse.swift
  49. 14 2
      Dependencies/rileylink_ios/OmniKit/OmnipodCommon/UnfinalizedDose.swift
  50. 25 22
      Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManager.swift
  51. 6 6
      Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManagerState.swift
  52. 7 7
      Dependencies/rileylink_ios/OmniKit/PumpManager/PodComms.swift
  53. 3 3
      Dependencies/rileylink_ios/OmniKit/PumpManager/PodState.swift
  54. 10 10
      Dependencies/rileylink_ios/OmniKitTests/MessageTests.swift
  55. 1 1
      Dependencies/rileylink_ios/OmniKitTests/PodCommsSessionTests.swift
  56. 4 4
      Dependencies/rileylink_ios/OmniKitTests/PodStateTests.swift
  57. 7 7
      Dependencies/rileylink_ios/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift
  58. 1 1
      Dependencies/rileylink_ios/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift
  59. 3 10
      Dependencies/rileylink_ios/OmniKitUI/ViewModels/RileyLinkListDataSource.swift
  60. 1 1
      Dependencies/rileylink_ios/OmniKitUI/Views/DeliveryUncertaintyRecoveryView.swift
  61. 1 0
      Dependencies/rileylink_ios/OmniKitUI/Views/ExpirationReminderSetupView.swift
  62. 4 2
      Dependencies/rileylink_ios/OmniKitUI/Views/OmnipodSettingsView.swift
  63. 2 1
      Dependencies/rileylink_ios/OmniKitUI/Views/RileyLinkSetupView.swift
  64. 48 16
      Dependencies/rileylink_ios/RileyLink.xcodeproj/project.pbxproj
  65. 1 1
      Dependencies/rileylink_ios/RileyLink.xcodeproj/xcshareddata/xcschemes/OmniKitPacketParser.xcscheme
  66. 30 16
      Dependencies/rileylink_ios/RileyLink/DeviceDataManager.swift
  67. 2 2
      Dependencies/rileylink_ios/RileyLink/Extensions/UserDefaults.swift
  68. 2 2
      Dependencies/rileylink_ios/RileyLink/View Controllers/MainViewController.swift
  69. 6 4
      Dependencies/rileylink_ios/RileyLinkBLEKit/CommandSession.swift
  70. 1 1
      Dependencies/rileylink_ios/RileyLinkBLEKit/PeripheralManager+RileyLink.swift
  71. 616 0
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkBluetoothDevice.swift
  72. 340 0
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkBluetoothDeviceProvider.swift
  73. 39 0
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkConnectionState.swift
  74. 49 601
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDevice.swift
  75. 3 3
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDeviceError.swift
  76. 34 0
      Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDeviceProvider.swift
  77. 52 52
      Dependencies/rileylink_ios/RileyLinkKit/PumpOpsSession.swift
  78. 15 23
      Dependencies/rileylink_ios/RileyLinkKit/RileyLinkPumpManager.swift
  79. 2 2
      Dependencies/rileylink_ios/RileyLinkKitUI/RileyLinkDeviceTableViewController.swift
  80. 4 7
      Dependencies/rileylink_ios/RileyLinkKitUI/RileyLinkDevicesTableViewDataSource.swift
  81. 3 11
      Dependencies/rileylink_ios/RileyLinkKitUI/RileyLinkSetupTableViewController.swift
  82. 1 1
      FreeAPS/Sources/APS/DeviceDataManager.swift
  83. 2 2
      FreeAPS/Sources/APS/Extensions/UserDefaultsExtensions.swift

+ 12 - 0
Dependencies/LoopKit/LoopKit.xcodeproj/project.pbxproj

@@ -788,6 +788,7 @@
 		C1F8403923DB84B700673141 /* DeviceLogEntry+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8403723DB84B700673141 /* DeviceLogEntry+CoreDataClass.swift */; };
 		C1F8403A23DB84B700673141 /* DeviceLogEntry+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8403823DB84B700673141 /* DeviceLogEntry+CoreDataProperties.swift */; };
 		C1F8B1E2223C3CC000DD66CF /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4303C4901E2D664200ADEDC8 /* TimeZone.swift */; };
+		C1FAC06328C7B0A100754AE2 /* reservoir_iob_test.json in Resources */ = {isa = PBXBuildFile; fileRef = C1FAC06228C7B0A100754AE2 /* reservoir_iob_test.json */; };
 		C1FAEC1D264AD6B400A3250B /* DeviceStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAEC1C264AD6B400A3250B /* DeviceStatusBadge.swift */; };
 		C1FAEC1F264AE12700A3250B /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47ECF8725DC20810024A54D /* UIImage.swift */; };
 		C1FAEC21264AEEA300A3250B /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAEC20264AEEA300A3250B /* UIImage.swift */; };
@@ -1641,6 +1642,7 @@
 		C1E84B8425C62FB100623C08 /* Modelv1v4.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = Modelv1v4.xcmappingmodel; sourceTree = "<group>"; };
 		C1F8403723DB84B700673141 /* DeviceLogEntry+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeviceLogEntry+CoreDataClass.swift"; sourceTree = "<group>"; };
 		C1F8403823DB84B700673141 /* DeviceLogEntry+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeviceLogEntry+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		C1FAC06228C7B0A100754AE2 /* reservoir_iob_test.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = reservoir_iob_test.json; sourceTree = "<group>"; };
 		C1FAEC1C264AD6B400A3250B /* DeviceStatusBadge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceStatusBadge.swift; sourceTree = "<group>"; };
 		C1FAEC20264AEEA300A3250B /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
 		E9077D2624ACD59F0066A88D /* InformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InformationView.swift; sourceTree = "<group>"; };
@@ -2277,6 +2279,7 @@
 		43D8FDC11C728FDF0073BE78 = {
 			isa = PBXGroup;
 			children = (
+				E9D95F9C24C8BC880079F47D /* Scripts */,
 				1F5DAB1B2118C91C00048054 /* Common */,
 				430059211CCDC7A200C861EA /* Extensions */,
 				43D8FDCD1C728FDF0073BE78 /* LoopKit */,
@@ -2614,6 +2617,7 @@
 				434FB6471D70096A007B9C70 /* reservoir_history_with_continuity_holes.json */,
 				43D8FED11C7294B80073BE78 /* reservoir_history_with_rewind_and_prime_input.json */,
 				43D8FED21C7294B80073BE78 /* reservoir_history_with_rewind_and_prime_output.json */,
+				C1FAC06228C7B0A100754AE2 /* reservoir_iob_test.json */,
 				43B99B031C74538D00D050F5 /* short_basal_dose.json */,
 				4378B6441ED55F8C000AE785 /* suspend_dose_reconciled_normalized_iob.json */,
 				4378B6451ED55F8C000AE785 /* suspend_dose_reconciled_normalized.json */,
@@ -2915,6 +2919,13 @@
 			path = "Settings Editors";
 			sourceTree = "<group>";
 		};
+		E9D95F9C24C8BC880079F47D /* Scripts */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			path = Scripts;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -3388,6 +3399,7 @@
 				4322B799202FA3CC0002837D /* grouped_by_overlapping_absorption_times_output.json in Resources */,
 				435D292D205F48750026F401 /* counteraction_effect_falling_glucose_insulin.json in Resources */,
 				437874D3202FDD2D00A3D8B9 /* short_basal_dose.json in Resources */,
+				C1FAC06328C7B0A100754AE2 /* reservoir_iob_test.json in Resources */,
 				437874C6202FDD2D00A3D8B9 /* iob_from_doses_output.json in Resources */,
 				437874D6202FDD2D00A3D8B9 /* suspend_dose_reconciled.json in Resources */,
 				4343951F205EED1F0056DC37 /* counteraction_effect_falling_glucose_output.json in Resources */,

+ 8 - 3
Dependencies/LoopKit/LoopKit/DeviceManager/PumpManager.swift

@@ -54,8 +54,13 @@ public protocol PumpManagerDelegate: DeviceManagerDelegate, PumpManagerStatusObs
     /// Reports an error that should be surfaced to the user
     func pumpManager(_ pumpManager: PumpManager, didError error: PumpManagerError)
 
-    /// This should be called any time the PumpManager synchronizes with the pump, even if there are no new events in the log.
-    func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastSync: Date?, completion: @escaping (_ error: Error?) -> Void)
+    /// This should be called any time the PumpManager synchronizes with the pump, even if there are no new doses in the log, as changes to lastReconciliation
+    /// indicate we can trust insulin delivery status up until that point, even if there are no new doses.
+    /// For pumps whose only source of dosing adjustments is Loop, lastReconciliation should be reflective of the last time we received telemetry from the pump.
+    /// For pumps with a user interface and dosing history capabilities, lastReconciliation should be reflective of the last time we reconciled fully with pump history, and know
+    /// that we have accounted for any external doses. It is possible for the pump to report reservoir data beyond the date of lastReconciliation, and Loop can use it for
+    /// calculating IOB.
+    func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void)
 
     func pumpManager(_ pumpManager: PumpManager, didReadReservoirValue units: Double, at date: Date, completion: @escaping (_ result: Result<(newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool), Error>) -> Void)
 
@@ -129,7 +134,7 @@ public protocol PumpManager: DeviceManager {
     /// The maximum reservoir volume of the pump
     var pumpReservoirCapacity: Double { get }
 
-    /// The time of the last sync with the pump's event history, or last status check if pump does not provide history.
+    /// The time of the last sync with the pump's event history, or reservoir,  or last status check if pump does not provide history.
     var lastSync: Date? { get }
     
     /// The most-recent status

+ 3 - 3
Dependencies/LoopKit/LoopKit/Insulin/ExponentialInsulinModelPreset.swift

@@ -25,7 +25,7 @@ extension ExponentialInsulinModelPreset {
         case .fiasp:
             return .minutes(360)
         case .lyumjev:
-            return .minutes(300) // 360
+            return .minutes(360)
         case .afrezza:
             return .minutes(300)
         }
@@ -40,7 +40,7 @@ extension ExponentialInsulinModelPreset {
         case .fiasp:
             return .minutes(55)
         case .lyumjev:
-            return .minutes(60) //55
+            return .minutes(55)
         case.afrezza:
             return .minutes(29)
         }
@@ -55,7 +55,7 @@ extension ExponentialInsulinModelPreset {
         case .fiasp:
             return .minutes(10)
         case .lyumjev:
-            return .minutes(5) //10
+            return .minutes(10)
         case.afrezza:
             return .minutes(10)
         }

+ 5 - 2
Dependencies/LoopKit/LoopKit/InsulinKit/DoseStore.swift

@@ -584,7 +584,9 @@ extension DoseStore {
                 throw DoseStoreError.configurationError
             }
 
-            let doses = try self.getReservoirObjects(since: start).reversed().doseEntries
+            // Attempt to get the reading before "start", so we can build those doses that have an end date after "start", but a start date before "start"
+            // Any extra doses will be filtered out below, via filterDateRange
+            let doses = try self.getReservoirObjects(since: start.addingTimeInterval(-.minutes(10))).reversed().doseEntries
 
             let normalizedDoses = doses.annotated(with: basalProfile)
             self.recentReservoirNormalizedDoseEntriesCache = normalizedDoses
@@ -1214,8 +1216,9 @@ extension DoseStore {
                         if self.areReservoirValuesValid, let reservoirEndDate = self.lastStoredReservoirValue?.startDate, reservoirEndDate > self.lastPumpEventsReconciliation ?? .distantPast {
                             let reservoirDoses = try self.getNormalizedReservoirDoseEntries(start: filteredStart, end: end)
                             let endOfReservoirData = self.lastStoredReservoirValue?.endDate ?? .distantPast
+                            let startOfReservoirData = reservoirDoses.first?.startDate ?? filteredStart
                             let mutableDoses = try self.getNormalizedMutablePumpEventDoseEntries(start: endOfReservoirData)
-                            doses = insulinDeliveryDoses + reservoirDoses.map({ $0.trimmed(from: filteredStart) }) + mutableDoses
+                            doses = insulinDeliveryDoses.map({ $0.trimmed(to: startOfReservoirData) }) + reservoirDoses + mutableDoses.map({ $0.trimmed(from: endOfReservoirData) })
                         } else {
                             // Includes mutable doses.
                             doses = insulinDeliveryDoses.appendedUnion(with: try self.getNormalizedPumpEventDoseEntries(start: filteredStart, end: end))

+ 7 - 25
Dependencies/LoopKit/LoopKit/InsulinKit/InsulinMath.swift

@@ -40,18 +40,10 @@ extension DoseEntry {
         }
 
         // Consider doses within the delta time window as momentary
-        //ken changes
-        //implement user set negative basal multiplier
-        let negativeBasalMultiplier = UserDefaults.standard.double(forKey: "negativeBasalMultiplier")
-        var modifiednetBasalUnits = netBasalUnits
-        if netBasalUnits < 0.0 {
-            modifiednetBasalUnits = netBasalUnits * negativeBasalMultiplier
-        }
-        //this used netBasalUnits as multiplier originally
         if endDate.timeIntervalSince(startDate) <= 1.05 * delta {
-            return modifiednetBasalUnits * model.percentEffectRemaining(at: time)
+            return netBasalUnits * model.percentEffectRemaining(at: time)
         } else {
-            return modifiednetBasalUnits * continuousDeliveryInsulinOnBoard(at: date, model: model, delta: delta)
+            return netBasalUnits * continuousDeliveryInsulinOnBoard(at: date, model: model, delta: delta)
         }
     }
 
@@ -85,21 +77,11 @@ extension DoseEntry {
         }
 
         // Consider doses within the delta time window as momentary
-        //ken changes
-            //if net basal is negative use a mulitplier (0-1)
-            //modified in user settings
-            
-            let negativeBasalMultiplier = UserDefaults.standard.double(forKey: "negativeBasalMultiplier")
-            var modifiednetBasalUnits = netBasalUnits
-            if netBasalUnits < 0.0 {
-                modifiednetBasalUnits = netBasalUnits * negativeBasalMultiplier
-            }
-            //originally used netBasalUnits
-            if endDate.timeIntervalSince(startDate) <= 1.05 * delta {
-                return modifiednetBasalUnits * -insulinSensitivity * (1.0 - model.percentEffectRemaining(at: time))
-            } else {
-                return modifiednetBasalUnits * -insulinSensitivity * continuousDeliveryGlucoseEffect(at: date, model: model, delta: delta)
-            }
+        if endDate.timeIntervalSince(startDate) <= 1.05 * delta {
+            return netBasalUnits * -insulinSensitivity * (1.0 - model.percentEffectRemaining(at: time))
+        } else {
+            return netBasalUnits * -insulinSensitivity * continuousDeliveryGlucoseEffect(at: date, model: model, delta: delta)
+        }
     }
 
     func trimmed(from start: Date? = nil, to end: Date? = nil, syncIdentifier: String? = nil) -> DoseEntry {

+ 1 - 1
Dependencies/LoopKit/LoopKit/QuantityFormatter.swift

@@ -179,7 +179,7 @@ public extension HKUnit {
     var pickerFractionDigits: Int {
         switch self {
         case .internationalUnit(), .internationalUnitsPerHour:
-            return 2
+            return 3
         case HKUnit.gram().unitDivided(by: .internationalUnit()):
             return 1
         case .millimolesPerLiter,

+ 1 - 2
Dependencies/LoopKit/LoopKit/TherapySettings.swift

@@ -40,8 +40,7 @@ public struct TherapySettings: Equatable {
             suspendThreshold != nil &&
             insulinSensitivitySchedule != nil &&
             carbRatioSchedule != nil &&
-            basalRateSchedule != nil &&
-            defaultRapidActingModel != nil
+            basalRateSchedule != nil
     }
     
     public init(

+ 63 - 6
Dependencies/LoopKit/LoopKitTests/DoseStoreTests.swift

@@ -12,11 +12,19 @@ import HealthKit
 
 class DoseStoreTests: PersistenceControllerTestCase {
 
-    func testEmptyDoseStoreReturnsZeroInsulinOnBoard() {
-        // 1. Create a DoseStore
-        let healthStore = HKHealthStoreMock()
+    func loadReservoirFixture(_ resourceName: String) -> [NewReservoirValue] {
 
-        let doseStore = DoseStore(
+        let fixture: [JSONDictionary] = loadFixture(resourceName)
+        let dateFormatter = ISO8601DateFormatter.localTimeDate(timeZone: .utcTimeZone)
+
+        return fixture.map {
+            return NewReservoirValue(startDate: dateFormatter.date(from: $0["date"] as! String)!, unitVolume: $0["amount"] as! Double)
+        }
+    }
+
+    func defaultStore(testingDate: Date? = nil) -> DoseStore {
+        let healthStore = HKHealthStoreMock()
+        return DoseStore(
             healthStore: healthStore,
             cacheStore: cacheStore,
             observationEnabled: false,
@@ -25,9 +33,20 @@ class DoseStoreTests: PersistenceControllerTestCase {
             basalProfile: BasalRateSchedule(rawValue: ["timeZone": -28800, "items": [["value": 0.75, "startTime": 0.0], ["value": 0.8, "startTime": 10800.0], ["value": 0.85, "startTime": 32400.0], ["value": 1.0, "startTime": 68400.0]]]),
             insulinSensitivitySchedule: InsulinSensitivitySchedule(rawValue: ["unit": "mg/dL", "timeZone": -28800, "items": [["value": 40.0, "startTime": 0.0], ["value": 35.0, "startTime": 21600.0], ["value": 40.0, "startTime": 57600.0]]]),
             syncVersion: 1,
-            provenanceIdentifier: Bundle.main.bundleIdentifier!
+            provenanceIdentifier: Bundle.main.bundleIdentifier!,
+            test_currentDate: testingDate
         )
-        
+    }
+
+    let testingDateFormatter = DateFormatter.descriptionFormatter
+
+    func testingDate(_ input: String) -> Date {
+        return testingDateFormatter.date(from: input)!
+    }
+
+    func testEmptyDoseStoreReturnsZeroInsulinOnBoard() {
+        let doseStore = defaultStore()
+
         let queryFinishedExpectation = expectation(description: "query finished")
         
         doseStore.insulinOnBoard(at: Date()) { (result) in
@@ -41,6 +60,44 @@ class DoseStoreTests: PersistenceControllerTestCase {
         }
         waitForExpectations(timeout: 3)
     }
+
+    func testGetNormalizedDoseEntriesUsingReservoir() {
+        let now = testingDate("2022-09-05 02:04:00 +0000")
+        let doseStore = defaultStore(testingDate: now)
+
+        let reservoirReadings = loadReservoirFixture("reservoir_iob_test")
+
+        let storageExpectations = expectation(description: "reservoir store finished")
+        storageExpectations.expectedFulfillmentCount = reservoirReadings.count + 1
+        for reading in reservoirReadings.reversed() {
+            doseStore.addReservoirValue(reading.unitVolume, at: reading.startDate) { _, _, _, _ in storageExpectations.fulfill() }
+        }
+
+        let bolusStart = testingDate("2022-09-05 01:49:47 +0000")
+        let bolusEnd = testingDate("2022-09-05 01:51:19 +0000")
+        let bolus = DoseEntry(type: .bolus, startDate: bolusStart, endDate: bolusEnd, value: 2.3, unit: .units, isMutable: true)
+        let pumpEvent = NewPumpEvent(date: bolus.startDate, dose: bolus, raw: Data(hexadecimalString: "0000")!, title: "Bolus 2.3U")
+
+        doseStore.addPumpEvents([pumpEvent], lastReconciliation: testingDate("2022-09-05 01:50:18 +0000")) { error in
+            storageExpectations.fulfill()
+        }
+        
+        waitForExpectations(timeout: 2)
+
+        let queryFinishedExpectation = expectation(description: "query finished")
+
+        doseStore.insulinOnBoard(at: now) { (result) in
+            switch result {
+            case .failure(let error):
+                XCTFail("Unexpected error: \(error)")
+            case .success(let value):
+                XCTAssertEqual(1.85, value.value, accuracy: 0.01)
+            }
+            queryFinishedExpectation.fulfill()
+        }
+        waitForExpectations(timeout: 3)
+    }
+
     
     func testPumpEventTypeDoseMigration() {
         cacheStore.managedObjectContext.performAndWait {

+ 282 - 0
Dependencies/LoopKit/LoopKitTests/Fixtures/InsulinKit/reservoir_iob_test.json

@@ -0,0 +1,282 @@
+[
+{
+   "date": "2022-09-05T02:00:22",
+   "amount": 41.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:55:16",
+   "amount": 41.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:50:03",
+   "amount": 43.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:45:17",
+   "amount": 43.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:36:13",
+   "amount": 43.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:25:25",
+   "amount": 43.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:20:15",
+   "amount": 43.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:15:15",
+   "amount": 43.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:05:31",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:05:24",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T01:00:24",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:55:19",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:55:15",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:50:20",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:50:16",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:45:22",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:40:18",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:35:22",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:30:19",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:25:15",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:20:13",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:15:27",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:15:24",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:10:21",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:10:17",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:05:15",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-05T00:00:59",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:54:49",
+   "amount": 43.6,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:50:15",
+   "amount": 44.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:45:16",
+   "amount": 44.2,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:40:18",
+   "amount": 44.2,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:35:15",
+   "amount": 44.3,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:30:27",
+   "amount": 44.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:30:24",
+   "amount": 44.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:25:19",
+   "amount": 44.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:25:15",
+   "amount": 44.5,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:20:03",
+   "amount": 44.9,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:15:17",
+   "amount": 47.9,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:10:17",
+   "amount": 47.9,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:05:15",
+   "amount": 47.9,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T23:00:15",
+   "amount": 47.9,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:55:17",
+   "amount": 48.0,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:55:13",
+   "amount": 48.0,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:50:15",
+   "amount": 48.0,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:45:19",
+   "amount": 48.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:45:15",
+   "amount": 48.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:40:02",
+   "amount": 48.9,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:35:15",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:30:15",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:25:13",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:20:19",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:15:16",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:10:15",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:05:25",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:05:22",
+   "amount": 49.1,
+   "unit": "U"
+},
+{
+   "date": "2022-09-04T22:00:04",
+   "amount": 49.1,
+   "unit": "U"
+}
+]

+ 2 - 2
Dependencies/LoopKit/LoopKitTests/HKUnitTests.swift

@@ -51,8 +51,8 @@ class HKUnitTests: XCTestCase {
     }
 
     func testPickerFractionDigits() throws {
-        XCTAssertEqual(HKUnit.internationalUnit().pickerFractionDigits, 2)
-        XCTAssertEqual(HKUnit.internationalUnit().unitDivided(by: .hour()).pickerFractionDigits, 2)
+        XCTAssertEqual(HKUnit.internationalUnit().pickerFractionDigits, 3)
+        XCTAssertEqual(HKUnit.internationalUnit().unitDivided(by: .hour()).pickerFractionDigits, 3)
 
         XCTAssertEqual(HKUnit.millimolesPerLiter.pickerFractionDigits, 1)
         XCTAssertEqual(HKUnit.millimolesPerLiter.unitDivided(by: .internationalUnit()).pickerFractionDigits, 1)

+ 13 - 12
Dependencies/LoopKit/LoopKitUI/CarbKit/FoodEmojiDataSource.swift

@@ -13,14 +13,11 @@ public func CarbAbsorptionInputController() -> EmojiInputController {
 private class FoodEmojiDataSource: EmojiDataSource {
     private static let fast: [String] = {
         var fast = [
-            "🍭", "🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍",
+            "🍭", "🍬", "🍯",
+            "🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍",
             "🍎", "🍏", "🍐", "🍑", "🍒", "🍓", "🥝",
-            "🍅", "🥔", "🥕", "🌽", "🌶", "🥒", "🥗", "🍄",
-            "🍞", "🥐", "🥖", "🥞", "🍿", "🍘", "🍙",
-            "🍚", "🍢", "🍣", "🍡", "🍦", "🍧", "🍨",
-            "🍩", "🍪", "🎂", "🍰", "🍫", "🍬", "🍮",
-            "🍯", "🍼", "🥛", "☕️", "🍵",
-            "🥥", "🥦", "🥨", "🥠", "🥧",
+            "🌽", "🍿", "🍘", "🍡", "🍦", "🍧", "🎂", "🥠",
+            "☕️",
         ]
 
         return fast
@@ -28,10 +25,14 @@ private class FoodEmojiDataSource: EmojiDataSource {
 
     private static let medium: [String] = {
         var medium = [
-            "🌮", "🍆", "🍟", "🍳", "🍲", "🍱", "🍛",
-            "🍜", "🍠", "🍤", "🍥", "🍹",
-            "🥪", "🥫", "🥟", "🥡",
-        ]
+            "🌮", "🍟", "🍳", "🍲", "🍱", "🍛",
+            "🍜", "🍠", "🍤", "🍥",
+            "🥪", "🥫", "🥟", "🥡", "🍢", "🍣",
+            "🍅", "🥔", "🥕", "🌶", "🥒", "🥗", "🍄", "🥦",
+            "🍆", "🥥", "🍞", "🥐", "🥖", "🥨", "🥞", "🍙", "🍚",
+            "🍼", "🥛", "🍮", "🥧",
+            "🍨", "🍩", "🍪", "🍰", "🍫",
+       ]
 
         return medium
     }()
@@ -48,7 +49,7 @@ private class FoodEmojiDataSource: EmojiDataSource {
     private static let other: [String] = {
         var other = [
             "🍶", "🍾", "🍷", "🍸", "🍺", "🍻", "🥂", "🥃",
-            "🥣", "🥤", "🥢",
+            "🍹", "🥣", "🥤", "🥢", "🍵",
             "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣",
             "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"
         ]

+ 5 - 0
Dependencies/LoopKit/LoopKitUI/View Controllers/EmojiInputController.swift

@@ -39,6 +39,11 @@ public class EmojiInputController: UIInputViewController, UICollectionViewDataSo
         }
 
         setupSectionIndex()
+
+        // Scroll to medium absorption
+        DispatchQueue.main.async {
+            self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 1), at: .left, animated: false)
+        }
     }
 
     private func setupSectionIndex() {

+ 3 - 0
Dependencies/LoopKit/LoopKitUI/ViewModels/TherapySettingsViewModel.swift

@@ -23,6 +23,7 @@ public class TherapySettingsViewModel: ObservableObject {
     @Published public var therapySettings: TherapySettings
     private let initialTherapySettings: TherapySettings
     let sensitivityOverridesEnabled: Bool
+    let adultChildInsulinModelSelectionEnabled: Bool
     public var prescription: Prescription?
 
     private weak var delegate: TherapySettingsViewModelDelegate?
@@ -30,11 +31,13 @@ public class TherapySettingsViewModel: ObservableObject {
     public init(therapySettings: TherapySettings,
                 pumpSupportedIncrements: (() -> PumpSupportedIncrements?)? = nil,
                 sensitivityOverridesEnabled: Bool = false,
+                adultChildInsulinModelSelectionEnabled: Bool = false,
                 prescription: Prescription? = nil,
                 delegate: TherapySettingsViewModelDelegate? = nil) {
         self.therapySettings = therapySettings
         self.initialTherapySettings = therapySettings
         self.sensitivityOverridesEnabled = sensitivityOverridesEnabled
+        self.adultChildInsulinModelSelectionEnabled = adultChildInsulinModelSelectionEnabled
         self.prescription = prescription
         self.delegate = delegate
     }

+ 5 - 1
Dependencies/LoopKit/LoopKitUI/Views/Information Screens/InformationView.swift

@@ -80,7 +80,11 @@ struct InformationView<InformationalContent: View> : View {
             informationalContent
             Spacer()
         }
-        .navigationBarItems(trailing: cancelButton)
+        .toolbar {
+            ToolbarItem(placement: .navigationBarTrailing) {
+                cancelButton
+            }
+        }
         .navigationBarTitle(title, displayMode: .large)
     }
 

+ 16 - 2
Dependencies/LoopKit/LoopKitUI/Views/ScheduleEditor.swift

@@ -145,11 +145,25 @@ struct ScheduleEditor<Value: Equatable, ValueContent: View, ValuePicker: View, A
         case .acceptanceFlow:
             page
                 .navigationBarBackButtonHidden(true)
-                .navigationBarItems(leading: backButton, trailing: trailingNavigationItems)
+                .toolbar {
+                    ToolbarItem(placement: .navigationBarLeading) {
+                        backButton
+                    }
+                    ToolbarItem(placement: .navigationBarTrailing) {
+                        trailingNavigationItems
+                    }
+                }
         case .settings:
             page
                 .navigationBarBackButtonHidden(shouldAddCancelButton)
-                .navigationBarItems(leading: leadingNavigationBarItem, trailing: trailingNavigationItems)
+                .toolbar {
+                    ToolbarItem(placement: .navigationBarLeading) {
+                        leadingNavigationBarItem
+                    }
+                    ToolbarItem(placement: .navigationBarTrailing) {
+                        trailingNavigationItems
+                    }
+                }
                 .navigationBarTitle("", displayMode: .inline)
         }
     }

+ 8 - 1
Dependencies/LoopKit/LoopKitUI/Views/Settings Editors/CorrectionRangeOverridesEditor.swift

@@ -85,7 +85,14 @@ public struct CorrectionRangeOverridesEditor: View {
     private var contentWithCancel: some View {
         content
             .navigationBarBackButtonHidden(shouldAddCancelButton)
-            .navigationBarItems(leading: leadingNavigationBarItem, trailing: deleteButton)
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    leadingNavigationBarItem
+                }
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    deleteButton
+                }
+            }
     }
 
     private var deleteButton: some View {

+ 8 - 2
Dependencies/LoopKit/LoopKitUI/Views/Settings Editors/TherapySettingsView.swift

@@ -96,7 +96,9 @@ public struct TherapySettingsView: View {
         cards.append(carbRatioSection)
         cards.append(basalRatesSection)
         cards.append(deliveryLimitsSection)
-        cards.append(insulinModelSection)
+        if viewModel.adultChildInsulinModelSelectionEnabled {
+            cards.append(insulinModelSection)
+        }
         cards.append(insulinSensitivitiesSection)
 
         return CardStack(cards: cards)
@@ -114,7 +116,11 @@ public struct TherapySettingsView: View {
                 Color(.systemGroupedBackground)
                     .edgesIgnoringSafeArea(.all)
                 content
-                    .navigationBarItems(trailing: dismissButton)
+                    .toolbar {
+                        ToolbarItem(placement: .navigationBarTrailing) {
+                            dismissButton
+                        }
+                    }
                     .navigationBarTitle(therapySettingsTitle, displayMode: .large)
             }
         }

+ 1 - 5
Dependencies/LoopKit/MockKit/MockCGMManager.swift

@@ -480,10 +480,6 @@ public final class MockCGMManager: TestingCGMManager {
     
     public static var healthKitStorageDelay: TimeInterval = 0
     
-    private func logDeviceCommunication(_ message: String, type: DeviceLogEntryType = .send) {
-        self.delegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: "MockId", type: type, message: message, completion: nil)
-    }
-
     private func logDeviceComms(_ type: DeviceLogEntryType, message: String) {
         self.delegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: "mockcgm", type: type, message: message, completion: nil)
     }
@@ -542,7 +538,7 @@ public final class MockCGMManager: TestingCGMManager {
 
     public func backfillData(datingBack duration: TimeInterval) {
         let now = Date()
-        self.logDeviceCommunication("backfillData(\(duration))")
+        self.logDeviceComms(.send, message: "backfillData(\(duration))")
         dataSource.backfillData(from: DateInterval(start: now.addingTimeInterval(-duration), end: now)) { result in
             switch result {
             case .error(let error):

+ 1 - 1
Dependencies/LoopKit/MockKit/MockPumpManager.swift

@@ -401,7 +401,7 @@ public final class MockPumpManager: TestingPumpManager {
         state.finalizeFinishedDoses()
         let pendingPumpEvents = state.pumpEventsToStore
         delegate.notify { (delegate) in
-            delegate?.pumpManager(self, hasNewPumpEvents: pendingPumpEvents, lastSync: self.lastSync) { error in
+            delegate?.pumpManager(self, hasNewPumpEvents: pendingPumpEvents, lastReconciliation: self.lastSync) { error in
                 if error == nil {
                     self.state.additionalPumpEvents = []
                 }

+ 5 - 1
Dependencies/LoopKit/MockKitUI/Views/DeliveryUncertaintyRecoveryView.swift

@@ -32,7 +32,11 @@ struct DeliveryUncertaintyRecoveryView: View, HorizontalSizeClassOverride {
             }
             .environment(\.horizontalSizeClass, horizontalOverride)
             .navigationBarTitle(Text("Comms Recovery"), displayMode: .large)
-            .navigationBarItems(leading: backButton)
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    backButton
+                }
+            }
         }
     }
     

+ 14 - 2
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/UnfinalizedDose.swift

@@ -199,6 +199,19 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti
         }
     }
 
+    public var eventTitle: String {
+        switch doseType {
+        case .bolus:
+            return NSLocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
+        case .resume:
+            return NSLocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
+        case .suspend:
+            return NSLocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
+        case .tempBasal:
+            return NSLocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
+        }
+    }
+
     // RawRepresentable
     public init?(rawValue: RawValue) {
         guard
@@ -278,9 +291,8 @@ private extension TimeInterval {
 
 extension NewPumpEvent {
     init(_ dose: UnfinalizedDose) {
-        let title = String(describing: dose)
         let entry = DoseEntry(dose)
-        self.init(date: dose.startTime, dose: entry, raw: dose.uniqueKey, title: title)
+        self.init(date: dose.startTime, dose: entry, raw: dose.uniqueKey, title: dose.eventTitle)
     }
 }
 

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift

@@ -2058,7 +2058,7 @@ extension OmniBLEPumpManager: PumpManager {
             }
 
 
-            delegate.pumpManager(self, hasNewPumpEvents: doses.map { NewPumpEvent($0) }, lastSync: lastSync, completion: { (error) in
+            delegate.pumpManager(self, hasNewPumpEvents: doses.map { NewPumpEvent($0) }, lastReconciliation: lastSync, completion: { (error) in
                 if let error = error {
                     self.log.error("Error storing pod events: %@", String(describing: error))
                 } else {

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift

@@ -203,7 +203,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public mutating func updateFromStatusResponse(_ response: StatusResponse) {
         let now = updatePodTimes(timeActive: response.timeActive)
         updateDeliveryStatus(deliveryStatus: response.deliveryStatus, podProgressStatus: response.podProgressStatus, bolusNotDelivered: response.bolusNotDelivered)
-        
+
         let setupUnits = setupUnitsDelivered ?? Pod.primeUnits + Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
 
         // Calculated new delivered value which will be a negative value until setup has completed OR after a pod reset fault

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

@@ -169,7 +169,7 @@ class DashUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
             viewModel.navigateTo = { [weak self] (screen) in
                 self?.navigateTo(screen)
             }
-            let view = OmniBLESettingsView(viewModel: viewModel)
+            let view = OmniBLESettingsView(viewModel: viewModel, supportedInsulinTypes: allowedInsulinTypes)
             return hostingController(rootView: view)
         case .pairPod:
             pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManager)

+ 1 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ExpirationReminderSetupView.swift

@@ -39,6 +39,7 @@ struct ExpirationReminderSetupView: View {
             .padding()
         }
         .navigationBarTitle("Expiration Reminder", displayMode: .automatic)
+        .navigationBarHidden(false)
         .toolbar {
             ToolbarItem(placement: .navigationBarTrailing) {
                 Button(LocalizedString("Cancel", comment: "Cancel button title"), action: {

+ 7 - 5
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift

@@ -17,15 +17,17 @@ struct OmniBLESettingsView: View  {
     
     @State private var showingDeleteConfirmation = false
     
-    @State private var showSuspendOptions = false;
+    @State private var showSuspendOptions = false
 
-    @State private var showManualTempBasalOptions = false;
+    @State private var showManualTempBasalOptions = false
 
-    @State private var showSyncTimeOptions = false;
+    @State private var showSyncTimeOptions = false
 
-    @State private var sendingTestBeepsCommand = false;
+    @State private var sendingTestBeepsCommand = false
 
     @State private var cancelingTempBasal = false
+
+    var supportedInsulinTypes: [InsulinType]
     
     @Environment(\.guidanceColors) var guidanceColors
     @Environment(\.insulinTintColor) var insulinTintColor
@@ -407,7 +409,7 @@ struct OmniBLESettingsView: View  {
                             .foregroundColor(.secondary)
                     }
                 }
-                NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: InsulinType.allCases, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) {
+                NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) {
                     HStack {
                         FrameworkLocalText("Insulin Type", comment: "Text for confidence reminders navigation link").foregroundColor(Color.primary)
                         if let currentTitle = viewModel.insulinType?.brandName {

+ 53 - 27
Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManager.swift

@@ -23,7 +23,7 @@ public class MinimedPumpManager: RileyLinkPumpManager {
         return MinimedPumpManager.managerIdentifier
     }
     
-    public init(state: MinimedPumpManagerState, rileyLinkDeviceProvider: RileyLinkDeviceProvider, rileyLinkConnectionManager: RileyLinkConnectionManager? = nil, pumpOps: PumpOps? = nil) {
+    public init(state: MinimedPumpManagerState, rileyLinkDeviceProvider: RileyLinkDeviceProvider, pumpOps: PumpOps? = nil) {
         self.lockedState = Locked(state)
 
         self.hkDevice = HKDevice(
@@ -37,27 +37,28 @@ public class MinimedPumpManager: RileyLinkPumpManager {
             udiDeviceIdentifier: nil
         )
         
-        super.init(rileyLinkDeviceProvider: rileyLinkDeviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
+        super.init(rileyLinkDeviceProvider: rileyLinkDeviceProvider)
 
         // Pump communication
         let idleListeningEnabled = state.pumpModel.hasMySentry && state.useMySentry
-        self.pumpOps = pumpOps ?? PumpOps(pumpSettings: state.pumpSettings, pumpState: state.pumpState, delegate: self)
+
+        self.pumpOps = pumpOps ?? MinimedPumpOps(pumpSettings: state.pumpSettings, pumpState: state.pumpState, delegate: self)
 
         self.rileyLinkDeviceProvider.idleListeningState = idleListeningEnabled ? MinimedPumpManagerState.idleListeningEnabledDefaults : .disabled
     }
 
     public required convenience init?(rawState: PumpManager.RawStateValue) {
         guard let state = MinimedPumpManagerState(rawValue: rawState),
-            let connectionManagerState = state.rileyLinkConnectionManagerState else
+            let connectionManagerState = state.rileyLinkConnectionState else
         {
             return nil
         }
+
+        let deviceProvider = RileyLinkBluetoothDeviceProvider(autoConnectIDs: connectionManagerState.autoConnectIDs)
+
+        self.init(state: state, rileyLinkDeviceProvider: deviceProvider)
         
-        let rileyLinkConnectionManager = RileyLinkConnectionManager(state: connectionManagerState)
-        
-        self.init(state: state, rileyLinkDeviceProvider: rileyLinkConnectionManager.deviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
-        
-        rileyLinkConnectionManager.delegate = self
+        deviceProvider.delegate = self
     }
 
     public private(set) var pumpOps: PumpOps!
@@ -175,6 +176,11 @@ public class MinimedPumpManager: RileyLinkPumpManager {
         }
     }
 
+    private func logDeviceCommunication(_ message: String, type: DeviceLogEntryType = .send) {
+        // Not dispatching here; if delegate queue is blocked, timestamps will be delayed
+        self.pumpDelegate.delegate?.deviceManager(self, logEventForDeviceIdentifier: state.pumpID, type: type, message: message, completion: nil)
+    }
+
     private let cgmDelegate = WeakSynchronizedDelegate<CGMManagerDelegate>()
     private let pumpDelegate = WeakSynchronizedDelegate<PumpManagerDelegate>()
 
@@ -186,13 +192,13 @@ public class MinimedPumpManager: RileyLinkPumpManager {
 
     // MARK: - RileyLink Updates
 
-    override public var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState? {
+    override public var rileyLinkConnectionManagerState: RileyLinkConnectionState? {
         get {
-            return state.rileyLinkConnectionManagerState
+            return state.rileyLinkConnectionState
         }
         set {
             setState { (state) in
-                state.rileyLinkConnectionManagerState = newValue
+                state.rileyLinkConnectionState = newValue
             }
         }
     }
@@ -718,7 +724,7 @@ extension MinimedPumpManager {
     ///   - error: An error describing why the fetch and/or store failed
     private func fetchPumpHistory(_ completion: @escaping (_ error: Error?) -> Void) {
         guard let insulinType = insulinType else {
-            completion(PumpManagerError.configuration(nil))
+            completion(PumpManagerError.configuration(MinimedPumpManagerError.insulinTypeNotConfigured))
             return
         }
         
@@ -757,9 +763,9 @@ extension MinimedPumpManager {
                             preconditionFailure("pumpManagerDelegate cannot be nil")
                         }
                         
-                        let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent })
+                        let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent() })
 
-                        delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastSync: self.lastSync, completion: { (error) in
+                        delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastReconciliation: self.state.lastReconciliation, completion: { (error) in
                             // Called on an unknown queue by the delegate
                             if error == nil {
                                 self.recents.lastAddedPumpEvents = Date()
@@ -791,9 +797,9 @@ extension MinimedPumpManager {
         }
     }
 
-    private func storePendingPumpEvents(_ completion: @escaping (_ error: MinimedPumpManagerError?) -> Void) {
+    private func storePendingPumpEvents(forceFinalization: Bool = false, _ completion: @escaping (_ error: MinimedPumpManagerError?) -> Void) {
         // Must be called from the sessionQueue
-        let events = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent })
+        let events = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent(forceFinalization: forceFinalization) })
                 
         log.debug("Storing pending pump events: %{public}@", String(describing: events))
 
@@ -802,7 +808,7 @@ extension MinimedPumpManager {
                 preconditionFailure("pumpManagerDelegate cannot be nil")
             }
 
-            delegate.pumpManager(self, hasNewPumpEvents: events, lastSync: self.lastSync, completion: { (error) in
+            delegate.pumpManager(self, hasNewPumpEvents: events, lastReconciliation: self.state.lastReconciliation, completion: { (error) in
                 // Called on an unknown queue by the delegate
                 if let error = error {
                     self.log.error("Pump event storage failed: %{public}@", String(describing: error))
@@ -1068,7 +1074,7 @@ extension MinimedPumpManager: PumpManager {
 
     public func suspendDelivery(completion: @escaping (Error?) -> Void) {
         guard let insulinType = insulinType else {
-            completion(PumpManagerError.configuration(nil))
+            completion(PumpManagerError.configuration(MinimedPumpManagerError.insulinTypeNotConfigured))
             return
         }
         
@@ -1077,7 +1083,7 @@ extension MinimedPumpManager: PumpManager {
 
     public func resumeDelivery(completion: @escaping (Error?) -> Void) {
         guard let insulinType = insulinType else {
-            completion(PumpManagerError.configuration(nil))
+            completion(PumpManagerError.configuration(MinimedPumpManagerError.insulinTypeNotConfigured))
             return
         }
         
@@ -1174,12 +1180,12 @@ extension MinimedPumpManager: PumpManager {
         }
         
         guard let insulinType = insulinType else {
-            completion(.configuration(nil))
+            completion(.configuration(MinimedPumpManagerError.insulinTypeNotConfigured))
             return
         }
 
 
-        pumpOps.runSession(withName: "Bolus", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+        pumpOps.runSession(withName: "Bolus", usingSelector: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
 
             guard let session = session else {
                 completion(.connection(MinimedPumpManagerError.noRileyLink))
@@ -1273,7 +1279,7 @@ extension MinimedPumpManager: PumpManager {
     public func cancelBolus(completion: @escaping (PumpManagerResult<DoseEntry?>) -> Void) {
         
         guard let insulinType = insulinType else {
-            completion(.failure(.configuration(nil)))
+            completion(.failure(.configuration(MinimedPumpManagerError.insulinTypeNotConfigured)))
             return
         }
 
@@ -1290,11 +1296,11 @@ extension MinimedPumpManager: PumpManager {
     
     public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) {
         guard let insulinType = insulinType else {
-            completion(.configuration(nil))
+            completion(.configuration(MinimedPumpManagerError.insulinTypeNotConfigured))
             return
         }
 
-        pumpOps.runSession(withName: "Set Temp Basal", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+        pumpOps.runSession(withName: "Set Temp Basal", usingSelector: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
             guard let session = session else {
                 completion(.connection(MinimedPumpManagerError.noRileyLink))
                 return
@@ -1400,7 +1406,7 @@ extension MinimedPumpManager: PumpManager {
     public func setMaximumTempBasalRate(_ rate: Double) { }
 
     public func syncBasalRateSchedule(items scheduleItems: [RepeatingScheduleValue<Double>], completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) {
-        pumpOps.runSession(withName: "Save Basal Profile", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+        pumpOps.runSession(withName: "Save Basal Profile", usingSelector: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
             guard let session = session else {
                 completion(.failure(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
                 return
@@ -1419,7 +1425,7 @@ extension MinimedPumpManager: PumpManager {
     }
 
     public func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (Result<DeliveryLimits, Error>) -> Void) {
-        pumpOps.runSession(withName: "Save Settings", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+        pumpOps.runSession(withName: "Save Settings", usingSelector: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
             guard let session = session else {
                 completion(.failure(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
                 return
@@ -1444,9 +1450,29 @@ extension MinimedPumpManager: PumpManager {
             }
         }
     }
+
+    public func deletePump(completion: @escaping () -> Void) {
+        storePendingPumpEvents(forceFinalization: true) { error in
+            self.notifyDelegateOfDeletion {
+                completion()
+            }
+        }
+    }
 }
 
 extension MinimedPumpManager: PumpOpsDelegate {
+    public func willSend(_ message: String) {
+        logDeviceCommunication(message, type: .send)
+    }
+
+    public func didReceive(_ message: String) {
+        logDeviceCommunication(message, type: .receive)
+    }
+
+    public func didError(_ message: String) {
+        logDeviceCommunication(message, type: .error)
+    }
+
     public func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState) {
         setState { (pumpManagerState) in
             pumpManagerState.pumpState = state

+ 5 - 0
Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManagerError.swift

@@ -11,6 +11,7 @@ public enum MinimedPumpManagerError: Error {
     case noRileyLink
     case bolusInProgress
     case pumpSuspended
+    case insulinTypeNotConfigured
     case noDate  // TODO: This is less of an error and more of a precondition/assertion state
     case tuneFailed(LocalizedError)
     case commsError(LocalizedError)
@@ -27,6 +28,8 @@ extension MinimedPumpManagerError: LocalizedError {
             return LocalizedString("Bolus in Progress", comment: "Error description when failure due to bolus in progress")
         case .pumpSuspended:
             return LocalizedString("Pump is Suspended", comment: "Error description when failure due to pump suspended")
+        case .insulinTypeNotConfigured:
+            return LocalizedString("Insulin Type is not configured", comment: "Error description for MinimedPumpManagerError.insulinTypeNotConfigured")
         case .noDate:
             return nil
         case .tuneFailed(let error):
@@ -51,6 +54,8 @@ extension MinimedPumpManagerError: LocalizedError {
         switch self {
         case .noRileyLink:
             return LocalizedString("Make sure your RileyLink is nearby and powered on", comment: "Recovery suggestion")
+        case .insulinTypeNotConfigured:
+            return LocalizedString("Go to pump settings and select insulin type", comment: "Recovery suggestion for MinimedPumpManagerError.insulinTypeNotConfigured")
         case .tuneFailed(let error):
             return error.recoverySuggestion
         default:

+ 14 - 12
Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManagerState.swift

@@ -98,7 +98,7 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
         }
     }
 
-    public var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?
+    public var rileyLinkConnectionState: RileyLinkConnectionState?
 
     public var timeZone: TimeZone
 
@@ -120,7 +120,7 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
     
     public var lastRileyLinkBatteryAlertDate: Date = .distantPast
     
-    public init(isOnboarded: Bool, useMySentry: Bool, pumpColor: PumpColor, pumpID: String, pumpModel: PumpModel, pumpFirmwareVersion: String, pumpRegion: PumpRegion, rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?, timeZone: TimeZone, suspendState: SuspendState, insulinType: InsulinType)
+    public init(isOnboarded: Bool, useMySentry: Bool, pumpColor: PumpColor, pumpID: String, pumpModel: PumpModel, pumpFirmwareVersion: String, pumpRegion: PumpRegion, rileyLinkConnectionState: RileyLinkConnectionState?, timeZone: TimeZone, suspendState: SuspendState, insulinType: InsulinType, lastTuned: Date?, lastValidFrequency: Measurement<UnitFrequency>?)
     {
         self.isOnboarded = isOnboarded
         self.useMySentry = useMySentry
@@ -129,10 +129,12 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
         self.pumpModel = pumpModel
         self.pumpFirmwareVersion = pumpFirmwareVersion
         self.pumpRegion = pumpRegion
-        self.rileyLinkConnectionManagerState = rileyLinkConnectionManagerState
+        self.rileyLinkConnectionState = rileyLinkConnectionState
         self.timeZone = timeZone
         self.suspendState = suspendState
         self.insulinType = insulinType
+        self.lastTuned = lastTuned
+        self.lastValidFrequency = lastValidFrequency
     }
 
     public init?(rawValue: RawValue) {
@@ -168,11 +170,11 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
             if let oldRileyLinkPumpManagerStateRaw = rawValue["rileyLinkPumpManagerState"] as? [String : Any],
                 let connectedPeripheralIDs = oldRileyLinkPumpManagerStateRaw["connectedPeripheralIDs"] as? [String]
             {
-                self.rileyLinkConnectionManagerState = RileyLinkConnectionManagerState(autoConnectIDs: Set(connectedPeripheralIDs))
+                self.rileyLinkConnectionState = RileyLinkConnectionState(autoConnectIDs: Set(connectedPeripheralIDs))
             }
         } else {
-            if let rawState = rawValue["rileyLinkConnectionManagerState"] as? RileyLinkConnectionManagerState.RawValue {
-                self.rileyLinkConnectionManagerState = RileyLinkConnectionManagerState(rawValue: rawState)
+            if let rawState = rawValue["rileyLinkConnectionManagerState"] as? RileyLinkConnectionState.RawValue {
+                self.rileyLinkConnectionState = RileyLinkConnectionState(rawValue: rawState)
             }
         }
         
@@ -237,7 +239,7 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
         }
         reconciliationMappings = recentlyReconciledEvents
         
-        lastReconciliation = rawValue["lastSync"] as? Date
+        lastReconciliation = rawValue["lastReconciliation"] as? Date
         
         if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue {
             insulinType = InsulinType(rawValue: rawInsulinType)
@@ -269,10 +271,10 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
         value["batteryPercentage"] = batteryPercentage
         value["lastReservoirReading"] = lastReservoirReading?.rawValue
         value["lastValidFrequency"] = lastValidFrequency?.converted(to: .megahertz).value
-        value["rileyLinkConnectionManagerState"] = rileyLinkConnectionManagerState?.rawValue
+        value["rileyLinkConnectionManagerState"] = rileyLinkConnectionState?.rawValue
         value["unfinalizedBolus"] = unfinalizedBolus?.rawValue
         value["unfinalizedTempBasal"] = unfinalizedTempBasal?.rawValue
-        value["lastSync"] = lastReconciliation
+        value["lastReconciliation"] = lastReconciliation
         value["insulinType"] = insulinType?.rawValue
         value["rileyLinkBatteryAlertLevel"] = rileyLinkBatteryAlertLevel
         value["lastRileyLinkBatteryAlertDate"] = lastRileyLinkBatteryAlertDate
@@ -283,7 +285,7 @@ public struct MinimedPumpManagerState: RawRepresentable, Equatable {
 
 
 extension MinimedPumpManagerState {
-    static let idleListeningEnabledDefaults: RileyLinkDevice.IdleListeningState = .enabled(timeout: .minutes(4), channel: 0)
+    static let idleListeningEnabledDefaults: RileyLinkBluetoothDevice.IdleListeningState = .enabled(timeout: .minutes(4), channel: 0)
 }
 
 
@@ -310,11 +312,11 @@ extension MinimedPumpManagerState: CustomDebugStringConvertible {
             "pendingDoses: \(pendingDoses)",
             "timeZone: \(timeZone)",
             "recentlyReconciledEvents: \(reconciliationMappings.values.map { "\($0.eventRaw.hexadecimalString) -> \($0.uuid)" })",
-            "lastSync: \(String(describing: lastReconciliation))",
+            "lastReconciliation: \(String(describing: lastReconciliation))",
             "insulinType: \(String(describing: insulinType))",
             "rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))",
             "lastRileyLinkBatteryAlertDate \(String(describing: lastRileyLinkBatteryAlertDate))",
-            String(reflecting: rileyLinkConnectionManagerState),
+            String(reflecting: rileyLinkConnectionState),
         ].joined(separator: "\n")
     }
 }

+ 164 - 0
Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpMessageSender.swift

@@ -0,0 +1,164 @@
+//
+//  MinimedPumpMessageSender.swift
+//  MinimedKit
+//
+//  Created by Pete Schwamb on 9/3/22.
+//  Copyright © 2022 Pete Schwamb. All rights reserved.
+//
+
+import Foundation
+import RileyLinkBLEKit
+import os.log
+
+
+public protocol CommsLogger: AnyObject {
+    // Comms logging
+    func willSend(_ message: String)
+    func didReceive(_ message: String)
+    func didError(_ message: String)
+}
+
+private let log = OSLog(category: "MinimedPumpMessageSender")
+
+struct MinimedPumpMessageSender: PumpMessageSender {
+
+    static let standardPumpResponseWindow: TimeInterval = .milliseconds(200)
+
+    var commandSession: CommandSession
+    weak var commsLogger: CommsLogger?
+
+    func resetRadioConfig() throws {
+        try commandSession.resetRadioConfig()
+    }
+
+    func updateRegister(_ address: RileyLinkBLEKit.CC111XRegister, value: UInt8) throws {
+        try commandSession.updateRegister(address, value: value)
+    }
+
+    func setBaseFrequency(_ frequency: Measurement<UnitFrequency>) throws {
+        try commandSession.setBaseFrequency(frequency)
+    }
+
+    func listen(onChannel channel: Int, timeout: TimeInterval) throws -> RileyLinkBLEKit.RFPacket? {
+        return try commandSession.listen(onChannel: channel, timeout: timeout)
+    }
+
+    func getRileyLinkStatistics() throws -> RileyLinkBLEKit.RileyLinkStatistics {
+        return try commandSession.getRileyLinkStatistics()
+    }
+
+    /// - Throws: PumpOpsError.deviceError
+    func send(_ msg: PumpMessage) throws {
+        do {
+            try commandSession.send(MinimedPacket(outgoingData: msg.txData).encodedData(), onChannel: 0, timeout: 0)
+        } catch let error as LocalizedError {
+            throw PumpOpsError.deviceError(error)
+        }
+    }
+
+    /// Sends a message to the pump, expecting a PumpMessage with specific response body type
+    ///
+    /// - Parameters:
+    ///   - message: The message to send
+    ///   - responseType: The expected response message type
+    ///   - repeatCount: The number of times to repeat the message before listening begins
+    ///   - timeout: The length of time to listen for a pump response
+    ///   - retryCount: The number of times to repeat the send & listen sequence
+    /// - Returns: The expected response message body
+    /// - Throws:
+    ///     - PumpOpsError.couldNotDecode
+    ///     - PumpOpsError.crosstalk
+    ///     - PumpOpsError.deviceError
+    ///     - PumpOpsError.noResponse
+    ///     - PumpOpsError.pumpError
+    ///     - PumpOpsError.unexpectedResponse
+    ///     - PumpOpsError.unknownResponse
+    func getResponse<T: MessageBody>(to message: PumpMessage, responseType: MessageType, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> T {
+
+        commsLogger?.willSend(String(describing: message))
+
+        do {
+            let response = try sendAndListen(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
+
+            guard response.messageType == responseType, let body = response.messageBody as? T else {
+                if let body = response.messageBody as? PumpErrorMessageBody {
+                    switch body.errorCode {
+                    case .known(let code):
+                        throw PumpOpsError.pumpError(code)
+                    case .unknown(let code):
+                        throw PumpOpsError.unknownPumpErrorCode(code)
+                    }
+                } else {
+                    throw PumpOpsError.unexpectedResponse(response, from: message)
+                }
+            }
+            commsLogger?.didReceive(String(describing: response))
+            return body
+        } catch {
+            commsLogger?.didError(error.localizedDescription)
+            throw error
+        }
+    }
+
+    /// Sends a message to the pump, listening for a any known PumpMessage in reply
+    ///
+    /// - Parameters:
+    ///   - message: The message to send
+    ///   - repeatCount: The number of times to repeat the message before listening begins
+    ///   - timeout: The length of time to listen for a pump response
+    ///   - retryCount: The number of times to repeat the send & listen sequence
+    /// - Returns: The message reply
+    /// - Throws: An error describing a failure in the sending or receiving of a message:
+    ///     - PumpOpsError.couldNotDecode
+    ///     - PumpOpsError.crosstalk
+    ///     - PumpOpsError.deviceError
+    ///     - PumpOpsError.noResponse
+    ///     - PumpOpsError.unknownResponse
+    func sendAndListen(_ message: PumpMessage, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> PumpMessage {
+        let rfPacket = try sendAndListenForPacket(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
+
+        guard let packet = MinimedPacket(encodedData: rfPacket.data) else {
+            throw PumpOpsError.couldNotDecode(rx: rfPacket.data, during: message)
+        }
+
+        guard let response = PumpMessage(rxData: packet.data) else {
+            // Unknown packet type or message type
+            throw PumpOpsError.unknownResponse(rx: packet.data, during: message)
+        }
+
+        guard response.address == message.address else {
+            throw PumpOpsError.crosstalk(response, during: message)
+        }
+
+        return response
+    }
+
+    // Send a PumpMessage, and listens for a packet; used by callers who need to see RSSI
+    /// - Throws:
+    ///     - PumpOpsError.noResponse
+    ///     - PumpOpsError.deviceError
+    func sendAndListenForPacket(_ message: PumpMessage, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket {
+        let packet: RFPacket?
+
+        do {
+            packet = try commandSession.sendAndListen(MinimedPacket(outgoingData: message.txData).encodedData(), repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
+        } catch let error as LocalizedError {
+            throw PumpOpsError.deviceError(error)
+        }
+
+        guard let rfPacket = packet else {
+            throw PumpOpsError.noResponse(during: message)
+        }
+
+        return rfPacket
+    }
+
+    /// - Throws: PumpOpsError.deviceError
+    func listenForPacket(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? {
+        do {
+            return try listen(onChannel: channel, timeout: timeout)
+        } catch let error as LocalizedError {
+            throw PumpOpsError.deviceError(error)
+        }
+    }
+}

+ 11 - 98
Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpMessageSender.swift

@@ -8,14 +8,8 @@
 
 import Foundation
 import RileyLinkBLEKit
-import os.log
 
-private let standardPumpResponseWindow: TimeInterval = .milliseconds(200)
-
-private let log = OSLog(category: "PumpMessageSender")
-
-
-protocol PumpMessageSender {
+public protocol PumpMessageSender {
     /// - Throws: LocalizedError
     func resetRadioConfig() throws
 
@@ -25,41 +19,16 @@ protocol PumpMessageSender {
     /// - Throws: LocalizedError
     func setBaseFrequency(_ frequency: Measurement<UnitFrequency>) throws
 
-    /// Sends data to the pump, listening for a reply
-    ///
-    /// - Parameters:
-    ///   - data: The data to send
-    ///   - repeatCount: The number of times to repeat the message before listening begins
-    ///   - timeout: The length of time to listen for a response before timing out
-    ///   - retryCount: The number of times to repeat the send & listen sequence
-    /// - Returns: The packet reply
-    /// - Throws: LocalizedError
-    func sendAndListen(_ data: Data, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket
-
     /// - Throws: LocalizedError
     func listen(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket?
 
     /// - Throws: LocalizedError
-    func send(_ data: Data, onChannel channel: Int, timeout: TimeInterval) throws
-    
-    /// - Throws: LocalizedError
-    func setCCLEDMode(_ mode: RileyLinkLEDMode) throws
-    
+    func send(_ msg: PumpMessage) throws
+
     /// - Throws: LocalizedError
     func getRileyLinkStatistics() throws -> RileyLinkStatistics
-}
 
-extension PumpMessageSender {
-    /// - Throws: PumpOpsError.deviceError
-    func send(_ msg: PumpMessage) throws {
-        do {
-            try send(MinimedPacket(outgoingData: msg.txData).encodedData(), onChannel: 0, timeout: 0)
-        } catch let error as LocalizedError {
-            throw PumpOpsError.deviceError(error)
-        }
-    }
-
-    /// Sends a message to the pump, expecting a specific response body
+    /// Sends a message to the pump, expecting a PumpMessage with specific response body type
     ///
     /// - Parameters:
     ///   - message: The message to send
@@ -76,30 +45,9 @@ extension PumpMessageSender {
     ///     - PumpOpsError.pumpError
     ///     - PumpOpsError.unexpectedResponse
     ///     - PumpOpsError.unknownResponse
-    func getResponse<T: MessageBody>(to message: PumpMessage, responseType: MessageType = .pumpAck, repeatCount: Int = 0, timeout: TimeInterval = standardPumpResponseWindow, retryCount: Int = 3) throws -> T {
-        
-        log.debug("getResponse() Sending: %{public}@, %d, %f, %d)", String(describing: message), repeatCount, timeout, retryCount)
-        
-        let response = try sendAndListen(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
-
-        guard response.messageType == responseType, let body = response.messageBody as? T else {
-            if let body = response.messageBody as? PumpErrorMessageBody {
-                switch body.errorCode {
-                case .known(let code):
-                    throw PumpOpsError.pumpError(code)
-                case .unknown(let code):
-                    throw PumpOpsError.unknownPumpErrorCode(code)
-                }
-            } else {
-                log.debug("getResponse() Received unexpected response: %{public}@", String(describing: response))
-                throw PumpOpsError.unexpectedResponse(response, from: message)
-            }
-        }
-        log.debug("getResponse() Received: %{public}@", String(describing: response))
-        return body
-    }
+    func getResponse<T: MessageBody>(to message: PumpMessage, responseType: MessageType, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> T
 
-    /// Sends a message to the pump, listening for a message in reply
+    /// Sends a message to the pump, listening for a any known PumpMessage in reply
     ///
     /// - Parameters:
     ///   - message: The message to send
@@ -113,50 +61,15 @@ extension PumpMessageSender {
     ///     - PumpOpsError.deviceError
     ///     - PumpOpsError.noResponse
     ///     - PumpOpsError.unknownResponse
-    func sendAndListen(_ message: PumpMessage, repeatCount: Int = 0, timeout: TimeInterval = standardPumpResponseWindow, retryCount: Int = 3) throws -> PumpMessage {
-        let rfPacket = try sendAndListenForPacket(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
-
-        guard let packet = MinimedPacket(encodedData: rfPacket.data) else {
-            throw PumpOpsError.couldNotDecode(rx: rfPacket.data, during: message)
-        }
-
-        guard let response = PumpMessage(rxData: packet.data) else {
-            // Unknown packet type or message type
-            throw PumpOpsError.unknownResponse(rx: packet.data, during: message)
-        }
-
-        guard response.address == message.address else {
-            throw PumpOpsError.crosstalk(response, during: message)
-        }
-
-        return response
-    }
+    func sendAndListen(_ message: PumpMessage, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> PumpMessage
 
+    // Send a PumpMessage, and listens for a packet; used by callers who need to see RSSI
     /// - Throws:
     ///     - PumpOpsError.noResponse
     ///     - PumpOpsError.deviceError
-    func sendAndListenForPacket(_ message: PumpMessage, repeatCount: Int = 0, timeout: TimeInterval = standardPumpResponseWindow, retryCount: Int = 3) throws -> RFPacket {
-        let packet: RFPacket?
-
-        do {
-            packet = try sendAndListen(MinimedPacket(outgoingData: message.txData).encodedData(), repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
-        } catch let error as LocalizedError {
-            throw PumpOpsError.deviceError(error)
-        }
-
-        guard let rfPacket = packet else {
-            throw PumpOpsError.noResponse(during: message)
-        }
-
-        return rfPacket
-    }
+    func sendAndListenForPacket(_ message: PumpMessage, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket
 
     /// - Throws: PumpOpsError.deviceError
-    func listenForPacket(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? {
-        do {
-            return try listen(onChannel: channel, timeout: timeout)
-        } catch let error as LocalizedError {
-            throw PumpOpsError.deviceError(error)
-        }
-    }
+    func listenForPacket(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket?
 }
+

+ 34 - 29
Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpOps.swift

@@ -13,20 +13,35 @@ import os.log
 import LoopKit
 
 
-public protocol PumpOpsDelegate: AnyObject {
-    // TODO: Audit clients of this as its called on the session queue
+public protocol PumpOpsDelegate: AnyObject, CommsLogger {
     func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState)
 }
 
+public protocol PumpOps {
+    func runSession(withName name: String, using device: RileyLinkDevice, _ block: @escaping (_ session: PumpOpsSession) -> Void)
+}
+
+extension PumpOps {
+    public func runSession(withName name: String, usingSelector deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void, _ block: @escaping (_ session: PumpOpsSession?) -> Void) {
+        deviceSelector { (device) in
+            guard let device = device else {
+                block(nil)
+                return
+            }
+
+            self.runSession(withName: name, using: device, block)
+        }
+    }
+}
 
-public class PumpOps {
+public class MinimedPumpOps: PumpOps {
     private let log = OSLog(category: "PumpOps")
 
-    public let pumpSettings: PumpSettings
+    private let pumpSettings: PumpSettings
 
-    public let pumpState: Locked<PumpState>
+    private let pumpState: Locked<PumpState>
 
-    private let configuredDevices: Locked<Set<RileyLinkDevice>> = Locked(Set())
+    private let configuredDevices: Locked<Set<UUID>> = Locked(Set())
 
     // Isolated to RileyLinkDeviceManager.sessionQueue
     private var sessionDevice: RileyLinkDevice?
@@ -46,20 +61,10 @@ public class PumpOps {
         }
     }
 
-    public func runSession(withName name: String, using deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void, _ block: @escaping (_ session: PumpOpsSession?) -> Void) {
-        deviceSelector { (device) in
-            guard let device = device else {
-                block(nil)
-                return
-            }
-
-            self.runSession(withName: name, using: device, block)
-        }
-    }
-
     public func runSession(withName name: String, using device: RileyLinkDevice, _ block: @escaping (_ session: PumpOpsSession) -> Void) {
         device.runSession(withName: name) { (commandSession) in
-            let session = PumpOpsSession(settings: self.pumpSettings, pumpState: self.pumpState.value, session: commandSession, delegate: self)
+            let minimedPumpMessageSender = MinimedPumpMessageSender(commandSession: commandSession, commsLogger: self.delegate)
+            let session = PumpOpsSession(settings: self.pumpSettings, pumpState: self.pumpState.value, messageSender: minimedPumpMessageSender, delegate: self)
             self.sessionDevice = device
             if !commandSession.firmwareVersion.isUnknown {
                 self.configureDevice(device, with: session)
@@ -74,7 +79,7 @@ public class PumpOps {
 
     // Must be called from within the RileyLinkDevice sessionQueue
     private func configureDevice(_ device: RileyLinkDevice, with session: PumpOpsSession) {
-        guard !self.configuredDevices.value.contains(device) else {
+        guard !self.configuredDevices.value.contains(device.peripheralIdentifier) else {
             return
         }
 
@@ -92,7 +97,7 @@ public class PumpOps {
         NotificationCenter.default.addObserver(self, selector: #selector(deviceRadioConfigDidChange(_:)), name: .DeviceRadioConfigDidChange, object: device)
         NotificationCenter.default.addObserver(self, selector: #selector(deviceRadioConfigDidChange(_:)), name: .DeviceConnectionStateDidChange, object: device)
         _ = configuredDevices.mutate { (value) in
-            value.insert(device)
+            value.insert(device.peripheralIdentifier)
         }
     }
 
@@ -105,45 +110,45 @@ public class PumpOps {
         NotificationCenter.default.removeObserver(self, name: .DeviceConnectionStateDidChange, object: device)
 
         _ = configuredDevices.mutate { (value) in
-            value.remove(device)
+            value.remove(device.peripheralIdentifier)
         }
     }
 }
 
 // Delivered on RileyLinkDeviceManager.sessionQueue
-extension PumpOps: PumpOpsSessionDelegate {
-    func pumpOpsSessionDidChangeRadioConfig(_ session: PumpOpsSession) {
+extension MinimedPumpOps: PumpOpsSessionDelegate {
+    public func pumpOpsSessionDidChangeRadioConfig(_ session: PumpOpsSession) {
         if let sessionDevice = self.sessionDevice {
-            self.configuredDevices.value = [sessionDevice]
+            self.configuredDevices.value = [sessionDevice.peripheralIdentifier]
         }
     }
     
-    func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState) {
+    public func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState) {
         self.pumpState.value = state
         delegate?.pumpOps(self, didChange: state)
         NotificationCenter.default.post(
             name: .PumpOpsStateDidChange,
             object: self,
-            userInfo: [PumpOps.notificationPumpStateKey: pumpState]
+            userInfo: [MinimedPumpOps.notificationPumpStateKey: pumpState]
         )
     }
 }
 
 
-extension PumpOps: CustomDebugStringConvertible {
+extension MinimedPumpOps: CustomDebugStringConvertible {
     public var debugDescription: String {
         return [
             "### PumpOps",
             "pumpSettings: \(String(reflecting: pumpSettings))",
             "pumpState: \(String(reflecting: pumpState.value))",
-            "configuredDevices: \(configuredDevices.value.map({ $0.peripheralIdentifier.uuidString }))",
+            "configuredDevices: \(configuredDevices.value.map({ $0.uuidString }))",
         ].joined(separator: "\n")
     }
 }
 
 
 /// Provide a notification contract that clients can use to inform RileyLink UI of changes to PumpOps.PumpState
-extension PumpOps {
+extension MinimedPumpOps {
     public static let notificationPumpStateKey = "com.rileylink.RileyLinkKit.PumpOps.PumpState"
 }
 

+ 2 - 31
Dependencies/rileylink_ios/MinimedKit/PumpManager/PumpOpsError.swift

@@ -38,35 +38,6 @@ extension PumpOpsError: LocalizedError {
     public var errorDescription: String? {
         switch self {
         case .bolusInProgress:
-            return nil
-        case .couldNotDecode:
-            return LocalizedString("Decoding Error", comment: "Error description")
-        case .crosstalk:
-            return nil
-        case .deviceError:
-            return LocalizedString("Device Error", comment: "Error description")
-        case .noResponse:
-            return nil
-        case .pumpError:
-            return LocalizedString("Pump Error", comment: "Error description")
-        case .pumpSuspended:
-            return nil
-        case .rfCommsFailure:
-            return nil
-        case .unexpectedResponse:
-            return nil
-        case .unknownPumpErrorCode:
-            return nil
-        case .unknownPumpModel:
-            return nil
-        case .unknownResponse:
-            return nil
-        }
-    }
-
-    public var failureReason: String? {
-        switch self {
-        case .bolusInProgress:
             return LocalizedString("A bolus is already in progress", comment: "Communications error for a bolus currently running")
         case .couldNotDecode(rx: let data, during: let during):
             return String(format: LocalizedString("Invalid response during %1$@: %2$@", comment: "Format string for failure reason. (1: The operation being performed) (2: The response data)"), String(describing: during), data.hexadecimalString)
@@ -78,8 +49,8 @@ extension PumpOpsError: LocalizedError {
             return LocalizedString("Pump is suspended", comment: "")
         case .rfCommsFailure(let msg):
             return msg
-        case .unexpectedResponse:
-            return LocalizedString("Pump responded unexpectedly", comment: "")
+        case .unexpectedResponse(let response, _):
+            return String(format: LocalizedString("Unexpected response %1$@", comment: "Format string for an unexpectedResponse. (2: The response)"), String(describing: response))
         case .unknownPumpErrorCode(let code):
             return String(format: LocalizedString("Unknown pump error code: %1$@", comment: "The format string description of an unknown pump error code. (1: The specific error code raw value)"), String(describing: code))
         case .unknownPumpModel(let model):

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

@@ -9,7 +9,7 @@ import HealthKit
 import RileyLinkBLEKit
 
 
-extension RileyLinkDevice.Status {
+extension RileyLinkDeviceStatus {
     func device(pumpID: String, pumpModel: PumpModel) -> HKDevice {
         return HKDevice(
             name: name,

+ 7 - 7
Dependencies/rileylink_ios/MinimedKit/PumpManager/UnfinalizedDose.swift

@@ -258,8 +258,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti
 // MARK: - UnfinalizedDose
 
 extension UnfinalizedDose {
-    var newPumpEvent: NewPumpEvent {
-        return NewPumpEvent(self)
+    func newPumpEvent(forceFinalization: Bool = false) -> NewPumpEvent {
+        return NewPumpEvent(self, forceFinalization: forceFinalization)
     }
 }
 
@@ -268,8 +268,8 @@ extension UnfinalizedDose {
 
 
 extension NewPumpEvent {
-    init(_ dose: UnfinalizedDose) {
-        let entry = DoseEntry(dose)
+    init(_ dose: UnfinalizedDose, forceFinalization: Bool = false) {
+        let entry = DoseEntry(dose, forceFinalization: forceFinalization)
         let raw = dose.uuid.asRaw
         self.init(date: dose.startTime, dose: entry, raw: raw, title: dose.eventTitle)
     }
@@ -283,12 +283,12 @@ extension NewPumpEvent {
 // MARK: - DoseEntry
 
 extension DoseEntry {
-    init (_ dose: UnfinalizedDose) {
+    init (_ dose: UnfinalizedDose, forceFinalization: Bool = false) {
         switch dose.doseType {
         case .bolus:
-            self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isFinished || !dose.isReconciledWithHistory)
+            self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: (!dose.isFinished || !dose.isReconciledWithHistory) && !forceFinalization)
         case .tempBasal:
-            self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, isMutable: !dose.isFinished || !dose.isReconciledWithHistory)
+            self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, isMutable: (!dose.isFinished || !dose.isReconciledWithHistory) && !forceFinalization)
         case .suspend:
             self = DoseEntry(suspendDate: dose.startTime)
         case .resume:

+ 34 - 100
Dependencies/rileylink_ios/MinimedKitTests/MinimedPumpManagerTests.swift

@@ -7,112 +7,46 @@
 //
 
 import XCTest
+import RileyLinkBLEKit
 @testable import MinimedKit
 import LoopKit
 
 class MinimedPumpManagerTests: XCTestCase {
 
-    func testPendingDoseUpdatesWithActualDeliveryFromHistoryDose() {
-        
-        let bolusTime = Date().addingTimeInterval(-TimeInterval(minutes: 5));
-        
-        let bolusEventTime = bolusTime.addingTimeInterval(2)
-
-        let cancelTime = bolusEventTime.addingTimeInterval(TimeInterval(minutes: 1))
-
-        let unfinalizedBolus = UnfinalizedDose(bolusAmount: 5.4, startTime: bolusTime, duration: TimeInterval(200), insulinType: .novolog, automatic: false, isReconciledWithHistory: false)
-        
-        // 5.4 bolus interrupted at 1.0 units
-        let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: cancelTime, value: unfinalizedBolus.units, unit: .units, deliveredUnits: 1.0)
-        
-        let bolusEvent = NewPumpEvent(
-            date: bolusEventTime,
-            dose: eventDose,
-            raw: Data(hexadecimalString: "abcdef")!,
-            title: "Test Bolus",
-            type: .bolus)
-        
-        let result = MinimedPumpManager.reconcilePendingDosesWith([bolusEvent], reconciliationMappings: [:], pendingDoses: [unfinalizedBolus])
-        
-        // Should mark pending bolus as reconciled
-        XCTAssertEqual(1, result.pendingDoses.count)
-        let pendingBolus = result.pendingDoses.first!
-        XCTAssertEqual(true, pendingBolus.isReconciledWithHistory)
-        
-        // Pending bolus should be updated with actual delivery amount
-        XCTAssertEqual(1.0, pendingBolus.units)
-        XCTAssertEqual(5.4, pendingBolus.programmedUnits)
-        XCTAssertEqual(TimeInterval(minutes: 1), pendingBolus.duration)
-        XCTAssertEqual(true, pendingBolus.isFinished)
-    }
-    
-    func testReconciledDosesShouldOnlyAppearInReturnedPendingDoses() {
-        
-        let bolusTime = Date().addingTimeInterval(-TimeInterval(minutes: 5));
-
-        // Shows up in history 2 seconds later
-        let bolusEventTime = bolusTime.addingTimeInterval(2)
-        
-        let bolusAmount = 1.5
-        
-        let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount)
-
-        let unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusAmount, startTime: bolusTime, duration: bolusDuration, insulinType: .novolog, automatic: false, isReconciledWithHistory: false)
-        
-        let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount)
-        
-        let bolusEvent = NewPumpEvent(
-            date: bolusEventTime,
-            dose: eventDose,
-            raw: Data(hexadecimalString: "abcdef")!,
-            title: "Test Bolus",
-            type: .bolus)
-        
-        let result = MinimedPumpManager.reconcilePendingDosesWith([bolusEvent], reconciliationMappings: [:], pendingDoses: [unfinalizedBolus])
-        
-        // Should mark pending bolus as reconciled
-        XCTAssertEqual(1, result.pendingDoses.count)
-        let pendingBolus = result.pendingDoses.first!
-        XCTAssertEqual(true, pendingBolus.isReconciledWithHistory)
-        
-        XCTAssertEqual(1, result.reconciliationMappings.count)
-        XCTAssertEqual(unfinalizedBolus.uuid, result.reconciliationMappings[bolusEvent.raw]?.uuid)
-        XCTAssertEqual(unfinalizedBolus.startTime, result.reconciliationMappings[bolusEvent.raw]?.startTime)
-
-        // Bolus should not be returned as history event
-        XCTAssert(result.remainingEvents.isEmpty)
+    var rlProvider: MockRileyLinkProvider!
+    var mockPumpManagerDelegate: MockPumpManagerDelegate!
+    var pumpManager: MinimedPumpManager!
+
+    override func setUpWithError() throws {
+        let device = MockRileyLinkDevice()
+        rlProvider = MockRileyLinkProvider(devices: [device])
+        let rlManagerState = RileyLinkConnectionState(autoConnectIDs: [])
+        let state = MinimedPumpManagerState(
+            isOnboarded: true,
+            useMySentry: true,
+            pumpColor: .blue,
+            pumpID: "123456",
+            pumpModel: .model523,
+            pumpFirmwareVersion: "VER 2.4A1.1",
+            pumpRegion: .northAmerica,
+            rileyLinkConnectionState: rlManagerState,
+            timeZone: .currentFixed,
+            suspendState: .resumed(Date()),
+            insulinType: .novolog,
+            lastTuned: nil,
+            lastValidFrequency: nil)
+        let pumpOps = MockPumpOps(pumpState: state.pumpState, pumpSettings: state.pumpSettings)
+        pumpManager = MinimedPumpManager(state: state, rileyLinkDeviceProvider: rlProvider, pumpOps: pumpOps)
+        mockPumpManagerDelegate = MockPumpManagerDelegate()
+        pumpManager.pumpManagerDelegate = mockPumpManagerDelegate
     }
-    
-    func testReconciledDosesShouldNotAppearInReturnedPumpEvents() {
-        
-        let bolusTime = Date().addingTimeInterval(-TimeInterval(minutes: 5));
 
-        // Shows up in history 2 seconds later
-        let bolusEventTime = bolusTime.addingTimeInterval(2)
-        
-        let bolusAmount = 1.5
-        
-        let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount)
-
-        let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount)
-        
-        let bolusEvent = NewPumpEvent(
-            date: bolusEventTime,
-            dose: eventDose,
-            raw: Data(hexadecimalString: "abcdef")!,
-            title: "Test Bolus",
-            type: .bolus)
-        
-        
-        
-        let reconciliationMappings: [Data:ReconciledDoseMapping] = [
-            bolusEvent.raw : ReconciledDoseMapping(startTime: bolusTime, uuid: UUID(), eventRaw: bolusEvent.raw)
-        ]
-        
-        let result = MinimedPumpManager.reconcilePendingDosesWith([bolusEvent], reconciliationMappings: reconciliationMappings, pendingDoses: [])
-        
-        // Bolus should not be returned as history event
-        XCTAssert(result.remainingEvents.isEmpty)
+    func testBolusWithInvalidResponse() {
+        let exp = expectation(description: "enactBolus callback")
+        pumpManager.enactBolus(units: 2.3, activationType: .manualNoRecommendation) { error in
+            XCTAssertNotNil(error)
+            exp.fulfill()
+        }
+        waitForExpectations(timeout: 2)
     }
-
 }

+ 10 - 10
Dependencies/rileylink_ios/MinimedKitTests/PumpOpsSynchronousBuildFromFramesTests.swift

@@ -20,7 +20,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
     var pumpID: String!
     var pumpRegion: PumpRegion!
     var pumpModel: PumpModel!
-    var messageSenderStub: PumpMessageSenderStub!
+    var mockPumpMessageSender: MockPumpMessageSender!
     var timeZone: TimeZone!
     
     override func setUp() {
@@ -30,7 +30,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
         pumpRegion = .worldWide
         pumpModel = PumpModel.model523
 
-        messageSenderStub = PumpMessageSenderStub()
+        mockPumpMessageSender = MockPumpMessageSender()
         timeZone = TimeZone(secondsFromGMT: 0)
         
         loadSUT()
@@ -42,11 +42,11 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
         pumpState.pumpModel = pumpModel
         pumpState.awakeUntil = Date(timeIntervalSinceNow: 100) // pump is awake
         
-        sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, session: messageSenderStub, delegate: messageSenderStub)
+        sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, messageSender: mockPumpMessageSender, delegate: mockPumpMessageSender)
     }
     
     func testErrorIsntThrown() {
-        messageSenderStub.responses = buildResponsesDictionary()
+        mockPumpMessageSender.responses = buildResponsesDictionary()
         
         assertNoThrow(try _ = sut.getHistoryEvents(since: Date()))
     }
@@ -58,7 +58,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
         pumpAckArray.insert(message, at: 0)
         responseDictionary[.getHistoryPage]! = pumpAckArray
         
-        messageSenderStub.responses = responseDictionary
+        mockPumpMessageSender.responses = responseDictionary
         
         // Didn't receive a .pumpAck short reponse so throw an error
         assertThrows(try _ = sut.getHistoryEvents(since: Date()))
@@ -71,14 +71,14 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
         pumpAckArray.insert(message, at: 1)
         responseDictionary[.getHistoryPage]! = pumpAckArray
         
-        messageSenderStub.responses = responseDictionary
+        mockPumpMessageSender.responses = responseDictionary
         
         // Didn't receive a .getHistoryPage as 2nd response so throw an error
         assertThrows(try _ = sut.getHistoryEvents(since: Date()))
     }
     
     func test332EventsReturnedUntilOutOrder() {
-        messageSenderStub.responses = buildResponsesDictionary()
+        mockPumpMessageSender.responses = buildResponsesDictionary()
         
         let date = Date(timeIntervalSince1970: 0)
         do {
@@ -91,7 +91,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
     }
     
     func testEventsReturnedAfterTime() {
-        messageSenderStub.responses = buildResponsesDictionary()
+        mockPumpMessageSender.responses = buildResponsesDictionary()
         timeZone = TimeZone.current
         
         loadSUT()
@@ -109,7 +109,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
     }
     
     func testGMTEventsAreTheSame() {
-        messageSenderStub.responses = buildResponsesDictionary()
+        mockPumpMessageSender.responses = buildResponsesDictionary()
         timeZone = TimeZone(secondsFromGMT:0)
         
         loadSUT()
@@ -126,7 +126,7 @@ class PumpOpsSynchronousBuildFromFramesTests: XCTestCase {
     }
     
     func testEventsReturnedAreAscendingOrder() {
-        messageSenderStub.responses = buildResponsesDictionary()
+        mockPumpMessageSender.responses = buildResponsesDictionary()
         
         //02/11/2017 @ 12:00am (UTC)
         let date = DateComponents(calendar: Calendar.current, timeZone: pumpState.timeZone, year: 2017, month: 2, day: 11, hour: 0, minute: 0, second: 0).date!

+ 86 - 11
Dependencies/rileylink_ios/MinimedKitTests/PumpOpsSynchronousTests.swift

@@ -20,7 +20,7 @@ class PumpOpsSynchronousTests: XCTestCase {
     var pumpID: String!
     var pumpRegion: PumpRegion!
     var pumpModel: PumpModel!
-    var messageSenderStub: PumpMessageSenderStub!
+    var mockMessageSender: MockPumpMessageSender!
 
     let dateComponents2007 = DateComponents(calendar: Calendar.current, year: 2007, month: 1, day: 1)
     let dateComponents2017 = DateComponents(calendar: Calendar.current, year: 2017, month: 1, day: 1)
@@ -46,7 +46,7 @@ class PumpOpsSynchronousTests: XCTestCase {
         pumpRegion = .worldWide
         pumpModel = PumpModel.model523
 
-        messageSenderStub = PumpMessageSenderStub()
+        mockMessageSender = MockPumpMessageSender()
         
         setUpSUT()
     }
@@ -58,7 +58,7 @@ class PumpOpsSynchronousTests: XCTestCase {
         pumpState.pumpModel = pumpModel
         pumpState.awakeUntil = Date(timeIntervalSinceNow: 100) // pump is awake
         
-        sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, session: messageSenderStub, delegate: messageSenderStub)
+        sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, messageSender: mockMessageSender, delegate: mockMessageSender)
     }
     
     /// Duplicates logic in setUp with a new PumpModel
@@ -312,7 +312,13 @@ func randomDataString(length:Int) -> String {
     return s
 }
 
-class PumpMessageSenderStub: PumpMessageSender {
+class MockPumpMessageSender: PumpMessageSender {
+
+    func listenForPacket(onChannel channel: Int, timeout: TimeInterval) throws -> RileyLinkBLEKit.RFPacket? {
+        // do nothing
+        return nil
+    }
+
     func getRileyLinkStatistics() throws -> RileyLinkStatistics {
         throw PumpOpsError.noResponse(during: "Tests")
     }
@@ -344,7 +350,8 @@ class PumpMessageSenderStub: PumpMessageSender {
 
             response = responseArray[numberOfResponsesReceived]
         } else {
-            response = PumpMessage(rxData: Data())!
+            let packet = MinimedPacket(encodedData: Data(hexadecimalString: "a969a39966b1566555b235")!)!
+            response = PumpMessage(rxData: packet.data)!
         }
 
         var encoded = MinimedPacket(outgoingData: response.txData).encodedData()
@@ -357,11 +364,83 @@ class PumpMessageSenderStub: PumpMessageSender {
         return rfPacket
     }
 
+    func getResponse<T: MessageBody>(to message: PumpMessage, responseType: MessageType, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> T {
+
+        let response = try sendAndListen(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
+
+        guard response.messageType == responseType, let body = response.messageBody as? T else {
+            if let body = response.messageBody as? PumpErrorMessageBody {
+                switch body.errorCode {
+                case .known(let code):
+                    throw PumpOpsError.pumpError(code)
+                case .unknown(let code):
+                    throw PumpOpsError.unknownPumpErrorCode(code)
+                }
+            } else {
+                throw PumpOpsError.unexpectedResponse(response, from: message)
+            }
+        }
+        return body
+    }
+
+    /// Sends a message to the pump, listening for a any known PumpMessage in reply
+    ///
+    /// - Parameters:
+    ///   - message: The message to send
+    ///   - repeatCount: The number of times to repeat the message before listening begins
+    ///   - timeout: The length of time to listen for a pump response
+    ///   - retryCount: The number of times to repeat the send & listen sequence
+    /// - Returns: The message reply
+    /// - Throws: An error describing a failure in the sending or receiving of a message:
+    ///     - PumpOpsError.couldNotDecode
+    ///     - PumpOpsError.crosstalk
+    ///     - PumpOpsError.deviceError
+    ///     - PumpOpsError.noResponse
+    ///     - PumpOpsError.unknownResponse
+    func sendAndListen(_ message: PumpMessage, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> PumpMessage {
+        let rfPacket = try sendAndListenForPacket(message, repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
+
+        guard let packet = MinimedPacket(encodedData: rfPacket.data) else {
+            throw PumpOpsError.couldNotDecode(rx: rfPacket.data, during: message)
+        }
+
+        guard let response = PumpMessage(rxData: packet.data) else {
+            // Unknown packet type or message type
+            throw PumpOpsError.unknownResponse(rx: packet.data, during: message)
+        }
+
+        guard response.address == message.address else {
+            throw PumpOpsError.crosstalk(response, during: message)
+        }
+
+        return response
+    }
+
+    // Send a PumpMessage, and listens for a packet; used by callers who need to see RSSI
+    /// - Throws:
+    ///     - PumpOpsError.noResponse
+    ///     - PumpOpsError.deviceError
+    func sendAndListenForPacket(_ message: PumpMessage, repeatCount: Int, timeout: TimeInterval, retryCount: Int) throws -> RFPacket {
+        let packet: RFPacket?
+
+        do {
+            packet = try sendAndListen(MinimedPacket(outgoingData: message.txData).encodedData(), repeatCount: repeatCount, timeout: timeout, retryCount: retryCount)
+        } catch let error as LocalizedError {
+            throw PumpOpsError.deviceError(error)
+        }
+
+        guard let rfPacket = packet else {
+            throw PumpOpsError.noResponse(during: message)
+        }
+
+        return rfPacket
+    }
+
     func listen(onChannel channel: Int, timeout: TimeInterval) throws -> RFPacket? {
         throw PumpOpsError.noResponse(during: "Tests")
     }
 
-    func send(_ data: Data, onChannel channel: Int, timeout: TimeInterval) throws {
+    func send(_ msg: MinimedKit.PumpMessage) throws {
         // Do nothing
     }
 
@@ -377,17 +456,13 @@ class PumpMessageSenderStub: PumpMessageSender {
         throw PumpOpsError.noResponse(during: "Tests")
     }
     
-    func setCCLEDMode(_ mode: RileyLinkLEDMode) throws {
-        throw PumpOpsError.noResponse(during: "Tests")
-    }
-    
     var responses = [MessageType: [PumpMessage]]()
     
     // internal tracking of how many times a response type has been received
     private var responsesHaveOccured = [MessageType: Int]()
 }
 
-extension PumpMessageSenderStub: PumpOpsSessionDelegate {
+extension MockPumpMessageSender: PumpOpsSessionDelegate {
     func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState) {
 
     }

+ 118 - 0
Dependencies/rileylink_ios/MinimedKitTests/ReconciliationTests.swift

@@ -0,0 +1,118 @@
+//
+//  ReconciliationTests.swift
+//  MinimedKitTests
+//
+//  Created by Pete Schwamb on 9/5/22.
+//  Copyright © 2022 Pete Schwamb. All rights reserved.
+//
+
+import XCTest
+import RileyLinkBLEKit
+@testable import MinimedKit
+import LoopKit
+
+final class ReconciliationTests: XCTestCase {
+
+    func testPendingDoseUpdatesWithActualDeliveryFromHistoryDose() {
+
+        let bolusTime = Date().addingTimeInterval(-TimeInterval(minutes: 5));
+
+        let bolusEventTime = bolusTime.addingTimeInterval(2)
+
+        let cancelTime = bolusEventTime.addingTimeInterval(TimeInterval(minutes: 1))
+
+        let unfinalizedBolus = UnfinalizedDose(bolusAmount: 5.4, startTime: bolusTime, duration: TimeInterval(200), insulinType: .novolog, automatic: false, isReconciledWithHistory: false)
+
+        // 5.4 bolus interrupted at 1.0 units
+        let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: cancelTime, value: unfinalizedBolus.units, unit: .units, deliveredUnits: 1.0)
+
+        let bolusEvent = NewPumpEvent(
+            date: bolusEventTime,
+            dose: eventDose,
+            raw: Data(hexadecimalString: "abcdef")!,
+            title: "Test Bolus",
+            type: .bolus)
+
+        let result = MinimedPumpManager.reconcilePendingDosesWith([bolusEvent], reconciliationMappings: [:], pendingDoses: [unfinalizedBolus])
+
+        // Should mark pending bolus as reconciled
+        XCTAssertEqual(1, result.pendingDoses.count)
+        let pendingBolus = result.pendingDoses.first!
+        XCTAssertEqual(true, pendingBolus.isReconciledWithHistory)
+
+        // Pending bolus should be updated with actual delivery amount
+        XCTAssertEqual(1.0, pendingBolus.units)
+        XCTAssertEqual(5.4, pendingBolus.programmedUnits)
+        XCTAssertEqual(TimeInterval(minutes: 1), pendingBolus.duration)
+        XCTAssertEqual(true, pendingBolus.isFinished)
+    }
+
+    func testReconciledDosesShouldOnlyAppearInReturnedPendingDoses() {
+
+        let bolusTime = Date().addingTimeInterval(-TimeInterval(minutes: 5));
+
+        // Shows up in history 2 seconds later
+        let bolusEventTime = bolusTime.addingTimeInterval(2)
+
+        let bolusAmount = 1.5
+
+        let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount)
+
+        let unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusAmount, startTime: bolusTime, duration: bolusDuration, insulinType: .novolog, automatic: false, isReconciledWithHistory: false)
+
+        let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount)
+
+        let bolusEvent = NewPumpEvent(
+            date: bolusEventTime,
+            dose: eventDose,
+            raw: Data(hexadecimalString: "abcdef")!,
+            title: "Test Bolus",
+            type: .bolus)
+
+        let result = MinimedPumpManager.reconcilePendingDosesWith([bolusEvent], reconciliationMappings: [:], pendingDoses: [unfinalizedBolus])
+
+        // Should mark pending bolus as reconciled
+        XCTAssertEqual(1, result.pendingDoses.count)
+        let pendingBolus = result.pendingDoses.first!
+        XCTAssertEqual(true, pendingBolus.isReconciledWithHistory)
+
+        XCTAssertEqual(1, result.reconciliationMappings.count)
+        XCTAssertEqual(unfinalizedBolus.uuid, result.reconciliationMappings[bolusEvent.raw]?.uuid)
+        XCTAssertEqual(unfinalizedBolus.startTime, result.reconciliationMappings[bolusEvent.raw]?.startTime)
+
+        // Bolus should not be returned as history event
+        XCTAssert(result.remainingEvents.isEmpty)
+    }
+
+    func testReconciledDosesShouldNotAppearInReturnedPumpEvents() {
+
+        let bolusTime = Date().addingTimeInterval(-TimeInterval(minutes: 5));
+
+        // Shows up in history 2 seconds later
+        let bolusEventTime = bolusTime.addingTimeInterval(2)
+
+        let bolusAmount = 1.5
+
+        let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount)
+
+        let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount)
+
+        let bolusEvent = NewPumpEvent(
+            date: bolusEventTime,
+            dose: eventDose,
+            raw: Data(hexadecimalString: "abcdef")!,
+            title: "Test Bolus",
+            type: .bolus)
+
+
+
+        let reconciliationMappings: [Data:ReconciledDoseMapping] = [
+            bolusEvent.raw : ReconciledDoseMapping(startTime: bolusTime, uuid: UUID(), eventRaw: bolusEvent.raw)
+        ]
+
+        let result = MinimedPumpManager.reconcilePendingDosesWith([bolusEvent], reconciliationMappings: reconciliationMappings, pendingDoses: [])
+
+        // Bolus should not be returned as history event
+        XCTAssert(result.remainingEvents.isEmpty)
+    }
+}

+ 5 - 0
Dependencies/rileylink_ios/MinimedKitTests/TimestampedHistoryEventTests.swift

@@ -69,6 +69,11 @@ class TimestampedHistoryEventTests: XCTestCase {
 
     }
 
+    func testBolusOnX22() {
+        let bolus = BolusNormalPumpEvent(availableData: Data(hexadecimalString: "567901e443494eda97dbfd38150216f3")!, pumpModel: .model522)!
+        XCTAssertEqual(bolus.wasRemotelyTriggered, true)
+    }
+
     func testSquareWaveIsMutableOnX23() {
         let squareBolus = BolusNormalPumpEvent(availableData: Data(hexadecimalString: "010080008000240209a24a1510")!, pumpModel: .model523)!
         let squareBolusTimestamp = squareBolus.timestamp.date!

+ 1 - 1
Dependencies/rileylink_ios/MinimedKitUI/CommandResponseViewController.swift

@@ -40,7 +40,7 @@ extension CommandResponseViewController {
 
     static func changeTime(ops: PumpOps?, rileyLinkDeviceProvider: RileyLinkDeviceProvider) -> T {
         return T { (completionHandler) -> String in
-            ops?.runSession(withName: "Set time", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+            ops?.runSession(withName: "Set time", usingSelector: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
                 let response: String
                 do {
                     guard let session = session else {

+ 3 - 2
Dependencies/rileylink_ios/MinimedKitUI/MinimedPumpManager+UI.swift

@@ -21,6 +21,7 @@ extension MinimedPumpManager: PumpManagerUI {
 
     static public func setupViewController(initialSettings settings: PumpManagerSetupSettings, bluetoothProvider: BluetoothProvider, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, allowedInsulinTypes: [InsulinType]) -> SetupUIResult<PumpManagerViewController, PumpManagerUI> {
         let navVC = MinimedPumpManagerSetupViewController.instantiateFromStoryboard()
+        navVC.supportedInsulinTypes = allowedInsulinTypes
         let didConfirm: (InsulinType) -> Void = { [weak navVC] (confirmedType) in
             if let navVC = navVC {
                 navVC.insulinType = confirmedType
@@ -45,14 +46,14 @@ extension MinimedPumpManager: PumpManagerUI {
     }
 
     public func settingsViewController(bluetoothProvider: BluetoothProvider, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, allowedInsulinTypes: [InsulinType]) -> PumpManagerViewController {
-        let settings = MinimedPumpSettingsViewController(pumpManager: self)
+        let settings = MinimedPumpSettingsViewController(pumpManager: self, supportedInsulinTypes: allowedInsulinTypes)
         let nav = PumpManagerSettingsNavigationViewController(rootViewController: settings)
         return nav
     }
     
     public func deliveryUncertaintyRecoveryViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> (UIViewController & CompletionNotifying) {
         // Return settings for now. No uncertainty handling atm.
-        let settings = MinimedPumpSettingsViewController(pumpManager: self)
+        let settings = MinimedPumpSettingsViewController(pumpManager: self, supportedInsulinTypes: [])
         let nav = SettingsNavigationViewController(rootViewController: settings)
         return nav
     }

+ 9 - 6
Dependencies/rileylink_ios/MinimedKitUI/MinimedPumpSettingsViewController.swift

@@ -15,6 +15,8 @@ import LoopKit
 class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
 
     let pumpManager: MinimedPumpManager
+
+    let supportedInsulinTypes: [InsulinType]
     
     private var ops: PumpOps {
         return pumpManager.pumpOps
@@ -63,8 +65,9 @@ class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
     }
 
     
-    init(pumpManager: MinimedPumpManager) {
+    init(pumpManager: MinimedPumpManager, supportedInsulinTypes: [InsulinType]) {
         self.pumpManager = pumpManager
+        self.supportedInsulinTypes = supportedInsulinTypes
         super.init(rileyLinkPumpManager: pumpManager, devicesSectionIndex: Section.rileyLinks.rawValue, style: .grouped)
     }
 
@@ -96,7 +99,7 @@ class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
         let mainQueue = OperationQueue.main
 
         center.addObserver(forName: .PumpOpsStateDidChange, object: pumpManager.pumpOps, queue: mainQueue) { [weak self] (note) in
-            if let state = note.userInfo?[PumpOps.notificationPumpStateKey] as? PumpState {
+            if let state = note.userInfo?[MinimedPumpOps.notificationPumpStateKey] as? PumpState {
                 self?.pumpState = state
             }
         }
@@ -106,7 +109,7 @@ class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
         let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:)))
         self.navigationItem.setRightBarButton(button, animated: false)
         
-        self.pumpState = pumpManager.pumpOps.pumpState.value
+        self.pumpState = pumpManager.state.pumpState
     }
 
     @objc func doneTapped(_ sender: Any) {
@@ -373,7 +376,7 @@ class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
 
                 show(vc, sender: sender)
             case .insulinType:
-                let view = InsulinTypeSetting(initialValue: pumpManager.insulinType ?? .novolog, supportedInsulinTypes: InsulinType.allCases, allowUnsetInsulinType: false) { (newType) in
+                let view = InsulinTypeSetting(initialValue: pumpManager.insulinType ?? .novolog, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false) { (newType) in
                     self.pumpManager.insulinType = newType
                 }
                 let vc = DismissibleHostingController(rootView: view)
@@ -397,8 +400,8 @@ class MinimedPumpSettingsViewController: RileyLinkSettingsViewController {
             let vc = RileyLinkDeviceTableViewController(
                 device: device,
                 batteryAlertLevel: pumpManager.rileyLinkBatteryAlertLevel,
-                batteryAlertLevelChanged: { value in
-                    self.pumpManager.rileyLinkBatteryAlertLevel = value
+                batteryAlertLevelChanged: { [weak self] value in
+                    self?.pumpManager.rileyLinkBatteryAlertLevel = value
                 }
             )
 

+ 14 - 8
Dependencies/rileylink_ios/MinimedKitUI/Setup/MinimedPumpIDSetupViewController.swift

@@ -90,16 +90,18 @@ class MinimedPumpIDSetupViewController: SetupTableViewController {
             
             return MinimedPumpManagerState(
                 isOnboarded: false,
-                useMySentry: true,
+                useMySentry: pumpState?.useMySentry ?? true,
                 pumpColor: pumpColor,
                 pumpID: pumpID,
                 pumpModel: pumpModel,
                 pumpFirmwareVersion: pumpFirmwareVersion,
                 pumpRegion: pumpRegion,
-                rileyLinkConnectionManagerState: rileyLinkPumpManager.rileyLinkConnectionManagerState,
+                rileyLinkConnectionState: rileyLinkPumpManager.rileyLinkConnectionManagerState,
                 timeZone: timeZone,
                 suspendState: .resumed(Date()),
-                insulinType: insulinType
+                insulinType: insulinType,
+                lastTuned: pumpState?.lastTuned,
+                lastValidFrequency: pumpState?.lastValidFrequency
             )
         }
     }
@@ -111,9 +113,7 @@ class MinimedPumpIDSetupViewController: SetupTableViewController {
 
         return MinimedPumpManager(
             state: pumpManagerState,
-            rileyLinkDeviceProvider: rileyLinkPumpManager.rileyLinkDeviceProvider,
-            rileyLinkConnectionManager: rileyLinkPumpManager.rileyLinkConnectionManager,
-            pumpOps: self.pumpOps)
+            rileyLinkDeviceProvider: rileyLinkPumpManager.rileyLinkDeviceProvider)
     }
 
     // MARK: -
@@ -247,9 +247,9 @@ class MinimedPumpIDSetupViewController: SetupTableViewController {
     private func setupPump(with settings: PumpSettings) {
         continueState = .reading
 
-        let pumpOps = PumpOps(pumpSettings: settings, pumpState: pumpState, delegate: self)
+        let pumpOps = MinimedPumpOps(pumpSettings: settings, pumpState: pumpState, delegate: self)
         self.pumpOps = pumpOps
-        pumpOps.runSession(withName: "Pump ID Setup", using: rileyLinkPumpManager.rileyLinkDeviceProvider.firstConnectedDevice, { (session) in
+        pumpOps.runSession(withName: "Pump ID Setup", usingSelector: rileyLinkPumpManager.rileyLinkDeviceProvider.firstConnectedDevice, { (session) in
             guard let session = session else {
                 DispatchQueue.main.async {
                     self.lastError = PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)
@@ -436,6 +436,12 @@ extension MinimedPumpIDSetupViewController: UITextFieldDelegate {
 
 
 extension MinimedPumpIDSetupViewController: PumpOpsDelegate {
+    // TODO: create PumpManager and report it to Loop before pump setup
+    // No pumpManager available yet, so no device logs.
+    func willSend(_ message: String) {}
+    func didReceive(_ message: String) {}
+    func didError(_ message: String) {}
+
     func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState) {
         DispatchQueue.main.async {
             self.pumpState = state

+ 3 - 1
Dependencies/rileylink_ios/MinimedKitUI/Setup/MinimedPumpManagerSetupViewController.swift

@@ -40,6 +40,8 @@ public class MinimedPumpManagerSetupViewController: RileyLinkManagerSetupViewCon
     
     internal var insulinType: InsulinType?
 
+    internal var supportedInsulinTypes: [InsulinType]?
+
     /*
      1. RileyLink
      - RileyLinkPumpManagerState
@@ -119,7 +121,7 @@ public class MinimedPumpManagerSetupViewController: RileyLinkManagerSetupViewCon
 
             pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didOnboardPumpManager: pumpManager)
 
-            let settingsViewController = MinimedPumpSettingsViewController(pumpManager: pumpManager)
+            let settingsViewController = MinimedPumpSettingsViewController(pumpManager: pumpManager, supportedInsulinTypes: supportedInsulinTypes!)
             setViewControllers([settingsViewController], animated: true)
         }
     }

+ 1 - 1
Dependencies/rileylink_ios/MinimedKitUI/Setup/MinimedPumpSentrySetupViewController.swift

@@ -89,7 +89,7 @@ class MinimedPumpSentrySetupViewController: SetupTableViewController {
 
         continueState = .listening
 
-        pumpManager.pumpOps.runSession(withName: "MySentry Pairing", using: pumpManager.rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+        pumpManager.pumpOps.runSession(withName: "MySentry Pairing", usingSelector: pumpManager.rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
             guard let session = session else {
                 DispatchQueue.main.async {
                     self.continueState = .notStarted

+ 3 - 3
Dependencies/rileylink_ios/OmniKit/OmnipodCommon/MessageBlocks/ErrorResponse.swift

@@ -8,10 +8,10 @@
 
 import Foundation
 
-fileprivate let errorResponseCode_badNonce: UInt8 = 0x14
+fileprivate let errorResponseCode_badNonce: UInt8 = 0x14 // only returned on Eros
 
 public enum ErrorResponseType {
-    case badNonce(nonceResyncKey: UInt16)
+    case badNonce(nonceResyncKey: UInt16) // only returned on Eros
     case nonretryableError(code: UInt8, faultEventCode: FaultEventCode, podProgress: PodProgressStatus)
 }
 
@@ -27,7 +27,7 @@ public struct ErrorResponse : MessageBlock {
         let errorCode = encodedData[2]
         switch (errorCode) {
         case errorResponseCode_badNonce:
-            // For this error code only the 2 next bytes are the encoded nonce resync key.
+            // For this error code only the 2 next bytes are the encoded nonce resync key (only returned on Eros)
             let nonceResyncKey: UInt16 = encodedData[3...].toBigEndian(UInt16.self)
             errorResponseType = .badNonce(nonceResyncKey: nonceResyncKey)
             break

Plik diff jest za duży
+ 8 - 8
Dependencies/rileylink_ios/OmniKit/OmnipodCommon/MessageBlocks/VersionResponse.swift


+ 14 - 2
Dependencies/rileylink_ios/OmniKit/OmnipodCommon/UnfinalizedDose.swift

@@ -199,6 +199,19 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti
         }
     }
 
+    public var eventTitle: String {
+        switch doseType {
+        case .bolus:
+            return NSLocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
+        case .resume:
+            return NSLocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
+        case .suspend:
+            return NSLocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
+        case .tempBasal:
+            return NSLocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
+        }
+    }
+
     // RawRepresentable
     public init?(rawValue: RawValue) {
         guard
@@ -278,9 +291,8 @@ private extension TimeInterval {
 
 extension NewPumpEvent {
     init(_ dose: UnfinalizedDose) {
-        let title = String(describing: dose)
         let entry = DoseEntry(dose)
-        self.init(date: dose.startTime, dose: entry, raw: dose.uniqueKey, title: title)
+        self.init(date: dose.startTime, dose: entry, raw: dose.uniqueKey, title: dose.eventTitle)
     }
 }
 

+ 25 - 22
Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -96,11 +96,11 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
     
     public let localizedTitle = LocalizedString("Omnipod", comment: "Generic title of the omnipod pump manager")
     
-    public init(state: OmnipodPumpManagerState, rileyLinkDeviceProvider: RileyLinkDeviceProvider, rileyLinkConnectionManager: RileyLinkConnectionManager? = nil, dateGenerator: @escaping () -> Date = Date.init) {
+    public init(state: OmnipodPumpManagerState, rileyLinkDeviceProvider: RileyLinkDeviceProvider, dateGenerator: @escaping () -> Date = Date.init) {
         self.lockedState = Locked(state)
         self.lockedPodComms = Locked(PodComms(podState: state.podState))
         self.dateGenerator = dateGenerator
-        super.init(rileyLinkDeviceProvider: rileyLinkDeviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
+        super.init(rileyLinkDeviceProvider: rileyLinkDeviceProvider)
 
         self.podComms.delegate = self
         self.podComms.messageLogger = self
@@ -113,11 +113,11 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
             return nil
         }
 
-        let rileyLinkConnectionManager = RileyLinkConnectionManager(state: connectionManagerState)
+        let deviceProvider = RileyLinkBluetoothDeviceProvider(autoConnectIDs: connectionManagerState.autoConnectIDs)
 
-        self.init(state: state, rileyLinkDeviceProvider: rileyLinkConnectionManager.deviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
+        self.init(state: state, rileyLinkDeviceProvider: deviceProvider)
 
-        rileyLinkConnectionManager.delegate = self
+        deviceProvider.delegate = self
     }
 
     private var podComms: PodComms {
@@ -169,7 +169,10 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
             }
 
             if oldValue.podState?.lastInsulinMeasurements?.reservoirLevel != newValue.podState?.lastInsulinMeasurements?.reservoirLevel {
-                if let lastInsulinMeasurements = newValue.podState?.lastInsulinMeasurements, let reservoirLevel = lastInsulinMeasurements.reservoirLevel {
+                if let lastInsulinMeasurements = newValue.podState?.lastInsulinMeasurements,
+                   let reservoirLevel = lastInsulinMeasurements.reservoirLevel,
+                   reservoirLevel != Pod.reservoirLevelAboveThresholdMagicNumber
+                {
                     self.pumpDelegate.notify({ (delegate) in
                         self.log.info("DU: updating reservoir level %{public}@", String(describing: reservoirLevel))
                         delegate?.pumpManager(self, didReadReservoirValue: reservoirLevel, at: lastInsulinMeasurements.validTime) { _ in }
@@ -227,7 +230,7 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
     
     // MARK: - RileyLink Updates
 
-    override public var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState? {
+    override public var rileyLinkConnectionManagerState: RileyLinkConnectionState? {
         get {
             return state.rileyLinkConnectionManagerState
         }
@@ -690,7 +693,7 @@ extension OmnipodPumpManager {
     #if targetEnvironment(simulator)
     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)
+        var podState = PodState(address: address, pmVersion: "jumpstarted", piVersion: "jumpstarted", lot: lot, tid: tid, insulinType: .novolog)
         podState.setupProgress = .podPaired
         podState.activatedAt = start
         podState.expiresAt = start + .hours(72)
@@ -738,22 +741,22 @@ extension OmnipodPumpManager {
         let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         let primeSession = { (result: PodComms.SessionRunResult) in
             switch result {
-            case .success(let session):
+            case .success(let messageSender):
                 // We're on the session queue
-                session.assertOnSessionQueue()
+                messageSender.assertOnSessionQueue()
 
                 self.log.default("Beginning pod prime")
 
                 // Clean up any previously un-stored doses if needed
                 let unstoredDoses = self.state.unstoredDoses
-                if self.store(doses: unstoredDoses, in: session) {
+                if self.store(doses: unstoredDoses, in: messageSender) {
                     self.setState({ (state) in
                         state.unstoredDoses.removeAll()
                     })
                 }
 
                 do {
-                    let primeFinishedAt = try session.prime()
+                    let primeFinishedAt = try messageSender.prime()
                     completion(.success(primeFinishedAt))
                 } catch let error {
                     completion(.failure(PumpManagerError.communication(error as? LocalizedError)))
@@ -861,14 +864,14 @@ extension OmnipodPumpManager {
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName:  "Insert cannula", using: rileyLinkSelector) { (result) in
             switch result {
-            case .success(let session):
+            case .success(let messageSender):
                 do {
                     if self.state.podState?.setupProgress.needsInitialBasalSchedule == true {
                         let scheduleOffset = timeZone.scheduleOffset(forDate: Date())
-                        try session.programInitialBasalSchedule(self.state.basalSchedule, scheduleOffset: scheduleOffset)
+                        try messageSender.programInitialBasalSchedule(self.state.basalSchedule, scheduleOffset: scheduleOffset)
 
-                        session.dosesForStorage() { (doses) -> Bool in
-                            return self.store(doses: doses, in: session)
+                        messageSender.dosesForStorage() { (doses) -> Bool in
+                            return self.store(doses: doses, in: messageSender)
                         }
                     }
 
@@ -880,7 +883,7 @@ extension OmnipodPumpManager {
                         .lowReservoir(self.state.lowReservoirReminderValue)
                     ]
 
-                    let finishWait = try session.insertCannula(optionalAlerts: alerts)
+                    let finishWait = try messageSender.insertCannula(optionalAlerts: alerts)
                     completion(.success(finishWait))
                 } catch let error {
                     completion(.failure(.communication(error)))
@@ -899,9 +902,9 @@ extension OmnipodPumpManager {
         let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName: "Check cannula insertion finished", using: deviceSelector) { (result) in
             switch result {
-            case .success(let session):
+            case .success(let messageSender):
                 do {
-                    try session.checkInsertionCompleted()
+                    try messageSender.checkInsertionCompleted()
                     completion(nil)
                 } catch let error {
                     self.log.error("Failed to fetch pod status: %{public}@", String(describing: error))
@@ -1053,9 +1056,9 @@ extension OmnipodPumpManager {
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName: "Deactivate pod", using: rileyLinkSelector) { (result) in
             switch result {
-            case .success(let session):
+            case .success(let messageSender):
                 do {
-                    try session.deactivatePod()
+                    try messageSender.deactivatePod()
                     completion(nil)
                 } catch let error {
                     completion(OmnipodPumpManagerError.communication(error))
@@ -2050,7 +2053,7 @@ extension OmnipodPumpManager: PumpManager {
                 preconditionFailure("pumpManagerDelegate cannot be nil")
             }
 
-            delegate.pumpManager(self, hasNewPumpEvents: doses.map { NewPumpEvent($0) }, lastSync: lastSync, completion: { (error) in
+            delegate.pumpManager(self, hasNewPumpEvents: doses.map { NewPumpEvent($0) }, lastReconciliation: lastSync, completion: { (error) in
                 if let error = error {
                     self.log.error("Error storing pod events: %@", String(describing: error))
                 } else {

+ 6 - 6
Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManagerState.swift

@@ -31,7 +31,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
     
     public var basalSchedule: BasalSchedule
     
-    public var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?
+    public var rileyLinkConnectionManagerState: RileyLinkConnectionState?
 
     public var unstoredDoses: [UnfinalizedDose]
 
@@ -93,7 +93,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
 
     // MARK: -
 
-    public init(isOnboarded: Bool, podState: PodState?, timeZone: TimeZone, basalSchedule: BasalSchedule, rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?, insulinType: InsulinType?, maximumTempBasalRate: Double) {
+    public init(isOnboarded: Bool, podState: PodState?, timeZone: TimeZone, basalSchedule: BasalSchedule, rileyLinkConnectionManagerState: RileyLinkConnectionState?, insulinType: InsulinType?, maximumTempBasalRate: Double) {
         self.isOnboarded = isOnboarded
         self.podState = podState
         self.timeZone = timeZone
@@ -154,11 +154,11 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
             timeZone = TimeZone.currentFixed
         }
         
-        let rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?
-        if let rileyLinkConnectionManagerStateRaw = rawValue["rileyLinkConnectionManagerState"] as? RileyLinkConnectionManagerState.RawValue {
-            rileyLinkConnectionManagerState = RileyLinkConnectionManagerState(rawValue: rileyLinkConnectionManagerStateRaw)
+        let rileyLinkConnectionManagerState: RileyLinkConnectionState?
+        if let rileyLinkConnectionManagerStateRaw = rawValue["rileyLinkConnectionManagerState"] as? RileyLinkConnectionState.RawValue {
+            rileyLinkConnectionManagerState = RileyLinkConnectionState(rawValue: rileyLinkConnectionManagerStateRaw)
         } else {
-            rileyLinkConnectionManagerState = RileyLinkConnectionManagerState(autoConnectIDs: [])
+            rileyLinkConnectionManagerState = RileyLinkConnectionState(autoConnectIDs: [])
         }
         
         var insulinType: InsulinType?

+ 7 - 7
Dependencies/rileylink_ios/OmniKit/PumpManager/PodComms.swift

@@ -19,7 +19,7 @@ protocol PodCommsDelegate: AnyObject {
 
 class PodComms: CustomDebugStringConvertible {
     
-    private let configuredDevices: Locked<Set<RileyLinkDevice>> = Locked(Set())
+    private let configuredDevices: Locked<Set<UUID>> = Locked(Set())
 
     weak var delegate: PodCommsDelegate?
     
@@ -189,8 +189,8 @@ class PodComms: CustomDebugStringConvertible {
                 log.default("Creating PodState for address %{public}@ [lot %u tid %u], packet #%d, message #%d", String(format: "%04X", config.address), config.lot, config.tid, transport.packetNumber, transport.messageNumber)
                 self.podState = PodState(
                     address: config.address,
-                    piVersion: String(describing: config.piVersion),
-                    pmVersion: String(describing: config.pmVersion),
+                    pmVersion: String(describing: config.firmwareVersion),
+                    piVersion: String(describing: config.iFirmwareVersion),
                     lot: config.lot,
                     tid: config.tid,
                     packetNumber: transport.packetNumber,
@@ -412,7 +412,7 @@ class PodComms: CustomDebugStringConvertible {
     private func configureDevice(_ device: RileyLinkDevice, with session: CommandSession) {
         session.assertOnSessionQueue()
 
-        guard !self.configuredDevices.value.contains(device) else {
+        guard !self.configuredDevices.value.contains(device.peripheralIdentifier) else {
             return
         }
         
@@ -431,7 +431,7 @@ class PodComms: CustomDebugStringConvertible {
         
         log.debug("added device %{public}@ to configuredDevices", device.name ?? "unknown")
         _ = configuredDevices.mutate { (value) in
-            value.insert(device)
+            value.insert(device.peripheralIdentifier)
         }
     }
     
@@ -445,7 +445,7 @@ class PodComms: CustomDebugStringConvertible {
         NotificationCenter.default.removeObserver(self, name: .DeviceConnectionStateDidChange, object: device)
 
         _ = configuredDevices.mutate { (value) in
-            value.remove(device)
+            value.remove(device.peripheralIdentifier)
         }
     }
     
@@ -455,7 +455,7 @@ class PodComms: CustomDebugStringConvertible {
         return [
             "## PodComms",
             "podState: \(String(reflecting: podState))",
-            "configuredDevices: \(configuredDevices.value.map { $0.peripheralIdentifier.uuidString })",
+            "configuredDevices: \(configuredDevices.value.map { $0.uuidString })",
             "delegate: \(String(describing: delegate != nil))",
             ""
         ].joined(separator: "\n")

+ 3 - 3
Dependencies/rileylink_ios/OmniKit/PumpManager/PodState.swift

@@ -60,8 +60,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
 
     public var setupUnitsDelivered: Double?
 
-    public let piVersion: String
     public let pmVersion: String
+    public let piVersion: String
     public let lot: UInt32
     public let tid: UInt32
     var activeAlertSlots: AlertSet
@@ -114,11 +114,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         return false
     }
 
-    public init(address: UInt32, piVersion: String, pmVersion: String, lot: UInt32, tid: UInt32, packetNumber: Int = 0, messageNumber: Int = 0, insulinType: InsulinType) {
+    public init(address: UInt32, pmVersion: String, piVersion: String, lot: UInt32, tid: UInt32, packetNumber: Int = 0, messageNumber: Int = 0, insulinType: InsulinType) {
         self.address = address
         self.nonceState = NonceState(lot: lot, tid: tid)
-        self.piVersion = piVersion
         self.pmVersion = pmVersion
+        self.piVersion = piVersion
         self.lot = lot
         self.tid = tid
         self.lastInsulinMeasurements = nil

+ 10 - 10
Dependencies/rileylink_ios/OmniKitTests/MessageTests.swift

@@ -78,8 +78,8 @@ 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("2.7.0", String(describing: config.firmwareVersion))
+            XCTAssertEqual("2.7.0", String(describing: config.iFirmwareVersion))
             XCTAssertEqual(42560, config.lot)
             XCTAssertEqual(621607, config.tid)
             XCTAssertEqual(0x1f08ced2, config.address)
@@ -103,8 +103,8 @@ class MessageTests: XCTestCase {
             let message = try Message(encodedData: Data(hexadecimalString: "ffffffff041d011b13881008340a5002070002070002030000a62b000447941f00ee878352")!)
             let config = message.messageBlocks[0] as! VersionResponse
             XCTAssertEqual(29, config.data.count)
-            XCTAssertEqual("2.7.0", String(describing: config.piVersion))
-            XCTAssertEqual("2.7.0", String(describing: config.pmVersion))
+            XCTAssertEqual("2.7.0", String(describing: config.firmwareVersion))
+            XCTAssertEqual("2.7.0", String(describing: config.iFirmwareVersion))
             XCTAssertEqual(42539, config.lot)
             XCTAssertEqual(280468, config.tid)
             XCTAssertEqual(0x1f00ee87, config.address)
@@ -127,8 +127,8 @@ class MessageTests: XCTestCase {
         do {
             let config = try VersionResponse(encodedData: Data(hexadecimalString: "0115031b0008080004020812a011000c175700ffffffff")!)
             XCTAssertEqual(23, config.data.count)
-            XCTAssertEqual("3.27.0", String(describing: config.pmVersion))
-            XCTAssertEqual("8.8.0", String(describing: config.piVersion))
+            XCTAssertEqual("3.27.0", String(describing: config.firmwareVersion))
+            XCTAssertEqual("8.8.0", String(describing: config.iFirmwareVersion))
             XCTAssertEqual(135438353, config.lot)
             XCTAssertEqual(792407, config.tid)
             XCTAssertEqual(0xFFFFFFFF, config.address)
@@ -152,8 +152,8 @@ class MessageTests: XCTestCase {
             let message = try Message(encodedData: Data(hexadecimalString: "ffffffff0c1d011b13881008340a50031b0008080004030812a011000c175717244389816c")!)
             let config = message.messageBlocks[0] as! VersionResponse
             XCTAssertEqual(29, config.data.count)
-            XCTAssertEqual("3.27.0", String(describing: config.pmVersion))
-            XCTAssertEqual("8.8.0", String(describing: config.piVersion))
+            XCTAssertEqual("3.27.0", String(describing: config.firmwareVersion))
+            XCTAssertEqual("8.8.0", String(describing: config.iFirmwareVersion))
             XCTAssertEqual(135438353, config.lot)
             XCTAssertEqual(792407, config.tid)
             XCTAssertEqual(0x17244389, config.address)
@@ -176,8 +176,8 @@ 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("2.7.0", String(describing: config.firmwareVersion))
+            XCTAssertEqual("2.7.0", String(describing: config.iFirmwareVersion))
             XCTAssertEqual(0x0000a5ad, config.lot)
             XCTAssertEqual(0x00053030, config.tid)
             XCTAssertEqual(0x1f086863, config.address)

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

@@ -20,7 +20,7 @@ class PodCommsSessionTests: XCTestCase {
 
 
     override func setUp() {
-        podState = PodState(address: address, piVersion: "2.7.0", pmVersion: "2.7.0", lot: 43620, tid: 560313, insulinType: .novolog)
+        podState = PodState(address: address, pmVersion: "2.7.0", piVersion: "2.7.0", lot: 43620, tid: 560313, insulinType: .novolog)
         mockTransport = MockMessageTransport(address: podState.address, messageNumber: 5)
     }
 

+ 4 - 4
Dependencies/rileylink_ios/OmniKitTests/PodStateTests.swift

@@ -12,7 +12,7 @@ import XCTest
 class PodStateTests: XCTestCase {
 
     func testNonceValues() {
-        var podState = PodState(address: 0x1f000000, piVersion: "1.1.0", pmVersion: "1.1.0", lot: 42560, tid: 661771, insulinType: .novolog)
+        var podState = PodState(address: 0x1f000000, pmVersion: "1.1.0", piVersion: "1.1.0", lot: 42560, tid: 661771, insulinType: .novolog)
         
         XCTAssertEqual(podState.currentNonce, 0x8c61ee59)
         podState.advanceToNextNonce()
@@ -26,12 +26,12 @@ class PodStateTests: XCTestCase {
     func testResyncNonce() {
         do {
             let config = try VersionResponse(encodedData: Data(hexadecimalString: "011502070002070002020000a62b0002249da11f00ee860318")!)
-            var podState = PodState(address: 0x1f00ee86, piVersion: "1.1.0", pmVersion: "1.1.0", lot: config.lot, tid: config.tid, insulinType: .novolog)
+            var podState = PodState(address: config.address, pmVersion: config.firmwareVersion.description, piVersion: config.iFirmwareVersion.description, lot: config.lot, tid: config.tid, insulinType: .novolog)
 
             XCTAssertEqual(42539, config.lot)
-            XCTAssertEqual(140445,  config.tid)
+            XCTAssertEqual(140445, config.tid)
             
-            XCTAssertEqual(0x8fd39264,  podState.currentNonce)
+            XCTAssertEqual(0x8fd39264, podState.currentNonce)
 
             // ID1:1f00ee86 PTYPE:PDM SEQ:26 ID2:1f00ee86 B9:24 BLEN:6 BODY:1c042e07c7c703c1 CRC:f4
             let sentPacket = try Packet(encodedData: Data(hexadecimalString: "1f00ee86ba1f00ee8624061c042e07c7c703c1f4")!)

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

@@ -189,15 +189,15 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
                     let vc = RileyLinkDeviceTableViewController(
                         device: device,
                         batteryAlertLevel: self.pumpManager.rileyLinkBatteryAlertLevel,
-                        batteryAlertLevelChanged: { value in
-                            self.pumpManager.rileyLinkBatteryAlertLevel = value
+                        batteryAlertLevelChanged: { [weak self] value in
+                            self?.pumpManager.rileyLinkBatteryAlertLevel = value
                         }
                     )
                     self.show(vc, sender: self)
                 }
             }
 
-            let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection)
+            let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection, supportedInsulinTypes: allowedInsulinTypes)
             return hostingController(rootView: view)
         case .pairPod:
             pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManager)
@@ -314,8 +314,8 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
                         let vc = RileyLinkDeviceTableViewController(
                             device: device,
                             batteryAlertLevel: self.pumpManager.rileyLinkBatteryAlertLevel,
-                            batteryAlertLevelChanged: { value in
-                                self.pumpManager.rileyLinkBatteryAlertLevel = value
+                            batteryAlertLevelChanged: { [weak self] value in
+                                self?.pumpManager.rileyLinkBatteryAlertLevel = value
                             }
                         )
                         self.show(vc, sender: self)
@@ -371,7 +371,7 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
         if pumpManager == nil, let pumpManagerSettings = pumpManagerSettings {
             let basalSchedule = pumpManagerSettings.basalSchedule
 
-            let rileyLinkConnectionManager = RileyLinkConnectionManager(autoConnectIDs: [])
+            let deviceProvider = RileyLinkBluetoothDeviceProvider(autoConnectIDs: [])
 
             let pumpManagerState = OmnipodPumpManagerState(
                 isOnboarded: false,
@@ -382,7 +382,7 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
                 insulinType: nil,
                 maximumTempBasalRate: pumpManagerSettings.maxBasalRateUnitsPerHour)
 
-            self.pumpManager = OmnipodPumpManager(state: pumpManagerState, rileyLinkDeviceProvider: rileyLinkConnectionManager.deviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
+            self.pumpManager = OmnipodPumpManager(state: pumpManagerState, rileyLinkDeviceProvider: deviceProvider)
         } else {
             guard let pumpManager = pumpManager else {
                 fatalError("Unable to create Omnipod PumpManager")

+ 1 - 1
Dependencies/rileylink_ios/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift

@@ -253,7 +253,7 @@ class OmnipodSettingsViewModel: ObservableObject {
     }
 
     func updateConnectionStatus() {
-        pumpManager.rileyLinkConnectionManager?.deviceProvider.getDevices { (devices) in
+        pumpManager.rileyLinkDeviceProvider.getDevices { (devices) in
             DispatchQueue.main.async { [weak self] in
                 self?.rileylinkConnected = devices.firstConnected != nil
             }

+ 3 - 10
Dependencies/rileylink_ios/OmniKitUI/ViewModels/RileyLinkListDataSource.swift

@@ -34,7 +34,7 @@ class RileyLinkListDataSource: ObservableObject {
     func autoconnectBinding(for device: RileyLinkDevice) -> Binding<Bool> {
         return Binding(
             get: { [weak self] in
-                if let connectionManager = self?.rileyLinkPumpManager.rileyLinkConnectionManager {
+                if let connectionManager = self?.rileyLinkPumpManager.rileyLinkDeviceProvider {
                     return connectionManager.shouldConnect(to: device.peripheralIdentifier.uuidString)
                 } else {
                     return false
@@ -59,7 +59,7 @@ class RileyLinkListDataSource: ObservableObject {
 
     public var isScanningEnabled: Bool = false {
         didSet {
-            rileyLinkPumpManager.rileyLinkConnectionManager?.setScanningEnabled(isScanningEnabled)
+            rileyLinkPumpManager.rileyLinkDeviceProvider.setScanningEnabled(isScanningEnabled)
 
             if isScanningEnabled {
                 rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true)
@@ -75,11 +75,7 @@ class RileyLinkListDataSource: ObservableObject {
         return true
         #else
 
-        guard let connectionManager = rileyLinkPumpManager.rileyLinkConnectionManager else {
-            return false
-        }
-
-        return connectionManager.connectingCount > 0
+        return rileyLinkPumpManager.rileyLinkDeviceProvider.connectingCount > 0
         #endif
     }
 
@@ -96,6 +92,3 @@ class RileyLinkListDataSource: ObservableObject {
         }
     }
 }
-
-
-extension RileyLinkDevice: Identifiable {}

+ 1 - 1
Dependencies/rileylink_ios/OmniKitUI/Views/DeliveryUncertaintyRecoveryView.swift

@@ -29,7 +29,7 @@ struct DeliveryUncertaintyRecoveryView: View {
                 Spacer()
                 ProgressView()
             }) {
-                ForEach(rileyLinkListDataSource.devices) { device in
+                ForEach(rileyLinkListDataSource.devices, id: \.peripheralIdentifier) { device in
                     Toggle(isOn: rileyLinkListDataSource.autoconnectBinding(for: device)) {
                         HStack {
                             Text(device.name ?? "Unknown")

+ 1 - 0
Dependencies/rileylink_ios/OmniKitUI/Views/ExpirationReminderSetupView.swift

@@ -39,6 +39,7 @@ struct ExpirationReminderSetupView: View {
             .padding()
         }
         .navigationBarTitle("Expiration Reminder", displayMode: .automatic)
+        .navigationBarHidden(false)
         .toolbar {
             ToolbarItem(placement: .navigationBarTrailing) {
                 Button(LocalizedString("Cancel", comment: "Cancel button title"), action: {

+ 4 - 2
Dependencies/rileylink_ios/OmniKitUI/Views/OmnipodSettingsView.swift

@@ -33,6 +33,8 @@ struct OmnipodSettingsView: View  {
 
     @State private var cancelingTempBasal = false
 
+    var supportedInsulinTypes: [InsulinType]
+
 
     @Environment(\.guidanceColors) var guidanceColors
     @Environment(\.insulinTintColor) var insulinTintColor
@@ -355,7 +357,7 @@ struct OmnipodSettingsView: View  {
                 Spacer()
                 ProgressView()
             }) {
-                ForEach(rileyLinkListDataSource.devices) { device in
+                ForEach(rileyLinkListDataSource.devices, id: \.peripheralIdentifier) { device in
                     Toggle(isOn: rileyLinkListDataSource.autoconnectBinding(for: device)) {
                         HStack {
                             Text(device.name ?? "Unknown")
@@ -445,7 +447,7 @@ struct OmnipodSettingsView: View  {
                             .foregroundColor(.secondary)
                     }
                 }
-                NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: InsulinType.allCases, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) {
+                NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) {
                     HStack {
                         FrameworkLocalText("Insulin Type", comment: "Text for confidence reminders navigation link").foregroundColor(Color.primary)
                         if let currentTitle = viewModel.insulinType?.brandName {

+ 2 - 1
Dependencies/rileylink_ios/OmniKitUI/Views/RileyLinkSetupView.swift

@@ -43,7 +43,7 @@ struct RileyLinkSetupView: View {
                     Spacer()
                     ProgressView()
                 }) {
-                    ForEach(dataSource.devices) { device in
+                    ForEach(dataSource.devices, id: \.peripheralIdentifier) { device in
                         Toggle(isOn: dataSource.autoconnectBinding(for: device)) {
                             HStack {
                                 Text(device.name ?? "Unknown")
@@ -67,6 +67,7 @@ struct RileyLinkSetupView: View {
                 })
             }
         }
+        .navigationBarHidden(false)
         .onAppear { dataSource.isScanningEnabled = true }
         .onDisappear { dataSource.isScanningEnabled = false }
     }

+ 48 - 16
Dependencies/rileylink_ios/RileyLink.xcodeproj/project.pbxproj

@@ -22,7 +22,7 @@
 		431CE7851F98564200255374 /* RileyLinkBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 431CE76F1F98564100255374 /* RileyLinkBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		431CE78D1F985B5400255374 /* PeripheralManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE78C1F985B5400255374 /* PeripheralManager.swift */; };
 		431CE78F1F985B6E00255374 /* CBPeripheral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE78E1F985B6E00255374 /* CBPeripheral.swift */; };
-		431CE7911F985D8D00255374 /* RileyLinkDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7901F985D8D00255374 /* RileyLinkDeviceManager.swift */; };
+		431CE7911F985D8D00255374 /* RileyLinkBluetoothDeviceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7901F985D8D00255374 /* RileyLinkBluetoothDeviceProvider.swift */; };
 		431CE7931F985DE700255374 /* PeripheralManager+RileyLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7921F985DE700255374 /* PeripheralManager+RileyLink.swift */; };
 		431CE7961F9B0F0200255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; };
 		431CE7971F9B0F0200255374 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CE7941F9B0DAE00255374 /* OSLog.swift */; };
@@ -53,7 +53,6 @@
 		4352A71720DEC78B00CAC200 /* PumpState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434AB0941CBA0DF600422F4A /* PumpState.swift */; };
 		4352A71820DEC7B900CAC200 /* PumpMessage+PumpOpsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435535DB1FB8B37E00CE5A23 /* PumpMessage+PumpOpsSession.swift */; };
 		4352A71920DEC7C100CAC200 /* BasalProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BF58B11FF5A22200499C46 /* BasalProfile.swift */; };
-		4352A71A20DEC7CB00CAC200 /* CommandSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436CCEF11FB953E800A6822B /* CommandSession.swift */; };
 		4352A71B20DEC7DC00CAC200 /* HistoryPage+PumpOpsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4384C8C51FB92F8100D916E6 /* HistoryPage+PumpOpsSession.swift */; };
 		4352A71C20DEC8C100CAC200 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */; };
 		4352A71D20DEC8CC00CAC200 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */; };
@@ -274,7 +273,6 @@
 		C125728F2121DB7C0061BA2F /* PumpManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C125728E2121DB7C0061BA2F /* PumpManagerState.swift */; };
 		C12572922121EEEE0061BA2F /* SettingsImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12572912121EEEE0061BA2F /* SettingsImageTableViewCell.swift */; };
 		C125729421220FEC0061BA2F /* MainStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C125729321220FEC0061BA2F /* MainStoryboard.storyboard */; };
-		C12572982125FA390061BA2F /* RileyLinkConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12572972125FA390061BA2F /* RileyLinkConnectionManager.swift */; };
 		C127160A2378C2270093DAB7 /* ResumePumpEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12716092378C2270093DAB7 /* ResumePumpEventTests.swift */; };
 		C1271B071A9A34E900B7C949 /* Log.m in Sources */ = {isa = PBXBuildFile; fileRef = C1271B061A9A34E900B7C949 /* Log.m */; };
 		C1274F771D8232580002912B /* DailyTotal515PumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1274F761D8232580002912B /* DailyTotal515PumpEvent.swift */; };
@@ -352,6 +350,7 @@
 		C17884611D519F1E00405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884601D519F1E00405663 /* BatteryIndicator.swift */; };
 		C17C5C0F21447383002A06F8 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43323EA61FA81A0F003FB0FA /* NumberFormatter.swift */; };
 		C17EDC4F2134D0CC0031D9F0 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4345D1CD1DA16AF300BAAD22 /* TimeZone.swift */; };
+		C17F6F5828C3DB0500E27990 /* MinimedPumpMessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17F6F5728C3DB0500E27990 /* MinimedPumpMessageSender.swift */; };
 		C1842BBB1C8E184300DB42AC /* PumpModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1842BBA1C8E184300DB42AC /* PumpModel.swift */; };
 		C1842BBD1C8E7C6E00DB42AC /* PumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1842BBC1C8E7C6E00DB42AC /* PumpEvent.swift */; };
 		C1842BBF1C8E855A00DB42AC /* PumpEventType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1842BBE1C8E855A00DB42AC /* PumpEventType.swift */; };
@@ -505,6 +504,8 @@
 		C1C9F88D283D945D00CFC769 /* LeadingImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9F88A283D945D00CFC769 /* LeadingImage.swift */; };
 		C1C9F88E283D945D00CFC769 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9F88B283D945D00CFC769 /* ErrorView.swift */; };
 		C1C9F890283D94CE00CFC769 /* BeepPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C9F88F283D94CE00CFC769 /* BeepPreference.swift */; };
+		C1CAB67328C696A600F6F715 /* RileyLinkDeviceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CAB67228C696A600F6F715 /* RileyLinkDeviceProvider.swift */; };
+		C1CAB67528C696D800F6F715 /* RileyLinkBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CAB67428C696D800F6F715 /* RileyLinkBluetoothDevice.swift */; };
 		C1CB13A521383F1E00F9EEDA /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2366EF212527DA0028B67D /* LocalizedString.swift */; };
 		C1CB13A72138453B00F9EEDA /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43323EA61FA81A0F003FB0FA /* NumberFormatter.swift */; };
 		C1D00E9D1E8986A400B733B7 /* PumpSuspendTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D00E9C1E8986A400B733B7 /* PumpSuspendTreatment.swift */; };
@@ -581,10 +582,15 @@
 		C1F6EB8B1F89C41200CFE393 /* MinimedPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F6EB8A1F89C41200CFE393 /* MinimedPacket.swift */; };
 		C1F6EB8D1F89C45500CFE393 /* MinimedPacketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F6EB8C1F89C45500CFE393 /* MinimedPacketTests.swift */; };
 		C1F8B1DF223AB1DF00DD66CF /* MinimedDoseProgressEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1DE223AB1DF00DD66CF /* MinimedDoseProgressEstimator.swift */; };
+		C1FAC05628C6A45800754AE2 /* MockPumpManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAC05528C6A45800754AE2 /* MockPumpManagerDelegate.swift */; };
+		C1FAC05828C6A7E300754AE2 /* ReconciliationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAC05728C6A7E300754AE2 /* ReconciliationTests.swift */; };
+		C1FAC05C28C6A88500754AE2 /* MockRileyLinkProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAC05B28C6A88500754AE2 /* MockRileyLinkProvider.swift */; };
+		C1FAC05F28C6AAF600754AE2 /* MockRileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAC05E28C6AAF600754AE2 /* MockRileyLinkDevice.swift */; };
+		C1FAC06128C6CF1500754AE2 /* MockPumpOps.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FAC06028C6CF1500754AE2 /* MockPumpOps.swift */; };
 		C1FC49EC2135CB2D007D0788 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1FC49EB2135CB2D007D0788 /* LaunchScreen.storyboard */; };
 		C1FDFCA91D964A3E00ADBC31 /* BolusReminderPumpEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FDFCA81D964A3E00ADBC31 /* BolusReminderPumpEvent.swift */; };
 		C1FFAF4D212944F600C50C1D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1FFAF4B212944F600C50C1D /* Localizable.strings */; };
-		C1FFAF6F212CB4F100C50C1D /* RileyLinkConnectionManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFAF6E212CB4F100C50C1D /* RileyLinkConnectionManagerState.swift */; };
+		C1FFAF6F212CB4F100C50C1D /* RileyLinkConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFAF6E212CB4F100C50C1D /* RileyLinkConnectionState.swift */; };
 		C1FFAF72212FAAEF00C50C1D /* RileyLink.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C1FFAF71212FAAEF00C50C1D /* RileyLink.xcassets */; };
 		C1FFAF81213323CC00C50C1D /* OmniKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1FFAF78213323CC00C50C1D /* OmniKit.framework */; };
 		C1FFAF8A213323CC00C50C1D /* OmniKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C1FFAF7A213323CC00C50C1D /* OmniKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -1012,7 +1018,7 @@
 		431CE7801F98564200255374 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		431CE78C1F985B5400255374 /* PeripheralManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralManager.swift; sourceTree = "<group>"; };
 		431CE78E1F985B6E00255374 /* CBPeripheral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBPeripheral.swift; sourceTree = "<group>"; };
-		431CE7901F985D8D00255374 /* RileyLinkDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceManager.swift; sourceTree = "<group>"; };
+		431CE7901F985D8D00255374 /* RileyLinkBluetoothDeviceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkBluetoothDeviceProvider.swift; sourceTree = "<group>"; };
 		431CE7921F985DE700255374 /* PeripheralManager+RileyLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PeripheralManager+RileyLink.swift"; sourceTree = "<group>"; };
 		431CE7941F9B0DAE00255374 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
 		431CE79B1F9B21BA00255374 /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = "<group>"; };
@@ -1043,7 +1049,6 @@
 		435D26AF20DA08CE00891C17 /* RileyLinkPumpManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkPumpManager.swift; sourceTree = "<group>"; };
 		435D26B320DA0AAE00891C17 /* RileyLinkDevicesHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkDevicesHeaderView.swift; sourceTree = "<group>"; };
 		435D26B520DA0BCC00891C17 /* RileyLinkDevicesTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevicesTableViewDataSource.swift; sourceTree = "<group>"; };
-		436CCEF11FB953E800A6822B /* CommandSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandSession.swift; sourceTree = "<group>"; };
 		43709ABA20DF1C6400F941B3 /* RileyLinkSetupTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkSetupTableViewController.swift; sourceTree = "<group>"; };
 		43709ABB20DF1C6400F941B3 /* RileyLinkManagerSetupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkManagerSetupViewController.swift; sourceTree = "<group>"; };
 		43709ABC20DF1C6400F941B3 /* RileyLinkSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkSettingsViewController.swift; sourceTree = "<group>"; };
@@ -1326,7 +1331,6 @@
 		C125728E2121DB7C0061BA2F /* PumpManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpManagerState.swift; sourceTree = "<group>"; };
 		C12572912121EEEE0061BA2F /* SettingsImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsImageTableViewCell.swift; sourceTree = "<group>"; };
 		C125729321220FEC0061BA2F /* MainStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MainStoryboard.storyboard; sourceTree = "<group>"; };
-		C12572972125FA390061BA2F /* RileyLinkConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkConnectionManager.swift; sourceTree = "<group>"; };
 		C12616431B685F0A001FAD87 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
 		C12716092378C2270093DAB7 /* ResumePumpEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumePumpEventTests.swift; sourceTree = "<group>"; };
 		C1271B061A9A34E900B7C949 /* Log.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Log.m; sourceTree = "<group>"; };
@@ -1403,6 +1407,7 @@
 		C178845C1D4EF3D800405663 /* ReadPumpStatusMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPumpStatusMessageBody.swift; sourceTree = "<group>"; };
 		C178845E1D5166BE00405663 /* COBStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = COBStatus.swift; sourceTree = "<group>"; };
 		C17884601D519F1E00405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = "<group>"; };
+		C17F6F5728C3DB0500E27990 /* MinimedPumpMessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimedPumpMessageSender.swift; sourceTree = "<group>"; };
 		C1842BBA1C8E184300DB42AC /* PumpModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpModel.swift; sourceTree = "<group>"; };
 		C1842BBC1C8E7C6E00DB42AC /* PumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpEvent.swift; sourceTree = "<group>"; };
 		C1842BBE1C8E855A00DB42AC /* PumpEventType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = PumpEventType.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
@@ -1518,6 +1523,8 @@
 		C1C9F88A283D945D00CFC769 /* LeadingImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LeadingImage.swift; sourceTree = "<group>"; };
 		C1C9F88B283D945D00CFC769 /* ErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
 		C1C9F88F283D94CE00CFC769 /* BeepPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeepPreference.swift; sourceTree = "<group>"; };
+		C1CAB67228C696A600F6F715 /* RileyLinkDeviceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceProvider.swift; sourceTree = "<group>"; };
+		C1CAB67428C696D800F6F715 /* RileyLinkBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkBluetoothDevice.swift; sourceTree = "<group>"; };
 		C1D00E9C1E8986A400B733B7 /* PumpSuspendTreatment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpSuspendTreatment.swift; sourceTree = "<group>"; };
 		C1D00EA01E8986F900B733B7 /* PumpResumeTreatment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpResumeTreatment.swift; sourceTree = "<group>"; };
 		C1D9E341283D88F5003CA0D3 /* PairPodView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PairPodView.swift; sourceTree = "<group>"; };
@@ -1593,6 +1600,11 @@
 		C1F6EB8A1F89C41200CFE393 /* MinimedPacket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinimedPacket.swift; sourceTree = "<group>"; };
 		C1F6EB8C1F89C45500CFE393 /* MinimedPacketTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MinimedPacketTests.swift; sourceTree = "<group>"; };
 		C1F8B1DE223AB1DF00DD66CF /* MinimedDoseProgressEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimedDoseProgressEstimator.swift; sourceTree = "<group>"; };
+		C1FAC05528C6A45800754AE2 /* MockPumpManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPumpManagerDelegate.swift; sourceTree = "<group>"; };
+		C1FAC05728C6A7E300754AE2 /* ReconciliationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReconciliationTests.swift; sourceTree = "<group>"; };
+		C1FAC05B28C6A88500754AE2 /* MockRileyLinkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRileyLinkProvider.swift; sourceTree = "<group>"; };
+		C1FAC05E28C6AAF600754AE2 /* MockRileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRileyLinkDevice.swift; sourceTree = "<group>"; };
+		C1FAC06028C6CF1500754AE2 /* MockPumpOps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPumpOps.swift; sourceTree = "<group>"; };
 		C1FB4272216E5DFD00FAB378 /* GetPumpFirmwareVersionMessageBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPumpFirmwareVersionMessageBody.swift; sourceTree = "<group>"; };
 		C1FC49EB2135CB2D007D0788 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
 		C1FDFCA81D964A3E00ADBC31 /* BolusReminderPumpEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusReminderPumpEvent.swift; sourceTree = "<group>"; };
@@ -1605,7 +1617,7 @@
 		C1FFAF542129450D00C50C1D /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1FFAF552129450F00C50C1D /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
 		C1FFAF562129451300C50C1D /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = "<group>"; };
-		C1FFAF6E212CB4F100C50C1D /* RileyLinkConnectionManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkConnectionManagerState.swift; sourceTree = "<group>"; };
+		C1FFAF6E212CB4F100C50C1D /* RileyLinkConnectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkConnectionState.swift; sourceTree = "<group>"; };
 		C1FFAF71212FAAEF00C50C1D /* RileyLink.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = RileyLink.xcassets; sourceTree = "<group>"; };
 		C1FFAF78213323CC00C50C1D /* OmniKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OmniKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		C1FFAF7A213323CC00C50C1D /* OmniKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OmniKit.h; sourceTree = "<group>"; };
@@ -1895,10 +1907,11 @@
 				43BA719A202591A70058961E /* Response.swift */,
 				43BA719C2026C9B00058961E /* ResponseBuffer.swift */,
 				431CE79B1F9B21BA00255374 /* RileyLinkDevice.swift */,
+				C1CAB67428C696D800F6F715 /* RileyLinkBluetoothDevice.swift */,
 				433ABFFB2016FDF700E6C1FF /* RileyLinkDeviceError.swift */,
-				431CE7901F985D8D00255374 /* RileyLinkDeviceManager.swift */,
-				C12572972125FA390061BA2F /* RileyLinkConnectionManager.swift */,
-				C1FFAF6E212CB4F100C50C1D /* RileyLinkConnectionManagerState.swift */,
+				C1CAB67228C696A600F6F715 /* RileyLinkDeviceProvider.swift */,
+				431CE7901F985D8D00255374 /* RileyLinkBluetoothDeviceProvider.swift */,
+				C1FFAF6E212CB4F100C50C1D /* RileyLinkConnectionState.swift */,
 			);
 			path = RileyLinkBLEKit;
 			sourceTree = "<group>";
@@ -2050,7 +2063,6 @@
 			isa = PBXGroup;
 			children = (
 				43BF58B11FF5A22200499C46 /* BasalProfile.swift */,
-				436CCEF11FB953E800A6822B /* CommandSession.swift */,
 				43D8708C20DE1C05006B549E /* DoseStore.swift */,
 				43D8708E20DE1C23006B549E /* EnliteSensorDisplayable.swift */,
 				4384C8C51FB92F8100D916E6 /* HistoryPage+PumpOpsSession.swift */,
@@ -2071,6 +2083,7 @@
 				43D8709420DE1C91006B549E /* RileyLinkDevice.swift */,
 				C1F8B1DE223AB1DF00DD66CF /* MinimedDoseProgressEstimator.swift */,
 				C164A56122F1F0A6000E3FA5 /* UnfinalizedDose.swift */,
+				C17F6F5728C3DB0500E27990 /* MinimedPumpMessageSender.swift */,
 			);
 			path = PumpManager;
 			sourceTree = "<group>";
@@ -2209,6 +2222,7 @@
 		C10D9BD01C8269D500378342 /* MinimedKitTests */ = {
 			isa = PBXGroup;
 			children = (
+				C1FAC05D28C6AADE00754AE2 /* Mocks */,
 				2F962EC61E7074B70070EFBD /* PumpEvents */,
 				54BC449F1DB6F6C100340EED /* GlucoseEvents */,
 				C121985D1C8DE72500BC374C /* Messages */,
@@ -2229,6 +2243,7 @@
 				54BC44B41DB7184D00340EED /* NSStringExtensions.swift */,
 				2F962EC01E6872170070EFBD /* TimestampedHistoryEventTests.swift */,
 				C1B73D64245F824400881A4F /* MinimedPumpManagerTests.swift */,
+				C1FAC05728C6A7E300754AE2 /* ReconciliationTests.swift */,
 			);
 			path = MinimedKitTests;
 			sourceTree = "<group>";
@@ -2753,6 +2768,17 @@
 			path = Messages;
 			sourceTree = "<group>";
 		};
+		C1FAC05D28C6AADE00754AE2 /* Mocks */ = {
+			isa = PBXGroup;
+			children = (
+				C1FAC05528C6A45800754AE2 /* MockPumpManagerDelegate.swift */,
+				C1FAC05B28C6A88500754AE2 /* MockRileyLinkProvider.swift */,
+				C1FAC05E28C6AAF600754AE2 /* MockRileyLinkDevice.swift */,
+				C1FAC06028C6CF1500754AE2 /* MockPumpOps.swift */,
+			);
+			path = Mocks;
+			sourceTree = "<group>";
+		};
 		C1FFAF79213323CC00C50C1D /* OmniKit */ = {
 			isa = PBXGroup;
 			children = (
@@ -3596,18 +3622,19 @@
 			files = (
 				7D2366F5212527DA0028B67D /* LocalizedString.swift in Sources */,
 				43BA719D2026C9B00058961E /* ResponseBuffer.swift in Sources */,
-				C1FFAF6F212CB4F100C50C1D /* RileyLinkConnectionManagerState.swift in Sources */,
+				C1FFAF6F212CB4F100C50C1D /* RileyLinkConnectionState.swift in Sources */,
 				431CE78F1F985B6E00255374 /* CBPeripheral.swift in Sources */,
 				431CE79F1F9C670600255374 /* TimeInterval.swift in Sources */,
 				433ABFFC2016FDF700E6C1FF /* RileyLinkDeviceError.swift in Sources */,
 				43BA719B202591A70058961E /* Response.swift in Sources */,
 				43D5E7881FAEDAC4004ACDB7 /* PeripheralManagerError.swift in Sources */,
-				C12572982125FA390061BA2F /* RileyLinkConnectionManager.swift in Sources */,
 				431CE79C1F9B21BA00255374 /* RileyLinkDevice.swift in Sources */,
 				431CE7A71F9D98F700255374 /* CommandSession.swift in Sources */,
 				431CE7A31F9D737F00255374 /* Command.swift in Sources */,
 				431CE7A11F9D195600255374 /* CBCentralManager.swift in Sources */,
-				431CE7911F985D8D00255374 /* RileyLinkDeviceManager.swift in Sources */,
+				431CE7911F985D8D00255374 /* RileyLinkBluetoothDeviceProvider.swift in Sources */,
+				C1CAB67528C696D800F6F715 /* RileyLinkBluetoothDevice.swift in Sources */,
+				C1CAB67328C696A600F6F715 /* RileyLinkDeviceProvider.swift in Sources */,
 				431CE78D1F985B5400255374 /* PeripheralManager.swift in Sources */,
 				431CE79E1F9BE73900255374 /* BLEFirmwareVersion.swift in Sources */,
 				432847C31FA57C0F00CDE69C /* RadioFirmwareVersion.swift in Sources */,
@@ -3852,7 +3879,6 @@
 				4352A71620DEC78B00CAC200 /* PumpSettings.swift in Sources */,
 				C1842C001C8FA45100DB42AC /* JournalEntryPumpLowReservoirPumpEvent.swift in Sources */,
 				54A840D11DB85D0600B1F202 /* UnknownGlucoseEvent.swift in Sources */,
-				4352A71A20DEC7CB00CAC200 /* CommandSession.swift in Sources */,
 				C1842BCF1C8F9E5100DB42AC /* PumpAlarmPumpEvent.swift in Sources */,
 				43D8708F20DE1C23006B549E /* EnliteSensorDisplayable.swift in Sources */,
 				C1EAD6C61C826B92006DBA60 /* Data.swift in Sources */,
@@ -3871,6 +3897,7 @@
 				546A85D01DF7BB8B00733213 /* SensorPacketGlucoseEvent.swift in Sources */,
 				C1842BFD1C8FA45100DB42AC /* ResumePumpEvent.swift in Sources */,
 				C1EAD6CC1C826B92006DBA60 /* MySentryPumpStatusMessageBody.swift in Sources */,
+				C17F6F5828C3DB0500E27990 /* MinimedPumpMessageSender.swift in Sources */,
 				C1842C141C8FA45100DB42AC /* ChangeMaxBasalPumpEvent.swift in Sources */,
 				C16A08311D389205001A200C /* JournalEntryMealMarkerPumpEvent.swift in Sources */,
 				C1842C101C8FA45100DB42AC /* ChangeReservoirWarningTimePumpEvent.swift in Sources */,
@@ -3912,12 +3939,14 @@
 				54BC44B11DB70F4A00340EED /* GlucoseSensorDataGlucoseEventTests.swift in Sources */,
 				43A068EC1CF6BA6900F9EFE4 /* ReadRemainingInsulinMessageBodyTests.swift in Sources */,
 				2F962EC81E7074E60070EFBD /* BolusNormalPumpEventTests.swift in Sources */,
+				C1FAC06128C6CF1500754AE2 /* MockPumpOps.swift in Sources */,
 				43DFB61320D37800008A7BAE /* ChangeMaxBasalRateMessageBodyTests.swift in Sources */,
 				C1EAD6D61C826C43006DBA60 /* MySentryPumpStatusMessageBodyTests.swift in Sources */,
 				43DAD00620A6B10A000F8529 /* ReadOtherDevicesIDsMessageBodyTests.swift in Sources */,
 				4352A71F20DEC93300CAC200 /* PumpOpsSynchronousBuildFromFramesTests.swift in Sources */,
 				C1EAD6D81C826C43006DBA60 /* ReadSettingsCarelinkMessageBodyTests.swift in Sources */,
 				4352A74920DED81D00CAC200 /* Data.swift in Sources */,
+				C1FAC05C28C6A88500754AE2 /* MockRileyLinkProvider.swift in Sources */,
 				43B0ADC01D0FC03200AAD278 /* NSDateComponentsTests.swift in Sources */,
 				546A85D21DF7BD5F00733213 /* SensorErrorGlucoseEventTests.swift in Sources */,
 				C1C357911C92733A009BDD4F /* MeterMessageTests.swift in Sources */,
@@ -3930,6 +3959,7 @@
 				C127160A2378C2270093DAB7 /* ResumePumpEventTests.swift in Sources */,
 				546A85CE1DF7B99D00733213 /* SensorPacketGlucoseEventTests.swift in Sources */,
 				43CA93311CB97191000026B5 /* ReadTempBasalCarelinkMessageBodyTests.swift in Sources */,
+				C1FAC05628C6A45800754AE2 /* MockPumpManagerDelegate.swift in Sources */,
 				54BC44A91DB704A600340EED /* CalBGForGHGlucoseEventTests.swift in Sources */,
 				C1F000501EBE727C00F65163 /* BasalScheduleTests.swift in Sources */,
 				43CEC07820D0CF7200F1BC19 /* ReadRemoteControlIDsMessageBodyTests.swift in Sources */,
@@ -3947,6 +3977,8 @@
 				43CA93351CB9727F000026B5 /* ChangeTempBasalCarelinkMessageBodyTests.swift in Sources */,
 				54BC44B71DB81B5100340EED /* GetGlucosePageMessageBodyTests.swift in Sources */,
 				43DFB60D20D22CCB008A7BAE /* ChangeRemoteControlIDMessageBodyTests.swift in Sources */,
+				C1FAC05828C6A7E300754AE2 /* ReconciliationTests.swift in Sources */,
+				C1FAC05F28C6AAF600754AE2 /* MockRileyLinkDevice.swift in Sources */,
 				4352A74820DED80300CAC200 /* TimeInterval.swift in Sources */,
 				4352A71D20DEC8CC00CAC200 /* TimeZone.swift in Sources */,
 				C1EAD6E01C82B910006DBA60 /* CRC8Tests.swift in Sources */,

+ 1 - 1
Dependencies/rileylink_ios/RileyLink.xcodeproj/xcshareddata/xcschemes/OmniKitPacketParser.xcscheme

@@ -61,7 +61,7 @@
       </BuildableProductRunnable>
       <CommandLineArguments>
          <CommandLineArgument
-            argument = "/Users/pete/Downloads/Loop-Report-2022-06-30-203038-0400.md"
+            argument = "/Users/pete/Downloads/Loop-Report-2022-08-21-185153-0400.md"
             isEnabled = "YES">
          </CommandLineArgument>
       </CommandLineArguments>

+ 30 - 16
Dependencies/rileylink_ios/RileyLink/DeviceDataManager.swift

@@ -21,7 +21,7 @@ import UserNotifications
 
 class DeviceDataManager {
 
-    let rileyLinkConnectionManager: RileyLinkConnectionManager
+    let rileyLinkDeviceProvider: RileyLinkDeviceProvider
     
     var pumpManager: PumpManagerUI? {
         didSet {
@@ -35,30 +35,32 @@ class DeviceDataManager {
     
     init() {
         
-        if let connectionManagerState = UserDefaults.standard.rileyLinkConnectionManagerState {
-            rileyLinkConnectionManager = RileyLinkConnectionManager(state: connectionManagerState)
-        } else {
-            rileyLinkConnectionManager = RileyLinkConnectionManager(autoConnectIDs: [])
-        }
-        rileyLinkConnectionManager.delegate = self
-        rileyLinkConnectionManager.setScanningEnabled(true)
+        let connectionManagerState = UserDefaults.standard.rileyLinkConnectionManagerState
+        rileyLinkDeviceProvider = RileyLinkBluetoothDeviceProvider(autoConnectIDs: connectionManagerState?.autoConnectIDs ?? [])
+        rileyLinkDeviceProvider.delegate = self
+        rileyLinkDeviceProvider.setScanningEnabled(true)
 
         if let pumpManagerRawValue = UserDefaults.standard.pumpManagerRawValue {
-            pumpManager = PumpManagerFromRawValue(pumpManagerRawValue, rileyLinkDeviceProvider: rileyLinkConnectionManager.deviceProvider) as? PumpManagerUI
+            pumpManager = PumpManagerFromRawValue(pumpManagerRawValue, rileyLinkDeviceProvider: rileyLinkDeviceProvider) as? PumpManagerUI
             pumpManager?.pumpManagerDelegate = self
         }
     }
 }
 
-extension DeviceDataManager: RileyLinkConnectionManagerDelegate {
-    func rileyLinkConnectionManager(_ rileyLinkConnectionManager: RileyLinkConnectionManager, didChange state: RileyLinkConnectionManagerState)
-    {
+extension DeviceDataManager: RileyLinkDeviceProviderDelegate {
+    func rileylinkDeviceProvider(_ rileylinkDeviceProvider: RileyLinkBLEKit.RileyLinkDeviceProvider, didChange state: RileyLinkBLEKit.RileyLinkConnectionState) {
         UserDefaults.standard.rileyLinkConnectionManagerState = state
-    }    
+    }
 }
 
 extension DeviceDataManager: PumpManagerDelegate {
-    
+    func pumpManagerPumpWasReplaced(_ pumpManager: LoopKit.PumpManager) {
+    }
+
+    var detectedSystemTimeOffset: TimeInterval {
+        return 0;
+    }
+
     func pumpManager(_ pumpManager: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval) {
         log.debug("didAdjustPumpClockBy %@", adjustment)
     }
@@ -88,7 +90,7 @@ extension DeviceDataManager: PumpManagerDelegate {
         log.error("pumpManager didError %@", String(describing: error))
     }
     
-    func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void) {
+    func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastSync lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void) {
     }
     
     func pumpManager(_ pumpManager: PumpManager, didReadReservoirValue units: Double, at date: Date, completion: @escaping (Result<(newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool), Error>) -> Void) {
@@ -104,11 +106,23 @@ extension DeviceDataManager: PumpManagerDelegate {
 
 // MARK: - DeviceManagerDelegate
 extension DeviceDataManager: DeviceManagerDelegate {
+    func doesIssuedAlertExist(identifier: LoopKit.Alert.Identifier, completion: @escaping (Result<Bool, Error>) -> Void) {
+    }
+
+    func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void) {
+    }
+
+    func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void) {
+    }
+
+    func recordRetractedAlert(_ alert: LoopKit.Alert, at date: Date) {
+    }
+
     func deviceManager(_ manager: DeviceManager, logEventForDeviceIdentifier deviceIdentifier: String?, type: DeviceLogEntryType, message: String, completion: ((Error?) -> Void)?) {}
 }
 
 // MARK: - AlertPresenter
-extension DeviceDataManager: AlertPresenter {
+extension DeviceDataManager: AlertIssuer {
     func issueAlert(_ alert: Alert) {
     }
     

+ 2 - 2
Dependencies/rileylink_ios/RileyLink/Extensions/UserDefaults.swift

@@ -25,13 +25,13 @@ extension UserDefaults {
         }
     }
     
-    var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState? {
+    var rileyLinkConnectionManagerState: RileyLinkConnectionState? {
         get {
             guard let rawValue = dictionary(forKey: Key.rileyLinkConnectionManagerState.rawValue) else
             {
                 return nil
             }
-            return RileyLinkConnectionManagerState(rawValue: rawValue)
+            return RileyLinkConnectionState(rawValue: rawValue)
         }
         set {
             set(newValue?.rawValue, forKey: Key.rileyLinkConnectionManagerState.rawValue)

+ 2 - 2
Dependencies/rileylink_ios/RileyLink/View Controllers/MainViewController.swift

@@ -85,7 +85,7 @@ class MainViewController: RileyLinkSettingsViewController {
         case setupOmnipod
     }
     
-    weak var rileyLinkManager: RileyLinkDeviceManager!
+    weak var rileyLinkManager: RileyLinkBluetoothDeviceProvider!
     
     @objc private func deviceConnectionStateDidChange() {
         DispatchQueue.main.async {
@@ -94,7 +94,7 @@ class MainViewController: RileyLinkSettingsViewController {
     }
     
     private var shouldAllowAddingPump: Bool {
-        return deviceDataManager.rileyLinkConnectionManager.connectingCount > 0
+        return rileyLinkManager.connectingCount > 0
     }
 
     // MARK: Data Source

+ 6 - 4
Dependencies/rileylink_ios/RileyLinkBLEKit/CommandSession.swift

@@ -99,8 +99,10 @@ public struct CommandSession {
             throw RileyLinkDeviceError.responseTimeout
         case .zeroData:
             throw RileyLinkDeviceError.invalidResponse(Data())
-        case .invalidParam, .unknownCommand:
-            throw RileyLinkDeviceError.invalidInput(command.data.hexadecimalString)
+        case .invalidParam:
+            throw RileyLinkDeviceError.errorResponse("RileyLink reported invalid param: " + command.data.hexadecimalString)
+        case .unknownCommand:
+            throw RileyLinkDeviceError.errorResponse("RileyLink reported unknown command: " + command.data.hexadecimalString)
         case .success:
             return response
         }
@@ -138,7 +140,7 @@ public struct CommandSession {
         let response: ReadRegisterResponse = try writeCommand(command, timeout: 0)
         
         guard response.code == .success else {
-            throw RileyLinkDeviceError.invalidInput("Unsupported register: \(String(describing: address))")
+            throw RileyLinkDeviceError.errorResponse("Unsupported register: \(String(describing: address))")
         }
 
         return response.value
@@ -273,7 +275,7 @@ public struct CommandSession {
         let response = try writeCommand(command, timeout: 0)
         
         guard response.code == .success else {
-            throw RileyLinkDeviceError.invalidInput(String(describing: swEncodingType))
+            throw RileyLinkDeviceError.unsupportedCommand("Set Software Encoding error")
         }
     }
     

+ 1 - 1
Dependencies/rileylink_ios/RileyLinkBLEKit/PeripheralManager+RileyLink.swift

@@ -275,7 +275,7 @@ extension PeripheralManager {
 
     func setCustomName(_ name: String, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
         guard let value = name.data(using: .utf8) else {
-            completion?(.invalidInput(name))
+            completion?(.errorResponse(name))
             return
         }
 

+ 616 - 0
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkBluetoothDevice.swift

@@ -0,0 +1,616 @@
+//
+//  RileyLinkBluetoothDevice.swift
+//  RileyLinkBLEKit
+//
+//  Created by Pete Schwamb on 9/5/22.
+//  Copyright © 2022 Pete Schwamb. All rights reserved.
+//
+
+import CoreBluetooth
+import os.log
+
+public class RileyLinkBluetoothDevice: RileyLinkDevice {
+    private let manager: PeripheralManager
+
+    private let log = OSLog(category: "RileyLinkDevice")
+
+    // Confined to `manager.queue`
+    private var bleFirmwareVersion: BLEFirmwareVersion?
+
+    // Confined to `manager.queue`
+    private var radioFirmwareVersion: RadioFirmwareVersion?
+
+    public var isConnected: Bool {
+        return manager.peripheral.state == .connected
+    }
+
+    func setPeripheral(_ peripheral: CBPeripheral) {
+        manager.peripheral = peripheral
+    }
+
+    public var rlFirmwareDescription: String {
+        let versions = [radioFirmwareVersion, bleFirmwareVersion].compactMap { (version: CustomStringConvertible?) -> String? in
+            if let version = version {
+                return String(describing: version)
+            } else {
+                return nil
+            }
+        }
+
+        return versions.joined(separator: " / ")
+    }
+
+    private var version: String {
+        switch hardwareType {
+        case .riley, .ema, .none:
+            return rlFirmwareDescription
+        case .orange:
+            return orangeLinkFirmwareHardwareVersion
+        }
+    }
+
+    // Confined to `lock`
+    private var idleListeningState: IdleListeningState = .disabled
+
+    // Confined to `lock`
+    private var lastIdle: Date?
+
+    // Confined to `lock`
+    // TODO: Tidy up this state/preference machine
+    private var isIdleListeningPending = false
+
+    // Confined to `lock`
+    private var isTimerTickEnabled = true
+
+    /// Serializes access to device state
+    private var lock = os_unfair_lock()
+
+    private var orangeLinkFirmwareHardwareVersion = "v1.x"
+    private var orangeLinkHardwareVersionMajorMinor: [Int]?
+    private var ledOn: Bool = false
+    private var vibrationOn: Bool = false
+    private var voltage: Float?
+    private var batteryLevel: Int?
+    private var hasPiezo: Bool {
+        if let olHW = orangeLinkHardwareVersionMajorMinor, olHW[0] == 1, olHW[1] >= 1 {
+            return true
+        } else if let olHW = orangeLinkHardwareVersionMajorMinor, olHW[0] == 2, olHW[1] == 6 {
+            return true
+       }
+        return false
+    }
+
+    public var hasOrangeLinkService: Bool {
+        return self.manager.peripheral.services?.itemWithUUID(RileyLinkServiceUUID.orange.cbUUID) != nil
+    }
+
+    public var hardwareType: RileyLinkHardwareType? {
+        guard let services = self.manager.peripheral.services else {
+            return nil
+        }
+
+        guard let bleComponents = self.bleFirmwareVersion else {
+            return nil
+        }
+
+        if services.itemWithUUID(RileyLinkServiceUUID.secureDFU.cbUUID) != nil {
+            return .orange
+        } else if bleComponents.components[0] == 3 {
+            // this returns true for riley with ema firmware, but that is OK
+            return .ema
+        } else {
+            // as long as riley ble remains at 2.x with ema at 3.x this will work
+            return .riley
+        }
+      }
+
+    /// The queue used to serialize sessions and observe when they've drained
+    private let sessionQueue: OperationQueue = {
+        let queue = OperationQueue()
+        queue.name = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.sessionQueue"
+        queue.maxConcurrentOperationCount = 1
+
+        return queue
+    }()
+
+    private var sessionQueueOperationCountObserver: NSKeyValueObservation!
+
+    public var rssi: Int?
+
+    init(peripheralManager: PeripheralManager, rssi: Int?) {
+        self.manager = peripheralManager
+        self.rssi = rssi
+        sessionQueue.underlyingQueue = peripheralManager.queue
+
+        peripheralManager.delegate = self
+
+        sessionQueueOperationCountObserver = sessionQueue.observe(\.operationCount, options: [.new]) { [weak self] (queue, change) in
+            if let newValue = change.newValue, newValue == 0 {
+                self?.log.debug("Session queue operation count is now empty")
+                self?.assertIdleListening(forceRestart: true)
+            }
+        }
+    }
+}
+
+
+// MARK: - Peripheral operations. Thread-safe.
+extension RileyLinkBluetoothDevice {
+    public var name: String? {
+        return manager.peripheral.name
+    }
+
+    public var deviceURI: String {
+        return "rileylink://\(name ?? peripheralIdentifier.uuidString)"
+    }
+
+    public var peripheralIdentifier: UUID {
+        return manager.peripheral.identifier
+    }
+
+    public var peripheralState: CBPeripheralState {
+        return manager.peripheral.state
+    }
+
+    public func readRSSI() {
+        guard case .connected = manager.peripheral.state, case .poweredOn? = manager.central?.state else {
+            return
+        }
+        manager.peripheral.readRSSI()
+    }
+
+    public func setCustomName(_ name: String) {
+        manager.setCustomName(name)
+    }
+
+    public func updateBatteryLevel() {
+        manager.readBatteryLevel { value in
+            if let batteryLevel = value {
+                self.batteryLevel = batteryLevel
+                NotificationCenter.default.post(
+                    name: .DeviceBatteryLevelUpdated,
+                    object: self,
+                    userInfo: [RileyLinkBluetoothDevice.batteryLevelKey: batteryLevel]
+                )
+                NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
+            }
+        }
+    }
+
+    public func orangeAction(_ command: OrangeLinkCommand) {
+        log.debug("orangeAction: %@", "\(command)")
+        manager.orangeAction(command)
+    }
+
+    public func setOrangeConfig(_ config: OrangeLinkConfigurationSetting, isOn: Bool) {
+        log.debug("setOrangeConfig: %@, %@", "\(String(describing: config))", "\(isOn)")
+        manager.setOrangeConfig(config, isOn: isOn)
+    }
+
+    public func orangeWritePwd() {
+        log.debug("orangeWritePwd")
+        manager.orangeWritePwd()
+    }
+
+    public func orangeClose() {
+        log.debug("orangeClose")
+        manager.orangeClose()
+    }
+
+    public func orangeReadSet() {
+        log.debug("orangeReadSet")
+        manager.orangeReadSet()
+    }
+
+    public func orangeReadVDC() {
+        log.debug("orangeReadVDC")
+        manager.orangeReadVDC()
+    }
+
+    public func findDevice() {
+        log.debug("findDevice")
+        manager.findDevice()
+    }
+
+    public func setDiagnosticeLEDModeForBLEChip(_ mode: RileyLinkLEDMode) {
+        manager.setLEDMode(mode: mode)
+    }
+
+    public func readDiagnosticLEDModeForBLEChip(completion: @escaping (RileyLinkLEDMode?) -> Void) {
+        manager.readDiagnosticLEDMode(completion: completion)
+    }
+
+    /// Asserts that the caller is currently on the session queue
+    public func assertOnSessionQueue() {
+        dispatchPrecondition(condition: .onQueue(manager.queue))
+    }
+
+    /// Schedules a closure to execute on the session queue after a specified time
+    ///
+    /// - Parameters:
+    ///   - deadline: The time after which to execute
+    ///   - execute: The closure to execute
+    public func sessionQueueAsyncAfter(deadline: DispatchTime, execute: @escaping () -> Void) {
+        manager.queue.asyncAfter(deadline: deadline, execute: execute)
+    }
+}
+
+
+extension RileyLinkBluetoothDevice: Equatable, Hashable {
+    public static func ==(lhs: RileyLinkBluetoothDevice, rhs: RileyLinkBluetoothDevice) -> Bool {
+        return lhs === rhs
+    }
+
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(peripheralIdentifier)
+    }
+}
+
+
+// MARK: - Status management
+extension RileyLinkBluetoothDevice {
+
+    public func getStatus(_ completion: @escaping (_ status: RileyLinkDeviceStatus) -> Void) {
+        os_unfair_lock_lock(&lock)
+        let lastIdle = self.lastIdle
+        os_unfair_lock_unlock(&lock)
+
+        self.manager.queue.async {
+            completion(RileyLinkDeviceStatus(
+                lastIdle: lastIdle,
+                name: self.name,
+                version: self.version,
+                ledOn: self.ledOn,
+                vibrationOn: self.vibrationOn,
+                voltage: self.voltage,
+                battery: self.batteryLevel,
+                hasPiezo: self.hasPiezo
+            ))
+        }
+    }
+}
+
+
+// MARK: - Command session management
+// CommandSessions are a way to serialize access to the RileyLink command/response facility.
+// All commands that send data out on the RL data characteristic need to be in a command session.
+// Accessing other characteristics on the RileyLink can be done without a command session.
+extension RileyLinkBluetoothDevice {
+    public func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void) {
+        self.log.default("Scheduling session %{public}@", name)
+        sessionQueue.addOperation(manager.configureAndRun({ [weak self] (manager) in
+            self?.log.default("======================== %{public}@ ===========================", name)
+            let bleFirmwareVersion = self?.bleFirmwareVersion
+            let radioFirmwareVersion = self?.radioFirmwareVersion
+
+            if bleFirmwareVersion == nil || radioFirmwareVersion == nil {
+                self?.log.error("Running session with incomplete configuration: bleFirmwareVersion %{public}@, radioFirmwareVersion: %{public}@", String(describing: bleFirmwareVersion), String(describing: radioFirmwareVersion))
+            }
+
+            block(CommandSession(manager: manager, responseType: bleFirmwareVersion?.responseType ?? .buffered, firmwareVersion: radioFirmwareVersion ?? .unknown))
+            self?.log.default("------------------------ %{public}@ ---------------------------", name)
+        }))
+    }
+}
+
+
+// MARK: - Idle management
+extension RileyLinkBluetoothDevice {
+    public enum IdleListeningState {
+        case enabled(timeout: TimeInterval, channel: UInt8)
+        case disabled
+    }
+
+    func setIdleListeningState(_ state: IdleListeningState) {
+        os_unfair_lock_lock(&lock)
+        let oldValue = idleListeningState
+        idleListeningState = state
+        os_unfair_lock_unlock(&lock)
+
+        switch (oldValue, state) {
+        case (.disabled, .enabled):
+            assertIdleListening(forceRestart: true)
+        case (.enabled, .enabled):
+            assertIdleListening(forceRestart: false)
+        default:
+            break
+        }
+    }
+
+    public func assertIdleListening(forceRestart: Bool = false) {
+        os_unfair_lock_lock(&lock)
+        guard case .enabled(timeout: let timeout, channel: let channel) = self.idleListeningState else {
+            os_unfair_lock_unlock(&lock)
+            return
+        }
+
+        guard case .connected = self.manager.peripheral.state, case .poweredOn? = self.manager.central?.state else {
+            os_unfair_lock_unlock(&lock)
+            return
+        }
+
+        guard forceRestart || (self.lastIdle ?? .distantPast).timeIntervalSinceNow < -timeout else {
+            os_unfair_lock_unlock(&lock)
+            return
+        }
+
+        guard !self.isIdleListeningPending else {
+            os_unfair_lock_unlock(&lock)
+            return
+        }
+
+        self.isIdleListeningPending = true
+        os_unfair_lock_unlock(&lock)
+
+        self.manager.startIdleListening(idleTimeout: timeout, channel: channel) { (error) in
+            os_unfair_lock_lock(&self.lock)
+            self.isIdleListeningPending = false
+
+            if let error = error {
+                self.log.error("Unable to start idle listening: %@", String(describing: error))
+                os_unfair_lock_unlock(&self.lock)
+            } else {
+                self.lastIdle = Date()
+                self.log.debug("Started idle listening")
+                os_unfair_lock_unlock(&self.lock)
+                NotificationCenter.default.post(name: .DeviceDidStartIdle, object: self)
+            }
+        }
+    }
+}
+
+
+// MARK: - Timer tick management
+extension RileyLinkBluetoothDevice {
+    func setTimerTickEnabled(_ enabled: Bool) {
+        os_unfair_lock_lock(&lock)
+        self.isTimerTickEnabled = enabled
+        os_unfair_lock_unlock(&lock)
+        self.assertTimerTick()
+    }
+
+    func assertTimerTick() {
+        os_unfair_lock_lock(&self.lock)
+        let isTimerTickEnabled = self.isTimerTickEnabled
+        os_unfair_lock_unlock(&self.lock)
+
+        if isTimerTickEnabled != self.manager.timerTickEnabled {
+            self.manager.setTimerTickEnabled(isTimerTickEnabled)
+        }
+    }
+}
+
+
+// MARK: - CBCentralManagerDelegate Proxying
+extension RileyLinkBluetoothDevice {
+    func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        if case .poweredOn = central.state {
+            assertIdleListening(forceRestart: false)
+            assertTimerTick()
+        }
+
+        manager.centralManagerDidUpdateState(central)
+    }
+
+    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
+        log.default("didConnect %{public}@", peripheral)
+        if case .connected = peripheral.state {
+            assertIdleListening(forceRestart: false)
+            assertTimerTick()
+        }
+
+        manager.centralManager(central, didConnect: peripheral)
+        NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
+    }
+
+    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
+        log.default("didDisconnectPeripheral %{public}@", peripheral)
+        NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
+    }
+
+    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
+        log.default("didFailToConnect %{public}@", peripheral)
+        NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
+    }
+}
+
+
+extension RileyLinkBluetoothDevice: PeripheralManagerDelegate {
+    func peripheralManager(_ manager: PeripheralManager, didUpdateNotificationStateFor characteristic: CBCharacteristic) {
+        log.debug("Did didUpdateNotificationStateFor %@", characteristic)
+    }
+
+    // If PeripheralManager receives a response on the data queue, without an outstanding request,
+    // it will pass the update to this method, which is called on the central's queue.
+    // This is how idle listen responses are handled
+    func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) {
+        let characteristicService: CBService? = characteristic.service
+        guard let cbService = characteristicService, let service = RileyLinkServiceUUID(rawValue: cbService.uuid.uuidString) else {
+            log.debug("Update from characteristic on unknown service: %@", String(describing: characteristic.service))
+            return
+        }
+
+        switch service {
+        case .main:
+            guard let mainCharacteristic = MainServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) else {
+                log.debug("Update from unknown characteristic %@ on main service.", characteristic.uuid.uuidString)
+                return
+            }
+            handleCharacteristicUpdate(mainCharacteristic, value: characteristic.value)
+
+        case .orange:
+            guard let orangeCharacteristic = OrangeServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) else {
+                log.debug("Update from unknown characteristic %@ on orange service.", characteristic.uuid.uuidString)
+                return
+            }
+            handleCharacteristicUpdate(orangeCharacteristic, value: characteristic.value)
+        default:
+            return
+        }
+    }
+
+    private func handleCharacteristicUpdate(_ characteristic: MainServiceCharacteristicUUID, value: Data?) {
+        switch characteristic {
+        case .data:
+            guard let value = value, value.count > 0 else {
+                return
+            }
+
+            self.manager.queue.async {
+                if let responseType = self.bleFirmwareVersion?.responseType {
+                    let response: PacketResponse?
+
+                    switch responseType {
+                    case .buffered:
+                        var buffer =  ResponseBuffer<PacketResponse>(endMarker: 0x00)
+                        buffer.append(value)
+                        response = buffer.responses.last
+                    case .single:
+                        response = PacketResponse(data: value)
+                    }
+
+                    if let response = response {
+                        switch response.code {
+                        case .commandInterrupted:
+                            self.log.debug("Received commandInterrupted during idle; assuming device is still listening.")
+                            return
+                        case .rxTimeout, .zeroData, .invalidParam, .unknownCommand:
+                            self.log.debug("Idle error received: %@", String(describing: response.code))
+                        case .success:
+                            if let packet = response.packet {
+                                self.log.default("Idle packet received: %{public}@", String(describing: packet))
+                                NotificationCenter.default.post(
+                                    name: .DevicePacketReceived,
+                                    object: self,
+                                    userInfo: [RileyLinkBluetoothDevice.notificationPacketKey: packet]
+                                )
+                            }
+                        }
+                    } else {
+                        self.log.error("Unknown idle response: %{public}@", value.hexadecimalString)
+                    }
+                } else {
+                    self.log.error("Skipping parsing characteristic value update due to missing BLE firmware version")
+                }
+                self.assertIdleListening(forceRestart: true)
+            }
+        case .responseCount:
+            // PeripheralManager.Configuration.valueUpdateMacros is responsible for handling this response.
+            break
+        case .timerTick:
+            NotificationCenter.default.post(name: .DeviceTimerDidTick, object: self)
+            assertIdleListening(forceRestart: false)
+        case .customName, .firmwareVersion, .ledMode:
+            break
+        }
+    }
+
+    private func handleCharacteristicUpdate(_ characteristic: OrangeServiceCharacteristicUUID, value: Data?) {
+        switch characteristic {
+        case .orangeRX, .orangeTX:
+            guard let data = value, !data.isEmpty else { return }
+            if data.first == 0xbb {
+                guard data.count > 6 else { return }
+                if data[1] == 0x09, data[2] == 0xaa {
+                    orangeLinkFirmwareHardwareVersion = "FW\(data[3]).\(data[4])/HW\(data[5]).\(data[6])"
+                    orangeLinkHardwareVersionMajorMinor = [Int(data[5]), Int(data[6])]
+                    NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
+                }
+            } else if data.first == OrangeLinkRequestType.cfgHeader.rawValue {
+                guard data.count > 2 else { return }
+                if data[1] == 0x01 {
+                    guard data.count > 5 else { return }
+                    ledOn = (data[3] != 0)
+                    vibrationOn = (data[4] != 0)
+                    NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
+                } else if data[1] == 0x03 {
+                    guard data.count > 4 else { return }
+                    let int = UInt16(bigEndian: Data(data[3...4]).withUnsafeBytes { $0.load(as: UInt16.self) })
+                    voltage = Float(int) / 1000
+                    NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
+                }
+            }
+        }
+    }
+
+    func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) {
+        self.rssi = Int(truncating: RSSI)
+        NotificationCenter.default.post(
+            name: .DeviceRSSIDidChange,
+            object: self,
+            userInfo: [RileyLinkBluetoothDevice.notificationRSSIKey: RSSI]
+        )
+    }
+
+    func peripheralManagerDidUpdateName(_ manager: PeripheralManager) {
+        NotificationCenter.default.post(
+            name: .DeviceNameDidChange,
+            object: self,
+            userInfo: nil
+        )
+    }
+
+    func completeConfiguration(for manager: PeripheralManager) throws {
+        // Read bluetooth version to determine compatibility
+        log.default("Reading firmware versions for PeripheralManager configuration")
+        let bleVersionString = try manager.readBluetoothFirmwareVersion(timeout: 1)
+        bleFirmwareVersion = BLEFirmwareVersion(versionString: bleVersionString)
+
+        let radioVersionString = try manager.readRadioFirmwareVersion(timeout: 1, responseType: bleFirmwareVersion?.responseType ?? .buffered)
+        radioFirmwareVersion = RadioFirmwareVersion(versionString: radioVersionString)
+
+        try manager.setOrangeNotifyOn()
+    }
+}
+
+
+extension RileyLinkBluetoothDevice: CustomDebugStringConvertible {
+
+    public var debugDescription: String {
+        os_unfair_lock_lock(&lock)
+        let lastIdle = self.lastIdle
+        let isIdleListeningPending = self.isIdleListeningPending
+        let isTimerTickEnabled = self.isTimerTickEnabled
+        os_unfair_lock_unlock(&lock)
+
+        return [
+            "## RileyLinkDevice",
+            "* name: \(name ?? "")",
+            "* lastIdle: \(lastIdle ?? .distantPast)",
+            "* isIdleListeningPending: \(isIdleListeningPending)",
+            "* isTimerTickEnabled: \(isTimerTickEnabled)",
+            "* isTimerTickNotifying: \(manager.timerTickEnabled)",
+            "* radioFirmware: \(String(describing: radioFirmwareVersion))",
+            "* bleFirmware: \(String(describing: bleFirmwareVersion))",
+            "* peripheralManager: \(manager)",
+            "* sessionQueue.operationCount: \(sessionQueue.operationCount)"
+        ].joined(separator: "\n")
+    }
+}
+
+extension RileyLinkBluetoothDevice {
+    public static let notificationPacketKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationPacket"
+
+    public static let notificationRSSIKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationRSSI"
+
+    public static let batteryLevelKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.BatteryLevel"
+}
+
+
+extension Notification.Name {
+    public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.ConnectionStateDidChange")
+
+    public static let DeviceDidStartIdle = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DidStartIdle")
+
+    public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.NameDidChange")
+
+    public static let DevicePacketReceived = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.PacketReceived")
+
+    public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.RSSIDidChange")
+
+    public static let DeviceTimerDidTick = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.TimerTickDidChange")
+
+    public static let DeviceStatusUpdated = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DeviceStatusUpdated")
+
+    public static let DeviceBatteryLevelUpdated = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.BatteryLevelUpdated")
+}

+ 340 - 0
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkBluetoothDeviceProvider.swift

@@ -0,0 +1,340 @@
+//
+//  RileyLinkDeviceManager.swift
+//  RileyLinkBLEKit
+//
+//  Copyright © 2017 Pete Schwamb. All rights reserved.
+//
+
+import CoreBluetooth
+import os.log
+import LoopKit
+
+public class RileyLinkBluetoothDeviceProvider: NSObject {
+    private let log = OSLog(category: "RileyLinkDeviceManager")
+
+    // Isolated to centralQueue
+    private var central: CBCentralManager!
+
+    private let centralQueue = DispatchQueue(label: "com.rileylink.RileyLinkBLEKit.BluetoothManager.centralQueue", qos: .unspecified)
+
+    internal let sessionQueue = DispatchQueue(label: "com.rileylink.RileyLinkBLEKit.RileyLinkDeviceManager.sessionQueue", qos: .unspecified)
+
+    public weak var delegate: RileyLinkDeviceProviderDelegate?
+
+    // Isolated to centralQueue
+    private var devices: [RileyLinkBluetoothDevice] = [] {
+        didSet {
+            NotificationCenter.default.post(name: .ManagerDevicesDidChange, object: self)
+        }
+    }
+
+    // Isolated to centralQueue
+    private var autoConnectIDs: Set<String> {
+        didSet {
+            delegate?.rileylinkDeviceProvider(self, didChange: RileyLinkConnectionState(autoConnectIDs: autoConnectIDs))
+        }
+    }
+
+    public var connectingCount: Int {
+        return self.autoConnectIDs.count
+    }
+
+    // Isolated to centralQueue
+    private var isScanningEnabled = false
+
+    public init(autoConnectIDs: Set<String>) {
+        self.autoConnectIDs = autoConnectIDs
+
+        super.init()
+
+        centralQueue.sync {
+            central = CBCentralManager(
+                delegate: self,
+                queue: centralQueue,
+                options: [
+                    CBCentralManagerOptionRestoreIdentifierKey: "com.rileylink.CentralManager"
+                ]
+            )
+        }
+    }
+
+    // MARK: - Configuration
+
+    public var idleListeningEnabled: Bool {
+        if case .disabled = idleListeningState {
+            return false
+        } else {
+            return true
+        }
+    }
+
+    public var idleListeningState: RileyLinkBluetoothDevice.IdleListeningState {
+        get {
+            return lockedIdleListeningState.value
+        }
+        set {
+            lockedIdleListeningState.value = newValue
+            centralQueue.async {
+                for device in self.devices {
+                    device.setIdleListeningState(newValue)
+                }
+            }
+        }
+    }
+    private let lockedIdleListeningState = Locked(RileyLinkBluetoothDevice.IdleListeningState.disabled)
+
+    public var timerTickEnabled: Bool {
+        get {
+            return lockedTimerTickEnabled.value
+        }
+        set {
+            lockedTimerTickEnabled.value = newValue
+            centralQueue.async {
+                for device in self.devices {
+                    if device.isConnected {
+                        device.setTimerTickEnabled(newValue)
+                    }
+                }
+            }
+        }
+    }
+    private let lockedTimerTickEnabled = Locked(true)
+}
+
+
+// MARK: - Connecting
+extension RileyLinkBluetoothDeviceProvider {
+    public func getAutoConnectIDs(_ completion: @escaping (_ autoConnectIDs: Set<String>) -> Void) {
+        centralQueue.async {
+            completion(self.autoConnectIDs)
+        }
+    }
+    
+    /// Asks the central manager for its peripheral instance for a given device.
+    /// It seems to be possible that this reference changes across a bluetooth reset, and not updating the reference can result in API MISUSE warnings
+    ///
+    /// - Parameter device: The device to reload
+    /// - Returns: The peripheral instance returned by the central manager
+    private func reloadPeripheral(for device: RileyLinkBluetoothDevice) -> CBPeripheral? {
+        dispatchPrecondition(condition: .onQueue(centralQueue))
+
+        guard let peripheral = central.retrievePeripherals(withIdentifiers: [device.peripheralIdentifier]).first else {
+            return nil
+        }
+
+        device.setPeripheral(peripheral)
+        return peripheral
+    }
+
+    private var hasDiscoveredAllAutoConnectDevices: Bool {
+        dispatchPrecondition(condition: .onQueue(centralQueue))
+
+        return autoConnectIDs.isSubset(of: devices.map { $0.peripheralIdentifier.uuidString })
+    }
+
+    private func autoConnectDevices() {
+        dispatchPrecondition(condition: .onQueue(centralQueue))
+
+        for device in devices where autoConnectIDs.contains(device.peripheralIdentifier.uuidString) {
+            log.info("Attempting reconnect to %@", String(describing: device))
+            connect(device)
+        }
+    }
+
+    private func addPeripheral(_ peripheral: CBPeripheral, rssi: Int? = nil) {
+        dispatchPrecondition(condition: .onQueue(centralQueue))
+
+        var device: RileyLinkBluetoothDevice! = devices.first(where: { $0.peripheralIdentifier == peripheral.identifier })
+
+        if let device = device {
+            device.setPeripheral(peripheral)
+        } else {
+            device = RileyLinkBluetoothDevice(peripheralManager: PeripheralManager(peripheral: peripheral, configuration: .rileyLink, centralManager: central, queue: sessionQueue), rssi: rssi)
+            if peripheral.state == .connected {
+                device.setTimerTickEnabled(timerTickEnabled)
+                device.setIdleListeningState(idleListeningState)
+            }
+
+            devices.append(device)
+
+            log.info("Created device for peripheral %@", peripheral)
+        }
+
+        if autoConnectIDs.contains(peripheral.identifier.uuidString) {
+            central.connectIfNecessary(peripheral)
+        }
+    }
+}
+
+extension RileyLinkBluetoothDeviceProvider: RileyLinkDeviceProvider {
+    public func connect(_ device: RileyLinkDevice) {
+        centralQueue.async {
+            self.autoConnectIDs.insert(device.peripheralIdentifier.uuidString)
+
+            guard let peripheral = self.reloadPeripheral(for: device as! RileyLinkBluetoothDevice) else {
+                return
+            }
+
+            self.central.connectIfNecessary(peripheral)
+        }
+    }
+
+    public func disconnect(_ device: RileyLinkDevice) {
+        centralQueue.async {
+            self.autoConnectIDs.remove(device.peripheralIdentifier.uuidString)
+
+            guard let peripheral = self.reloadPeripheral(for: device as! RileyLinkBluetoothDevice) else {
+                return
+            }
+
+            self.central.cancelPeripheralConnectionIfNecessary(peripheral)
+        }
+    }
+
+    public func getDevices(_ completion: @escaping (_ devices: [RileyLinkDevice]) -> Void) {
+        centralQueue.async {
+            completion(self.devices)
+        }
+    }
+
+    public func deprioritize(_ device: RileyLinkDevice, completion: (() -> Void)? = nil) {
+        centralQueue.async {
+            self.devices.deprioritize(device as! RileyLinkBluetoothDevice)
+            completion?()
+        }
+    }
+    
+    public func setScanningEnabled(_ enabled: Bool) {
+        centralQueue.async {
+            self.isScanningEnabled = enabled
+
+            if case .poweredOn = self.central.state {
+                if enabled {
+                    self.central.scanForPeripherals()
+                } else if self.central.isScanning {
+                    self.central.stopScan()
+                }
+            }
+        }
+    }
+
+    public func assertIdleListening(forcingRestart: Bool) {
+        centralQueue.async {
+            for device in self.devices {
+                device.assertIdleListening(forceRestart: forcingRestart)
+            }
+        }
+    }
+
+    public func shouldConnect(to deviceID: String) -> Bool {
+        return self.autoConnectIDs.contains(deviceID)
+    }
+}
+
+extension Array where Element == RileyLinkBluetoothDevice {
+    mutating func deprioritize(_ element: Element) {
+        if let index = self.firstIndex(where: { $0 === element }) {
+            self.swapAt(index, self.count - 1)
+        }
+    }
+}
+
+
+// MARK: - Delegate methods called on `centralQueue`
+extension RileyLinkBluetoothDeviceProvider: CBCentralManagerDelegate {
+    public func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
+        log.default("%@", #function)
+
+        guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
+            return
+        }
+
+        for peripheral in peripherals {
+            addPeripheral(peripheral)
+        }
+    }
+
+    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        log.default("%@: %@", #function, central.state.description)
+        if case .poweredOn = central.state {
+            autoConnectDevices()
+
+            if isScanningEnabled || !hasDiscoveredAllAutoConnectDevices {
+                central.scanForPeripherals()
+            } else if central.isScanning {
+                central.stopScan()
+            }
+        }
+
+        for device in devices {
+            device.centralManagerDidUpdateState(central)
+        }
+    }
+
+    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
+        log.default("Discovered %@ at %@", peripheral, RSSI)
+
+        addPeripheral(peripheral, rssi: Int(truncating: RSSI))
+
+        // TODO: Should we keep scanning? There's no UI to remove a lost RileyLink, which could result in a battery drain due to indefinite scanning.
+        if !isScanningEnabled && central.isScanning && hasDiscoveredAllAutoConnectDevices {
+            log.default("All peripherals discovered")
+            central.stopScan()
+        }
+    }
+
+    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
+        // Notify the device so it can begin configuration
+        for device in devices where device.peripheralIdentifier == peripheral.identifier {
+            device.centralManager(central, didConnect: peripheral)
+        }
+    }
+
+    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
+        for device in devices where device.peripheralIdentifier == peripheral.identifier {
+            device.centralManager(central, didDisconnectPeripheral: peripheral, error: error)
+        }
+
+        autoConnectDevices()
+    }
+
+    public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
+        log.error("%@: %@: %@", #function, peripheral, String(describing: error))
+
+        for device in devices where device.peripheralIdentifier == peripheral.identifier {
+            device.centralManager(central, didFailToConnect: peripheral, error: error)
+        }
+
+        autoConnectDevices()
+    }
+}
+
+
+extension RileyLinkBluetoothDeviceProvider {
+    public override var debugDescription: String {
+        var report = [
+            "## RileyLinkDeviceManager",
+            "central: \(central!)",
+            "autoConnectIDs: \(autoConnectIDs)",
+            "timerTickEnabled: \(timerTickEnabled)",
+            "idleListeningState: \(idleListeningState)"
+        ]
+
+        for device in devices {
+            report.append(String(reflecting: device))
+            report.append("")
+        }
+
+        return report.joined(separator: "\n\n")
+    }
+}
+
+
+extension Notification.Name {
+    public static let ManagerDevicesDidChange = Notification.Name("com.rileylink.RileyLinkBLEKit.DevicesDidChange")
+}
+
+extension RileyLinkBluetoothDeviceProvider {
+    public static let autoConnectIDsStateKey = "com.rileylink.RileyLinkBLEKit.AutoConnectIDs"
+}
+

+ 39 - 0
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkConnectionState.swift

@@ -0,0 +1,39 @@
+//
+//  RileyLinkConnectionManagerState.swift
+//  RileyLinkBLEKit
+//
+//  Created by Pete Schwamb on 8/21/18.
+//  Copyright © 2018 Pete Schwamb. All rights reserved.
+//
+
+import Foundation
+
+public struct RileyLinkConnectionState: RawRepresentable, Equatable {
+    
+    public typealias RawValue = RileyLinkDeviceProvider.RawStateValue
+    
+    public var autoConnectIDs: Set<String>
+
+    public init(autoConnectIDs: Set<String>) {
+        self.autoConnectIDs = autoConnectIDs
+    }
+    
+    public init?(rawValue: RileyLinkDeviceProvider.RawStateValue) {
+        guard
+            let autoConnectIDs = rawValue["autoConnectIDs"] as? [String]
+            else {
+                return nil
+        }
+        
+        self.init(autoConnectIDs: Set(autoConnectIDs))
+    }
+    
+    public var rawValue: RawValue {
+        return [
+            "autoConnectIDs": Array(autoConnectIDs),
+        ]
+    }
+
+    
+    
+}

+ 49 - 601
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDevice.swift

@@ -6,7 +6,6 @@
 //
 
 import CoreBluetooth
-import os.log
 
 public enum RileyLinkHardwareType {
     case riley
@@ -21,619 +20,68 @@ public enum RileyLinkHardwareType {
     }
 }
 
-/// TODO: Should we be tracking the most recent "pump" RSSI?
-public class RileyLinkDevice {
-    let manager: PeripheralManager
-
-    private let log = OSLog(category: "RileyLinkDevice")
-
-    // Confined to `manager.queue`
-    private var bleFirmwareVersion: BLEFirmwareVersion?
-
-    // Confined to `manager.queue`
-    private var radioFirmwareVersion: RadioFirmwareVersion?
-
-    public var isConnected: Bool {
-        return manager.peripheral.state == .connected
-    }
-    
-    public var rlFirmwareDescription: String {
-        let versions = [radioFirmwareVersion, bleFirmwareVersion].compactMap { (version: CustomStringConvertible?) -> String? in
-            if let version = version {
-                return String(describing: version)
-            } else {
-                return nil
-            }
-        }
-
-        return versions.joined(separator: " / ")
-    }
-
-    private var version: String {
-        switch hardwareType {
-        case .riley, .ema, .none:
-            return rlFirmwareDescription
-        case .orange:
-            return orangeLinkFirmwareHardwareVersion
-        }
-    }
-
-    // Confined to `lock`
-    private var idleListeningState: IdleListeningState = .disabled
-
-    // Confined to `lock`
-    private var lastIdle: Date?
-    
-    // Confined to `lock`
-    // TODO: Tidy up this state/preference machine
-    private var isIdleListeningPending = false
-
-    // Confined to `lock`
-    private var isTimerTickEnabled = true
-    
-    /// Serializes access to device state
-    private var lock = os_unfair_lock()
-    
-    private var orangeLinkFirmwareHardwareVersion = "v1.x"
-    private var orangeLinkHardwareVersionMajorMinor: [Int]?
-    private var ledOn: Bool = false
-    private var vibrationOn: Bool = false
-    private var voltage: Float?
-    private var batteryLevel: Int?
-    private var hasPiezo: Bool {
-        if let olHW = orangeLinkHardwareVersionMajorMinor, olHW[0] == 1, olHW[1] >= 1 {
-            return true
-        } else if let olHW = orangeLinkHardwareVersionMajorMinor, olHW[0] == 2, olHW[1] == 6 {
-            return true
-       }
-        return false
-    }
-    
-    public var hasOrangeLinkService: Bool {
-        return self.manager.peripheral.services?.itemWithUUID(RileyLinkServiceUUID.orange.cbUUID) != nil
-    }
-    
-    public var hardwareType: RileyLinkHardwareType? {
-        guard let services = self.manager.peripheral.services else {
-            return nil
-        }
-        
-        guard let bleComponents = self.bleFirmwareVersion else {
-            return nil
-        }
-
-        if services.itemWithUUID(RileyLinkServiceUUID.secureDFU.cbUUID) != nil {
-            return .orange
-        } else if bleComponents.components[0] == 3 {
-            // this returns true for riley with ema firmware, but that is OK
-            return .ema
-        } else {
-            // as long as riley ble remains at 2.x with ema at 3.x this will work
-            return .riley
-        }
-      }
-    
-    /// The queue used to serialize sessions and observe when they've drained
-    private let sessionQueue: OperationQueue = {
-        let queue = OperationQueue()
-        queue.name = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.sessionQueue"
-        queue.maxConcurrentOperationCount = 1
-
-        return queue
-    }()
-
-    private var sessionQueueOperationCountObserver: NSKeyValueObservation!
-
-    public var rssi: Int?
-
-    init(peripheralManager: PeripheralManager, rssi: Int?) {
-        self.manager = peripheralManager
-        self.rssi = rssi
-        sessionQueue.underlyingQueue = peripheralManager.queue
-
-        peripheralManager.delegate = self
-
-        sessionQueueOperationCountObserver = sessionQueue.observe(\.operationCount, options: [.new]) { [weak self] (queue, change) in
-            if let newValue = change.newValue, newValue == 0 {
-                self?.log.debug("Session queue operation count is now empty")
-                self?.assertIdleListening(forceRestart: true)
-            }
-        }
-    }
-}
-
-
-// MARK: - Peripheral operations. Thread-safe.
-extension RileyLinkDevice {
-    public var name: String? {
-        return manager.peripheral.name
-    }
-
-    public var deviceURI: String {
-        return "rileylink://\(name ?? peripheralIdentifier.uuidString)"
-    }
-
-    public var peripheralIdentifier: UUID {
-        return manager.peripheral.identifier
-    }
-
-    public var peripheralState: CBPeripheralState {
-        return manager.peripheral.state
-    }
-
-    public func readRSSI() {
-        guard case .connected = manager.peripheral.state, case .poweredOn? = manager.central?.state else {
-            return
-        }
-        manager.peripheral.readRSSI()
-    }
-
-    public func setCustomName(_ name: String) {
-        manager.setCustomName(name)
-    }
-    
-    public func updateBatteryLevel() {
-        manager.readBatteryLevel { value in
-            if let batteryLevel = value {
-                self.batteryLevel = batteryLevel
-                NotificationCenter.default.post(
-                    name: .DeviceBatteryLevelUpdated,
-                    object: self,
-                    userInfo: [RileyLinkDevice.batteryLevelKey: batteryLevel]
-                )
-                NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
-            }
-        }
-    }
-    
-    public func orangeAction(_ command: OrangeLinkCommand) {
-        log.debug("orangeAction: %@", "\(command)")
-        manager.orangeAction(command)
-    }
-    
-    public func setOrangeConfig(_ config: OrangeLinkConfigurationSetting, isOn: Bool) {
-        log.debug("setOrangeConfig: %@, %@", "\(String(describing: config))", "\(isOn)")
-        manager.setOrangeConfig(config, isOn: isOn)
-    }
-    
-    public func orangeWritePwd() {
-        log.debug("orangeWritePwd")
-        manager.orangeWritePwd()
-    }
-    
-    public func orangeClose() {
-        log.debug("orangeClose")
-        manager.orangeClose()
-    }
-    
-    public func orangeReadSet() {
-        log.debug("orangeReadSet")
-        manager.orangeReadSet()
-    }
-    
-    public func orangeReadVDC() {
-        log.debug("orangeReadVDC")
-        manager.orangeReadVDC()
-    }
-    
-    public func findDevice() {
-        log.debug("findDevice")
-        manager.findDevice()
-    }
-    
-    public func setDiagnosticeLEDModeForBLEChip(_ mode: RileyLinkLEDMode) {
-        manager.setLEDMode(mode: mode)
-    }
-    
-    public func readDiagnosticLEDModeForBLEChip(completion: @escaping (RileyLinkLEDMode?) -> Void) {
-        manager.readDiagnosticLEDMode(completion: completion)
-    }
-
-    /// Asserts that the caller is currently on the session queue
-    public func assertOnSessionQueue() {
-        dispatchPrecondition(condition: .onQueue(manager.queue))
-    }
-
-    /// Schedules a closure to execute on the session queue after a specified time
-    ///
-    /// - Parameters:
-    ///   - deadline: The time after which to execute
-    ///   - execute: The closure to execute
-    public func sessionQueueAsyncAfter(deadline: DispatchTime, execute: @escaping () -> Void) {
-        manager.queue.asyncAfter(deadline: deadline, execute: execute)
-    }
-}
-
-
-extension RileyLinkDevice: Equatable, Hashable {
-    public static func ==(lhs: RileyLinkDevice, rhs: RileyLinkDevice) -> Bool {
-        return lhs === rhs
-    }
-
-    public func hash(into hasher: inout Hasher) {
-        hasher.combine(peripheralIdentifier)
-    }
-}
-
-
-// MARK: - Status management
-extension RileyLinkDevice {
-    public struct Status {
-        public let lastIdle: Date?
-
-        public let name: String?
-        
-        public let version: String
-
-        public let ledOn: Bool
-        public let vibrationOn: Bool
-        public let voltage: Float?
-        public let battery: Int?
-        public let hasPiezo: Bool
-    }
-
-    public func getStatus(_ completion: @escaping (_ status: Status) -> Void) {
-        os_unfair_lock_lock(&lock)
-        let lastIdle = self.lastIdle
-        os_unfair_lock_unlock(&lock)
-
-        self.manager.queue.async {
-            completion(Status(
-                lastIdle: lastIdle,
-                name: self.name,
-                version: self.version,
-                ledOn: self.ledOn,
-                vibrationOn: self.vibrationOn,
-                voltage: self.voltage,
-                battery: self.batteryLevel,
-                hasPiezo: self.hasPiezo
-            ))
-        }
+public struct RileyLinkDeviceStatus {
+    public let lastIdle: Date?
+    public let name: String?
+    public let version: String
+    public let ledOn: Bool
+    public let vibrationOn: Bool
+    public let voltage: Float?
+    public let battery: Int?
+    public let hasPiezo: Bool
+
+    public init(lastIdle: Date?, name: String?, version: String, ledOn: Bool, vibrationOn: Bool, voltage: Float?, battery: Int?, hasPiezo: Bool) {
+        self.lastIdle = lastIdle
+        self.name = name
+        self.version = version
+        self.ledOn = ledOn
+        self.vibrationOn = vibrationOn
+        self.voltage = voltage
+        self.battery = battery
+        self.hasPiezo = hasPiezo
     }
 }
 
 
-// MARK: - Command session management
-// CommandSessions are a way to serialize access to the RileyLink command/response facility.
-// All commands that send data out on the RL data characteristic need to be in a command session.
-// Accessing other characteristics on the RileyLink can be done without a command session.
-extension RileyLinkDevice {
-    public func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void) {
-        self.log.default("Scheduling session %{public}@", name)
-        sessionQueue.addOperation(manager.configureAndRun({ [weak self] (manager) in
-            self?.log.default("======================== %{public}@ ===========================", name)
-            let bleFirmwareVersion = self?.bleFirmwareVersion
-            let radioFirmwareVersion = self?.radioFirmwareVersion
-
-            if bleFirmwareVersion == nil || radioFirmwareVersion == nil {
-                self?.log.error("Running session with incomplete configuration: bleFirmwareVersion %{public}@, radioFirmwareVersion: %{public}@", String(describing: bleFirmwareVersion), String(describing: radioFirmwareVersion))
-            }
-
-            block(CommandSession(manager: manager, responseType: bleFirmwareVersion?.responseType ?? .buffered, firmwareVersion: radioFirmwareVersion ?? .unknown))
-            self?.log.default("------------------------ %{public}@ ---------------------------", name)
-        }))
-    }
-}
-
-
-// MARK: - Idle management
-extension RileyLinkDevice {
-    public enum IdleListeningState {
-        case enabled(timeout: TimeInterval, channel: UInt8)
-        case disabled
-    }
-
-    func setIdleListeningState(_ state: IdleListeningState) {
-        os_unfair_lock_lock(&lock)
-        let oldValue = idleListeningState
-        idleListeningState = state
-        os_unfair_lock_unlock(&lock)
+public protocol RileyLinkDevice {
 
-        switch (oldValue, state) {
-        case (.disabled, .enabled):
-            assertIdleListening(forceRestart: true)
-        case (.enabled, .enabled):
-            assertIdleListening(forceRestart: false)
-        default:
-            break
-        }
-    }
+    var isConnected: Bool { get }
+    var rlFirmwareDescription: String { get }
+    var hasOrangeLinkService: Bool { get }
+    var hardwareType: RileyLinkHardwareType? { get }
+    var rssi: Int? { get }
 
-    public func assertIdleListening(forceRestart: Bool = false) {
-        os_unfair_lock_lock(&lock)
-        guard case .enabled(timeout: let timeout, channel: let channel) = self.idleListeningState else {
-            os_unfair_lock_unlock(&lock)
-            return
-        }
+    var name: String? { get }
+    var deviceURI: String { get }
+    var peripheralIdentifier: UUID { get }
+    var peripheralState: CBPeripheralState { get }
 
-        guard case .connected = self.manager.peripheral.state, case .poweredOn? = self.manager.central?.state else {
-            os_unfair_lock_unlock(&lock)
-            return
-        }
+    func readRSSI()
+    func setCustomName(_ name: String)
 
-        guard forceRestart || (self.lastIdle ?? .distantPast).timeIntervalSinceNow < -timeout else {
-            os_unfair_lock_unlock(&lock)
-            return
-        }
+    func updateBatteryLevel()
 
-        guard !self.isIdleListeningPending else {
-            os_unfair_lock_unlock(&lock)
-            return
-        }
-
-        self.isIdleListeningPending = true
-        os_unfair_lock_unlock(&lock)
-
-        self.manager.startIdleListening(idleTimeout: timeout, channel: channel) { (error) in
-            os_unfair_lock_lock(&self.lock)
-            self.isIdleListeningPending = false
-
-            if let error = error {
-                self.log.error("Unable to start idle listening: %@", String(describing: error))
-                os_unfair_lock_unlock(&self.lock)
-            } else {
-                self.lastIdle = Date()
-                self.log.debug("Started idle listening")
-                os_unfair_lock_unlock(&self.lock)
-                NotificationCenter.default.post(name: .DeviceDidStartIdle, object: self)
-            }
-        }
-    }
-}
-
-
-// MARK: - Timer tick management
-extension RileyLinkDevice {
-    func setTimerTickEnabled(_ enabled: Bool) {
-        os_unfair_lock_lock(&lock)
-        self.isTimerTickEnabled = enabled
-        os_unfair_lock_unlock(&lock)
-        self.assertTimerTick()
-    }
-
-    func assertTimerTick() {
-        os_unfair_lock_lock(&self.lock)
-        let isTimerTickEnabled = self.isTimerTickEnabled
-        os_unfair_lock_unlock(&self.lock)
-
-        if isTimerTickEnabled != self.manager.timerTickEnabled {
-            self.manager.setTimerTickEnabled(isTimerTickEnabled)
-        }
-    }
-}
-
-
-// MARK: - CBCentralManagerDelegate Proxying
-extension RileyLinkDevice {
-    func centralManagerDidUpdateState(_ central: CBCentralManager) {
-        if case .poweredOn = central.state {
-            assertIdleListening(forceRestart: false)
-            assertTimerTick()
-        }
-
-        manager.centralManagerDidUpdateState(central)
-    }
-
-    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
-        log.default("didConnect %{public}@", peripheral)
-        if case .connected = peripheral.state {
-            assertIdleListening(forceRestart: false)
-            assertTimerTick()
-        }
-
-        manager.centralManager(central, didConnect: peripheral)
-        NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
-    }
-
-    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
-        log.default("didDisconnectPeripheral %{public}@", peripheral)
-        NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
-    }
+    func orangeAction(_ command: OrangeLinkCommand)
+    func setOrangeConfig(_ config: OrangeLinkConfigurationSetting, isOn: Bool)
+    func orangeWritePwd()
+    func orangeClose()
+    func orangeReadSet()
+    func orangeReadVDC()
+    func findDevice()
+    func setDiagnosticeLEDModeForBLEChip(_ mode: RileyLinkLEDMode)
+    func readDiagnosticLEDModeForBLEChip(completion: @escaping (RileyLinkLEDMode?) -> Void)
+    func assertOnSessionQueue()
+    func sessionQueueAsyncAfter(deadline: DispatchTime, execute: @escaping () -> Void)
 
-    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
-        log.default("didFailToConnect %{public}@", peripheral)
-        NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
-    }
+    func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void)
+    func getStatus(_ completion: @escaping (_ status: RileyLinkDeviceStatus) -> Void)
 }
 
-
-extension RileyLinkDevice: PeripheralManagerDelegate {
-    func peripheralManager(_ manager: PeripheralManager, didUpdateNotificationStateFor characteristic: CBCharacteristic) {
-        log.debug("Did didUpdateNotificationStateFor %@", characteristic)
-    }
-    
-    // If PeripheralManager receives a response on the data queue, without an outstanding request,
-    // it will pass the update to this method, which is called on the central's queue.
-    // This is how idle listen responses are handled
-    func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) {
-        let characteristicService: CBService? = characteristic.service
-        guard let cbService = characteristicService, let service = RileyLinkServiceUUID(rawValue: cbService.uuid.uuidString) else {
-            log.debug("Update from characteristic on unknown service: %@", String(describing: characteristic.service))
-            return
-        }
-
-        switch service {
-        case .main:
-            guard let mainCharacteristic = MainServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) else {
-                log.debug("Update from unknown characteristic %@ on main service.", characteristic.uuid.uuidString)
-                return
-            }
-            handleCharacteristicUpdate(mainCharacteristic, value: characteristic.value)
-
-        case .orange:
-            guard let orangeCharacteristic = OrangeServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) else {
-                log.debug("Update from unknown characteristic %@ on orange service.", characteristic.uuid.uuidString)
-                return
-            }
-            handleCharacteristicUpdate(orangeCharacteristic, value: characteristic.value)
-        default:
-            return
-        }
-    }
-
-    private func handleCharacteristicUpdate(_ characteristic: MainServiceCharacteristicUUID, value: Data?) {
-        switch characteristic {
-        case .data:
-            guard let value = value, value.count > 0 else {
-                return
-            }
-
-            self.manager.queue.async {
-                if let responseType = self.bleFirmwareVersion?.responseType {
-                    let response: PacketResponse?
-
-                    switch responseType {
-                    case .buffered:
-                        var buffer =  ResponseBuffer<PacketResponse>(endMarker: 0x00)
-                        buffer.append(value)
-                        response = buffer.responses.last
-                    case .single:
-                        response = PacketResponse(data: value)
-                    }
-
-                    if let response = response {
-                        switch response.code {
-                        case .commandInterrupted:
-                            self.log.debug("Received commandInterrupted during idle; assuming device is still listening.")
-                            return
-                        case .rxTimeout, .zeroData, .invalidParam, .unknownCommand:
-                            self.log.debug("Idle error received: %@", String(describing: response.code))
-                        case .success:
-                            if let packet = response.packet {
-                                self.log.default("Idle packet received: %{public}@", String(describing: packet))
-                                NotificationCenter.default.post(
-                                    name: .DevicePacketReceived,
-                                    object: self,
-                                    userInfo: [RileyLinkDevice.notificationPacketKey: packet]
-                                )
-                            }
-                        }
-                    } else {
-                        self.log.error("Unknown idle response: %{public}@", value.hexadecimalString)
-                    }
-                } else {
-                    self.log.error("Skipping parsing characteristic value update due to missing BLE firmware version")
-                }
-                self.assertIdleListening(forceRestart: true)
-            }
-        case .responseCount:
-            // PeripheralManager.Configuration.valueUpdateMacros is responsible for handling this response.
-            break
-        case .timerTick:
-            NotificationCenter.default.post(name: .DeviceTimerDidTick, object: self)
-            assertIdleListening(forceRestart: false)
-        case .customName, .firmwareVersion, .ledMode:
-            break
-        }
-    }
-
-    private func handleCharacteristicUpdate(_ characteristic: OrangeServiceCharacteristicUUID, value: Data?) {
-        switch characteristic {
-        case .orangeRX, .orangeTX:
-            guard let data = value, !data.isEmpty else { return }
-            if data.first == 0xbb {
-                guard data.count > 6 else { return }
-                if data[1] == 0x09, data[2] == 0xaa {
-                    orangeLinkFirmwareHardwareVersion = "FW\(data[3]).\(data[4])/HW\(data[5]).\(data[6])"
-                    orangeLinkHardwareVersionMajorMinor = [Int(data[5]), Int(data[6])]
-                    NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
-                }
-            } else if data.first == OrangeLinkRequestType.cfgHeader.rawValue {
-                guard data.count > 2 else { return }
-                if data[1] == 0x01 {
-                    guard data.count > 5 else { return }
-                    ledOn = (data[3] != 0)
-                    vibrationOn = (data[4] != 0)
-                    NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
-                } else if data[1] == 0x03 {
-                    guard data.count > 4 else { return }
-                    let int = UInt16(bigEndian: Data(data[3...4]).withUnsafeBytes { $0.load(as: UInt16.self) })
-                    voltage = Float(int) / 1000
-                    NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
-                }
-            }
+extension Array where Element == RileyLinkDevice {
+    public var firstConnected: Element? {
+        return self.first { (device) -> Bool in
+            return device.isConnected
         }
     }
-
-    func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) {
-        self.rssi = Int(truncating: RSSI)
-        NotificationCenter.default.post(
-            name: .DeviceRSSIDidChange,
-            object: self,
-            userInfo: [RileyLinkDevice.notificationRSSIKey: RSSI]
-        )
-    }
-
-    func peripheralManagerDidUpdateName(_ manager: PeripheralManager) {
-        NotificationCenter.default.post(
-            name: .DeviceNameDidChange,
-            object: self,
-            userInfo: nil
-        )
-    }
-
-    func completeConfiguration(for manager: PeripheralManager) throws {
-        // Read bluetooth version to determine compatibility
-        log.default("Reading firmware versions for PeripheralManager configuration")
-        let bleVersionString = try manager.readBluetoothFirmwareVersion(timeout: 1)
-        bleFirmwareVersion = BLEFirmwareVersion(versionString: bleVersionString)
-
-        let radioVersionString = try manager.readRadioFirmwareVersion(timeout: 1, responseType: bleFirmwareVersion?.responseType ?? .buffered)
-        radioFirmwareVersion = RadioFirmwareVersion(versionString: radioVersionString)
-        
-        try manager.setOrangeNotifyOn()
-    }
 }
 
-
-extension RileyLinkDevice: CustomDebugStringConvertible {
-    
-    public var debugDescription: String {
-        os_unfair_lock_lock(&lock)
-        let lastIdle = self.lastIdle
-        let isIdleListeningPending = self.isIdleListeningPending
-        let isTimerTickEnabled = self.isTimerTickEnabled
-        os_unfair_lock_unlock(&lock)
-
-        return [
-            "## RileyLinkDevice",
-            "* name: \(name ?? "")",
-            "* lastIdle: \(lastIdle ?? .distantPast)",
-            "* isIdleListeningPending: \(isIdleListeningPending)",
-            "* isTimerTickEnabled: \(isTimerTickEnabled)",
-            "* isTimerTickNotifying: \(manager.timerTickEnabled)",
-            "* radioFirmware: \(String(describing: radioFirmwareVersion))",
-            "* bleFirmware: \(String(describing: bleFirmwareVersion))",
-            "* peripheralManager: \(manager)",
-            "* sessionQueue.operationCount: \(sessionQueue.operationCount)"
-        ].joined(separator: "\n")
-    }
-}
-
-
-extension RileyLinkDevice {
-    public static let notificationPacketKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationPacket"
-
-    public static let notificationRSSIKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationRSSI"
-    
-    public static let batteryLevelKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.BatteryLevel"
-}
-
-
-extension Notification.Name {
-    public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.ConnectionStateDidChange")
-
-    public static let DeviceDidStartIdle = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DidStartIdle")
-
-    public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.NameDidChange")
-
-    public static let DevicePacketReceived = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.PacketReceived")
-
-    public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.RSSIDidChange")
-
-    public static let DeviceTimerDidTick = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.TimerTickDidChange")
-    
-    public static let DeviceStatusUpdated = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DeviceStatusUpdated")
-
-    public static let DeviceBatteryLevelUpdated = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.BatteryLevelUpdated")
-}

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

@@ -8,7 +8,7 @@
 
 public enum RileyLinkDeviceError: Error {
     case peripheralManagerError(LocalizedError)
-    case invalidInput(String)
+    case errorResponse(String)
     case writeSizeLimitExceeded(maxLength: Int)
     case invalidResponse(Data)
     case responseTimeout
@@ -22,8 +22,8 @@ extension RileyLinkDeviceError: LocalizedError {
         switch self {
         case .peripheralManagerError(let error):
             return error.errorDescription
-        case .invalidInput(let input):
-            return String(format: LocalizedString("Input %@ is invalid", comment: "Invalid input error description (1: input)"), String(describing: input))
+        case .errorResponse(let message):
+            return message
         case .invalidResponse(let response):
             return String(format: LocalizedString("Response %@ is invalid", comment: "Invalid response error description (1: response)"), String(describing: response))
         case .writeSizeLimitExceeded(let maxLength):

+ 34 - 0
Dependencies/rileylink_ios/RileyLinkBLEKit/RileyLinkDeviceProvider.swift

@@ -0,0 +1,34 @@
+//
+//  RileyLinkDeviceProvider.swift
+//  RileyLinkBLEKit
+//
+//  Created by Pete Schwamb on 9/5/22.
+//  Copyright © 2022 Pete Schwamb. All rights reserved.
+//
+
+import Foundation
+
+public protocol RileyLinkDeviceProviderDelegate : AnyObject {
+    func rileylinkDeviceProvider(_ rileylinkDeviceProvider: RileyLinkDeviceProvider, didChange state: RileyLinkConnectionState)
+}
+
+public protocol RileyLinkDeviceProvider: AnyObject {
+    typealias RawStateValue = [String : Any]
+
+    var delegate: RileyLinkDeviceProviderDelegate? { get set }
+
+    var idleListeningState: RileyLinkBluetoothDevice.IdleListeningState { get set }
+    var idleListeningEnabled: Bool { get }
+    var timerTickEnabled: Bool { get set }
+    var connectingCount: Int { get }
+
+    func deprioritize(_ device: RileyLinkDevice, completion: (() -> Void)?)
+    func assertIdleListening(forcingRestart: Bool)
+    func getDevices(_ completion: @escaping (_ devices: [RileyLinkDevice]) -> Void)
+    func connect(_ device: RileyLinkDevice)
+    func disconnect(_ device: RileyLinkDevice)
+    func setScanningEnabled(_ enabled: Bool)
+    func shouldConnect(to deviceID: String) -> Bool
+
+    var debugDescription: String { get }
+}

+ 52 - 52
Dependencies/rileylink_ios/RileyLinkKit/PumpOpsSession.swift

@@ -11,7 +11,7 @@ import LoopKit
 import RileyLinkBLEKit
 
 
-protocol PumpOpsSessionDelegate: AnyObject {
+public protocol PumpOpsSessionDelegate: AnyObject {
     func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState)
     func pumpOpsSessionDidChangeRadioConfig(_ session: PumpOpsSession)
 }
@@ -25,14 +25,14 @@ public class PumpOpsSession {
         }
     }
     public let settings: PumpSettings
-    private let session: PumpMessageSender
+    private let messageSender: PumpMessageSender
 
     private unowned let delegate: PumpOpsSessionDelegate
     
-    internal init(settings: PumpSettings, pumpState: PumpState, session: PumpMessageSender, delegate: PumpOpsSessionDelegate) {
+    public init(settings: PumpSettings, pumpState: PumpState, messageSender: PumpMessageSender, delegate: PumpOpsSessionDelegate) {
         self.settings = settings
         self.pump = pumpState
-        self.session = session
+        self.messageSender = messageSender
         self.delegate = delegate
     }
 }
@@ -68,13 +68,13 @@ extension PumpOpsSession {
         if pump.pumpModel == nil || !pump.pumpModel!.hasMySentry {
             // Older pumps have a longer sleep cycle between wakeups, so send an initial burst
             do {
-                let _: PumpAckMessageBody = try session.getResponse(to: shortPowerMessage, repeatCount: 255, timeout: .milliseconds(1), retryCount: 0)
+                let _: PumpAckMessageBody = try messageSender.getResponse(to: shortPowerMessage, responseType: .pumpAck, repeatCount: 255, timeout: .milliseconds(1), retryCount: 0)
             }
             catch { }
         }
 
         do {
-            let _: PumpAckMessageBody = try session.getResponse(to: shortPowerMessage, repeatCount: 255, timeout: .seconds(12), retryCount: 0)
+            let _: PumpAckMessageBody = try messageSender.getResponse(to: shortPowerMessage, responseType: .pumpAck, repeatCount: 255, timeout: .seconds(12), retryCount: 0)
         } catch let error as PumpOpsError {
             throw PumpCommandError.command(error)
         }
@@ -82,7 +82,7 @@ extension PumpOpsSession {
 
     private func isPumpResponding() -> Bool {
         do {
-            let _: GetPumpModelCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel, retryCount: 1)
+            let _: GetPumpModelCarelinkMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 1)
             return true
         } catch {
             return false
@@ -119,7 +119,7 @@ extension PumpOpsSession {
         // Arguments
         do {
             let longPowerMessage = PumpMessage(settings: settings, type: .powerOn, body: PowerOnCarelinkMessageBody(duration: duration))
-            let _: PumpAckMessageBody = try session.getResponse(to: longPowerMessage)
+            let _: PumpAckMessageBody = try messageSender.getResponse(to: longPowerMessage, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         } catch let error as PumpOpsError {
             throw PumpCommandError.arguments(error)
         } catch {
@@ -153,7 +153,7 @@ extension PumpOpsSession {
         }
 
         try wakeup()
-        let body: GetPumpModelCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel)
+        let body: GetPumpModelCarelinkMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
 
         guard let pumpModel = PumpModel(rawValue: body.model) else {
             throw PumpOpsError.unknownPumpModel(body.model)
@@ -179,7 +179,7 @@ extension PumpOpsSession {
     public func getPumpFirmwareVersion() throws -> String {
         
         try wakeup()
-        let body: GetPumpFirmwareVersionMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readFirmwareVersion), responseType: .readFirmwareVersion)
+        let body: GetPumpFirmwareVersionMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readFirmwareVersion), responseType: .readFirmwareVersion, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         
         return body.version
     }
@@ -195,7 +195,7 @@ extension PumpOpsSession {
     ///     - PumpOpsError.unknownResponse
     public func getBatteryStatus() throws -> GetBatteryCarelinkMessageBody {
         try wakeup()
-        return try session.getResponse(to: PumpMessage(settings: settings, type: .getBattery), responseType: .getBattery)
+        return try messageSender.getResponse(to: PumpMessage(settings: settings, type: .getBattery), responseType: .getBattery, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
     }
 
     /// - Throws:
@@ -209,7 +209,7 @@ extension PumpOpsSession {
     ///     - PumpOpsError.unknownResponse
     internal func getPumpStatus() throws -> ReadPumpStatusMessageBody {
         try wakeup()
-        return try session.getResponse(to: PumpMessage(settings: settings, type: .readPumpStatus), responseType: .readPumpStatus)
+        return try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readPumpStatus), responseType: .readPumpStatus, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
     }
 
     /// - Throws:
@@ -223,7 +223,7 @@ extension PumpOpsSession {
     ///     - PumpOpsError.unknownResponse
     public func getSettings() throws -> ReadSettingsCarelinkMessageBody {
         try wakeup()
-        return try session.getResponse(to: PumpMessage(settings: settings, type: .readSettings), responseType: .readSettings)
+        return try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readSettings), responseType: .readSettings, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
     }
 
     /// Reads the pump's time, returning a set of DateComponents in the pump's presumed time zone.
@@ -240,7 +240,7 @@ extension PumpOpsSession {
     ///     - PumpOpsError.unknownResponse
     public func getTime() throws -> DateComponents {
         try wakeup()
-        let response: ReadTimeCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTime), responseType: .readTime)
+        let response: ReadTimeCarelinkMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readTime), responseType: .readTime, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         var components = response.dateComponents
         components.timeZone = pump.timeZone
         return components
@@ -265,7 +265,7 @@ extension PumpOpsSession {
         var message = PumpMessage(settings: settings, type: profile.readMessageType)
         var scheduleData = Data()
         while (!isFinished) {
-            let body: DataFrameMessageBody = try session.getResponse(to: message, responseType: profile.readMessageType)
+            let body: DataFrameMessageBody = try messageSender.getResponse(to: message, responseType: profile.readMessageType, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
 
             scheduleData.append(body.contents)
             isFinished = body.isLastFrame
@@ -285,7 +285,7 @@ extension PumpOpsSession {
     public func getOtherDevicesIDs() throws -> ReadOtherDevicesIDsMessageBody {
         try wakeup()
 
-        return try session.getResponse(to: PumpMessage(settings: settings, type: .readOtherDevicesIDs), responseType: .readOtherDevicesIDs)
+        return try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readOtherDevicesIDs), responseType: .readOtherDevicesIDs, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
     }
 
     /// - Throws:
@@ -298,7 +298,7 @@ extension PumpOpsSession {
     public func getOtherDevicesEnabled() throws -> Bool {
         try wakeup()
 
-        let response: ReadOtherDevicesStatusMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readOtherDevicesStatus), responseType: .readOtherDevicesStatus)
+        let response: ReadOtherDevicesStatusMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readOtherDevicesStatus), responseType: .readOtherDevicesStatus, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         return response.isEnabled
     }
 
@@ -312,7 +312,7 @@ extension PumpOpsSession {
     public func getRemoteControlIDs() throws -> ReadRemoteControlIDsMessageBody {
         try wakeup()
 
-        return try session.getResponse(to: PumpMessage(settings: settings, type: .readRemoteControlIDs), responseType: .readRemoteControlIDs)
+        return try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readRemoteControlIDs), responseType: .readRemoteControlIDs, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
     }
 }
 
@@ -350,7 +350,7 @@ extension PumpOpsSession {
         let pumpModel = try getPumpModel()
         let pumpClock = try getTime()
 
-        let reservoir: ReadRemainingInsulinMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readRemainingInsulin), responseType: .readRemainingInsulin)
+        let reservoir: ReadRemainingInsulinMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readRemainingInsulin), responseType: .readRemainingInsulin, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
 
         return (
             units: reservoir.getUnitsRemaining(insulinBitPackingScale: pumpModel.insulinBitPackingScale),
@@ -395,13 +395,13 @@ extension PumpOpsSession {
             try wakeup()
 
             let shortMessage = PumpMessage(packetType: message.packetType, address: message.address.hexadecimalString, messageType: message.messageType, messageBody: CarelinkShortMessageBody())
-            let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
+            let _: PumpAckMessageBody = try messageSender.getResponse(to: shortMessage, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         } catch let error as PumpOpsError {
             throw PumpCommandError.command(error)
         }
 
         do {
-            return try session.getResponse(to: message, responseType: responseType)
+            return try messageSender.getResponse(to: message, responseType: responseType, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         } catch let error as PumpOpsError {
             throw PumpCommandError.arguments(error)
         }
@@ -467,13 +467,13 @@ extension PumpOpsSession {
                     try wakeup()
 
                     let shortMessage = PumpMessage(packetType: message.packetType, address: message.address.hexadecimalString, messageType: message.messageType, messageBody: CarelinkShortMessageBody())
-                    let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
+                    let _: PumpAckMessageBody = try messageSender.getResponse(to: shortMessage, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
                 } catch let error as PumpOpsError {
                     throw PumpCommandError.command(error)
                 }
 
                 do {
-                    let _: PumpAckMessageBody = try session.getResponse(to: message, retryCount: 0)
+                    let _: PumpAckMessageBody = try messageSender.getResponse(to: message, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
                 } catch PumpOpsError.pumpError(let errorCode) {
                     lastError = .arguments(.pumpError(errorCode))
                     break  // Stop because we have a pump error response
@@ -484,7 +484,7 @@ extension PumpOpsSession {
                     // The pump does not ACK a successful temp basal. We'll check manually below if it was successful.
                 }
 
-                let response: ReadTempBasalCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal)
+                let response: ReadTempBasalCarelinkMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
 
                 if response.timeRemaining == duration && response.rateType == .absolute {
                     return .success(response)
@@ -507,7 +507,7 @@ extension PumpOpsSession {
         
         try wakeup()
         
-        let response: ReadTempBasalCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal)
+        let response: ReadTempBasalCarelinkMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         
         return response.rate
     }
@@ -521,7 +521,7 @@ extension PumpOpsSession {
 
         do {
             let shortMessage = PumpMessage(settings: settings, type: .changeTime)
-            let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
+            let _: PumpAckMessageBody = try messageSender.getResponse(to: shortMessage, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         } catch let error as PumpOpsError {
             throw PumpCommandError.command(error)
         }
@@ -529,7 +529,7 @@ extension PumpOpsSession {
         do {
             let components = generator()
             let message = PumpMessage(settings: settings, type: .changeTime, body: ChangeTimeCarelinkMessageBody(dateComponents: components)!)
-            let _: PumpAckMessageBody = try session.getResponse(to: message)
+            let _: PumpAckMessageBody = try messageSender.getResponse(to: message, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
             self.pump.timeZone = components.timeZone?.fixed ?? .currentFixed
         } catch let error as PumpOpsError {
             throw PumpCommandError.arguments(error)
@@ -636,7 +636,7 @@ extension PumpOpsSession {
         for nextFrame in frames.dropFirst() {
             let message = PumpMessage(settings: settings, type: type, body: nextFrame)
             do {
-                let _: PumpAckMessageBody = try session.getResponse(to: message)
+                let _: PumpAckMessageBody = try messageSender.getResponse(to: message, responseType: .pumpAck, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
             } catch let error as PumpOpsError {
                 throw PumpCommandError.arguments(error)
             }
@@ -644,7 +644,7 @@ extension PumpOpsSession {
     }
     
     public func getStatistics() throws -> RileyLinkStatistics {
-        return try session.getRileyLinkStatistics()
+        return try messageSender.getRileyLinkStatistics()
     }
 }
 
@@ -665,7 +665,7 @@ extension PumpOpsSession {
         let commandTimeout = TimeInterval(seconds: 30)
 
         // Wait for the pump to start polling
-        guard let encodedData = try session.listenForPacket(onChannel: 0, timeout: commandTimeout)?.data else {
+        guard let encodedData = try messageSender.listenForPacket(onChannel: 0, timeout: commandTimeout)?.data else {
             throw PumpOpsError.noResponse(during: "Watchdog listening")
         }
 
@@ -687,7 +687,7 @@ extension PumpOpsSession {
         // Identify as a MySentry device
         let findMessageResponse = PumpMessage(packetType: .mySentry, address: settings.pumpID, messageType: .pumpAck, messageBody: findMessageResponseBody)
 
-        let linkMessage = try session.sendAndListen(findMessageResponse, timeout: commandTimeout)
+        let linkMessage = try messageSender.sendAndListen(findMessageResponse, repeatCount: 0, timeout: commandTimeout, retryCount: 3)
 
         guard let
             linkMessageBody = linkMessage.messageBody as? DeviceLinkMessageBody,
@@ -699,7 +699,7 @@ extension PumpOpsSession {
         // Acknowledge the pump linked with us
         let linkMessageResponse = PumpMessage(packetType: .mySentry, address: settings.pumpID, messageType: .pumpAck, messageBody: linkMessageResponseBody)
 
-        try session.send(linkMessageResponse)
+        try messageSender.send(linkMessageResponse)
     }
 }
 
@@ -772,7 +772,7 @@ extension PumpOpsSession {
         let drate_e = UInt8(0x9) // exponent of symbol rate (16kbps)
         let chanbw = mode.rawValue
         do {
-            try session.updateRegister(.mdmcfg4, value: chanbw | drate_e)
+            try messageSender.updateRegister(.mdmcfg4, value: chanbw | drate_e)
         } catch let error as LocalizedError {
             throw PumpOpsError.deviceError(error)
         }
@@ -782,7 +782,7 @@ extension PumpOpsSession {
     ///     - PumpOpsError.deviceError
     ///     - RileyLinkDeviceError
     func configureRadio(for region: PumpRegion, frequency: Measurement<UnitFrequency>?) throws {
-        try session.resetRadioConfig()
+        try messageSender.resetRadioConfig()
         
         switch region {
         case .worldWide:
@@ -790,21 +790,21 @@ extension PumpOpsSession {
             try setRXFilterMode(.wide)
             //try session.updateRegister(.mdmcfg3, value: 0x66)
             //try session.updateRegister(.mdmcfg2, value: 0x33)
-            try session.updateRegister(.mdmcfg1, value: 0x62)
-            try session.updateRegister(.mdmcfg0, value: 0x1A)
-            try session.updateRegister(.deviatn, value: 0x13)
+            try messageSender.updateRegister(.mdmcfg1, value: 0x62)
+            try messageSender.updateRegister(.mdmcfg0, value: 0x1A)
+            try messageSender.updateRegister(.deviatn, value: 0x13)
         case .northAmerica, .canada:
             //try session.updateRegister(.mdmcfg4, value: 0x99)
             try setRXFilterMode(.narrow)
             //try session.updateRegister(.mdmcfg3, value: 0x66)
             //try session.updateRegister(.mdmcfg2, value: 0x33)
-            try session.updateRegister(.mdmcfg1, value: 0x61)
-            try session.updateRegister(.mdmcfg0, value: 0x7E)
-            try session.updateRegister(.deviatn, value: 0x15)
+            try messageSender.updateRegister(.mdmcfg1, value: 0x61)
+            try messageSender.updateRegister(.mdmcfg0, value: 0x7E)
+            try messageSender.updateRegister(.deviatn, value: 0x15)
         }
         
         if let frequency = frequency {
-            try session.setBaseFrequency(frequency)
+            try messageSender.setBaseFrequency(frequency)
         }
     }
 
@@ -821,7 +821,7 @@ extension PumpOpsSession {
         
         do {
             // Needed to put the pump in listen mode
-            try session.setBaseFrequency(middleFreq)
+            try messageSender.setBaseFrequency(middleFreq)
             try wakeup()
         } catch {
             // Continue anyway; the pump likely heard us, even if we didn't hear it.
@@ -830,11 +830,11 @@ extension PumpOpsSession {
         for freq in frequencies {
             var trial = FrequencyTrial(frequency: freq)
 
-            try session.setBaseFrequency(freq)
+            try messageSender.setBaseFrequency(freq)
             var sumRSSI = 0
             for _ in 1...tries {
                 // Ignore failures here
-                let rfPacket = try? session.sendAndListenForPacket(PumpMessage(settings: settings, type: .getPumpModel), timeout: .milliseconds(130))
+                let rfPacket = try? messageSender.sendAndListenForPacket(PumpMessage(settings: settings, type: .getPumpModel), repeatCount: 0, timeout: .milliseconds(130), retryCount: 3)
                 if  let rfPacket = rfPacket,
                     let pkt = MinimedPacket(encodedData: rfPacket.data),
                     let response = PumpMessage(rxData: pkt.data), response.messageType == .getPumpModel
@@ -854,7 +854,7 @@ extension PumpOpsSession {
         })
 
         guard sortedTrials.first!.successes > 0 else {
-            try session.setBaseFrequency(fallback ?? middleFreq)
+            try messageSender.setBaseFrequency(fallback ?? middleFreq)
             throw PumpOpsError.rfCommsFailure("No pump responses during scan")
         }
 
@@ -863,7 +863,7 @@ extension PumpOpsSession {
             bestFrequency: sortedTrials.first!.frequency
         )
         
-        try session.setBaseFrequency(results.bestFrequency)
+        try messageSender.setBaseFrequency(results.bestFrequency)
 
         return results
     }
@@ -950,9 +950,9 @@ extension PumpOpsSession {
             expectedFrameNum += 1
             let msg = PumpMessage(settings: settings, type: .pumpAck)
             if !curResp.lastFrame {
-                curResp = try session.getResponse(to: msg, responseType: .getHistoryPage)
+                curResp = try messageSender.getResponse(to: msg, responseType: .getHistoryPage, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
             } else {
-                try session.send(msg)
+                try messageSender.send(msg)
                 break
             }
         }
@@ -991,7 +991,7 @@ extension PumpOpsSession {
         
         var events = [TimestampedGlucoseEvent]()
         
-        let currentGlucosePage: ReadCurrentGlucosePageMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readCurrentGlucosePage), responseType: .readCurrentGlucosePage)
+        let currentGlucosePage: ReadCurrentGlucosePageMessageBody = try messageSender.getResponse(to: PumpMessage(settings: settings, type: .readCurrentGlucosePage), responseType: .readCurrentGlucosePage, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
         let startPage = Int(currentGlucosePage.pageNum)
         //max lookback of 15 pages or when page is 0
         let endPage = max(startPage - 15, 0)
@@ -1057,9 +1057,9 @@ extension PumpOpsSession {
             expectedFrameNum += 1
             let msg = PumpMessage(settings: settings, type: .pumpAck)
             if !curResp.lastFrame {
-                curResp = try session.getResponse(to: msg, responseType: .getGlucosePage)
+                curResp = try messageSender.getResponse(to: msg, responseType: .getGlucosePage, repeatCount: 0, timeout: MinimedPumpMessageSender.standardPumpResponseWindow,  retryCount: 3)
             } else {
-                try session.send(msg)
+                try messageSender.send(msg)
                 break
             }
         }
@@ -1074,6 +1074,6 @@ extension PumpOpsSession {
         try wakeup()
 
         let shortWriteTimestamp = PumpMessage(settings: settings, type: .writeGlucoseHistoryTimestamp)
-        let _: PumpAckMessageBody = try session.getResponse(to: shortWriteTimestamp, timeout: .seconds(12))
+        let _: PumpAckMessageBody = try messageSender.getResponse(to: shortWriteTimestamp, responseType: .pumpAck, repeatCount: 0, timeout: .seconds(12),  retryCount: 3)
     }
 }

+ 15 - 23
Dependencies/rileylink_ios/RileyLinkKit/RileyLinkPumpManager.swift

@@ -9,25 +9,21 @@ import LoopKit
 import RileyLinkBLEKit
 
 open class RileyLinkPumpManager {
+
+    open var rileyLinkConnectionManagerState: RileyLinkConnectionState?
     
-    public init(rileyLinkDeviceProvider: RileyLinkDeviceProvider,
-                rileyLinkConnectionManager: RileyLinkConnectionManager? = nil) {
+    public init(rileyLinkDeviceProvider: RileyLinkDeviceProvider) {
         
         self.rileyLinkDeviceProvider = rileyLinkDeviceProvider
-        self.rileyLinkConnectionManager = rileyLinkConnectionManager
-        self.rileyLinkConnectionManagerState = rileyLinkConnectionManager?.state
-        
+
+        rileyLinkDeviceProvider.delegate = self
+
         // Listen for device notifications
         NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkPacketNotification(_:)), name: .DevicePacketReceived, object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkTimerTickNotification(_:)), name: .DeviceTimerDidTick, object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(receivedRileyLinkBatteryUpdate(_:)), name: .DeviceBatteryLevelUpdated, object: nil)
-    }
-    
-    /// Manages all the RileyLinks - access to management is optional
-    public let rileyLinkConnectionManager: RileyLinkConnectionManager?
 
-    // TODO: Not thread-safe
-    open var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState?
+    }
     
     /// Access to rileylink devices
     public let rileyLinkDeviceProvider: RileyLinkDeviceProvider
@@ -51,7 +47,6 @@ open class RileyLinkPumpManager {
     open var debugDescription: String {
         return [
             "## RileyLinkPumpManager",
-            "rileyLinkConnectionManager: \(String(reflecting: rileyLinkConnectionManager))",
             "lastTimerTick: \(String(describing: lastTimerTick.value))",
             "",
             String(reflecting: rileyLinkDeviceProvider),
@@ -71,7 +66,7 @@ extension RileyLinkPumpManager {
      */
     @objc private func receivedRileyLinkPacketNotification(_ note: Notification) {
         guard let device = note.object as? RileyLinkDevice,
-            let packet = note.userInfo?[RileyLinkDevice.notificationPacketKey] as? RFPacket
+            let packet = note.userInfo?[RileyLinkBluetoothDevice.notificationPacketKey] as? RFPacket
         else {
             return
         }
@@ -94,7 +89,7 @@ extension RileyLinkPumpManager {
 
     @objc private func receivedRileyLinkBatteryUpdate(_ note: Notification) {
         guard let device = note.object as? RileyLinkDevice,
-              let batteryLevel = note.userInfo?[RileyLinkDevice.batteryLevelKey] as? Int
+              let batteryLevel = note.userInfo?[RileyLinkBluetoothDevice.batteryLevelKey] as? Int
         else {
             return
         }
@@ -105,21 +100,18 @@ extension RileyLinkPumpManager {
     }
     
     
-    open func connectToRileyLink(_ device: RileyLinkDevice) {
-        rileyLinkConnectionManager?.connect(device)
+    public func connectToRileyLink(_ device: RileyLinkDevice) {
+        rileyLinkDeviceProvider.connect(device)
     }
 
-    open func disconnectFromRileyLink(_ device: RileyLinkDevice) {
-        rileyLinkConnectionManager?.disconnect(device)
+    public func disconnectFromRileyLink(_ device: RileyLinkDevice) {
+        rileyLinkDeviceProvider.disconnect(device)
     }
     
 }
 
-// MARK: - RileyLinkConnectionManagerDelegate
-extension RileyLinkPumpManager: RileyLinkConnectionManagerDelegate {
-    public func rileyLinkConnectionManager(_ rileyLinkConnectionManager: RileyLinkConnectionManager, didChange state: RileyLinkConnectionManagerState) {
+extension RileyLinkPumpManager: RileyLinkDeviceProviderDelegate {
+    public func rileylinkDeviceProvider(_ rileylinkDeviceProvider: RileyLinkBLEKit.RileyLinkDeviceProvider, didChange state: RileyLinkBLEKit.RileyLinkConnectionState) {
         self.rileyLinkConnectionManagerState = state
     }
 }
-
-

+ 2 - 2
Dependencies/rileylink_ios/RileyLinkKitUI/RileyLinkDeviceTableViewController.swift

@@ -276,7 +276,7 @@ public class RileyLinkDeviceTableViewController: UITableViewController {
             }
         },
             center.addObserver(forName: .DeviceRSSIDidChange, object: device, queue: mainQueue) { [weak self] (note) -> Void in
-            self?.bleRSSI = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int
+            self?.bleRSSI = note.userInfo?[RileyLinkBluetoothDevice.notificationRSSIKey] as? Int
             
             if let cell = self?.cellForRow(.rssi), let formatter = self?.integerFormatter {
                 cell.setDetailRSSI(self?.bleRSSI, formatter: formatter)
@@ -667,7 +667,7 @@ public class RileyLinkDeviceTableViewController: UITableViewController {
                 return false
             }
         case .rileyLinkCommands:
-            return device.peripheralState == .connected
+            return device.isConnected
         case .alert:
             return true
         }

+ 4 - 7
Dependencies/rileylink_ios/RileyLinkKitUI/RileyLinkDevicesTableViewDataSource.swift

@@ -54,7 +54,7 @@ public class RileyLinkDevicesTableViewDataSource: NSObject {
 
     public var isScanningEnabled: Bool = false {
         didSet {
-            rileyLinkPumpManager.rileyLinkConnectionManager?.setScanningEnabled(isScanningEnabled)
+            rileyLinkPumpManager.rileyLinkDeviceProvider.setScanningEnabled(isScanningEnabled)
 
             if isScanningEnabled {
                 rssiFetchTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(updateRSSI), userInfo: nil, repeats: true)
@@ -91,10 +91,7 @@ public class RileyLinkDevicesTableViewDataSource: NSObject {
     /// - Parameter device: The peripheral
     /// - Returns: The adjusted connection state
     private func preferenceStateForDevice(_ device: RileyLinkDevice) -> CBPeripheralState? {
-        guard let connectionManager = rileyLinkPumpManager.rileyLinkConnectionManager else {
-            return nil
-        }
-        let isAutoConnectDevice = connectionManager.shouldConnect(to: device.peripheralIdentifier.uuidString)
+        let isAutoConnectDevice = rileyLinkPumpManager.rileyLinkDeviceProvider.shouldConnect(to: device.peripheralIdentifier.uuidString)
         var state = device.peripheralState
 
         switch state {
@@ -129,8 +126,8 @@ public class RileyLinkDevicesTableViewDataSource: NSObject {
 
     @objc private func deviceDidUpdate(_ note: Notification) {
         DispatchQueue.main.async {
-            if let device = note.object as? RileyLinkDevice, let index = self.devices.firstIndex(where: { $0 === device }) {
-                if let rssi = note.userInfo?[RileyLinkDevice.notificationRSSIKey] as? Int {
+            if let device = note.object as? RileyLinkDevice, let index = self.devices.firstIndex(where: { $0.peripheralIdentifier == device.peripheralIdentifier }) {
+                if let rssi = note.userInfo?[RileyLinkBluetoothDevice.notificationRSSIKey] as? Int {
                     self.deviceRSSI[device.peripheralIdentifier] = rssi
                 }
 

+ 3 - 11
Dependencies/rileylink_ios/RileyLinkKitUI/RileyLinkSetupTableViewController.swift

@@ -23,11 +23,8 @@ public class RileyLinkSetupTableViewController: SetupTableViewController {
     }()
     
     public required init?(coder aDecoder: NSCoder) {
-        let rileyLinkConnectionManager = RileyLinkConnectionManager(autoConnectIDs: [])        
-        rileyLinkPumpManager = RileyLinkPumpManager(rileyLinkDeviceProvider: rileyLinkConnectionManager.deviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
-        
-        rileyLinkConnectionManager.delegate = rileyLinkPumpManager
-        
+        let deviceProvider = RileyLinkBluetoothDeviceProvider(autoConnectIDs: [])
+        rileyLinkPumpManager = RileyLinkPumpManager(rileyLinkDeviceProvider: deviceProvider)
         super.init(coder: aDecoder)
     }
 
@@ -149,12 +146,7 @@ public class RileyLinkSetupTableViewController: SetupTableViewController {
         #if targetEnvironment(simulator)
         return true
         #else
-        
-        guard let connectionManager = rileyLinkPumpManager.rileyLinkConnectionManager else {
-            return false
-        }
-        
-        return connectionManager.connectingCount > 0
+        return rileyLinkPumpManager.rileyLinkDeviceProvider.connectingCount > 0
         #endif
     }
 

+ 1 - 1
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -397,7 +397,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManager(
         _: PumpManager,
         hasNewPumpEvents events: [NewPumpEvent],
-        lastSync _: Date?,
+        lastReconciliation _: Date?,
         completion: @escaping (_ error: Error?) -> Void
     ) {
         dispatchPrecondition(condition: .onQueue(processQueue))

+ 2 - 2
FreeAPS/Sources/APS/Extensions/UserDefaultsExtensions.swift

@@ -18,13 +18,13 @@ extension UserDefaults {
         }
     }
 
-    var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState? {
+    var rileyLinkConnectionManagerState: RileyLinkConnectionState? {
         get {
             guard let rawValue = dictionary(forKey: Key.rileyLinkConnectionManagerState.rawValue)
             else {
                 return nil
             }
-            return RileyLinkConnectionManagerState(rawValue: rawValue)
+            return RileyLinkConnectionState(rawValue: rawValue)
         }
         set {
             set(newValue?.rawValue, forKey: Key.rileyLinkConnectionManagerState.rawValue)