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

Merge remote-tracking branch 'upstream/dev' into superBolus_test

polscm32 2 лет назад
Родитель
Сommit
a44d2814b9
100 измененных файлов с 3944 добавлено и 1556 удалено
  1. 2 2
      .github/workflows/build_iAPS.yml
  2. 1 1
      Config.xcconfig
  3. 12 0
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  4. 1 1
      Dependencies/CGMBLEKit/CGMBLEKitUI/de.lproj/Localizable.strings
  5. 1 1
      Dependencies/G7SensorKit/G7SensorKitUI/de.lproj/Localizable.strings
  6. 3 3
      Dependencies/G7SensorKit/de.lproj/Localizable.strings
  7. 16 0
      Dependencies/OmniBLE/Localizations/en.lproj/Localizable.strings
  8. 1 1
      Dependencies/OmniBLE/Localizations/nl.lproj/Localizable.strings
  9. 9 0
      Dependencies/OmniBLE/Localizations/sv.lproj/Localizable.strings
  10. 44 12
      Dependencies/OmniBLE/OmniBLE.xcodeproj/project.pbxproj
  11. 0 2
      Dependencies/OmniBLE/OmniBLE/Bluetooth/BluetoothServices.swift
  12. 0 12
      Dependencies/OmniBLE/OmniBLE/Bluetooth/EnDecrypt/EnDecrypt.swift
  13. 0 9
      Dependencies/OmniBLE/OmniBLE/Bluetooth/PeripheralManager+OmniBLE.swift
  14. 1 1
      Dependencies/OmniBLE/OmniBLE/Bluetooth/PodProtocolError.swift
  15. 482 145
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/AlertSlot.swift
  16. 7 5
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BasalDeliveryTable.swift
  17. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BasalSchedule.swift
  18. 2 2
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BeepPreference.swift
  19. 1 1
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/CRC16.swift
  20. 309 296
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/FaultEventCode.swift
  21. 33 23
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/CancelDeliveryCommand.swift
  22. 12 4
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift
  23. 2 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift
  24. 23 23
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  25. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  26. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/StatusResponse.swift
  27. 1 1
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/TempBasalExtraCommand.swift
  28. 8 7
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/PendingCommand.swift
  29. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/Pod.swift
  30. 36 46
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/PumpManagerAlert.swift
  31. 32 0
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/SilencePodPreference.swift
  32. 4 4
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/UnfinalizedDose.swift
  33. 2 7
      Dependencies/OmniBLE/OmniBLE/PumpManager/MessageTransport.swift
  34. 257 100
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift
  35. 14 4
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManagerState.swift
  36. 38 19
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodCommsSession.swift
  37. 72 34
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift
  38. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewControllers/DashUICoordinator.swift
  39. 36 17
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/DeactivatePodViewModel.swift
  40. 43 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift
  41. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/PodLifeState.swift
  42. 36 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ActivityView.swift
  43. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/AttachPodView.swift
  44. 5 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/BeepPreferenceSelectionView.swift
  45. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ExpirationReminderPickerView.swift
  46. 30 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/FirstAppear.swift
  47. 2 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ManualTempBasalEntryView.swift
  48. 11 8
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/NotificationSettingsView.swift
  49. 54 24
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift
  50. 101 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PlayTestBeepsView.swift
  51. 4 7
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PodDetailsView.swift
  52. 100 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PumpManagerDetailsView.swift
  53. 167 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPodStatusView.swift
  54. 128 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPulseLogView.swift
  55. 143 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/SilencePodSelectionView.swift
  56. 2 2
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/UncertaintyRecoveredView.swift
  57. 65 33
      Dependencies/OmniKit/OmniKit.xcodeproj/project.pbxproj
  58. 473 136
      Dependencies/OmniKit/OmniKit/OmnipodCommon/AlertSlot.swift
  59. 7 5
      Dependencies/OmniKit/OmniKit/OmnipodCommon/BasalDeliveryTable.swift
  60. 2 2
      Dependencies/OmniKit/OmniKit/OmnipodCommon/BeepPreference.swift
  61. 263 263
      Dependencies/OmniKit/OmniKit/OmnipodCommon/FaultEventCode.swift
  62. 33 23
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/CancelDeliveryCommand.swift
  63. 12 4
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift
  64. 2 3
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift
  65. 24 24
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  66. 4 4
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  67. 1 1
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/StatusResponse.swift
  68. 1 1
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/TempBasalExtraCommand.swift
  69. 0 3
      Dependencies/OmniKit/OmniKit/OmnipodCommon/Pod.swift
  70. 15 25
      Dependencies/OmniKit/OmniKit/OmnipodCommon/PumpManagerAlert.swift
  71. 32 0
      Dependencies/OmniKit/OmniKit/OmnipodCommon/SilencePodPreference.swift
  72. 259 80
      Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift
  73. 18 5
      Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManagerState.swift
  74. 0 1
      Dependencies/OmniKit/OmniKit/PumpManager/PodComms.swift
  75. 37 18
      Dependencies/OmniKit/OmniKit/PumpManager/PodCommsSession.swift
  76. 66 27
      Dependencies/OmniKit/OmniKit/PumpManager/PodState.swift
  77. 4 4
      Dependencies/OmniKit/OmniKitTests/PodInfoTests.swift
  78. BIN
      Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/Cannula Inserted.imageset/CannulaInserted.png
  79. 21 0
      Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/Cannula Inserted.imageset/Contents.json
  80. 16 0
      Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_mask_swiftui.imageset/Contents.json
  81. 55 0
      Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_mask_swiftui.imageset/pod_reservoir_mask.svg
  82. 15 0
      Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_swiftui.imageset/Contents.json
  83. 59 0
      Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_swiftui.imageset/pod_reservoir.svg
  84. 1 1
      Dependencies/OmniKit/OmniKitUI/Resources/nl.lproj/Localizable.strings
  85. 1 1
      Dependencies/OmniKit/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift
  86. 30 11
      Dependencies/OmniKit/OmniKitUI/ViewModels/DeactivatePodViewModel.swift
  87. 41 3
      Dependencies/OmniKit/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift
  88. 1 1
      Dependencies/OmniKit/OmniKitUI/ViewModels/PodLifeState.swift
  89. 36 0
      Dependencies/OmniKit/OmniKitUI/Views/ActivityView.swift
  90. 2 2
      Dependencies/OmniKit/OmniKitUI/Views/AttachPodView.swift
  91. 6 6
      Dependencies/OmniKit/OmniKitUI/Views/BeepPreferenceSelectionView.swift
  92. 1 1
      Dependencies/OmniKit/OmniKitUI/Views/CheckInsertedCannulaView.swift
  93. 1 1
      Dependencies/OmniKit/OmniKitUI/Views/DeactivatePodView.swift
  94. 1 1
      Dependencies/OmniKit/OmniKitUI/Views/ExpirationReminderSetupView.swift
  95. 30 0
      Dependencies/OmniKit/OmniKitUI/Views/FirstAppear.swift
  96. 1 1
      Dependencies/OmniKit/OmniKitUI/Views/InsertCannulaView.swift
  97. 1 1
      Dependencies/OmniKit/OmniKitUI/Views/InsulinTypeConfirmation.swift
  98. 1 1
      Dependencies/OmniKit/OmniKitUI/Views/LowReservoirReminderSetupView.swift
  99. 2 5
      Dependencies/OmniKit/OmniKitUI/Views/ManualTempBasalEntryView.swift
  100. 0 0
      Dependencies/OmniKit/OmniKitUI/Views/NotificationSettingsView.swift

+ 2 - 2
.github/workflows/build_iAPS.yml

@@ -22,7 +22,7 @@ jobs:
     steps:
       # Uncomment to manually select Xcode version if needed
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_14.3.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
 
       # Checks-out the repo
       - name: Checkout Repo
@@ -64,4 +64,4 @@ jobs:
           name: build-artifacts
           path: |
             artifacts
-            buildlog
+            buildlog

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.2.7
+APP_VERSION = 2.2.9
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##

+ 12 - 0
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,5 +1,16 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="22G120" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+    <entity name="Autosens_" representedClassName="Autosens_" syncable="YES" codeGenerationType="class">
+        <attribute name="newisf" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="ratio" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+    </entity>
+    <entity name="Autotune_" representedClassName="Autotune_" syncable="YES" codeGenerationType="class">
+        <attribute name="basalProfile" optional="YES" attributeType="Transformable"/>
+        <attribute name="carbRatio" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="sensitivity" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
+    </entity>
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -118,6 +129,7 @@
     </entity>
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="direction" optional="YES" attributeType="String"/>
         <attribute name="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="String"/>
     </entity>

+ 1 - 1
Dependencies/CGMBLEKit/CGMBLEKitUI/de.lproj/Localizable.strings

@@ -39,7 +39,7 @@ Title text for the button to remove a CGM from Loop */
 "Remote Data Synchronization" = "Remote Daten Synchronisation";
 
 /* Title describing sensor expiration */
-"Sensor Expires" = "Sensor-Ablaufzeitpunkt";
+"Sensor Expires" = "Sensor läuft ab";
 
 /* Title describing past sensor expiration */
 "Sensor Expired" = "Sensor abgelaufen";

+ 1 - 1
Dependencies/G7SensorKit/G7SensorKitUI/de.lproj/Localizable.strings

@@ -54,7 +54,7 @@
 "Last Reading" = "Letzte Messung";
 
 /* Descriptive text on G7StartupView */
-"iAPS can read G7 CGM data, but you must still use the Dexcom G7 App for pairing, calibration, and other sensor management." = "iAPS kann CGM Daten vom G7 direkt lesen. Zum Verbinden, Kalibrieren und weiteres Sensor Management braucht man die G7 App.";
+"iAPS can read G7 CGM data, but you must still use the Dexcom G7 App for pairing, calibration, and other sensor management." = "iAPS kann CGM Daten direkt vom G7 lesen. Zum Verbinden, Kalibrieren und erweitertem Sensor Management benötigt man die G7 App.";
 
 /* String displayed instead of a glucose value below the CGM range */
 "LOW" = "NIEDRIG";

+ 3 - 3
Dependencies/G7SensorKit/de.lproj/Localizable.strings

@@ -2,7 +2,7 @@
 "Dexcom G7" = "Dexcom G7";
 
 /* Descriptive text on G7StartupView */
-"iAPS can read G7 CGM data, but you must still use the Dexcom G7 App for pairing, calibration, and other sensor management." = "iAPS kann CGM Daten vom G7 direkt lesen. Zum Verbinden, Kalibrieren und weiteres Sensor Management braucht man die G7 App.";
+"iAPS can read G7 CGM data, but you must still use the Dexcom G7 App for pairing, calibration, and other sensor management." = "iAPS kann CGM Daten direkt vom G7 lesen. Zum Verbinden, Kalibrieren und erweitertes Sensor Management benötigt man die G7 App.";
 
 /* Button title for starting setup */
 "Continue" = "Fortsetzen";
@@ -11,7 +11,7 @@
 "Cancel" = "Abbrechen";
 
 /* Error description for unreliable state */
-"Glucose data is unavailable" = "Blutzuckerwerte sind nicht verfügbar";
+"Glucose data is unavailable" = "Glukosewerte sind nicht verfügbar";
 
 /* The description of sensor algorithm state when sensor is ok. */
 "Sensor is OK" = "Sensor ist OK";
@@ -70,7 +70,7 @@
 "Configuration" = "Konfiguration";
 
 /* title for g7 config settings to upload readings */
-"Upload Readings" = "Upload von Messwerten";
+"Upload Readings" = "Werte hochladen";
 
 /* Button */
 "Scan for new sensor" = "Nach neuem Sensor suchen";

Разница между файлами не показана из-за своего большого размера
+ 16 - 0
Dependencies/OmniBLE/Localizations/en.lproj/Localizable.strings


+ 1 - 1
Dependencies/OmniBLE/Localizations/nl.lproj/Localizable.strings

@@ -571,7 +571,7 @@
 "Critical Alerts" = "Kritieke waarschuwingen";
 
 /* Description text for critical alerts */
-"The reminders above will not sound if your device is in Silent or Do Not Disturb mode.\n\nThere are other critical Pod alerts and alarms that will sound even if your device is set to Silent or Do Not Disturb mode." = "De bovenstaande meldingen waarschuwen zonder geluid als je apparaat in de modus Stil of Niet storen staat.\n\nEr zijn andere belangrijke Pod waarschuwingen en -alarmen die wel klinken, zelfs als je apparaat in de modus Stil of Niet storen staat.";
+"The reminders above will not sound if your device is in Silent or Do Not Disturb mode.\n\nThere are other critical Pod alerts and alarms that will sound even if your device is set to Silent or Do Not Disturb mode." = "De bovenstaande meldingen waarschuwen zonder geluid als je apparaat in de modus 'Stil' of 'Niet storen' staat.\n\nEr zijn andere belangrijke Pod waarschuwingen en -alarmen die wel klinken, zelfs als je apparaat in de modus 'Stil' of 'Niet storen' staat.";
 /* navigation title for notification settings */
 "Notification Settings" = "Instellingen voor meldingen";
 

Разница между файлами не показана из-за своего большого размера
+ 9 - 0
Dependencies/OmniBLE/Localizations/sv.lproj/Localizable.strings


+ 44 - 12
Dependencies/OmniBLE/OmniBLE.xcodeproj/project.pbxproj

@@ -68,6 +68,7 @@
 		10389A3F26FF7841002115E9 /* CRC16.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A2026FF7841002115E9 /* CRC16.swift */; };
 		10389A4126FF7841002115E9 /* MessageTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A2226FF7841002115E9 /* MessageTransport.swift */; };
 		191DB66D2A06F17800212AC9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 191DB6522A06F17800212AC9 /* Localizable.strings */; };
+		196A6F232AFFFD1700E3C089 /* SilencePodPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196A6F222AFFFD1200E3C089 /* SilencePodPreference.swift */; };
 		84752E9326ED0FFE009FD801 /* OmniBLE.h in Headers */ = {isa = PBXBuildFile; fileRef = 84752E8526ED0FFE009FD801 /* OmniBLE.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		84752ED626ED13F5009FD801 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84752EBF26ED13F5009FD801 /* Id.swift */; };
 		84752ED726ED13F5009FD801 /* X25519KeyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84752EC126ED13F5009FD801 /* X25519KeyGenerator.swift */; };
@@ -168,6 +169,13 @@
 		D802CD0A27DD98C10072E3A1 /* TempBasalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD0527DD98C10072E3A1 /* TempBasalTests.swift */; };
 		D802CD1027DD99AB0072E3A1 /* CRC16Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD0F27DD99AB0072E3A1 /* CRC16Tests.swift */; };
 		D802CD1227DD9AE10072E3A1 /* BasalScheduleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD1127DD9AE10072E3A1 /* BasalScheduleTests.swift */; };
+		D845A1372AF89F5500EA0853 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1362AF89F5500EA0853 /* ActivityView.swift */; };
+		D845A1392AF89F6300EA0853 /* FirstAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1382AF89F6300EA0853 /* FirstAppear.swift */; };
+		D845A13B2AF89F7100EA0853 /* PlayTestBeepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A13A2AF89F7100EA0853 /* PlayTestBeepsView.swift */; };
+		D845A13F2AF89F8400EA0853 /* ReadPodStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A13C2AF89F8400EA0853 /* ReadPodStatusView.swift */; };
+		D845A1402AF89F8400EA0853 /* ReadPulseLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A13D2AF89F8400EA0853 /* ReadPulseLogView.swift */; };
+		D845A1412AF89F8400EA0853 /* PumpManagerDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A13E2AF89F8400EA0853 /* PumpManagerDetailsView.swift */; };
+		D845A1432AF89F9200EA0853 /* SilencePodSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1422AF89F9200EA0853 /* SilencePodSelectionView.swift */; };
 		D8896C6227890E6B00E09A96 /* DetailedStatus+OmniBLE.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8896C6127890E6B00E09A96 /* DetailedStatus+OmniBLE.swift */; };
 		D895BF5B275DE64000D51FC7 /* StringLengthPrefixEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D895BF5A275DE64000D51FC7 /* StringLengthPrefixEncoding.swift */; };
 		D897B06B29347ED500FDB009 /* BolusDeliveryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D897B06A29347ED500FDB009 /* BolusDeliveryTable.swift */; };
@@ -294,6 +302,7 @@
 		191DB66A2A06F17800212AC9 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		191DB66B2A06F17800212AC9 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
 		191DB66C2A06F17800212AC9 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
+		196A6F222AFFFD1200E3C089 /* SilencePodPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodPreference.swift; sourceTree = "<group>"; };
 		4B23AA6328D909E2009B453B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
 		4B23AA6428D909E7009B453B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		4B23AA6528D909E9009B453B /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -420,6 +429,13 @@
 		D802CD0527DD98C10072E3A1 /* TempBasalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempBasalTests.swift; sourceTree = "<group>"; };
 		D802CD0F27DD99AB0072E3A1 /* CRC16Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CRC16Tests.swift; sourceTree = "<group>"; };
 		D802CD1127DD9AE10072E3A1 /* BasalScheduleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalScheduleTests.swift; sourceTree = "<group>"; };
+		D845A1362AF89F5500EA0853 /* ActivityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
+		D845A1382AF89F6300EA0853 /* FirstAppear.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstAppear.swift; sourceTree = "<group>"; };
+		D845A13A2AF89F7100EA0853 /* PlayTestBeepsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayTestBeepsView.swift; sourceTree = "<group>"; };
+		D845A13C2AF89F8400EA0853 /* ReadPodStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodStatusView.swift; sourceTree = "<group>"; };
+		D845A13D2AF89F8400EA0853 /* ReadPulseLogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPulseLogView.swift; sourceTree = "<group>"; };
+		D845A13E2AF89F8400EA0853 /* PumpManagerDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpManagerDetailsView.swift; sourceTree = "<group>"; };
+		D845A1422AF89F9200EA0853 /* SilencePodSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodSelectionView.swift; sourceTree = "<group>"; };
 		D8896C6127890E6B00E09A96 /* DetailedStatus+OmniBLE.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DetailedStatus+OmniBLE.swift"; sourceTree = "<group>"; };
 		D895BF5A275DE64000D51FC7 /* StringLengthPrefixEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringLengthPrefixEncoding.swift; sourceTree = "<group>"; };
 		D897B06A29347ED500FDB009 /* BolusDeliveryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusDeliveryTable.swift; sourceTree = "<group>"; };
@@ -477,6 +493,7 @@
 				1016325627185EE4007A3BC2 /* PodProgressStatus.swift */,
 				C1F67ED927979E400017487F /* PumpManagerAlert.swift */,
 				C1F67EE127985F580017487F /* ReservoirLevel.swift */,
+				196A6F222AFFFD1200E3C089 /* SilencePodPreference.swift */,
 				1016325827185EE4007A3BC2 /* UnfinalizedDose.swift */,
 			);
 			path = OmnipodCommon;
@@ -705,12 +722,12 @@
 		8475311D26ED8246009FD801 /* PumpManagerUI */ = {
 			isa = PBXGroup;
 			children = (
-				C1F67EBB27975F060017487F /* ViewModels */,
+				C1F67EDF2797B1EF0017487F /* OmniBLEHUDProvider.swift */,
 				1029AE4A27094DDC00B7F5B6 /* OmniBLEPumpManager+UI.swift */,
 				1029AE4E27094E1900B7F5B6 /* OmniBLEUI.xcassets */,
 				8475312626ED838A009FD801 /* ViewControllers */,
+				C1F67EBB27975F060017487F /* ViewModels */,
 				8475311E26ED838A009FD801 /* Views */,
-				C1F67EDF2797B1EF0017487F /* OmniBLEHUDProvider.swift */,
 			);
 			path = PumpManagerUI;
 			sourceTree = "<group>";
@@ -718,34 +735,41 @@
 		8475311E26ED838A009FD801 /* Views */ = {
 			isa = PBXGroup;
 			children = (
-				C1C001C327A2351D00533D35 /* OmniBLEReservoirView.swift */,
-				C1C001C227A2351D00533D35 /* OmniBLEReservoirView.xib */,
-				C1F67EB227975E710017487F /* DesignElements */,
+				D845A1362AF89F5500EA0853 /* ActivityView.swift */,
 				C1F67E7327975B830017487F /* AttachPodView.swift */,
 				C1F67E7A27975B830017487F /* BasalStateView.swift */,
+				C1ED1E7127BAE44E00FED71C /* BeepPreferenceSelectionView.swift */,
 				C1F67E8027975B830017487F /* CheckInsertedCannulaView.swift */,
-				C1F67E7727975B830017487F /* OmniBLESettingsView.swift */,
 				C1F67E7927975B830017487F /* DeactivatePodView.swift */,
 				C1F67E8227975B830017487F /* DeliveryUncertaintyRecoveryView.swift */,
+				C1F67EB227975E710017487F /* DesignElements */,
 				C1F67E7B27975B830017487F /* ExpirationReminderPickerView.swift */,
 				C1F67E7827975B830017487F /* ExpirationReminderSetupView.swift */,
+				D845A1382AF89F6300EA0853 /* FirstAppear.swift */,
 				C1F67E7D27975B830017487F /* HUDAssets.xcassets */,
 				C1F67E8527975B830017487F /* InsertCannulaView.swift */,
+				C187C190278FCEC9006E3557 /* InsulinTypeConfirmation.swift */,
 				C1F67E8627975B830017487F /* LowReservoirReminderEditView.swift */,
 				C1F67E7C27975B830017487F /* LowReservoirReminderSetupView.swift */,
+				C1DBD512282FF79D009FCF74 /* ManualTempBasalEntryView.swift */,
 				C1F67E8127975B830017487F /* NotificationSettingsView.swift */,
+				C1C001C327A2351D00533D35 /* OmniBLEReservoirView.swift */,
+				C1C001C227A2351D00533D35 /* OmniBLEReservoirView.xib */,
+				C1F67E7727975B830017487F /* OmniBLESettingsView.swift */,
 				C1F67E7F27975B830017487F /* PairPodView.swift */,
+				D845A13A2AF89F7100EA0853 /* PlayTestBeepsView.swift */,
 				C1F67E8A27975B830017487F /* PodDetailsView.swift */,
+				8475311F26ED838A009FD801 /* PodLifeHUDView.swift */,
+				8475312426ED838A009FD801 /* PodLifeHUDView.xib */,
 				C1F67E8827975B830017487F /* PodSetupView.swift */,
+				D845A13E2AF89F8400EA0853 /* PumpManagerDetailsView.swift */,
+				D845A13C2AF89F8400EA0853 /* ReadPodStatusView.swift */,
+				D845A13D2AF89F8400EA0853 /* ReadPulseLogView.swift */,
 				C1F67E7427975B830017487F /* ScheduledExpirationReminderEditView.swift */,
 				C1F67E8727975B830017487F /* SetupCompleteView.swift */,
+				D845A1422AF89F9200EA0853 /* SilencePodSelectionView.swift */,
 				C1F67E7627975B830017487F /* TimeView.swift */,
 				C1F67E8427975B830017487F /* UncertaintyRecoveredView.swift */,
-				8475311F26ED838A009FD801 /* PodLifeHUDView.swift */,
-				8475312426ED838A009FD801 /* PodLifeHUDView.xib */,
-				C187C190278FCEC9006E3557 /* InsulinTypeConfirmation.swift */,
-				C1ED1E7127BAE44E00FED71C /* BeepPreferenceSelectionView.swift */,
-				C1DBD512282FF79D009FCF74 /* ManualTempBasalEntryView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -836,10 +860,10 @@
 		C1F67EBB27975F060017487F /* ViewModels */ = {
 			isa = PBXGroup;
 			children = (
-				C1F67EC227975F360017487F /* OmniBLESettingsViewModel.swift */,
 				C1F67EC027975F360017487F /* DeactivatePodViewModel.swift */,
 				C1F67EC127975F360017487F /* DeliveryUncertaintyRecoveryViewModel.swift */,
 				C1F67EBD27975F360017487F /* InsertCannulaViewModel.swift */,
+				C1F67EC227975F360017487F /* OmniBLESettingsViewModel.swift */,
 				C1F67EBE27975F360017487F /* PairPodViewModel.swift */,
 				C1F67EC327975F360017487F /* PodLifeState.swift */,
 			);
@@ -1059,6 +1083,7 @@
 				1029AE4927094D0E00B7F5B6 /* OmniBLEPumpManagerState.swift in Sources */,
 				10289E7D2739F893000339E6 /* Milenage.swift in Sources */,
 				10389A3A26FF7841002115E9 /* SetInsulinScheduleCommand.swift in Sources */,
+				D845A13B2AF89F7100EA0853 /* PlayTestBeepsView.swift in Sources */,
 				10389A3826FF7841002115E9 /* DetailedStatus.swift in Sources */,
 				C1F67E9927975B830017487F /* NotificationSettingsView.swift in Sources */,
 				10389A2B26FF7841002115E9 /* PlaceholderMessageBlock.swift in Sources */,
@@ -1103,6 +1128,7 @@
 				C1ED1E7227BAE44E00FED71C /* BeepPreferenceSelectionView.swift in Sources */,
 				84752EDE26ED13F5009FD801 /* PeripheralManagerError.swift in Sources */,
 				10389A2526FF7841002115E9 /* PodInfoActivationTime.swift in Sources */,
+				D845A1432AF89F9200EA0853 /* SilencePodSelectionView.swift in Sources */,
 				C1F67E9A27975B830017487F /* DeliveryUncertaintyRecoveryView.swift in Sources */,
 				1021114D2709467400784F13 /* PodComms.swift in Sources */,
 				10289E6E27309327000339E6 /* CBPeripheral.swift in Sources */,
@@ -1120,7 +1146,9 @@
 				C1F67EC927975F360017487F /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */,
 				1016325C27185EE5007A3BC2 /* BasalDeliveryTable.swift in Sources */,
 				84752EE326ED13F5009FD801 /* BLEPacket.swift in Sources */,
+				D845A1402AF89F8400EA0853 /* ReadPulseLogView.swift in Sources */,
 				102111472709462300784F13 /* PodState.swift in Sources */,
+				196A6F232AFFFD1700E3C089 /* SilencePodPreference.swift in Sources */,
 				1021114B2709462300784F13 /* BasalSchedule+LoopKit.swift in Sources */,
 				84752EE626ED13F5009FD801 /* LTKExchanger.swift in Sources */,
 				10389A2A26FF7841002115E9 /* MessageBlock.swift in Sources */,
@@ -1129,6 +1157,7 @@
 				C1F67E9227975B830017487F /* BasalStateView.swift in Sources */,
 				1024E32B27446DB000DE01F2 /* MessagePacket.swift in Sources */,
 				10289E7B2739F886000339E6 /* EapMessage.swift in Sources */,
+				D845A1392AF89F6300EA0853 /* FirstAppear.swift in Sources */,
 				C1C001C127A2349D00533D35 /* OmniBLE.swift in Sources */,
 				10389A3326FF7841002115E9 /* CancelDeliveryCommand.swift in Sources */,
 				C1DBD513282FF79D009FCF74 /* ManualTempBasalEntryView.swift in Sources */,
@@ -1136,6 +1165,7 @@
 				C1F67EA027975B830017487F /* PodSetupView.swift in Sources */,
 				C1ED1E7027BAE1A600FED71C /* BeepPreference.swift in Sources */,
 				10389A3B26FF7841002115E9 /* ConfigureAlertsCommand.swift in Sources */,
+				D845A13F2AF89F8400EA0853 /* ReadPodStatusView.swift in Sources */,
 				D8896C6227890E6B00E09A96 /* DetailedStatus+OmniBLE.swift in Sources */,
 				10389A3926FF7841002115E9 /* PodInfoResponse.swift in Sources */,
 				84752EE226ED13F5009FD801 /* PayloadJoiner.swift in Sources */,
@@ -1145,6 +1175,7 @@
 				C1F67EC827975F360017487F /* DeactivatePodViewModel.swift in Sources */,
 				C1F67E9127975B830017487F /* DeactivatePodView.swift in Sources */,
 				84752EE726ED13F5009FD801 /* PairResult.swift in Sources */,
+				D845A1412AF89F8400EA0853 /* PumpManagerDetailsView.swift in Sources */,
 				8475315D26EDA193009FD801 /* Data.swift in Sources */,
 				C1F67E9427975B830017487F /* LowReservoirReminderSetupView.swift in Sources */,
 				C1F67EB627975E710017487F /* RoundedCard.swift in Sources */,
@@ -1162,6 +1193,7 @@
 				10389A2E26FF7841002115E9 /* FaultConfigCommand.swift in Sources */,
 				C1F67E9F27975B830017487F /* SetupCompleteView.swift in Sources */,
 				C1F67EE227985F580017487F /* ReservoirLevel.swift in Sources */,
+				D845A1372AF89F5500EA0853 /* ActivityView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 0 - 2
Dependencies/OmniBLE/OmniBLE/Bluetooth/BluetoothServices.swift

@@ -56,7 +56,6 @@ extension PeripheralManager.Configuration {
                     guard let characteristic = manager.peripheral.getCommandCharacteristic() else { return }
                     guard let value = characteristic.value else { return }
 
-                    manager.log.default("CMD <<< %{public}@", value.hexadecimalString)
                     manager.queueLock.lock()
                     manager.cmdQueue.append(value)
                     manager.queueLock.signal()
@@ -66,7 +65,6 @@ extension PeripheralManager.Configuration {
                     guard let characteristic = manager.peripheral.getDataCharacteristic() else { return }
                     guard let value = characteristic.value else { return }
 
-                    manager.log.default("DATA <<< %{public}@", value.hexadecimalString)
                     manager.queueLock.lock()
                     manager.dataQueue.append(value)
                     manager.queueLock.signal()

+ 0 - 12
Dependencies/OmniBLE/OmniBLE/Bluetooth/EnDecrypt/EnDecrypt.swift

@@ -26,15 +26,9 @@ class EnDecrypt {
         let header = msg.asData(forEncryption: false).subdata(in: 0..<16)
 
         let n = nonce.toData(sqn: nonceSeq, podReceiving: false)
-        log.debug("Decrypt ck %@", ck.hexadecimalString)
-        log.debug("Decrypt header %@", header.hexadecimalString)
-        log.debug("Decrypt payload: %@", payload.hexadecimalString)
-        log.debug("Decrypt Nonce %@", n.hexadecimalString)
-        log.debug("Decrypt Tag: %@", Data(payload).subdata(in: (payload.count - MAC_SIZE)..<payload.count).hexadecimalString)
         let ccm = CCM(iv: n.bytes, tagLength: MAC_SIZE, messageLength: payload.count - MAC_SIZE, additionalAuthenticatedData: header.bytes)
         let aes = try AES(key: ck.bytes, blockMode: ccm, padding: .noPadding)
         let decryptedPayload = try aes.decrypt(payload.bytes)
-        log.debug("Decrypted payload %@", Data(decryptedPayload).hexadecimalString)
         
         var msgCopy = msg
         msgCopy.payload = Data(decryptedPayload)
@@ -46,15 +40,9 @@ class EnDecrypt {
         let header = headerMessage.asData(forEncryption: true).subdata(in: 0..<16)
 
         let n = nonce.toData(sqn: nonceSeq, podReceiving: true)
-        log.debug("Encrypt Ck %@", ck.hexadecimalString)
-        log.debug("Encrypt Header %@", header.hexadecimalString)
-        log.debug("Encrypt Payload: %@", payload.hexadecimalString)
-        log.debug("Encrypt Nonce %@", n.hexadecimalString)
         let ccm = CCM(iv: n.bytes, tagLength: MAC_SIZE, messageLength: payload.count, additionalAuthenticatedData: header.bytes)
         let aes = try AES(key: ck.bytes, blockMode: ccm, padding: .noPadding)
         let encryptedPayload = try aes.encrypt(payload.bytes)
-        log.debug("Encrypted payload: %@", Data(encryptedPayload).subdata(in: 0..<(encryptedPayload.count - MAC_SIZE)).hexadecimalString)
-        log.debug("Encrypt Tag: %@", Data(encryptedPayload).subdata(in: (encryptedPayload.count - MAC_SIZE)..<encryptedPayload.count).hexadecimalString)
 
         var msgCopy = headerMessage
         msgCopy.payload = Data(encryptedPayload)

+ 0 - 9
Dependencies/OmniBLE/OmniBLE/Bluetooth/PeripheralManager+OmniBLE.swift

@@ -20,7 +20,6 @@ extension PeripheralManager {
         dispatchPrecondition(condition: .onQueue(queue))
 
         let controllerId = Id.fromUInt32(myId).address
-        log.default("Sending Hello %{public}@", controllerId.hexadecimalString)
         guard let characteristic = peripheral.getCommandCharacteristic() else {
             throw PeripheralManagerError.notReady
         }
@@ -148,7 +147,6 @@ extension PeripheralManager {
         guard let characteristic = peripheral.getCommandCharacteristic() else {
             throw PeripheralManagerError.notReady
         }
-        log.default("CMD >>> %{public}@", Data([command.rawValue]).hexadecimalString)
         
         try writeValue(Data([command.rawValue]), for: characteristic, type: .withResponse, timeout: timeout)
     }
@@ -157,8 +155,6 @@ extension PeripheralManager {
     func waitForCommand(_ command: PodCommand, timeout: TimeInterval = 5) throws {
         dispatchPrecondition(condition: .onQueue(queue))
 
-        log.debug("waitForCommand %{public}@", Data([command.rawValue]).hexadecimalString)
-        
         // Wait for data to be read.
         queueLock.lock()
         if (cmdQueue.count == 0) {
@@ -199,8 +195,6 @@ extension PeripheralManager {
             throw PeripheralManagerError.notReady
         }
         
-        log.default("DATA >>> %{public}@", value.hexadecimalString)
-        
         try writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
     }
 
@@ -208,8 +202,6 @@ extension PeripheralManager {
     func waitForData(sequence: UInt8, timeout: TimeInterval) throws -> Data {
         dispatchPrecondition(condition: .onQueue(queue))
 
-        log.default("waitForData sequence %02x", sequence)
-
         // Wait for data to be read.
         queueLock.lock()
         if (dataQueue.count == 0) {
@@ -235,7 +227,6 @@ extension PeripheralManager {
                 log.error("waitForData failed data[0] != sequence (%d != %d).", data[0], sequence)
                 throw PeripheralManagerError.incorrectResponse
             }
-            log.default("waitForData success %{public}@", data.hexadecimalString)
             return data
         }
         

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/Bluetooth/PodProtocolError.swift

@@ -1,6 +1,6 @@
 //
 //  PodProtocolError.swift
-//  OmnipodKit
+//  OmniBLE
 //
 //  Created by Randall Knutson on 8/3/21.
 //

+ 482 - 145
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/AlertSlot.swift

@@ -9,11 +9,29 @@
 
 import Foundation
 
+fileprivate let defaultShutdownImminentTime = Pod.serviceDuration - Pod.endOfServiceImminentWindow
+fileprivate let defaultExpirationReminderTime = Pod.nominalPodLife - Pod.defaultExpirationReminderOffset
+fileprivate let defaultExpiredTime = Pod.nominalPodLife
+
+// PDM and pre-SwiftUI use every1MinuteFor3MinutesAndRepeatEvery15Minutes, but with SwiftUI use every15Minutes
+fileprivate let suspendTimeExpiredBeepRepeat = BeepRepeat.every15Minutes
+
 public enum AlertTrigger {
     case unitsRemaining(Double)
     case timeUntilAlert(TimeInterval)
 }
 
+extension AlertTrigger: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch self {
+        case .unitsRemaining(let units):
+            return "\(Int(units))U"
+        case .timeUntilAlert(let triggerTime):
+            return "triggerTime=\(triggerTime.timeIntervalStr)"
+        }
+    }
+}
+
 public enum BeepRepeat: UInt8 {
     case once = 0
     case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1
@@ -30,29 +48,48 @@ public enum BeepRepeat: UInt8 {
 public struct AlertConfiguration {
 
     let slot: AlertSlot
-    let trigger: AlertTrigger
     let active: Bool
     let duration: TimeInterval
+    let trigger: AlertTrigger
     let beepRepeat: BeepRepeat
     let beepType: BeepType
+    let silent: Bool
     let autoOffModifier: Bool
 
     static let length = 6
 
-    public init(alertType: AlertSlot, active: Bool = true, autoOffModifier: Bool = false, duration: TimeInterval, trigger: AlertTrigger, beepRepeat: BeepRepeat, beepType: BeepType) {
+    public init(alertType: AlertSlot, active: Bool = true, duration: TimeInterval = 0, trigger: AlertTrigger, beepRepeat: BeepRepeat, beepType: BeepType, silent: Bool = false, autoOffModifier: Bool = false)
+    {
         self.slot = alertType
         self.active = active
-        self.autoOffModifier = autoOffModifier
         self.duration = duration
         self.trigger = trigger
         self.beepRepeat = beepRepeat
         self.beepType = beepType
+        self.silent = silent
+        self.autoOffModifier = autoOffModifier
     }
 }
 
 extension AlertConfiguration: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "AlertConfiguration(slot:\(slot), active:\(active), autoOffModifier:\(autoOffModifier), duration:\(duration), trigger:\(trigger), beepRepeat:\(beepRepeat), beepType:\(beepType))"
+        var str = "slot:\(slot)"
+        if !active {
+            str += ", active:\(active)"
+        }
+        if duration != 0 {
+            str += ", duration:\(duration.timeIntervalStr)"
+        }
+        str += ", trigger:\(trigger), beepRepeat:\(beepRepeat)"
+        if beepType != .noBeepNonCancel {
+            str += ", beepType:\(beepType)"
+        } else {
+            str += ", silent:\(silent)"
+        }
+        if autoOffModifier {
+            str += ", autoOffModifier:\(autoOffModifier)"
+        }
+        return "\nAlertConfiguration(\(str))"
     }
 }
 
@@ -61,54 +98,73 @@ extension AlertConfiguration: CustomDebugStringConvertible {
 public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
     public typealias RawValue = [String: Any]
 
-    // 2 hours long, time for user to start pairing process
-    case waitingForPairingReminder
+    // slot0AutoOff: auto-off timer; requires user input every x minutes -- NOT IMPLEMENTED
+    case autoOff(active: Bool, offset: TimeInterval, countdownDuration: TimeInterval, silent: Bool = false)
 
-    // 1 hour long, time for user to finish priming, cannula insertion
-    case finishSetupReminder
+    // slot1NotUsed
+    case notUsed
 
-    // User configurable with PDM (1-24 hours before 72 hour expiration) "Change Pod Soon"
-    case expirationReminder(TimeInterval)
+    // slot2ShutdownImminent: 79 hour alarm (1 hour before shutdown)
+    // 2 sets of beeps every 15 minutes for 1 hour
+    case shutdownImminent(offset: TimeInterval, absAlertTime: TimeInterval, silent: Bool = false)
 
-    // 72 hour alarm
-    case expired(alertTime: TimeInterval, duration: TimeInterval)
+    // slot3ExpirationReminder: User configurable with PDM (1-24 hours before 72 hour expiration)
+    // 2 sets of beeps every minute for 3 minutes and repeat every 15 minutes
+    // The PDM doesn't use a duration for this alert (presumably because it is limited to 2^9-1 minutes or 8h31m)
+    case expirationReminder(offset: TimeInterval, absAlertTime: TimeInterval, duration: TimeInterval = 0, silent: Bool = false)
 
-    // 79 hour alarm (1 hour before shutdown)
-    case shutdownImminent(TimeInterval)
+    // slot4LowReservoir: reservoir below configured value alert
+    case lowReservoir(units: Double, silent: Bool = false)
 
-    // reservoir below configured value alert
-    case lowReservoir(Double)
+    // slot5SuspendedReminder: pod suspended reminder, before suspendTime;
+    // short beep every 15 minutes if > 30 min, else short beep every 5 minutes
+    case podSuspendedReminder(active: Bool, offset: TimeInterval, suspendTime: TimeInterval, timePassed: TimeInterval = 0, silent: Bool = false)
 
-    // auto-off timer; requires user input every x minutes
-    case autoOff(active: Bool, countdownDuration: TimeInterval)
+    // slot6SuspendTimeExpired: pod suspend time expired alarm, after suspendTime;
+    // 2 sets of beeps every minute for 3 minutes repeated every 15 minutes (PDM & pre-SwiftUI implementations)
+    // 2 sets of beeps every 15 minutes (for SwiftUI PumpManagerAlerts implementations)
+    case suspendTimeExpired(offset: TimeInterval, suspendTime: TimeInterval, silent: Bool = false)
 
-    // pod suspended reminder, before suspendTime; short beep every 15 minutes if > 30 min, else every 5 minutes
-    case podSuspendedReminder(active: Bool, suspendTime: TimeInterval)
+    // slot7Expired: 2 hours long, time for user to start pairing process
+    case waitingForPairingReminder
 
-    // pod suspend time expired alarm, after suspendTime; 2 sets of beeps every min for 3 minutes repeated every 15 minutes
-    case suspendTimeExpired(suspendTime: TimeInterval)
+    // slot7Expired: 1 hour long, time for user to finish priming, cannula insertion
+    case finishSetupReminder
+
+    // slot7Expired: 72 hour alarm
+    case expired(offset: TimeInterval, absAlertTime: TimeInterval, duration: TimeInterval, silent: Bool = false)
 
     public var description: String {
         var alertName: String
         switch self {
-        case .waitingForPairingReminder:
-            return LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder")
-        case .finishSetupReminder:
-            return LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder")
-        case .expirationReminder:
-            alertName = LocalizedString("Expiration alert", comment: "Description for expiration alert")
-        case .expired:
-            alertName = LocalizedString("Expiration advisory", comment: "Description for expiration advisory")
-        case .shutdownImminent:
-            alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent")
-        case .lowReservoir(let units):
-            alertName = String(format: LocalizedString("Low reservoir advisory (%1$gU)", comment: "Format string for description for low reservoir advisory (1: reminder units)"), units)
+        // slot0AutoOff
         case .autoOff:
-            alertName = LocalizedString("Auto-off", comment: "Description for auto-off")
+            alertName = LocalizedString("Auto-off", comment: "Description for auto-off alert")
+        // slot1NotUsed
+        case .notUsed:
+            alertName = LocalizedString("Not used", comment: "Description for not used slot alert")
+        // slot2ShutdownImminent
+        case .shutdownImminent:
+            alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent alert")
+        // slot3ExpirationReminder
+        case .expirationReminder:
+            alertName = LocalizedString("Expiration reminder", comment: "Description for expiration reminder alert")
+        // slot4LowReservoir
+        case .lowReservoir:
+            alertName = LocalizedString("Low reservoir", comment: "Format string for description for low reservoir alert")
+        // slot5SuspendedReminder
         case .podSuspendedReminder:
-            alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder")
+            alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder alert")
+        // slot6SuspendTimeExpired
         case .suspendTimeExpired:
-            alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired")
+            alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired alert")
+        // slot7Expired
+        case .waitingForPairingReminder:
+            alertName = LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder alert")
+        case .finishSetupReminder:
+            alertName = LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder alert")
+        case .expired:
+            alertName = LocalizedString("Pod expired", comment: "Description for pod expired alert")
         }
         if self.configuration.active == false {
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
@@ -118,71 +174,126 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
     public var configuration: AlertConfiguration {
         switch self {
-        case .waitingForPairingReminder:
-            return AlertConfiguration(alertType: .slot7, duration: .minutes(110), trigger: .timeUntilAlert(.minutes(10)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .finishSetupReminder:
-            return AlertConfiguration(alertType: .slot7, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .expirationReminder(let alertTime):
-            let active = alertTime != 0 // disable if alertTime is 0
-            return AlertConfiguration(alertType: .slot3, active: active, duration: 0, trigger: .timeUntilAlert(alertTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .expired(let alarmTime, let duration):
-            let active = alarmTime != 0 // disable if alarmTime is 0
-            return AlertConfiguration(alertType: .slot7, active: active, duration: duration, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .shutdownImminent(let alarmTime):
-            let active = alarmTime != 0 // disable if alarmTime is 0
-            return AlertConfiguration(alertType: .slot2, active: active, duration: 0, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .lowReservoir(let units):
+        // slot0AutoOff
+        case .autoOff(let active, _, let countdownDuration, let silent):
+            return AlertConfiguration(alertType: .slot0AutoOff, active: active, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent, autoOffModifier: true)
+
+        // slot1NotUsed
+        case .notUsed:
+            return AlertConfiguration(alertType: .slot1NotUsed, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .noBeepNonCancel)
+
+        // slot2ShutdownImminent
+        case .shutdownImminent(let offset, let absAlertTime, let silent):
+            let active = absAlertTime != 0 // disable if absAlertTime is 0
+            let triggerTime: TimeInterval
+            if active {
+                triggerTime = absAlertTime - offset
+            } else {
+                triggerTime = 0
+            }
+            return AlertConfiguration(alertType: .slot2ShutdownImminent, active: active, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
+
+        // slot3ExpirationReminder
+        case .expirationReminder(let offset, let absAlertTime, let duration, let silent):
+            let active = absAlertTime != 0 // disable if absAlertTime is 0
+            let triggerTime: TimeInterval
+            if active {
+                triggerTime = absAlertTime - offset
+            } else {
+                triggerTime = 0
+            }
+            return AlertConfiguration(alertType: .slot3ExpirationReminder, active: active, duration: duration, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
+
+        // slot4LowReservoir
+        case .lowReservoir(let units, let silent):
             let active = units != 0 // disable if units is 0
-            return AlertConfiguration(alertType: .slot4, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .autoOff(let active, let countdownDuration):
-            return AlertConfiguration(alertType: .slot0, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .podSuspendedReminder(let active, let suspendTime):
-            // A suspendTime of 0 is an untimed suspend
+            return AlertConfiguration(alertType: .slot4LowReservoir, active: active, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
+
+        // slot5SuspendedReminder
+        // A suspendTime of 0 is an untimed suspend
+        // timePassed will be > 0 for an existing pod suspended reminder changing its silent state
+        case .podSuspendedReminder(let active, _, let suspendTime, let timePassed, let silent):
             let reminderInterval, duration: TimeInterval
-            let trigger: AlertTrigger
-            let beepRepeat: BeepRepeat
+            var beepRepeat: BeepRepeat
             let beepType: BeepType
-            if active {
-                if suspendTime >= TimeInterval(minutes :30) {
-                    // Use 15-minute pod suspended reminder beeps for longer scheduled suspend times as per PDM.
-                    reminderInterval = TimeInterval(minutes: 15)
-                    beepRepeat = .every15Minutes
-                } else {
-                    // Use 5-minute pod suspended reminder beeps for shorter scheduled suspend times.
-                    reminderInterval = TimeInterval(minutes: 5)
-                    beepRepeat = .every5Minutes
-                }
+            let trigger: AlertTrigger
+            var isActive: Bool = active
+
+            if suspendTime == 0 || suspendTime >= TimeInterval(minutes: 30) {
+                // Use 15-minute pod suspended reminder beeps for untimed or longer scheduled suspend times.
+                reminderInterval = TimeInterval(minutes: 15)
+                beepRepeat = .every15Minutes
+            } else {
+                // Use 5-minute pod suspended reminder beeps for shorter scheduled suspend times.
+                reminderInterval = TimeInterval(minutes: 5)
+                beepRepeat = .every5Minutes
+            }
+
+            // Make alert inactive if there isn't enough remaining in suspend time for a reminder beep.
+            let suspendTimeRemaining = suspendTime - timePassed
+            if suspendTime != 0 && suspendTimeRemaining <= reminderInterval {
+                isActive = false
+            }
+
+            if isActive {
+                // Compute the alert trigger time as the interval until the next upcoming reminder interval
+                let triggerTime: TimeInterval = .seconds(reminderInterval - Double((Int(timePassed) % Int(reminderInterval))))
+
                 if suspendTime == 0 {
                     duration = 0 // Untimed suspend, no duration
-                } else if suspendTime > reminderInterval {
-                    duration = suspendTime - reminderInterval // End after suspendTime total time
                 } else {
-                    duration = .minutes(1) // Degenerate case, end ASAP
+                    // duration is from triggerTime to suspend time remaining
+                    duration = suspendTimeRemaining - triggerTime
                 }
-                trigger = .timeUntilAlert(reminderInterval) // Start after reminderInterval has passed
+                trigger = .timeUntilAlert(triggerTime) // time to next reminder interval with the suspend time
                 beepType = .beep
             } else {
+                beepRepeat = .once
                 duration = 0
                 trigger = .timeUntilAlert(.minutes(0))
-                beepRepeat = .once
                 beepType = .noBeepCancel
             }
-            return AlertConfiguration(alertType: .slot5, active: active, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType)
-        case .suspendTimeExpired(let suspendTime):
+            return AlertConfiguration(alertType: .slot5SuspendedReminder, active: isActive, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType, silent: silent)
+
+        // slot6SuspendTimeExpired
+        case .suspendTimeExpired(_, let suspendTime, let silent):
             let active = suspendTime != 0 // disable if suspendTime is 0
             let trigger: AlertTrigger
             let beepRepeat: BeepRepeat
             let beepType: BeepType
             if active {
                 trigger = .timeUntilAlert(suspendTime)
-                beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes
+                beepRepeat = suspendTimeExpiredBeepRepeat
                 beepType = .bipBeepBipBeepBipBeepBipBeep
             } else {
                 trigger = .timeUntilAlert(.minutes(0))
                 beepRepeat = .once
                 beepType = .noBeepCancel
             }
-            return AlertConfiguration(alertType: .slot6, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType)
+            return AlertConfiguration(alertType: .slot6SuspendTimeExpired, active: active, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType, silent: silent)
+
+        // slot7Expired
+        case .waitingForPairingReminder:
+            // After pod is powered up, beep every 10 minutes for up to 2 hours before pairing before failing
+            let totalDuration: TimeInterval = .hours(2)
+            let startOffset: TimeInterval = .minutes(10)
+            return AlertConfiguration(alertType: .slot7Expired, duration: totalDuration - startOffset, trigger: .timeUntilAlert(startOffset), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
+        case .finishSetupReminder:
+            // After pod is paired, beep every 5 minutes for up to 1 hour for pod setup to complete before failing
+            let totalDuration: TimeInterval = .hours(1)
+            let startOffset: TimeInterval = .minutes(5)
+            return AlertConfiguration(alertType: .slot7Expired, duration: totalDuration - startOffset, trigger: .timeUntilAlert(startOffset), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
+        case .expired(let offset, let absAlertTime, let duration, let silent):
+            // Normally used to alert at Pod.nominalPodLife (72 hours) for Pod.expirationAdvisoryWindow (7 hours)
+            // 2 sets of beeps repeating every 60 minutes
+            let active = absAlertTime != 0 // disable if absAlertTime is 0
+            let triggerTime: TimeInterval
+            if active {
+                triggerTime = absAlertTime - offset
+            } else {
+                triggerTime = .minutes(0)
+            }
+            return AlertConfiguration(alertType: .slot7Expired, active: active, duration: duration, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
         }
     }
 
@@ -195,51 +306,92 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
         }
 
         switch name {
-        case "waitingForPairingReminder":
-            self = .waitingForPairingReminder
-        case "finishSetupReminder":
-            self = .finishSetupReminder
-        case "expirationReminder":
-            guard let alertTime = rawValue["alertTime"] as? Double else {
-                return nil
-            }
-            self = .expirationReminder(TimeInterval(alertTime))
-        case "expired":
-            guard let alarmTime = rawValue["alarmTime"] as? Double,
-                let duration = rawValue["duration"] as? Double else
+        case "autoOff":
+            guard let active = rawValue["active"] as? Bool,
+                let countdownDuration = rawValue["countdownDuration"] as? TimeInterval else
             {
                 return nil
             }
-            self = .expired(alertTime: TimeInterval(alarmTime), duration: TimeInterval(duration))
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .autoOff(active: active, offset: offset, countdownDuration: countdownDuration, silent: silent)
         case "shutdownImminent":
-            guard let alarmTime = rawValue["alarmTime"] as? Double else {
+            guard let alarmTime = rawValue["alarmTime"] as? TimeInterval else {
                 return nil
             }
-            self = .shutdownImminent(alarmTime)
-        case "lowReservoir":
-            guard let units = rawValue["units"] as? Double else {
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let offsetToUse, absAlertTime: TimeInterval
+            if offset == 0 {
+                // use default values as no offset value was found
+                absAlertTime = defaultShutdownImminentTime
+                offsetToUse = absAlertTime - alarmTime
+            } else {
+                absAlertTime = offset + alarmTime
+                offsetToUse = offset
+            }
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .shutdownImminent(offset: offsetToUse, absAlertTime: absAlertTime, silent: silent)
+        case "expirationReminder":
+            guard let alertTime = rawValue["alertTime"] as? TimeInterval else {
                 return nil
             }
-            self = .lowReservoir(units)
-        case "autoOff":
-            guard let active = rawValue["active"] as? Bool,
-                let countdownDuration = rawValue["countdownDuration"] as? Double else
-            {
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let offsetToUse, absAlertTime: TimeInterval
+            if offset == 0 {
+                // use default values as no offset value was found
+                absAlertTime = defaultExpirationReminderTime
+                offsetToUse = absAlertTime - alertTime
+            } else {
+                absAlertTime = offset + alertTime
+                offsetToUse = offset
+            }
+            let duration = rawValue["duration"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .expirationReminder(offset: offsetToUse, absAlertTime: absAlertTime, duration: duration,  silent: silent)
+        case "lowReservoir":
+            guard let units = rawValue["units"] as? Double else {
                 return nil
             }
-            self = .autoOff(active: active, countdownDuration: TimeInterval(countdownDuration))
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .lowReservoir(units: units, silent: silent)
         case "podSuspendedReminder":
             guard let active = rawValue["active"] as? Bool,
-                let suspendTime = rawValue["suspendTime"] as? Double else
+                let suspendTime = rawValue["suspendTime"] as? TimeInterval else
             {
                 return nil
             }
-            self = .podSuspendedReminder(active: active, suspendTime: suspendTime)
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .podSuspendedReminder(active: active, offset: offset, suspendTime: suspendTime, silent: silent)
         case "suspendTimeExpired":
             guard let suspendTime = rawValue["suspendTime"] as? Double else {
                 return nil
             }
-            self = .suspendTimeExpired(suspendTime: suspendTime)
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .suspendTimeExpired(offset: offset, suspendTime: suspendTime, silent: silent)
+        case "waitingForPairingReminder":
+            self = .waitingForPairingReminder
+        case "finishSetupReminder":
+            self = .finishSetupReminder
+        case "expired":
+            guard let alarmTime = rawValue["alarmTime"] as? TimeInterval,
+                let duration = rawValue["duration"] as? TimeInterval else
+            {
+                return nil
+            }
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let offsetToUse, absAlertTime: TimeInterval
+            if offset == 0 {
+                // use default values as no offset value was found
+                absAlertTime = defaultExpiredTime
+                offsetToUse = absAlertTime - alarmTime
+            } else {
+                absAlertTime = offset + alarmTime
+                offsetToUse = offset
+            }
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .expired(offset: offsetToUse, absAlertTime: absAlertTime, duration: duration, silent: silent)
         default:
             return nil
         }
@@ -249,50 +401,65 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
         let name: String = {
             switch self {
-            case .waitingForPairingReminder:
-                return "waitingForPairingReminder"
-            case .finishSetupReminder:
-                return "finishSetupReminder"
-            case .expirationReminder:
-                return "expirationReminder"
-            case .expired:
-                return "expired"
+            case .autoOff:
+                return "autoOff"
+            case .notUsed:
+                return "notUsed"
             case .shutdownImminent:
                 return "shutdownImminent"
+            case .expirationReminder:
+                return "expirationReminder"
             case .lowReservoir:
                 return "lowReservoir"
-            case .autoOff:
-                return "autoOff"
             case .podSuspendedReminder:
                 return "podSuspendedReminder"
             case .suspendTimeExpired:
                 return "suspendTimeExpired"
+            case .waitingForPairingReminder:
+                return "waitingForPairingReminder"
+            case .finishSetupReminder:
+                return "finishSetupReminder"
+            case .expired:
+                return "expired"
             }
         }()
 
-
         var rawValue: RawValue = [
             "name": name,
         ]
 
         switch self {
-        case .expirationReminder(let alertTime):
-            rawValue["alertTime"] = alertTime
-        case .expired(let alarmTime, let duration):
-            rawValue["alarmTime"] = alarmTime
-            rawValue["duration"] = duration
-        case .shutdownImminent(let alarmTime):
-            rawValue["alarmTime"] = alarmTime
-        case .lowReservoir(let units):
-            rawValue["units"] = units
-        case .autoOff(let active, let countdownDuration):
+        case .autoOff(let active, let offset, let countdownDuration, let silent):
             rawValue["active"] = active
+            rawValue["offset"] = offset
             rawValue["countdownDuration"] = countdownDuration
-        case .podSuspendedReminder(let active, let suspendTime):
+            rawValue["silent"] = silent
+        case .shutdownImminent(let offset, let absAlertTime, let silent):
+            rawValue["offset"] = offset
+            rawValue["alarmTime"] = absAlertTime - offset
+            rawValue["silent"] = silent
+        case .expirationReminder(let offset, let absAlertTime, let duration, let silent):
+            rawValue["offset"] = offset
+            rawValue["alertTime"] = absAlertTime - offset
+            rawValue["duration"] = duration
+            rawValue["silent"] = silent
+        case .lowReservoir(let units, let silent):
+            rawValue["units"] = units
+            rawValue["silent"] = silent
+        case .podSuspendedReminder(let active, let offset, let suspendTime, _, let silent):
             rawValue["active"] = active
+            rawValue["offset"] = offset
             rawValue["suspendTime"] = suspendTime
-        case .suspendTimeExpired(let suspendTime):
+            rawValue["silent"] = silent
+        case .suspendTimeExpired(let offset, let suspendTime, let silent):
+            rawValue["offset"] = offset
             rawValue["suspendTime"] = suspendTime
+            rawValue["silent"] = silent
+        case .expired(let offset, let absAlertTime, let duration, let silent):
+            rawValue["offset"] = offset
+            rawValue["alarmTime"] = absAlertTime - offset
+            rawValue["duration"] = duration
+            rawValue["silent"] = silent
         default:
             break
         }
@@ -302,42 +469,42 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 }
 
 public enum AlertSlot: UInt8 {
-    case slot0 = 0x00
-    case slot1 = 0x01
-    case slot2 = 0x02
-    case slot3 = 0x03
-    case slot4 = 0x04
-    case slot5 = 0x05
-    case slot6 = 0x06
-    case slot7 = 0x07
+    case slot0AutoOff = 0x00
+    case slot1NotUsed = 0x01
+    case slot2ShutdownImminent = 0x02
+    case slot3ExpirationReminder = 0x03
+    case slot4LowReservoir = 0x04
+    case slot5SuspendedReminder = 0x05
+    case slot6SuspendTimeExpired = 0x06
+    case slot7Expired = 0x07
 
     public var bitMaskValue: UInt8 {
         return 1<<rawValue
     }
 
     public typealias AllCases = [AlertSlot]
-    
+
     static var allCases: AllCases {
         return (0..<8).map { AlertSlot(rawValue: $0)! }
     }
 }
 
 public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, Equatable {
-    
+
     public typealias RawValue = UInt8
     public typealias Index = Int
-    
+
     public let startIndex: Int
     public let endIndex: Int
-    
+
     private let elements: [AlertSlot]
-    
+
     public static let none = AlertSet(rawValue: 0)
-    
+
     public var rawValue: UInt8 {
         return elements.reduce(0) { $0 | $1.bitMaskValue }
     }
-    
+
     public init(slots: [AlertSlot]) {
         self.elements = slots
         self.startIndex = 0
@@ -351,11 +518,11 @@ public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, E
     public subscript(index: Index) -> AlertSlot {
         return elements[index]
     }
-    
+
     public func index(after i: Int) -> Int {
         return i+1
     }
-    
+
     public var description: String {
         if elements.count == 0 {
             return LocalizedString("No alerts", comment: "Pod alert state when no alerts are active")
@@ -374,9 +541,179 @@ public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, E
 
 // Returns true if there are any active suspend related alerts
 public func hasActiveSuspendAlert(configuredAlerts: [AlertSlot : PodAlert]) -> Bool {
-    // slot5 is for podSuspendedReminder and slot6 is for suspendTimeExpired
-    if configuredAlerts.contains(where: { ($0.key == .slot5 || $0.key == .slot6) && $0.value.configuration.active }) {
+    if configuredAlerts.contains(where: { ($0.key == .slot5SuspendedReminder || $0.key == .slot6SuspendTimeExpired) && $0.value.configuration.active })
+    {
         return true
     }
     return false
 }
+
+// Returns a descriptive string for all the alerts in alertSet
+public func alertSetString(alertSet: AlertSet) -> String {
+
+    if alertSet.isEmpty {
+        // Don't bother displaying any additional info for an inactive alert
+        return String(describing: alertSet)
+    }
+
+    let alertDescription = alertSet.map { (slot) -> String in
+        switch slot {
+        case .slot0AutoOff:
+            return PodAlert.autoOff(active: true, offset: 0, countdownDuration: 0).description
+        case .slot1NotUsed:
+            return PodAlert.notUsed.description
+        case .slot2ShutdownImminent:
+            return PodAlert.shutdownImminent(offset: 0, absAlertTime: defaultShutdownImminentTime).description
+        case .slot3ExpirationReminder:
+            return PodAlert.expirationReminder(offset: 0, absAlertTime: defaultExpirationReminderTime).description
+        case .slot4LowReservoir:
+            return PodAlert.lowReservoir(units: Pod.maximumReservoirReading).description
+        case .slot5SuspendedReminder:
+            return PodAlert.podSuspendedReminder(active: true, offset: 0, suspendTime: .minutes(30)).description
+        case .slot6SuspendTimeExpired:
+            return PodAlert.suspendTimeExpired(offset: 0, suspendTime: .minutes(30)).description
+        case .slot7Expired:
+            return PodAlert.expired(offset: 0, absAlertTime: defaultExpiredTime, duration: Pod.expirationAdvisoryWindow).description
+        }
+    }
+
+    return alertDescription.joined(separator: ", ")
+}
+
+func configuredAlertsString(configuredAlerts: [AlertSlot : PodAlert]) -> String {
+
+    if configuredAlerts.isEmpty {
+        return String(describing: configuredAlerts)
+    }
+
+    let configuredAlertString = configuredAlerts.map { (configuredAlert) -> String in
+
+        let podAlert = configuredAlert.value
+        let description = podAlert.description
+        guard podAlert.configuration.active else {
+            return description
+        }
+
+        switch podAlert {
+        case .shutdownImminent(_, let absAlertTime, _):
+            return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr)
+        case .expirationReminder(_, let absAlertTime, _, _):
+            return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr)
+        case .lowReservoir(let unitTrigger, _):
+            return String(format: "%@ @ %dU", description, Int(unitTrigger))
+        case .podSuspendedReminder(_, let offset, let suspendTime, _, _):
+            return String(format: "%@ ending @ %@ after %@", description, (offset + suspendTime).timeIntervalStr, suspendTime.timeIntervalStr)
+        case .suspendTimeExpired(let offset, let suspendTime, _):
+            return String(format: "%@ @ %@ after %@", description, (offset + suspendTime).timeIntervalStr, suspendTime.timeIntervalStr)
+        case .expired(_, let absAlertTime, _, _):
+            return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr)
+        default:
+            return ""
+        }
+    }
+
+    return configuredAlertString.joined(separator: ", ")
+}
+
+// Returns an array of appropriate PodAlerts with the specified silent value
+// for all the configuredAlerts given all the current pod conditions.
+func regeneratePodAlerts(silent: Bool, configuredAlerts: [AlertSlot: PodAlert], activeAlertSlots: AlertSet, currentPodTime: TimeInterval, currentReservoirLevel: Double) -> [PodAlert] {
+    var podAlerts: [PodAlert] = []
+
+    for alert in configuredAlerts {
+        // Just skip this alert if not previously active
+        guard alert.value.configuration.active else {
+            continue
+        }
+
+        // Map alerts to corresponding appropriate new ones at the current pod time using the specified silent value.
+        switch alert.value {
+
+        case .shutdownImminent(let offset, let alertTime, _):
+            // alertTime is absolute when offset is non-zero, otherwise use  default value
+            var absAlertTime = offset != 0 ? alertTime : defaultShutdownImminentTime
+            if currentPodTime >= absAlertTime {
+                // alert trigger is not in the future, make inactive using a 0 value
+                absAlertTime = 0
+            }
+            // create new shutdownImminent podAlert using the current timeActive and the original absolute alert time
+            podAlerts.append(PodAlert.shutdownImminent(offset: currentPodTime, absAlertTime: absAlertTime, silent: silent))
+
+        case .expirationReminder(let offset, let alertTime, let alertDuration, _):
+            let duration: TimeInterval
+
+            // alertTime is absolute when offset is non-zero, otherwise use default value
+            var absAlertTime = offset != 0 ? alertTime : defaultExpirationReminderTime
+            if currentPodTime >= absAlertTime {
+                // alert trigger is not in the future, make inactive using a 0 value
+                absAlertTime = 0
+                duration = 0
+            } else {
+                duration = alertDuration
+            }
+            // create new expirationReminder podAlert using the current active time and the original absolute alert time and duration
+            podAlerts.append(PodAlert.expirationReminder(offset: currentPodTime, absAlertTime: absAlertTime, duration: duration, silent: silent))
+
+        case .lowReservoir(let unitTrigger, _):
+            let units: Double
+            if currentReservoirLevel > unitTrigger {
+                units = unitTrigger
+            } else {
+                // reservoir is no longer more than the unitTrigger, make inactive using a 0 value
+                units = 0
+            }
+            podAlerts.append(PodAlert.lowReservoir(units: units, silent: silent))
+
+        case .podSuspendedReminder(let active, let offset, let suspendTime, _, _):
+            let timePassed: TimeInterval = min(currentPodTime - offset, .hours(2))
+            // Pass along the computed time passed since alert was originally set so creation routine can
+            // do all the grunt work dealing with varying reminder intervals and time passing scenarios.
+            podAlerts.append(PodAlert.podSuspendedReminder(active: active, offset: offset, suspendTime: suspendTime, timePassed: timePassed, silent: silent))
+
+        case .suspendTimeExpired(let lastOffset, let lastSuspendTime, _):
+            let absAlertTime = lastOffset + lastSuspendTime
+            let suspendTime: TimeInterval
+            if currentPodTime >= absAlertTime {
+                // alert trigger is no longer in the future
+                if activeAlertSlots.contains(where: { $0 == .slot6SuspendTimeExpired } ) {
+                    // The suspendTimeExpired alert has yet been acknowledged,
+                    // set up a suspendTimeExpired alert for the next 15m interval.
+                    // Compute a new suspendTime that is a multiple of 15 minutes
+                    // from lastOffset which is at least one minute in the future.
+                    let newOffsetSuspendTime = ceil((currentPodTime - lastOffset) / .minutes(15)) * .minutes(15)
+                    let newAbsAlertTime = lastOffset + newOffsetSuspendTime
+                    suspendTime = max(newAbsAlertTime - currentPodTime, .minutes(1))
+                } else {
+                    // The suspendTimeExpired alert was already been acknowledged,
+                    // so now make this alert inactive by using a 0 suspendTime.
+                    suspendTime = 0
+                }
+            } else {
+                // recompute a new suspendTime based on the current pod time
+                suspendTime = absAlertTime - currentPodTime
+                print("setting new suspendTimeExpired suspendTime of \(suspendTime) with currentPodTime\(currentPodTime) and absAlertTime=\(absAlertTime)")
+            }
+            // create a new suspendTimeExpired PodAlert using the current active time and the computed suspendTime (if any)
+            podAlerts.append(PodAlert.suspendTimeExpired(offset: currentPodTime, suspendTime: suspendTime, silent: silent))
+
+        case .expired(let offset, let alertTime, let alertDuration, _):
+            let duration: TimeInterval
+
+            // alertTime is absolute when offset is non-zero, otherwise use default value
+            var absAlertTime = offset != 0 ? alertTime : defaultExpiredTime
+            if currentPodTime >= absAlertTime {
+                // alert trigger is not in the future, make inactive using a 0 value
+                absAlertTime = 0
+                duration = 0
+            } else {
+                duration = alertDuration
+            }
+            // create new expired podAlert using the current active time and the original absolute alert time and duration
+            podAlerts.append(PodAlert.expired(offset: currentPodTime, absAlertTime: absAlertTime, duration: duration, silent: silent))
+
+        default:
+            break
+        }
+    }
+    return podAlerts
+}

+ 7 - 5
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BasalDeliveryTable.swift

@@ -164,8 +164,9 @@ public struct RateEntry {
             // Eros zero TB is the only case not using pulses
             return 0
         } else {
-            // Use delayBetweenPulses to compute rate, works for non-Eros near zero rates
-            return (.hours(1) / delayBetweenPulses) / Pod.pulsesPerUnit
+            // Use delayBetweenPulses to compute rate which will also work for non-Eros near zero rates.
+            // Round the rate calculation to a two digit value to avoid slightly off values for some cases.
+            return round(((.hours(1) / delayBetweenPulses) / Pod.pulsesPerUnit) * 100) / 100.0
         }
     }
     
@@ -174,8 +175,9 @@ public struct RateEntry {
             // Eros zero TB case uses fixed 30 minute rate entries
             return TimeInterval(minutes: 30)
         } else {
-            // Use delayBetweenPulses to compute duration, works for non-Eros near zero rates
-            return delayBetweenPulses * totalPulses
+            // Use delayBetweenPulses to compute duration which will also work for non-Eros near zero rates.
+            // Round to nearest second to not be slightly off of the 30 minute rate entry boundary for some cases.
+            return round(delayBetweenPulses * totalPulses)
         }
     }
     
@@ -231,6 +233,6 @@ public struct RateEntry {
 
 extension RateEntry: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "RateEntry(rate:\(rate) duration:\(duration.stringValue))"
+        return "RateEntry(rate:\(rate), duration:\(duration.timeIntervalStr))"
     }
 }

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BasalSchedule.swift

@@ -25,7 +25,7 @@ public struct BasalScheduleEntry: RawRepresentable, Equatable {
         self.rate = rrate
         self.startTime = startTime
     }
-    
+
     // MARK: - RawRepresentable
     public init?(rawValue: RawValue) {
         
@@ -61,13 +61,13 @@ public struct BasalSchedule: RawRepresentable, Equatable {
         let (_, entry, _) = lookup(offset: offset)
         return entry.rate
     }
-    
+
     // Only valid for fixed offset timezones
     public func currentRate(using calendar: Calendar, at date: Date = Date()) -> Double {
         let midnight = calendar.startOfDay(for: date)
         return rateAt(offset: date.timeIntervalSince(midnight))
     }
-    
+
     // Returns index, entry, and time remaining
     func lookup(offset: TimeInterval) -> (Int, BasalScheduleEntry, TimeInterval) {
         guard offset >= 0 && offset < .hours(24) else {

+ 2 - 2
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BeepPreference.swift

@@ -29,9 +29,9 @@ public enum BeepPreference: Int, CaseIterable {
         case .silent:
             return LocalizedString("No confidence reminders are used.", comment: "Description for BeepPreference.silent")
         case .manualCommands:
-            return LocalizedString("Confidence reminders will sound for commands you initiate, like bolus, cancel bolus, suspend, resume, save notification reminders, etc. When Loop automatically adjusts delivery, no confidence reminders are used.", comment: "Description for BeepPreference.manualCommands")
+            return LocalizedString("Confidence reminders will sound for commands you initiate, like bolus, cancel bolus, suspend, resume, save notification reminders, etc. When the app automatically adjusts delivery, no confidence reminders are used.", comment: "Description for BeepPreference.manualCommands")
         case .extended:
-            return LocalizedString("Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.", comment: "Description for BeepPreference.extended")
+            return LocalizedString("Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.", comment: "Description for BeepPreference.extended")
         }
     }
 

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/CRC16.swift

@@ -4,7 +4,7 @@
 //
 //  From OmniKit/MessageTransport/CRC16.swift
 //  Created by Pete Schwamb on 10/14/17.
-//  Copyright © 2017 Pete Schwambs. All rights reserved.
+//  Copyright © 2017 Pete Schwamb. All rights reserved.
 //
 
 import Foundation

+ 309 - 296
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/FaultEventCode.swift

@@ -26,6 +26,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case invalidBeepRepeatPattern             = 0x09
         case bf0notEqualToBF1                     = 0x0A
         case tableCorruptionTempBasalSubcommand   = 0x0B
+
         case resetDueToCOP                        = 0x0D
         case resetDueToIllegalOpcode              = 0x0E
         case resetDueToIllegalAddress             = 0x0F
@@ -76,6 +77,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case testInProgress                       = 0x3C
         case problemWithPumpAnchor                = 0x3D
         case errorFlashWrite                      = 0x3E
+
         case encoderCountTooHigh                  = 0x40
         case encoderCountExcessiveVariance        = 0x41
         case encoderCountTooLow                   = 0x42
@@ -90,6 +92,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case trimICSTooCloseTo0x1FF               = 0x4B
         case problemFindingBestTrimValue          = 0x4C
         case badSetTPM1MultiCasesValue            = 0x4D
+        case sawTrimError                         = 0x4E
         case unexpectedRFErrorFlagDuringReset     = 0x4F
         case timerPulseWidthModulatorOverflow     = 0x50
         case tickcntError                         = 0x51
@@ -110,11 +113,13 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case occlusionCheckStartup1               = 0x60
         case occlusionCheckStartup2               = 0x61
         case occlusionCheckTimeouts1              = 0x62
+
         case occlusionCheckTimeouts2              = 0x66
         case occlusionCheckTimeouts3              = 0x67
         case occlusionCheckPulseIssue             = 0x68
         case occlusionCheckBolusProblem           = 0x69
         case occlusionCheckAboveThreshold         = 0x6A
+
         case basalUnderInfusion                   = 0x80
         case basalOverInfusion                    = 0x81
         case tempBasalUnderInfusion               = 0x82
@@ -138,9 +143,11 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case illegalInterLockChan                 = 0x95
         case badStateInClearBolusIST2AndVars      = 0x96
         case badStateInMaybeInc33D                = 0x97
+
         case bleTimeout                           = 0xA0
         case bleInitiated                         = 0xA1
         case bleUnkAlarm                          = 0xA2
+
         case bleIaas                              = 0xA6
         case crcFailure                           = 0xA8
         case bleWdPingTimeout                     = 0xA9
@@ -148,6 +155,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case bleNakError                          = 0xAB
         case bleReqHighTimeout                    = 0xAC
         case bleUnknownResp                       = 0xAD
+
         case bleReqStuckHigh                      = 0xAF
         case bleStateMachine1                     = 0xB1
         case bleStateMachine2                     = 0xB2
@@ -155,7 +163,17 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case bleEr48DualNack                      = 0xC0
         case bleQnExceedMaxRetry                  = 0xC1
         case bleQnCritVarFail                     = 0xC2
-        case valuesDoNotMatchOrAreGreaterThan0xC2 = 0xC3
+
+        case unknown0xCB                          = 0xCB
+
+        case unknown0xD4                          = 0xD4
+        case unknown0xD5                          = 0xD5
+        case resetFault0xD6                       = 0xD6
+        case resetFault0xD7                       = 0xD7
+        case unknown0xD8                          = 0xD8
+        case unknown0xD9                          = 0xD9
+
+        case valuesDoNotMatch                     = 0xFF
     }
 
     public var faultType: FaultEventType? {
@@ -165,302 +183,297 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
     init(rawValue: UInt8) {
         self.rawValue = rawValue
     }
-    
-    public var description: String {
-        let faultDescription: String
-        
-        if let faultType = faultType {
-            faultDescription = {
-                switch faultType {
-                case .noFaults:
-                    return "No fault"
-                case .failedFlashErase:
-                    return "Flash erase failed"
-                case .failedFlashStore:
-                    return "Flash store failed"
-                case .tableCorruptionBasalSubcommand:
-                    return "Basal subcommand table corruption"
-                case .basalPulseTableCorruption:
-                    return "Basal pulse table corruption"
-                case .corruptionByte720:
-                    return "Corruption in byte_720"
-                case .dataCorruptionInTestRTCInterrupt:
-                    return "Data corruption error in test_RTC_interrupt"
-                case .rtcInterruptHandlerInconsistentState:
-                    return "RTC interrupt handler called with inconstent state"
-                case .valueGreaterThan8:
-                    return "Value > 8"
-                case .invalidBeepRepeatPattern:
-                    return "Invalid beep repeat pattern"
-                case .bf0notEqualToBF1:
-                    return "Corruption in byte_BF0"
-                case .tableCorruptionTempBasalSubcommand:
-                    return "Temp basal subcommand table corruption"
-                case .resetDueToCOP:
-                    return "Reset due to COP"
-                case .resetDueToIllegalOpcode:
-                    return "Reset due to illegal opcode"
-                case .resetDueToIllegalAddress:
-                    return "Reset due to illegal address"
-                case .resetDueToSAWCOP:
-                    return "Reset due to SAWCOP"
-                case .corruptionInByte_866:
-                    return "Corruption in byte_866"
-                case .resetDueToLVD:
-                    return "Reset due to LVD"
-                case .messageLengthTooLong:
-                    return "Message length too long"
-                case .occluded:
-                    return "Occluded"
-                case .corruptionInWord129:
-                    return "Corruption in word_129 table/word_86A/dword_86E"
-                case .corruptionInByte868:
-                    return "Corruption in byte_868"
-                case .corruptionInAValidatedTable:
-                    return "Corruption in a validated table"
-                case .reservoirEmpty:
-                    return "Reservoir empty or exceeded maximum pulse delivery"
-                case .badPowerSwitchArrayValue1:
-                    return "Bad Power Switch Array Status and Control Register value 1 before starting pump"
-                case .badPowerSwitchArrayValue2:
-                    return "Bad Power Switch Array Status and Control Register value 2 before starting pump"
-                case .badLoadCnthValue:
-                    return "Bad LOADCNTH value when running pump"
-                case .exceededMaximumPodLife80Hrs:
-                    return "Exceeded maximum Pod life of 80 hours"
-                case .badStateCommand1AScheduleParse:
-                    return "Unexpected internal state in command_1A_schedule_parse_routine_wrapper"
-                case .unexpectedStateInRegisterUponReset:
-                    return "Unexpected commissioned state in status and control register upon reset"
-                case .wrongSummaryForTable129:
-                    return "Sum mismatch for word_129 table"
-                case .validateCountErrorWhenBolusing:
-                    return "Validate encoder count error when bolusing"
-                case .badTimerVariableState:
-                    return "Bad timer variable state"
-                case .unexpectedRTCModuleValueDuringReset:
-                    return "Unexpected RTC Modulo Register value during reset"
-                case .problemCalibrateTimer:
-                    return "Problem in calibrate_timer_case_3"
-                case .tickcntErrorRTC:
-                    return "Tick count error RTC"
-                case .tickFailure:
-                    return "Tick failure"
-                case .rtcInterruptHandlerUnexpectedCall:
-                    return "RTC interrupt handler unexpectedly called"
-                case .missing2hourAlertToFillTank:
-                    return "Failed to set up 2 hour alert for tank fill operation"
-                case .faultEventSetupPod:
-                    return "Bad arg or state in update_insulin_variables, verify_and_start_pump or main_loop_control_pump"
-                case .autoOff0:
-                    return "Alert #0 auto-off timeout"
-                case .autoOff1:
-                    return "Alert #1 auto-off timeout"
-                case .autoOff2:
-                    return "Alert #2 auto-off timeout"
-                case .autoOff3:
-                    return "Alert #3 auto-off timeout"
-                case .autoOff4:
-                    return "Alert #4 auto-off timeout"
-                case .autoOff5:
-                    return "Alert #5 auto-off timeout"
-                case .autoOff6:
-                    return "Alert #6 auto-off timeout"
-                case .autoOff7:
-                    return "Alert #7 auto-off timeout"
-                case .insulinDeliveryCommandError:
-                    return "Incorrect pod state for command or error during insulin command setup"
-                case .badValueStartupTest:
-                    return "Bad value during startup testing"
-                case .connectedPodCommandTimeout:
-                    return "Connected Pod command timeout"
-                case .resetFromUnknownCause:
-                    return "Reset from unknown cause"
-                case .vetoNotSet:
-                    return "Veto not set"
-                case .errorFlashInitialization:
-                    return "Flash initialization error"
-                case .badPiezoValue:
-                    return "Bad piezo value"
-                case .unexpectedValueByte358:
-                    return "Unexpected byte_358 value"
-                case .problemWithLoad1and2:
-                    return "Problem with LOAD1/LOAD2"
-                case .aGreaterThan7inMessage:
-                    return "A > 7 in message processing"
-                case .failedTestSawReset:
-                    return "SAW reset testing fail"
-                case .testInProgress:
-                    return "402D is 'Z' - test in progress"
-                case .problemWithPumpAnchor:
-                    return "Problem with pump anchor"
-                case .errorFlashWrite:
-                    return "Flash initialization or write error"
-                case .encoderCountTooHigh:
-                    return "Encoder count too high"
-                case .encoderCountExcessiveVariance:
-                    return "Encoder count excessive variance"
-                case .encoderCountTooLow:
-                    return "Encoder count too low"
-                case .encoderCountProblem:
-                    return "Encoder count problem"
-                case .checkVoltageOpenWire1:
-                    return "Check voltage open wire 1 problem"
-                case .checkVoltageOpenWire2:
-                    return "Check voltage open wire 2 problem"
-                case .problemWithLoad1and2type46:
-                    return "Problem with LOAD1/LOAD2"
-                case .problemWithLoad1and2type47:
-                    return "Problem with LOAD1/LOAD2"
-                case .badTimerCalibration:
-                    return "Bad timer calibration"
-                case .badTimerRatios:
-                    return "Bad timer values: COP timer ratio bad"
-                case .badTimerValues:
-                    return "Bad timer values"
-                case .trimICSTooCloseTo0x1FF:
-                    return "ICS trim too close to 0x1FF"
-                case .problemFindingBestTrimValue:
-                    return "find_best_trim_value problem"
-                case .badSetTPM1MultiCasesValue:
-                    return "Bad set_TPM1_multi_cases value"
-                case .unexpectedRFErrorFlagDuringReset:
-                    return "Unexpected TXSCR2 RF Tranmission Error Flag set during reset"
-                case .timerPulseWidthModulatorOverflow:
-                    return "Timer pulse-width modulator overflow"
-                case .tickcntError:
-                    return "Bad tick count state before starting pump"
-                case .badRfmXtalStart:
-                    return "TXOK issue in process_input_buffer"
-                case .badRxSensitivity:
-                    return "Bad Rx word_107 sensitivity value during input message processing"
-                case .packetFrameLengthTooLong:
-                    return "Packet frame length too long"
-                case .unexpectedIRQHighinTimerTick:
-                    return "Unexpected IRQ high in timer_tick"
-                case .unexpectedIRQLowinTimerTick:
-                    return "Unexpected IRQ low in timer_tick"
-                case .badArgToGetEntry:
-                    return "Corrupt constants table at byte_37A[] or flash byte_4036[]"
-                case .badArgToUpdate37ATable:
-                    return "Bad argument to update_37A_table"
-                case .errorUpdating37ATable:
-                    return "Error updating constants byte_37A table"
-                case .occlusionCheckValueTooHigh:
-                    return "Occlusion check value too high for detection"
-                case .loadTableCorruption:
-                    return "Load table corruption"
-                case .primeOpenCountTooLow:
-                    return "Prime open count too low"
-                case .badValueByte109:
-                    return "Bad byte_109 value"
-                case .disableFlashSecurityFailed:
-                    return "Write flash byte to disable flash security failed"
-                case .checkVoltageFailure:
-                    return "Two check voltage failures before starting pump"
-                case .occlusionCheckStartup1:
-                    return "Occlusion check startup problem 1"
-                case .occlusionCheckStartup2:
-                    return "Occlusion check startup problem 2"
-                case .occlusionCheckTimeouts1:
-                    return "Occlusion check excess timeouts 1"
-                case .occlusionCheckTimeouts2:
-                    return "Occlusion check excess timeouts 2"
-                case .occlusionCheckTimeouts3:
-                    return "Occlusion check excess timeouts 3"
-                case .occlusionCheckPulseIssue:
-                    return "Occlusion check pulse issue"
-                case .occlusionCheckBolusProblem:
-                    return "Occlusion check bolus problem"
-                case .occlusionCheckAboveThreshold:
-                    return "Occlusion check above threshold"
-                case .basalUnderInfusion:
-                    return "Basal under infusion"
-                case .basalOverInfusion:
-                    return "Basal over infusion"
-                case .tempBasalUnderInfusion:
-                    return "Temp basal under infusion"
-                case .tempBasalOverInfusion:
-                    return "Temp basal over infusion"
-                case .bolusUnderInfusion:
-                    return "Bolus under infusion"
-                case .bolusOverInfusion:
-                    return "Bolus over infusion"
-                case .basalOverInfusionPulse:
-                    return "Basal over infusion pulse"
-                case .tempBasalOverInfusionPulse:
-                    return "Temp basal over infusion pulse"
-                case .bolusOverInfusionPulse:
-                    return "Bolus over infusion pulse"
-                case .immediateBolusOverInfusionPulse:
-                    return "Immediate bolus under infusion pulse"
-                case .extendedBolusOverInfusionPulse:
-                    return "Extended bolus over infusion pulse"
-                case .corruptionOfTables:
-                    return "Corruption of $283/$2E3/$315 tables"
-                case .unrecognizedPulse:
-                    return "Bad pulse value to verify_and_start_pump"
-                case .syncWithoutTempActive:
-                    return "Pump sync req 5 with no temp basal active"
-                case .command1AParseUnexpectedFailed:
-                    return "Command 1A parse routine unexpected failed"
-                case .illegalChanParam:
-                    return "Bad parameter for $283/$2E3/$315 channel table specification"
-                case .basalPulseChanInactive:
-                    return "Pump basal request with basal IST not set"
-                case .tempPulseChanInactive:
-                    return "Pump temp basal request with temp basal IST not set"
-                case .bolusPulseChanInactive:
-                    return "Pump bolus request and bolus IST not set"
-                case .intSemaphoreNotSet:
-                    return "Bad table specifier field6 in 1A command"
-                case .illegalInterLockChan:
-                    return "Illegal interlock channel"
-                case .badStateInClearBolusIST2AndVars:
-                    return "Bad variable state in clear_Bolus_IST2_and_vars"
-                case .badStateInMaybeInc33D:
-                    return "Bad variable state in maybe_inc_33D"
-                case .bleTimeout:
-                    return "BLE timeout"
-                case .bleInitiated:
-                    return "BLE initiated"
-                case .bleUnkAlarm:
-                    return "BLE unknown alarm"
-                case .bleIaas:
-                    return "BLE IAAS"
-                case .crcFailure:
-                    return "CRC failure"
-                case .bleWdPingTimeout:
-                    return "BLE WD ping timeout"
-                case .bleExcessiveResets:
-                    return "BLE excessive resets"
-                case .bleNakError:
-                    return "BLE NAK error"
-                case .bleReqHighTimeout:
-                    return "BLE request high timeout"
-                case .bleUnknownResp:
-                    return "BLE unknown response"
-                case .bleReqStuckHigh:
-                    return "BLE request stuck high"
-                case .bleStateMachine1:
-                    return "BLE state machine 1"
-                case .bleStateMachine2:
-                    return "BLE state machine 2"
-                case .bleArbLost:
-                    return "BLE arbitration lost"
-                case .bleEr48DualNack:
-                    return "BLE dual Nack"
-                case .bleQnExceedMaxRetry:
-                    return "BLE QN exceed max retry"
-                case .bleQnCritVarFail:
-                    return "BLE QN critical variable fail"
-                case .valuesDoNotMatchOrAreGreaterThan0xC2:
-                    return "Unknown fault code"
-                }
-            }()
-        } else {
-            faultDescription = "Unknown Fault"
+
+    public var faultDescription: String {
+        switch faultType {
+        case .noFaults:
+            return "No fault"
+        case .failedFlashErase:
+            return "Flash erase failed"
+        case .failedFlashStore:
+            return "Flash store failed"
+        case .tableCorruptionBasalSubcommand:
+            return "Basal subcommand table corruption"
+        case .basalPulseTableCorruption:
+            return "Basal pulse table corruption"
+        case .corruptionByte720:
+            return "Corruption in byte_720"
+        case .dataCorruptionInTestRTCInterrupt:
+            return "Data corruption error in test_RTC_interrupt"
+        case .rtcInterruptHandlerInconsistentState:
+            return "RTC interrupt handler called with inconstent state"
+        case .valueGreaterThan8:
+            return "Value > 8"
+        case .invalidBeepRepeatPattern:
+            return "Invalid beep repeat pattern"
+        case .bf0notEqualToBF1:
+            return "Corruption in byte_BF0"
+        case .tableCorruptionTempBasalSubcommand:
+            return "Temp basal subcommand table corruption"
+        case .resetDueToCOP:
+            return "Reset due to COP"
+        case .resetDueToIllegalOpcode:
+            return "Reset due to illegal opcode"
+        case .resetDueToIllegalAddress:
+            return "Reset due to illegal address"
+        case .resetDueToSAWCOP:
+            return "Reset due to SAWCOP"
+        case .corruptionInByte_866:
+            return "Corruption in byte_866"
+        case .resetDueToLVD:
+            return "Reset due to LVD"
+        case .messageLengthTooLong:
+            return "Message length too long"
+        case .occluded:
+            return "Occluded"
+        case .corruptionInWord129:
+            return "Corruption in word_129 table/word_86A/dword_86E"
+        case .corruptionInByte868:
+            return "Corruption in byte_868"
+        case .corruptionInAValidatedTable:
+            return "Corruption in a validated table"
+        case .reservoirEmpty:
+            return "Reservoir empty or exceeded maximum pulse delivery"
+        case .badPowerSwitchArrayValue1:
+            return "Bad Power Switch Array Status and Control Register value 1 before starting pump"
+        case .badPowerSwitchArrayValue2:
+            return "Bad Power Switch Array Status and Control Register value 2 before starting pump"
+        case .badLoadCnthValue:
+            return "Bad LOADCNTH value when running pump"
+        case .exceededMaximumPodLife80Hrs:
+            return "Exceeded maximum Pod life of 80 hours"
+        case .badStateCommand1AScheduleParse:
+            return "Unexpected internal state in command_1A_schedule_parse_routine_wrapper"
+        case .unexpectedStateInRegisterUponReset:
+            return "Unexpected commissioned state in status and control register upon reset"
+        case .wrongSummaryForTable129:
+            return "Sum mismatch for word_129 table"
+        case .validateCountErrorWhenBolusing:
+            return "Validate encoder count error when bolusing"
+        case .badTimerVariableState:
+            return "Bad timer variable state"
+        case .unexpectedRTCModuleValueDuringReset:
+            return "Unexpected RTC Modulo Register value during reset"
+        case .problemCalibrateTimer:
+            return "Problem in calibrate_timer_case_3"
+        case .tickcntErrorRTC:
+            return "Tick count error RTC"
+        case .tickFailure:
+            return "Tick failure"
+        case .rtcInterruptHandlerUnexpectedCall:
+            return "RTC interrupt handler unexpectedly called"
+        case .missing2hourAlertToFillTank:
+            return "Failed to set up 2 hour alert for tank fill operation"
+        case .faultEventSetupPod:
+            return "Bad arg or state in update_insulin_variables, verify_and_start_pump or main_loop_control_pump"
+        case .autoOff0:
+            return "Alert #0 auto-off timeout"
+        case .autoOff1:
+            return "Alert #1 auto-off timeout"
+        case .autoOff2:
+            return "Alert #2 auto-off timeout"
+        case .autoOff3:
+            return "Alert #3 auto-off timeout"
+        case .autoOff4:
+            return "Alert #4 auto-off timeout"
+        case .autoOff5:
+            return "Alert #5 auto-off timeout"
+        case .autoOff6:
+            return "Alert #6 auto-off timeout"
+        case .autoOff7:
+            return "Alert #7 auto-off timeout"
+        case .insulinDeliveryCommandError:
+            return "Incorrect pod state for command or error during insulin command setup"
+        case .badValueStartupTest:
+            return "Bad value during startup testing"
+        case .connectedPodCommandTimeout:
+            return "Connected Pod command timeout"
+        case .resetFromUnknownCause:
+            return "Reset from unknown cause"
+        case .vetoNotSet:
+            return "Veto not set"
+        case .errorFlashInitialization:
+            return "Flash initialization error"
+        case .badPiezoValue:
+            return "Bad piezo value"
+        case .unexpectedValueByte358:
+            return "Unexpected byte_358 value"
+        case .problemWithLoad1and2:
+            return "Problem with LOAD1/LOAD2"
+        case .aGreaterThan7inMessage:
+            return "A > 7 in message processing"
+        case .failedTestSawReset:
+            return "SAW reset testing fail"
+        case .testInProgress:
+            return "test in progress"
+        case .problemWithPumpAnchor:
+            return "Problem with pump anchor"
+        case .errorFlashWrite:
+            return "Flash initialization or write error"
+        case .encoderCountTooHigh:
+            return "Encoder count too high"
+        case .encoderCountExcessiveVariance:
+            return "Encoder count excessive variance"
+        case .encoderCountTooLow:
+            return "Encoder count too low"
+        case .encoderCountProblem:
+            return "Encoder count problem"
+        case .checkVoltageOpenWire1:
+            return "Check voltage open wire 1 problem"
+        case .checkVoltageOpenWire2:
+            return "Check voltage open wire 2 problem"
+        case .problemWithLoad1and2type46:
+            return "Problem with LOAD1/LOAD2"
+        case .problemWithLoad1and2type47:
+            return "Problem with LOAD1/LOAD2"
+        case .badTimerCalibration:
+            return "Bad timer calibration"
+        case .badTimerRatios:
+            return "Bad timer values: COP timer ratio bad"
+        case .badTimerValues:
+            return "Bad timer values"
+        case .trimICSTooCloseTo0x1FF:
+            return "ICS trim too close to 0x1FF"
+        case .problemFindingBestTrimValue:
+            return "find_best_trim_value problem"
+        case .badSetTPM1MultiCasesValue:
+            return "Bad set_TPM1_multi_cases value"
+        case .unexpectedRFErrorFlagDuringReset:
+            return "Unexpected TXSCR2 RF Tranmission Error Flag set during reset"
+        case .timerPulseWidthModulatorOverflow:
+            return "Timer pulse-width modulator overflow"
+        case .tickcntError:
+            return "Bad tick count state before starting pump"
+        case .badRfmXtalStart:
+            return "TXOK issue in process_input_buffer"
+        case .badRxSensitivity:
+            return "Bad Rx word_107 sensitivity value during input message processing"
+        case .packetFrameLengthTooLong:
+            return "Packet frame length too long"
+        case .unexpectedIRQHighinTimerTick:
+            return "Unexpected IRQ high in timer_tick"
+        case .unexpectedIRQLowinTimerTick:
+            return "Unexpected IRQ low in timer_tick"
+        case .badArgToGetEntry:
+            return "Corrupt constants table at byte_37A[] or flash byte_4036[]"
+        case .badArgToUpdate37ATable:
+            return "Bad argument to update_37A_table"
+        case .errorUpdating37ATable:
+            return "Error updating constants byte_37A table"
+        case .occlusionCheckValueTooHigh:
+            return "Occlusion check value too high for detection"
+        case .loadTableCorruption:
+            return "Load table corruption"
+        case .primeOpenCountTooLow:
+            return "Prime open count too low"
+        case .badValueByte109:
+            return "Bad byte_109 value"
+        case .disableFlashSecurityFailed:
+            return "Write flash byte to disable flash security failed"
+        case .checkVoltageFailure:
+            return "Two check voltage failures before starting pump"
+        case .occlusionCheckStartup1:
+            return "Occlusion check startup problem 1"
+        case .occlusionCheckStartup2:
+            return "Occlusion check startup problem 2"
+        case .occlusionCheckTimeouts1:
+            return "Occlusion check excess timeouts 1"
+        case .occlusionCheckTimeouts2:
+            return "Occlusion check excess timeouts 2"
+        case .occlusionCheckTimeouts3:
+            return "Occlusion check excess timeouts 3"
+        case .occlusionCheckPulseIssue:
+            return "Occlusion check pulse issue"
+        case .occlusionCheckBolusProblem:
+            return "Occlusion check bolus problem"
+        case .occlusionCheckAboveThreshold:
+            return "Occlusion check above threshold"
+        case .basalUnderInfusion:
+            return "Basal under infusion"
+        case .basalOverInfusion:
+            return "Basal over infusion"
+        case .tempBasalUnderInfusion:
+            return "Temp basal under infusion"
+        case .tempBasalOverInfusion:
+            return "Temp basal over infusion"
+        case .bolusUnderInfusion:
+            return "Bolus under infusion"
+        case .bolusOverInfusion:
+            return "Bolus over infusion"
+        case .basalOverInfusionPulse:
+            return "Basal over infusion pulse"
+        case .tempBasalOverInfusionPulse:
+            return "Temp basal over infusion pulse"
+        case .bolusOverInfusionPulse:
+            return "Bolus over infusion pulse"
+        case .immediateBolusOverInfusionPulse:
+            return "Immediate bolus under infusion pulse"
+        case .extendedBolusOverInfusionPulse:
+            return "Extended bolus over infusion pulse"
+        case .corruptionOfTables:
+            return "Corruption of $283/$2E3/$315 tables"
+        case .unrecognizedPulse:
+            return "Bad pulse value to verify_and_start_pump"
+        case .syncWithoutTempActive:
+            return "Pump sync req 5 with no temp basal active"
+        case .command1AParseUnexpectedFailed:
+            return "Command 1A parse routine unexpected failed"
+        case .illegalChanParam:
+            return "Bad parameter for $283/$2E3/$315 channel table specification"
+        case .basalPulseChanInactive:
+            return "Pump basal request with basal IST not set"
+        case .tempPulseChanInactive:
+            return "Pump temp basal request with temp basal IST not set"
+        case .bolusPulseChanInactive:
+            return "Pump bolus request and bolus IST not set"
+        case .intSemaphoreNotSet:
+            return "Bad table specifier field6 in 1A command"
+        case .illegalInterLockChan:
+            return "Illegal interlock channel"
+        case .badStateInClearBolusIST2AndVars:
+            return "Bad variable state in clear_Bolus_IST2_and_vars"
+        case .badStateInMaybeInc33D:
+            return "Bad variable state in maybe_inc_33D"
+        case .bleTimeout:
+            return "BLE timeout"
+        case .bleInitiated:
+            return "BLE initiated"
+        case .bleUnkAlarm:
+            return "BLE unknown alarm"
+        case .bleIaas:
+            return "BLE IAAS"
+        case .crcFailure:
+            return "CRC failure"
+        case .bleWdPingTimeout:
+            return "BLE WD ping timeout"
+        case .bleExcessiveResets:
+            return "BLE excessive resets"
+        case .bleNakError:
+            return "BLE NAK error"
+        case .bleReqHighTimeout:
+            return "BLE request high timeout"
+        case .bleUnknownResp:
+            return "BLE unknown response"
+        case .bleReqStuckHigh:
+            return "BLE request stuck high"
+        case .bleStateMachine1:
+            return "BLE state machine 1"
+        case .bleStateMachine2:
+            return "BLE state machine 2"
+        case .bleArbLost:
+            return "BLE arbitration lost"
+        case .bleEr48DualNack:
+            return "BLE dual Nack"
+        case .bleQnExceedMaxRetry:
+            return "BLE QN exceed max retry"
+        case .bleQnCritVarFail:
+            return "BLE QN critical variable fail"
+        default:
+            return "Unknown fault code"
         }
+    }
+
+    public var description: String {
         return String(format: "Fault Event Code 0x%02x: %@", rawValue, faultDescription)
     }
     

+ 33 - 23
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/CancelDeliveryCommand.swift

@@ -10,30 +10,24 @@
 import Foundation
 
 
-
 public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
-    
+
     public let blockType: MessageBlockType = .cancelDelivery
-    
-    // ID1:1f00ee84 PTYPE:PDM SEQ:26 ID2:1f00ee84 B9:ac BLEN:7 MTYPE:1f05 BODY:e1f78752078196 CRC:03
-    
-    // Cancel bolus
-    // 1f 05 be1b741a 64 - 1U
-    // 1f 05 a00a1a95 64 - 1U over 1hr
-    // 1f 05 ff52f6c8 64 - 1U immediate, 1U over 1hr
-    
-    // Cancel temp basal
-    // 1f 05 f76d34c4 62 - 30U/hr
-    // 1f 05 156b93e8 62 - ?
-    // 1f 05 62723698 62 - 0%
-    // 1f 05 2933db73 62 - 03ea
-    
-    // Suspend is a Cancel delivery, followed by a configure alerts command (0x19)
-    // 1f 05 50f02312 03 191050f02312580f000f06046800001e0302
-    
-    // Deactivate pod:
+    // OFF 1  2 3 4 5  6
+    // 1F 05 NNNNNNNN AX
+
+    // Cancel bolus (with confirmation beep)
+    // 1f 05 be1b741a 64
+
+    // Cancel temp basal (with confirmation beep)
+    // 1f 05 f76d34c4 62
+
+    // Cancel all (before deactivate pod)
     // 1f 05 e1f78752 07
-    
+
+    // Cancel basal & temp basal for a suspend, followed by a configure alerts command (0x19) for alerts 5 & 6
+    // 1f 05 50f02312 03 19 10 50f02312 580f 000f 0604 6800 001e 0302
+
     public struct DeliveryType: OptionSet, Equatable {
         public let rawValue: UInt8
         
@@ -48,7 +42,23 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
         public init(rawValue: UInt8) {
             self.rawValue = rawValue
         }
-        
+
+        var debugDescription: String {
+            switch self {
+            case .none:
+                return "None"
+            case .basal:
+                return "Basal"
+            case .tempBasal:
+                return "TempBasal"
+            case .all:
+                return "All"
+            case .allButBasal:
+                return "AllButBasal"
+            default:
+                return "\(self.rawValue)"
+            }
+        }
     }
     
     public let deliveryType: DeliveryType
@@ -85,6 +95,6 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
 
 extension CancelDeliveryCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "CancelDeliveryCommand(nonce:\(Data(bigEndian: nonce).hexadecimalString), deliveryType:\(deliveryType), beepType:\(beepType))"
+        return "CancelDeliveryCommand(nonce:\(Data(bigEndian: nonce).hexadecimalString), deliveryType:\(deliveryType.debugDescription), beepType:\(beepType))"
     }
 }

+ 12 - 4
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift

@@ -22,7 +22,9 @@ public struct ConfigureAlertsCommand : NonceResyncableMessageBlock {
             UInt8(4 + configurations.count * AlertConfiguration.length),
             ])
         data.appendBigEndian(nonce)
-        for config in configurations {
+        // Sorting the alerts not required, but it can be helpful for log analysis
+        let sorted = configurations.sorted { $0.slot.rawValue < $1.slot.rawValue }
+        for config in sorted {
             data.append(contentsOf: config.data)
         }
         return data
@@ -93,6 +95,7 @@ extension AlertConfiguration {
         }
         self.beepType = beepType
 
+        self.silent = (beepType == .noBeepNonCancel)
     }
 
     public var data: Data {
@@ -105,12 +108,16 @@ extension AlertConfiguration {
         if autoOffModifier {
             firstByte += 1 << 1
         }
+
+        // The 9-bit duration is limited to 2^9-1 minutes max value
+        let durationMinutes = min(UInt(duration.minutes), 0x1ff)
+
         // High bit of duration
-        firstByte += UInt8((Int(duration.minutes) >> 8) & 0x1)
+        firstByte += UInt8((durationMinutes >> 8) & 0x1)
 
         var data = Data([
             firstByte,
-            UInt8(Int(duration.minutes) & 0xff)
+            UInt8(durationMinutes & 0xff)
             ])
 
         switch trigger {
@@ -123,7 +130,8 @@ extension AlertConfiguration {
             data.appendBigEndian(minutes)
         }
         data.append(beepRepeat.rawValue)
-        data.append(beepType.rawValue)
+        let beepTypeToSet: BeepType = silent ? .noBeepNonCancel : beepType
+        data.append(beepTypeToSet.rawValue)
 
         return data
     }

+ 2 - 3
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift

@@ -10,14 +10,13 @@
 import Foundation
 
 public struct DeactivatePodCommand : NonceResyncableMessageBlock {
-    
-    // ID1:1f00ee84 PTYPE:PDM SEQ:09 ID2:1f00ee84 B9:34 BLEN:6 MTYPE:1c04 BODY:0f7dc4058344 CRC:f1
+    // OFF 1  2 3 4 5
+    // 1C 04 NNNNNNNN
     
     public let blockType: MessageBlockType = .deactivatePod
     
     public var nonce: UInt32
     
-    // e1f78752 07 8196
     public var data: Data {
         var data = Data([
             blockType.rawValue,

+ 23 - 23
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DetailedStatus.swift

@@ -88,7 +88,7 @@ public struct DetailedStatus : PodInfo, Equatable {
         
         // For Eros, encodedData[19] (XX) byte is the same previousPodProgressStatus nibble in the VV byte on fault.
         // For Dash, encodedData[19] (XX) byte is uninitialized or unknown, so use VV byte for previousPodProgressStatus.
-        
+
         // Decode YYYY based on whether there was a pod fault
         if encodedData[8] == 0 {
             // For non-faults, YYYY contents not valid (either uninitialized data for Eros or some unknown content for Dash).
@@ -119,9 +119,8 @@ extension DetailedStatus: CustomDebugStringConvertible {
             "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U",
             "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U",
-            "* faultEventCode: \(faultEventCode.description)",
-            "* reservoirLevel: \(reservoirLevel > Pod.maximumReservoirReading ? "50+" : reservoirLevel.twoDecimals) U",
-            "* timeActive: \(timeActive.stringValue)",
+            "* reservoirLevel: \(reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : reservoirLevel.twoDecimals) U",
+            "* timeActive: \(timeActive.timeIntervalStr)",
             "* unacknowledgedAlerts: \(unacknowledgedAlerts)",
             "",
             ].joined(separator: "\n")
@@ -134,8 +133,9 @@ extension DetailedStatus: CustomDebugStringConvertible {
         }
         if faultEventCode.faultType != .noFaults {
             result += [
+                "* faultEventCode: \(faultEventCode.description)",
                 "* faultAccessingTables: \(faultAccessingTables)",
-                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "NA")",
+                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.timeIntervalStr ?? "NA")",
                 "* errorEventInfo: \(errorEventInfo?.description ?? "NA")",
                 "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
                 "* possibleFaultCallingAddress: \(possibleFaultCallingAddress != nil ? String(format: "0x%04x", possibleFaultCallingAddress!) : "NA")",
@@ -161,21 +161,21 @@ extension DetailedStatus: RawRepresentable {
 }
 
 extension TimeInterval {
-    var stringValue: String {
-        let totalSeconds = self
-        let minutes = Int(totalSeconds / 60) % 60
-        let hours = Int(totalSeconds / 3600) - (Int(self / 3600)/24 * 24)
-        let days = Int((totalSeconds / 3600) / 24)
-        var pluralFormOfDays = "days"
-        if days == 1 {
-            pluralFormOfDays = "day"
+    var timeIntervalStr: String {
+        var str: String = ""
+        let hours = UInt(self / 3600)
+        let minutes = UInt(self / 60) % 60
+        let seconds = UInt(self) % 60
+        if hours != 0 {
+            str += String(format: "%uh", hours)
         }
-        let timeComponent = String(format: "%02d:%02d", hours, minutes)
-        if days > 0 {
-            return String(format: "%d \(pluralFormOfDays) plus %@", days, timeComponent)
-        } else {
-            return timeComponent
+        if minutes != 0 || hours != 0 {
+            str += String(format: "%um", minutes)
+        }
+        if seconds != 0 || str.isEmpty {
+            str += String(format: "%us", seconds)
         }
+        return str
     }
 }
 
@@ -192,11 +192,11 @@ extension Double {
 // dddd: Pod Progress at time of first logged fault event
 //
 public struct ErrorEventInfo: CustomStringConvertible, Equatable {
-    let rawValue: UInt8
-    let insulinStateTableCorruption: Bool // 'a' bit
-    let occlusionType: Int // 'bb' 2-bit occlusion type
-    let immediateBolusInProgress: Bool // 'c' bit
-    let podProgressStatus: PodProgressStatus // 'dddd' bits
+    public let rawValue: UInt8
+    public let insulinStateTableCorruption: Bool // 'a' bit
+    public let occlusionType: Int // 'bb' 2-bit occlusion type
+    public let immediateBolusInProgress: Bool // 'c' bit
+    public let podProgressStatus: PodProgressStatus // 'dddd' bits
 
     public var errorEventInfo: ErrorEventInfo? {
         return ErrorEventInfo(rawValue: rawValue)

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift

@@ -91,13 +91,13 @@ extension BinaryInteger {
 }
 
 func pulseLogString(pulseLogEntries: [UInt32], lastPulseNumber: Int) -> String {
-    var str: String = "Pulse eeeeee0a pppliiib cccccccc dfgggggg\n"
+    var str: String = "Pulse eeeeee0a pppliiib cccccccc dfgggggg"
     var index = pulseLogEntries.count - 1
     var pulseNumber = lastPulseNumber
     while index >= 0 {
-        str += String(format: "%04d:", pulseNumber) + UInt32(pulseLogEntries[index]).binaryDescription + "\n"
+        str += String(format: "\n%04d:", pulseNumber) + UInt32(pulseLogEntries[index]).binaryDescription
         index -= 1
         pulseNumber -= 1
     }
-    return str + "\n"
+    return str
 }

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/StatusResponse.swift

@@ -52,9 +52,9 @@ public struct StatusResponse : MessageBlock {
         self.lastProgrammingMessageSeqNum = (encodedData[4] >> 3) & 0xf
         
         self.bolusNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit
-        
+
         self.alerts = AlertSet(rawValue: ((encodedData[6] & 0x7f) << 1) | (encodedData[7] >> 7))
-        
+
         self.reservoirLevel = Double((Int(encodedData[8] & 0x3) << 8) + Int(encodedData[9])) / Pod.pulsesPerUnit
     }
 
@@ -95,7 +95,7 @@ public struct StatusResponse : MessageBlock {
 
 extension StatusResponse: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulinDelivered), bolusNotDelivered:\(bolusNotDelivered), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))"
+        return "StatusResponse(deliveryStatus:\(deliveryStatus.description), progressStatus:\(podProgressStatus), timeActive:\(timeActive.timeIntervalStr), reservoirLevel:\(reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : reservoirLevel.twoDecimals), insulinDelivered:\(insulinDelivered.twoDecimals), bolusNotDelivered:\(bolusNotDelivered.twoDecimals), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))"
     }
 }
 

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/TempBasalExtraCommand.swift

@@ -75,6 +75,6 @@ public struct TempBasalExtraCommand : MessageBlock {
 
 extension TempBasalExtraCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "TempBasalExtraCommand(completionBeep:\(completionBeep), programReminderInterval:\(programReminderInterval.stringValue) remainingPulses:\(remainingPulses), delayUntilFirstPulse:\(delayUntilFirstPulse.stringValue), rateEntries:\(rateEntries))"
+        return "TempBasalExtraCommand(completionBeep:\(completionBeep), programReminderInterval:\(programReminderInterval.timeIntervalStr), remainingPulses:\(remainingPulses), delayUntilFirstPulse:\(delayUntilFirstPulse.timeIntervalStr), rateEntries:\(rateEntries))"
     }
 }

+ 8 - 7
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/PendingCommand.swift

@@ -16,11 +16,11 @@ public enum StartProgram: RawRepresentable {
     case bolus(volume: Double, automatic: Bool)
     case basalProgram(schedule: BasalSchedule)
     case tempBasal(unitsPerHour: Double, duration: TimeInterval, isHighTemp: Bool, automatic: Bool)
-    
+
     private enum StartProgramType: Int {
         case bolus, basalProgram, tempBasal
     }
-    
+
     public var rawValue: RawValue {
         switch self {
         case .bolus(let volume, let automatic):
@@ -77,7 +77,7 @@ public enum StartProgram: RawRepresentable {
             self = .tempBasal(unitsPerHour: unitsPerHour, duration: duration, isHighTemp: isHighTemp, automatic: automatic)
         }
     }
-    
+
     public static func == (lhs: StartProgram, rhs: StartProgram) -> Bool {
         switch(lhs, rhs) {
         case (.bolus(let lhsVolume, let lhsAutomatic), .bolus(let rhsVolume, let rhsAutomatic)):
@@ -97,7 +97,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
 
     case program(StartProgram, Int, Date, Bool = true)
     case stopProgram(CancelDeliveryCommand.DeliveryType, Int, Date, Bool = true)
-    
+
     private enum PendingCommandType: Int {
         case startProgram, stopProgram
     }
@@ -142,7 +142,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
         guard let rawPendingCommandType = rawValue["type"] as? PendingCommandType.RawValue else {
             return nil
         }
-        
+
         guard let commandDate = rawValue["date"] as? Date else {
             return nil
         }
@@ -176,7 +176,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
 
     public var rawValue: RawValue {
         var rawValue: RawValue = [:]
-        
+
         switch self {
         case .program(let program, let sequence, let date, let inflight):
             rawValue["type"] = PendingCommandType.startProgram.rawValue
@@ -193,7 +193,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
         }
         return rawValue
     }
-    
+
     public static func == (lhs: PendingCommand, rhs: PendingCommand) -> Bool {
         switch(lhs, rhs) {
         case (.program(let lhsProgram, let lhsSequence, let lhsDate, let lhsInflight), .program(let rhsProgram, let rhsSequence, let rhsDate, let rhsInflight)):
@@ -206,3 +206,4 @@ public enum PendingCommand: RawRepresentable, Equatable {
     }
 }
 
+

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

@@ -85,13 +85,13 @@ public struct Pod {
     public static let defaultExpirationReminderOffset = TimeInterval(hours: 2)
     public static let expirationReminderAlertMinHoursBeforeExpiration = 1
     public static let expirationReminderAlertMaxHoursBeforeExpiration = 24
-    
+
     // Threshold used to display pod end of life warnings
     public static let timeRemainingWarningThreshold = TimeInterval(days: 1)
-    
+
     // Default low reservoir alert limit in Units
     public static let defaultLowReservoirReminder: Double = 10
-    
+
     // Allowed Low Reservoir reminder values
     public static let allowedLowReservoirReminderValues = Array(stride(from: 10, through: 50, by: 1))
 }

+ 36 - 46
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/PumpManagerAlert.swift

@@ -1,5 +1,5 @@
 //
-//  PodAlert.swift
+//  PumpManagerAlert.swift
 //  OmniBLE
 //
 //  Created by Pete Schwamb on 7/9/20.
@@ -11,7 +11,6 @@ import LoopKit
 import HealthKit
 
 public enum PumpManagerAlert: Hashable {
-    case multiCommand(triggeringSlot: AlertSlot?)
     case podExpireImminent(triggeringSlot: AlertSlot?)
     case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval)
     case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double)
@@ -19,12 +18,13 @@ public enum PumpManagerAlert: Hashable {
     case suspendEnded(triggeringSlot: AlertSlot?)
     case podExpiring(triggeringSlot: AlertSlot?)
     case finishSetupReminder(triggeringSlot: AlertSlot?)
+    case unexpectedAlert(triggeringSlot: AlertSlot?)
     case timeOffsetChangeDetected
-    
+
     var isRepeating: Bool {
         return repeatInterval != nil
     }
-    
+
     var repeatInterval: TimeInterval? {
         switch self {
         case .suspendEnded:
@@ -33,11 +33,9 @@ public enum PumpManagerAlert: Hashable {
             return nil
         }
     }
-        
+
     var contentTitle: String {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content title for multiCommand pod alert")
         case .userPodExpiration:
             return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert")
         case .podExpiring:
@@ -52,15 +50,15 @@ public enum PumpManagerAlert: Hashable {
             return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert")
         case .finishSetupReminder:
             return LocalizedString("Pod Pairing Incomplete", comment: "Alert content title for finishSetupReminder pod alert")
+        case .unexpectedAlert:
+            return LocalizedString("Unexpected Alert", comment: "Alert content title for unexpected pod alert")
         case .timeOffsetChangeDetected:
             return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert")
         }
     }
-    
+
     var contentBody: String {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content body for multiCommand pod alert")
         case .userPodExpiration(_, let offset):
             let formatter = DateComponentsFormatter()
             formatter.allowedUnits = [.hour]
@@ -81,15 +79,16 @@ public enum PumpManagerAlert: Hashable {
             return LocalizedString("The insulin suspension period has ended.\n\nYou can resume delivery from the banner on the home screen or from your pump settings screen. You will be reminded again in 15 minutes.", comment: "Alert content body for suspendEnded pod alert")
         case .finishSetupReminder:
             return LocalizedString("Please finish pairing your pod.", comment: "Alert content body for finishSetupReminder pod alert")
+        case .unexpectedAlert(let triggeringSlot):
+            let slotNumberString = triggeringSlot != nil ? String(describing: triggeringSlot!.rawValue) : "?"
+            return String(format: LocalizedString("Unexpected Pod Alert #%1@!", comment: "Alert content body for unexpected pod alert (1: slotNumberString)"), slotNumberString)
         case .timeOffsetChangeDetected:
             return LocalizedString("The time on your pump is different from the current time. You can review the pump time and and sync to current time in settings.", comment: "Alert content body for timeOffsetChangeDetected pod alert")
         }
     }
-    
+
     var triggeringSlot: AlertSlot? {
         switch self {
-        case .multiCommand(let slot):
-            return slot
         case .userPodExpiration(let slot, _):
             return slot
         case .podExpiring(let slot):
@@ -104,17 +103,19 @@ public enum PumpManagerAlert: Hashable {
             return slot
         case .finishSetupReminder(let slot):
             return slot
+        case .unexpectedAlert(let slot):
+            return slot
         case .timeOffsetChangeDetected:
             return nil
         }
     }
-    
+
     // Override background (UserNotification) content
-    
+
     var backgroundContentTitle: String {
         return contentTitle
     }
-    
+
     var backgroundContentBody: String {
         switch self {
         case .suspendEnded:
@@ -124,23 +125,21 @@ public enum PumpManagerAlert: Hashable {
         }
     }
 
-    
+
     var actionButtonLabel: String {
         return LocalizedString("Ok", comment: "Action button default text for PodAlerts")
     }
-    
+
     var foregroundContent: Alert.Content {
         return Alert.Content(title: contentTitle, body: contentBody, acknowledgeActionButtonLabel: actionButtonLabel)
     }
-    
+
     var backgroundContent: Alert.Content {
         return Alert.Content(title: backgroundContentTitle, body: backgroundContentBody, acknowledgeActionButtonLabel: actionButtonLabel)
     }
-    
+
     var alertIdentifier: String {
         switch self {
-        case .multiCommand:
-            return "multiCommand"
         case .userPodExpiration:
             return "userPodExpiration"
         case .podExpiring:
@@ -153,38 +152,38 @@ public enum PumpManagerAlert: Hashable {
             return "suspendInProgress"
         case .suspendEnded:
             return "suspendEnded"
-        case .timeOffsetChangeDetected:
-            return "timeOffsetChangeDetected"
         case .finishSetupReminder:
             return "finishSetupReminder"
+        case .unexpectedAlert:
+            return "unexpectedAlert"
+        case .timeOffsetChangeDetected:
+            return "timeOffsetChangeDetected"
         }
     }
-        
+
     var repeatingAlertIdentifier: String {
         return alertIdentifier + "-repeating"
     }
 }
 
 extension PumpManagerAlert: RawRepresentable {
-    
+
     public typealias RawValue = [String: Any]
-    
+
     public init?(rawValue: RawValue) {
         guard let identifier = rawValue["identifier"] as? String else {
             return nil
         }
-        
+
         let slot: AlertSlot?
-        
+
         if let rawSlot = rawValue["slot"] as? AlertSlot.RawValue {
             slot = AlertSlot(rawValue: rawSlot)
         } else {
             slot = nil
         }
-        
+
         switch identifier {
-        case "multiCommand":
-            self = .multiCommand(triggeringSlot: slot)
         case "userPodExpiration":
             guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else {
                 return nil
@@ -203,6 +202,8 @@ extension PumpManagerAlert: RawRepresentable {
             self = .suspendInProgress(triggeringSlot: slot)
         case "suspendEnded":
             self = .suspendEnded(triggeringSlot: slot)
+        case "unexpectedAlert":
+            self = .unexpectedAlert(triggeringSlot: slot)
         case "timeOffsetChangeDetected":
             self = .timeOffsetChangeDetected
         default:
@@ -214,9 +215,9 @@ extension PumpManagerAlert: RawRepresentable {
         var rawValue: RawValue = [
             "identifier": alertIdentifier
         ]
-        
+
         rawValue["slot"] = triggeringSlot?.rawValue
-        
+
         switch self {
         case .lowReservoir(_, lowReservoirReminderValue: let value):
             rawValue["value"] = value
@@ -225,18 +226,7 @@ extension PumpManagerAlert: RawRepresentable {
         default:
             break
         }
-        
-        return rawValue
-    }
-}
 
-extension PodAlert {
-    var isIgnored: Bool {
-        switch self {
-        case .podSuspendedReminder, .finishSetupReminder:
-            return true
-        default:
-            return false
-        }
+        return rawValue
     }
 }

+ 32 - 0
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/SilencePodPreference.swift

@@ -0,0 +1,32 @@
+//
+//  SilencePodPreference.swift
+//  OmniBLE
+//
+//  Created by Joe Moran on 8/30/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public enum SilencePodPreference: Int, CaseIterable {
+    case disabled
+    case enabled
+
+    var title: String {
+        switch self {
+        case .disabled:
+            return LocalizedString("Disabled", comment: "Title string for SilencePodPreference.disabled")
+        case .enabled:
+            return LocalizedString("Silenced", comment: "Title string for SilencePodPreference.enabled")
+        }
+    }
+
+    var description: String {
+        switch self {
+        case .disabled:
+            return LocalizedString("Normal operation mode where audible Pod beeps are used for all Pod alerts and when confidence reminders are enabled.", comment: "Description for SilencePodPreference.disabled")
+        case .enabled:
+            return LocalizedString("All Pod alerts use no beeps and confirmation reminder beeps are suppressed. The Pod will only beep for fatal Pod faults and when playing test beeps.\n\n⚠️Warning - Whenever the Pod is silenced it must be kept within Bluetooth range of this device to receive notifications for Pod alerts.", comment: "Description for SilencePodPreference.enabled")
+        }
+    }
+}

+ 4 - 4
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/UnfinalizedDose.swift

@@ -202,13 +202,13 @@ 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")
+            return LocalizedString("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")
+            return LocalizedString("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")
+            return LocalizedString("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")
+            return LocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
         }
     }
 

+ 2 - 7
Dependencies/OmniBLE/OmniBLE/PumpManager/MessageTransport.swift

@@ -246,8 +246,6 @@ class PodMessageTransport: MessageTransport {
             payloads: [cmd.encoded(), Data()]
         )
 
-        log.debug("Sending command: %@", wrapped.hexadecimalString)
-
         let msg = MessagePacket(
             type: MessageType.ENCRYPTED,
             source: self.myId,
@@ -272,14 +270,11 @@ class PodMessageTransport: MessageTransport {
         incrementNonceSeq()
         let decrypted = try enDecrypt.decrypt(readMessage, nonceSeq)
 
-        log.debug("Received response: %@", decrypted.payload.hexadecimalString)
-
         let response = try parseResponse(decrypted: decrypted)
 
         incrementMsgSeq()
         incrementNonceSeq()
         let ack = try getAck(response: decrypted)
-        log.debug("Sending ACK: %@ in packet $ack", ack.payload.hexadecimalString)
         let ackResult = manager.sendMessagePacket(ack)
         guard case .sentWithAcknowledgment = ackResult else {
             throw PodProtocolError.messageIOException("Could not write $msgType: \(ackResult)")
@@ -296,14 +291,14 @@ class PodMessageTransport: MessageTransport {
     private func parseResponse(decrypted: MessagePacket) throws -> Message {
 
         let data = try StringLengthPrefixEncoding.parseKeys([RESPONSE_PREFIX], decrypted.payload)[0]
-        log.info("Received decrypted response: %@ in packet: %@", data.hexadecimalString, decrypted.payload.hexadecimalString)
+        log.debug("Received decrypted response: %{public}@ in packet: %{public}@", data.hexadecimalString, decrypted.payload.hexadecimalString)
 
         // Dash pods generates a CRC16 for Omnipod Messages, but the actual algorithm is not understood and doesn't match the CRC16
         // that the pod enforces for incoming Omnipod command message. The Dash PDM explicitly ignores the CRC16 for incoming messages,
         // so we ignore them as well and rely on higher level BLE & Dash message data checking to provide data corruption protection.
         let response = try Message(encodedData: data, checkCRC: false)
 
-        log.default("Recv(Hex): %@", data.hexadecimalString)
+        log.default("Recv(Hex): %{public}@", data.hexadecimalString)
         messageLogger?.didReceive(data)
 
         return response

+ 257 - 100
Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift

@@ -135,14 +135,6 @@ public class OmniBLEPumpManager: DeviceManager {
         return setStateWithResult(changes)
     }
 
-    @discardableResult
-    private func mutateState(_ changes: (_ state: inout OmniBLEPumpManagerState) -> Void) -> OmniBLEPumpManagerState {
-        return setStateWithResult({ (state) -> OmniBLEPumpManagerState in
-            changes(&state)
-            return state
-        })
-    }
-
     // Status can change even when state does not, because some status changes
     // purely based on time. This provides a mechanism to evaluate status changes
     // as time progresses and trigger status updates to clients.
@@ -285,13 +277,15 @@ public class OmniBLEPumpManager: DeviceManager {
     public var debugDescription: String {
         let lines = [
             "## OmniBLEPumpManager",
-            "podComms: \(String(reflecting: podComms))",
             "provideHeartbeat: \(provideHeartbeat)",
             "connected: \(isConnected)",
-            "state: \(String(reflecting: state))",
+            "",
+            "podComms: \(String(reflecting: podComms))",
+            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
             "status: \(String(describing: status))",
+            "",
             "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)",
-            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
+            "state: \(String(reflecting: state))",
         ]
         return lines.joined(separator: "\n")
     }
@@ -419,10 +413,22 @@ extension OmniBLEPumpManager {
         return false
     }
 
+
+    private var podTime: TimeInterval {
+        get {
+            guard let podState = state.podState else {
+                return 0
+            }
+            let elapsed = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0)
+            let podActiveTime = podState.podTime + elapsed
+            return podActiveTime
+        }
+    }
+
     // Returns a suitable beep command MessageBlock based the current beep preferences and
     // whether there is an unfinializedDose for a manual temp basal &/or a manual bolus.
     private func beepMessageBlock(beepType: BeepType) -> MessageBlock? {
-        guard self.beepPreference.shouldBeepForManualCommand else {
+        guard self.beepPreference.shouldBeepForManualCommand && !self.silencePod else {
             return nil
         }
 
@@ -505,6 +511,13 @@ extension OmniBLEPumpManager {
         }
     }
 
+    // Thread-safe
+    public var silencePod: Bool {
+        get {
+            return state.silencePod
+        }
+    }
+
     // From last status response
     public var reservoirLevel: ReservoirLevel? {
         return state.reservoirLevel
@@ -526,7 +539,7 @@ extension OmniBLEPumpManager {
 
     public var defaultExpirationReminderOffset: TimeInterval {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.defaultExpirationReminderOffset = newValue
             }
         }
@@ -537,7 +550,7 @@ extension OmniBLEPumpManager {
 
     public var lowReservoirReminderValue: Double {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.lowReservoirReminderValue = newValue
             }
         }
@@ -548,7 +561,7 @@ extension OmniBLEPumpManager {
 
     public var podAttachmentConfirmed: Bool {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.podAttachmentConfirmed = newValue
             }
         }
@@ -559,7 +572,7 @@ extension OmniBLEPumpManager {
 
     public var initialConfigurationCompleted: Bool {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.initialConfigurationCompleted = newValue
             }
         }
@@ -935,15 +948,13 @@ extension OmniBLEPumpManager {
                         }
                     }
 
-                    let expiration = self.podExpiresAt ?? Date().addingTimeInterval(Pod.nominalPodLife)
-                    let timeUntilExpirationReminder = expiration.addingTimeInterval(-self.state.defaultExpirationReminderOffset).timeIntervalSince(self.dateGenerator())
-
+                    let expirationReminderTime = Pod.nominalPodLife - self.state.defaultExpirationReminderOffset
                     let alerts: [PodAlert] = [
-                        .expirationReminder(self.state.defaultExpirationReminderOffset > 0 ? timeUntilExpirationReminder : 0),
-                        .lowReservoir(self.state.lowReservoirReminderValue)
+                        .expirationReminder(offset: self.podTime, absAlertTime: self.state.defaultExpirationReminderOffset > 0 ? expirationReminderTime : 0),
+                        .lowReservoir(units: self.state.lowReservoirReminderValue)
                     ]
 
-                    let finishWait = try session.insertCannula(optionalAlerts: alerts)
+                    let finishWait = try session.insertCannula(optionalAlerts: alerts, silent: self.silencePod)
                     completion(.success(finishWait))
                 } catch let error {
                     completion(.failure(.communication(error)))
@@ -1004,15 +1015,44 @@ extension OmniBLEPumpManager {
         }
     }
 
+    public func getDetailedStatus(completion: ((_ result: PumpManagerResult<DetailedStatus>) -> Void)? = nil) {
+
+        // use hasSetupPod here instead of hasActivePod as DetailedStatus can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion?(.failure(PumpManagerError.configuration(OmniBLEPumpManagerError.noPodPaired)))
+            return
+        }
+
+        podComms.runSession(withName: "Get detailed status") { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .bipBip)
+                    let detailedStatus = try session.getDetailedStatus(beepBlock: beepBlock)
+                    session.dosesForStorage({ (doses) -> Bool in
+                        self.store(doses: doses, in: session)
+                    })
+                    completion?(.success(detailedStatus))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion?(.failure(.communication(error as? LocalizedError)))
+                self.log.error("Failed to fetch detailed status: %{public}@", String(describing: error))
+            }
+        }
+    }
+
+
     // MARK: - Pump Commands
 
-    public func acknowledgePodAlerts(_ alertsToAcknowledge: AlertSet, completion: @escaping (_ alerts: [AlertSlot: PodAlert]?) -> Void) {
+    public func acknowledgePodAlerts(_ alertsToAcknowledge: AlertSet, completion: @escaping (_ alerts: AlertSet?) -> Void) {
         guard self.hasActivePod else {
             completion(nil)
             return
         }
 
-        self.podComms.runSession(withName: "Acknowledge Alarms") { (result) in
+        self.podComms.runSession(withName: "Acknowledge Alerts") { (result) in
             let session: PodCommsSession
             switch result {
             case .success(let s):
@@ -1049,7 +1089,7 @@ extension OmniBLEPumpManager {
             switch result {
             case .success(let session):
                 do {
-                    let beep = self.beepPreference.shouldBeepForManualCommand
+                    let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                     let _ = try session.setTime(timeZone: timeZone, basalSchedule: self.state.basalSchedule, date: Date(), acknowledgementBeep: beep)
                     self.clearSuspendReminder()
                     self.setState { (state) in
@@ -1107,7 +1147,7 @@ extension OmniBLEPumpManager {
                     case .success:
                         break
                     }
-                    let beep = self.beepPreference.shouldBeepForManualCommand
+                    let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                     let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                     self.clearSuspendReminder()
 
@@ -1169,12 +1209,12 @@ extension OmniBLEPumpManager {
         self.podComms.runSession(withName: "Play Test Beeps") { (result) in
             switch result {
             case .success(let session):
-                // preserve Pod completion beep state for any unfinalized manual insulin delivery
-                let beep = self.beepPreference.shouldBeepForManualCommand
+                // preserve the pod's completion beep state which gets reset playing beeps
+                let enabled: Bool = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                 let result = session.beepConfig(
                     beepType: .bipBeepBipBeepBipBeepBipBeep,
-                    tempBasalCompletionBeep: beep && self.hasUnfinalizedManualTempBasal,
-                    bolusCompletionBeep: beep && self.hasUnfinalizedManualBolus
+                    tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal,
+                    bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus
                 )
 
                 switch result {
@@ -1227,16 +1267,20 @@ extension OmniBLEPumpManager {
     }
 
     public func setConfirmationBeeps(newPreference: BeepPreference, completion: @escaping (OmniBLEPumpManagerError?) -> Void) {
-        self.log.default("Set Confirmation Beeps to %s", String(describing: newPreference))
-        guard self.hasActivePod else {
+
+        // If there isn't an active pod or the pod is currently silenced,
+        // just need to update the internal state without any pod commands.
+        let name = String(format: "Set Beep Preference to %@", String(describing: newPreference))
+        if !self.hasActivePod || self.silencePod {
+            self.log.default("%{public}@ for internal state only", name)
             self.setState { state in
-                state.confirmationBeeps = newPreference // set here to allow changes on a faulted Pod
+                state.confirmationBeeps = newPreference
             }
             completion(nil)
             return
         }
 
-        self.podComms.runSession(withName: "Set Confirmation Beeps Preference") { (result) in
+        self.podComms.runSession(withName: name) { (result) in
             switch result {
             case .success(let session):
                 // enable/disable Pod completion beep state for any unfinalized manual insulin delivery
@@ -1262,6 +1306,75 @@ extension OmniBLEPumpManager {
             }
         }
     }
+
+    // Reconfigures all active alerts in pod to be silent or not as well as sets/clears the
+    // self.silencePod state variable which silences all confirmation beeping when enabled.
+    public func setSilencePod(silencePod: Bool, completion: @escaping (OmniBLEPumpManagerError?) -> Void) {
+
+        let name = String(format: "%@ Pod", silencePod ? "Silence" : "Unsilence")
+        // allow Silence Pod changes without an active Pod
+        guard self.hasActivePod else {
+            self.log.default("%{public}@", name)
+            self.setState { state in
+                state.silencePod = silencePod
+            }
+            completion(nil)
+            return
+        }
+
+        self.podComms.runSession(withName: name) { (result) in
+
+            let session: PodCommsSession
+            switch result {
+            case .success(let s):
+                session = s
+            case .failure(let error):
+                completion(.communication(error))
+                return
+            }
+
+            guard let configuredAlerts = self.state.podState?.configuredAlerts,
+                  let activeAlertSlots = self.state.podState?.activeAlertSlots,
+                  let reservoirLevel = self.state.podState?.lastInsulinMeasurements?.reservoirLevel?.rawValue else
+            {
+                self.log.error("Missing podState") // should never happen
+                completion(OmniBLEPumpManagerError.noPodPaired)
+                return
+            }
+
+            let beepBlock: MessageBlock?
+            if !self.beepPreference.shouldBeepForManualCommand {
+                // No enabled completion beeps to worry about for any in-progress manual delivery
+                beepBlock = nil
+            } else if silencePod {
+                // Disable completion beeps for any in-progress manual delivery w/o beeping
+                beepBlock = BeepConfigCommand(beepType: .noBeepNonCancel)
+            } else {
+                // Emit a confirmation beep and enable completion beeps for any in-progress manual delivery
+                beepBlock = BeepConfigCommand(
+                    beepType: .bipBip,
+                    tempBasalCompletionBeep: self.hasUnfinalizedManualTempBasal,
+                    bolusCompletionBeep: self.hasUnfinalizedManualBolus
+                )
+            }
+
+            let podAlerts = regeneratePodAlerts(silent: silencePod, configuredAlerts: configuredAlerts, activeAlertSlots: activeAlertSlots, currentPodTime: self.podTime, currentReservoirLevel: reservoirLevel)
+            do {
+                // Since non-responsive pod comms are currently only resolved for insulin related commands,
+                // it's possible that a previous pod alert was successfully configured will lose its response
+                // and thus the alert won't get reset when reconfiguring pod alerts with a new silence pod state.
+                // So acknowledge all alerts now to be absolutely sure that no triggered alert will be forgotten.
+                try session.configureAlerts(podAlerts, acknowledgeAll: true, beepBlock: beepBlock)
+                self.setState { (state) in
+                    state.silencePod = silencePod
+                }
+                completion(nil)
+            } catch {
+                self.log.error("Configure alerts %{public}@ failed: %{public}@", String(describing: podAlerts), String(describing: error))
+                completion(.communication(error))
+            }
+        }
+    }
 }
 
 // MARK: - PumpManager
@@ -1425,7 +1538,7 @@ extension OmniBLEPumpManager: PumpManager {
 
             // Use a beepBlock for the confirmation beep to avoid getting 3 beeps using cancel command beeps!
             let beepBlock = self.beepMessageBlock(beepType: .beeeeeep)
-            let result = session.suspendDelivery(suspendReminder: suspendReminder, beepBlock: beepBlock)
+            let result = session.suspendDelivery(suspendReminder: suspendReminder, silent: self.silencePod, beepBlock: beepBlock)
             switch result {
             case .certainFailure(let error):
                 self.log.error("Failed to suspend: %{public}@", String(describing: error))
@@ -1471,7 +1584,7 @@ extension OmniBLEPumpManager: PumpManager {
 
             do {
                 let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
-                let beep = self.beepPreference.shouldBeepForManualCommand
+                let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                 let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                 self.clearSuspendReminder()
                 session.dosesForStorage() { (doses) -> Bool in
@@ -1536,8 +1649,14 @@ extension OmniBLEPumpManager: PumpManager {
         // Round to nearest supported volume
         let enactUnits = roundToSupportedBolusVolume(units: units)
 
-        let acknowledgementBeep = self.beepPreference.shouldBeepForCommand(automatic: activationType.isAutomatic)
-        let completionBeep = beepPreference.shouldBeepForManualCommand && !activationType.isAutomatic
+        let acknowledgementBeep, completionBeep: Bool
+        if self.silencePod {
+            acknowledgementBeep = false
+            completionBeep = false
+        } else {
+            acknowledgementBeep = self.beepPreference.shouldBeepForCommand(automatic: activationType.isAutomatic)
+            completionBeep = beepPreference.shouldBeepForManualCommand && !activationType.isAutomatic
+        }
 
         self.podComms.runSession(withName: "Bolus") { (result) in
             let session: PodCommsSession
@@ -1626,8 +1745,8 @@ extension OmniBLEPumpManager: PumpManager {
                 }
 
                 // when cancelling a bolus use the built-in type 6 beeeeeep to match PDM if confirmation beeps are enabled
-                let beeptype: BeepType = self.beepPreference.shouldBeepForManualCommand ? .beeeeeep : .noBeepCancel
-                let result = session.cancelDelivery(deliveryType: .bolus, beepType: beeptype)
+                let beepType: BeepType = self.beepPreference.shouldBeepForManualCommand && !self.silencePod ? .beeeeeep : .noBeepCancel
+                let result = session.cancelDelivery(deliveryType: .bolus, beepType: beepType)
                 switch result {
                 case .certainFailure(let error):
                     throw error
@@ -1660,8 +1779,14 @@ extension OmniBLEPumpManager: PumpManager {
         // Round to nearest supported rate
         let rate = roundToSupportedBasalRate(unitsPerHour: unitsPerHour)
 
-        let acknowledgementBeep = beepPreference.shouldBeepForCommand(automatic: automatic)
-        let completionBeep = beepPreference.shouldBeepForManualCommand && !automatic
+        let acknowledgementBeep, completionBeep: Bool
+        if self.silencePod {
+            acknowledgementBeep = false
+            completionBeep = false
+        } else {
+            acknowledgementBeep = beepPreference.shouldBeepForCommand(automatic: automatic)
+            completionBeep = beepPreference.shouldBeepForManualCommand && !automatic
+        }
 
         self.podComms.runSession(withName: "Enact Temp Basal") { (result) in
             self.log.info("Enact temp basal %.03fU/hr for %ds", rate, Int(duration))
@@ -1689,28 +1814,37 @@ extension OmniBLEPumpManager: PumpManager {
                     throw PodCommsError.unfinalizedBolus
                 }
 
-                let status: StatusResponse
+                // Did the last message have comms issues or is the last delivery status not verified correctly?
+                let uncertainDeliveryStatus = self.state.podState?.lastCommsOK == false || self.state.podState?.deliveryStatusVerified == false
 
-                // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
-                let beepType: BeepType = resumingScheduledBasal && acknowledgementBeep ? .beep : .noBeepCancel
-                let result = session.cancelDelivery(deliveryType: .tempBasal, beepType: beepType)
-                switch result {
-                case .certainFailure(let error):
-                    throw error
-                case .unacknowledged(let error):
-                    throw error
-                case .success(let cancelTempStatus, _):
-                    status = cancelTempStatus
-                }
+                // Do the cancel temp basal command if currently running a temp basal OR
+                // if resuming scheduled basal delivery OR if the delivery status is uncertain.
+                if self.state.podState?.unfinalizedTempBasal != nil || resumingScheduledBasal || uncertainDeliveryStatus {
+                    let status: StatusResponse
 
-                // If pod is bolusing, fail if not resuming the scheduled basal
-                guard !status.deliveryStatus.bolusing || resumingScheduledBasal else {
-                    throw PodCommsError.unfinalizedBolus
-                }
+                    // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
+                    let beepType: BeepType = resumingScheduledBasal && acknowledgementBeep ? .beep : .noBeepCancel
+                    let result = session.cancelDelivery(deliveryType: .tempBasal, beepType: beepType)
+                    switch result {
+                    case .certainFailure(let error):
+                        throw error
+                    case .unacknowledged(let error):
+                        throw error
+                    case .success(let cancelTempStatus, _):
+                        status = cancelTempStatus
+                    }
 
-                guard status.deliveryStatus != .suspended else {
-                    self.log.info("Canceling temp basal because status return indicates pod is suspended.")
-                    throw PodCommsError.podSuspended
+                    // If pod is bolusing, fail if not resuming the scheduled basal
+                    guard !status.deliveryStatus.bolusing || resumingScheduledBasal else {
+                        throw PodCommsError.unfinalizedBolus
+                    }
+
+                    guard status.deliveryStatus != .suspended else {
+                        self.log.info("Canceling temp basal because status return indicates pod is suspended.")
+                        throw PodCommsError.podSuspended
+                    }
+                } else {
+                    self.log.info("Skipped Cancel TB command before enacting temp basal")
                 }
 
                 defer {
@@ -1778,7 +1912,7 @@ extension OmniBLEPumpManager: PumpManager {
     }
 
     public func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (Result<DeliveryLimits, Error>) -> Void) {
-        mutateState { state in
+        setState { state in
             if let rate = deliveryLimits.maximumBasalRate?.doubleValue(for: .internationalUnitsPerHour) {
                 state.maximumTempBasalRate = rate
                 completion(.success(deliveryLimits))
@@ -1823,16 +1957,25 @@ extension OmniBLEPumpManager: PumpManager {
                 return
             }
 
-            var timeUntilReminder : TimeInterval = 0
+            let podTime = self.podTime
+            var expirationReminderPodTime: TimeInterval = 0 // default to expiration reminder alert inactive
+
+            // If the interval before expiration is not a positive value (e.g., it's in the past),
+            // then the pod alert will get the default alert time of 0 making this alert inactive.
             if let intervalBeforeExpiration = intervalBeforeExpiration, intervalBeforeExpiration > 0 {
-                timeUntilReminder = expiresAt.addingTimeInterval(-intervalBeforeExpiration).timeIntervalSince(self.dateGenerator())
+                let timeUntilReminder = expiresAt.addingTimeInterval(-intervalBeforeExpiration).timeIntervalSince(self.dateGenerator())
+                // Only bother to set an expiration reminder pod alert if it is still at least a couple of minutes in the future
+                if timeUntilReminder > .minutes(2) {
+                    expirationReminderPodTime = podTime + timeUntilReminder
+                    self.log.debug("Update Expiration timeUntilReminder=%@, podTime=%@, expirationReminderPodTime=%@", timeUntilReminder.timeIntervalStr, podTime.timeIntervalStr, expirationReminderPodTime.timeIntervalStr)
+                }
             }
 
-            let expirationReminder = PodAlert.expirationReminder(timeUntilReminder)
+            let expirationReminder = PodAlert.expirationReminder(offset: podTime, absAlertTime: expirationReminderPodTime, silent: self.silencePod)
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([expirationReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
+                self.setState({ (state) in
                     state.scheduledExpirationReminderOffset = intervalBeforeExpiration
                 })
                 completion(nil)
@@ -1856,7 +1999,8 @@ extension OmniBLEPumpManager: PumpManager {
             expiration.addingTimeInterval(.hours(Double(i)))
         }
         let now = dateGenerator()
-        return allDates.filter { $0.timeIntervalSince(now) > 0 }
+        // Have a couple minutes of slop to avoid confusion trying to set an expiration reminder too close to now
+        return allDates.filter { $0.timeIntervalSince(now) > .minutes(2) }
     }
 
     public var scheduledExpirationReminder: Date? {
@@ -1869,9 +2013,26 @@ extension OmniBLEPumpManager: PumpManager {
         return expiration.addingTimeInterval(-.hours(round(offset.hours)))
     }
 
+    // Updates the low reservior reminder value both for the current pod (when applicable) and for future pods
     public func updateLowReservoirReminder(_ value: Int, completion: @escaping (OmniBLEPumpManagerError?) -> Void) {
+
+        let supportedValue = min(max(0, Double(value)), Pod.maximumReservoirReading)
+        let setLowReservoirReminderValue = {
+            self.log.default("Set Low Reservoir Reminder to %d U", value)
+            self.lowReservoirReminderValue = supportedValue
+            completion(nil)
+        }
+
         guard self.hasActivePod else {
-            completion(OmniBLEPumpManagerError.noPodPaired)
+            // no active pod, just set the internal state for the next pod
+            setLowReservoirReminderValue()
+            return
+        }
+
+        guard let currentReservoirLevel = self.reservoirLevel?.rawValue, currentReservoirLevel > supportedValue else {
+            // Since the new low reservoir alert level is not below the current reservoir value,
+            // just set the internal state for the next pod to prevent an immediate low reservoir alert.
+            setLowReservoirReminderValue()
             return
         }
 
@@ -1886,13 +2047,11 @@ extension OmniBLEPumpManager: PumpManager {
                 return
             }
 
-            let lowReservoirReminder = PodAlert.lowReservoir(Double(value))
+            let lowReservoirReminder = PodAlert.lowReservoir(units: supportedValue, silent: self.silencePod)
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
-                    state.lowReservoirReminderValue = Double(value)
-                })
+                self.lowReservoirReminderValue = supportedValue
                 completion(nil)
             } catch {
                 completion(.communication(error))
@@ -1917,7 +2076,7 @@ extension OmniBLEPumpManager: PumpManager {
             }
         }
 
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.insert(alert)
         }
     }
@@ -1933,7 +2092,7 @@ extension OmniBLEPumpManager: PumpManager {
                 delegate?.retractAlert(identifier: repeatingIdentifier)
             }
         }
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.remove(alert)
         }
     }
@@ -1954,6 +2113,8 @@ extension OmniBLEPumpManager: PumpManager {
                 }
             } else {
                 log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot))
+                let pumpManagerAlert = PumpManagerAlert.unexpectedAlert(triggeringSlot: slot)
+                issueAlert(alert: pumpManagerAlert)
             }
         }
         for alert in removed {
@@ -1962,34 +2123,24 @@ extension OmniBLEPumpManager: PumpManager {
     }
 
     private func getPumpManagerAlert(for podAlert: PodAlert, slot: AlertSlot) -> PumpManagerAlert? {
-        guard let podState = state.podState, let expiresAt = podState.expiresAt else {
-            preconditionFailure("trying to lookup alert info without podState")
-        }
-
-        guard !podAlert.isIgnored else {
-            return nil
-        }
 
         switch podAlert {
-        case .podSuspendedReminder:
-            return PumpManagerAlert.suspendInProgress(triggeringSlot: slot)
+        case .shutdownImminent:
+            return PumpManagerAlert.podExpireImminent(triggeringSlot: slot)
         case .expirationReminder:
-            guard let offset = state.scheduledExpirationReminderOffset, offset > 0 else {
-                return nil
+            guard let podState = state.podState, let expiresAt = podState.expiresAt else {
+                preconditionFailure("trying to lookup expiresAt")
             }
             let timeToExpiry = TimeInterval(hours: expiresAt.timeIntervalSince(dateGenerator()).hours.rounded())
             return PumpManagerAlert.userPodExpiration(triggeringSlot: slot, scheduledExpirationReminderOffset: timeToExpiry)
-        case .expired:
-            return PumpManagerAlert.podExpiring(triggeringSlot: slot)
-        case .shutdownImminent:
-            return PumpManagerAlert.podExpireImminent(triggeringSlot: slot)
-        case .lowReservoir(let units):
+        case .lowReservoir(let units, _):
             return PumpManagerAlert.lowReservoir(triggeringSlot: slot, lowReservoirReminderValue: units)
-        case .finishSetupReminder, .waitingForPairingReminder:
-            return PumpManagerAlert.finishSetupReminder(triggeringSlot: slot)
         case .suspendTimeExpired:
             return PumpManagerAlert.suspendEnded(triggeringSlot: slot)
+        case .expired:
+            return PumpManagerAlert.podExpiring(triggeringSlot: slot)
         default:
+            // No PumpManagerAlerts are used for any other pod alerts (including suspendInProgress).
             return nil
         }
     }
@@ -2006,7 +2157,7 @@ extension OmniBLEPumpManager: PumpManager {
                         } catch {
                             return
                         }
-                        self.mutateState { state in
+                        self.setState { state in
                             state.activeAlerts.remove(alert)
                             state.alertsWithPendingAcknowledgment.remove(alert)
                         }
@@ -2149,7 +2300,7 @@ extension OmniBLEPumpManager: PodCommsDelegate {
             }
         } else {
             // Resetting podState
-            mutateState { state in
+            setState { state in
                 state.updatePodStateFromPodComms(podState)
             }
         }
@@ -2178,6 +2329,13 @@ extension OmniBLEPumpManager {
             if alert.alertIdentifier == alertIdentifier {
                 // If this alert was triggered by the pod find the slot to clear it.
                 if let slot = alert.triggeringSlot {
+                    if case .some(.suspended) = self.state.podState?.suspendState, slot == .slot6SuspendTimeExpired {
+                        // Don't clear this pod alert here with the pod still suspended so that the suspend time expired
+                        // pod alert beeping will continue until the pod is resumed which will then deactivate this alert.
+                        log.default("Skipping acknowledgement of suspend time expired alert with a suspended pod")
+                        completion(nil)
+                        return
+                    }
                     self.podComms.runSession(withName: "Acknowledge Alert") { (result) in
                         switch result {
                         case .success(let session):
@@ -2185,27 +2343,26 @@ extension OmniBLEPumpManager {
                                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                                 let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock)
                             } catch {
-                                self.mutateState { state in
+                                self.setState { state in
                                     state.alertsWithPendingAcknowledgment.insert(alert)
                                 }
                                 completion(error)
                                 return
                             }
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.activeAlerts.remove(alert)
                             }
                             completion(nil)
                         case .failure(let error):
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.alertsWithPendingAcknowledgment.insert(alert)
                             }
                             completion(error)
-                            return
                         }
                     }
                 } else {
                     // Non-pod alert
-                    self.mutateState { state in
+                    self.setState { state in
                         state.activeAlerts.remove(alert)
                         if alert == .timeOffsetChangeDetected {
                             state.acknowledgedTimeOffsetAlert = true
@@ -2228,7 +2385,7 @@ extension FaultEventCode {
         case .exceededMaximumPodLife80Hrs:
             return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification")
         default:
-            return LocalizedString("Critical Pod Error", comment: "The title for AlarmCode.other notification")
+            return String(format: LocalizedString("Critical Pod Fault %1$03d", comment: "The title for AlarmCode.other notification: (1: fault code value)"), self.rawValue)
         }
     }
 

+ 14 - 4
Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManagerState.swift

@@ -30,6 +30,8 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
 
     public var unstoredDoses: [UnfinalizedDose]
 
+    public var silencePod: Bool
+
     public var confirmationBeeps: BeepPreference
     
     public var controllerId: UInt32 = 0
@@ -96,6 +98,7 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
         self.timeZone = timeZone
         self.basalSchedule = basalSchedule
         self.unstoredDoses = []
+        self.silencePod = false
         self.confirmationBeeps = .manualCommands
         if controllerId != nil && podId != nil {
             self.controllerId = controllerId!
@@ -192,6 +195,8 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
             self.unstoredDoses = []
         }
 
+        self.silencePod = rawValue["silencePod"] as? Bool ?? false
+
         if let rawBeeps = rawValue["confirmationBeeps"] as? BeepPreference.RawValue, let confirmationBeeps = BeepPreference(rawValue: rawBeeps) {
             self.confirmationBeeps = confirmationBeeps
         } else {
@@ -246,6 +251,7 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
             "timeZone": timeZone.secondsFromGMT(),
             "basalSchedule": basalSchedule.rawValue,
             "unstoredDoses": unstoredDoses.map { $0.rawValue },
+            "silencePod": silencePod,
             "confirmationBeeps": confirmationBeeps.rawValue,
             "activeAlerts": activeAlerts.map { $0.rawValue },
             "podAttachmentConfirmed": podAttachmentConfirmed,
@@ -299,20 +305,24 @@ extension OmniBLEPumpManagerState: CustomDebugStringConvertible {
             "* tempBasalEngageState: \(String(describing: tempBasalEngageState))",
             "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))",
             "* isPumpDataStale: \(String(describing: isPumpDataStale))",
+            "* silencePod: \(String(describing: silencePod))",
             "* confirmationBeeps: \(String(describing: confirmationBeeps))",
             "* controllerId: \(String(format: "%08X", controllerId))",
             "* podId: \(String(format: "%08X", podId))",
             "* insulinType: \(String(describing: insulinType))",
-            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset))",
-            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset)",
+            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))",
+            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)",
             "* lowReservoirReminderValue: \(lowReservoirReminderValue)",
             "* podAttachmentConfirmed: \(podAttachmentConfirmed)",
             "* activeAlerts: \(activeAlerts)",
             "* alertsWithPendingAcknowledgment: \(alertsWithPendingAcknowledgment)",
             "* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)",
             "* initialConfigurationCompleted: \(initialConfigurationCompleted)",
-            String(reflecting: podState),
-            "* PreviousPodState: \(String(reflecting: previousPodState))"
+            "",
+            "* PodState: " + (podState == nil ? "nil" : String(describing: podState!)),
+            "",
+            "* PreviousPodState: " + (previousPodState == nil ? "nil" : String(describing: previousPodState!)),
+            "",
         ].joined(separator: "\n")
     }
 }

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

@@ -1,6 +1,6 @@
 //
 //  PodCommsSession.swift
-//  OmnipodKit
+//  OmniBLE
 //
 //  From OmniKit/PumpManager/PodCommsSession.swift
 //  Created by Pete Schwamb on 10/13/17.
@@ -274,6 +274,7 @@ public class PodCommsSession {
 
             let message = Message(address: podState.address, messageBlocks: blocksToSend, sequenceNum: messageNumber, expectFollowOnMessage: expectFollowOnMessage)
 
+            self.podState.lastCommsOK = false // mark last comms as not OK until we get the expected response
             let response = try transport.sendMessage(message)
             
             // Simulate fault
@@ -282,6 +283,7 @@ public class PodCommsSession {
 
             if let responseMessageBlock = response.messageBlocks[0] as? T {
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
+                self.podState.lastCommsOK = true // message successfully sent and expected response received
                 return responseMessageBlock
             }
 
@@ -379,10 +381,16 @@ public class PodCommsSession {
     }
 
     @discardableResult
-    func configureAlerts(_ alerts: [PodAlert], beepBlock: MessageBlock? = nil) throws -> StatusResponse {
+    func configureAlerts(_ alerts: [PodAlert], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse {
         let configurations = alerts.map { $0.configuration }
         let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations)
-        let status: StatusResponse = try send([configureAlerts], beepBlock: beepBlock)
+        var blocksToSend: [MessageBlock] = [configureAlerts]
+        if acknowledgeAll {
+            // requested to acknowledge any possible pending pod alerts out of an abundnace of caution
+            let acknowledgeAll = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: AlertSet(rawValue: ~0))
+            blocksToSend += [acknowledgeAll]
+        }
+        let status: StatusResponse = try send(blocksToSend, beepBlock: beepBlock)
         for alert in alerts {
             podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
         }
@@ -415,11 +423,11 @@ public class PodCommsSession {
         }
     }
 
-    public func insertCannula(optionalAlerts: [PodAlert] = []) throws -> TimeInterval {
+    public func insertCannula(optionalAlerts: [PodAlert] = [], silent: Bool) throws -> TimeInterval {
         let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
         let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
-        guard let activatedAt = podState.activatedAt else {
+        guard podState.activatedAt != nil else {
             throw PodCommsError.noPodPaired
         }
 
@@ -438,12 +446,12 @@ public class PodCommsSession {
             }
             podState.updateFromStatusResponse(status, at: currentDate)
         } else {
-            // Configure all the non-optional Pod Alarms
-            let expirationTime = activatedAt + Pod.nominalPodLife
-            let timeUntilExpirationAdvisory = expirationTime.timeIntervalSinceNow
-            let expirationAdvisoryAlarm = PodAlert.expired(alertTime: timeUntilExpirationAdvisory, duration: Pod.expirationAdvisoryWindow)
-            let endOfServiceTime = activatedAt + Pod.serviceDuration
-            let shutdownImminentAlarm = PodAlert.shutdownImminent((endOfServiceTime - Pod.endOfServiceImminentWindow).timeIntervalSinceNow)
+            let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0)
+            let podTime = podState.podTime + elapsed
+
+            // Configure the mandatory Pod Alerts for shutdown imminent alert (79 hours) and pod expiration alert (72 hours) along with any optional alerts
+            let shutdownImminentAlarm = PodAlert.shutdownImminent(offset: podTime, absAlertTime: Pod.serviceDuration - Pod.endOfServiceImminentWindow, silent: silent)
+            let expirationAdvisoryAlarm = PodAlert.expired(offset: podTime, absAlertTime: Pod.nominalPodLife, duration: Pod.expirationAdvisoryWindow, silent: silent)
             try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm] + optionalAlerts)
         }
         
@@ -494,7 +502,9 @@ public class PodCommsSession {
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, units: units, timeBetweenPulses: timeBetweenPulses, extendedUnits: extendedUnits, extendedDuration: extendedDuration)
         
-        if podState.unfinalizedBolus != nil {
+        // Do a getstatus to verify that there isn't an on-going bolus in progress if the last bolus command is still
+        // finalized, if the last delivery status wasn't successfully verified or the last comms attempt wasn't OK
+        if podState.unfinalizedBolus != nil || !podState.deliveryStatusVerified || !podState.lastCommsOK {
             var ongoingBolus = true
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
                 podState.updateFromStatusResponse(statusResponse, at: currentDate)
@@ -596,7 +606,8 @@ public class PodCommsSession {
     // A suspendReminder of 0 is an untimed suspend which only uses podSuspendedReminder alert beeps.
     // A suspendReminder of 1-5 minutes will only use suspendTimeExpired alert beeps.
     // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts.
-    public func suspendDelivery(suspendReminder: TimeInterval? = nil, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult {
+    // The configured alerts will set up as silent pod alerts if silent is true.
+    public func suspendDelivery(suspendReminder: TimeInterval? = nil, silent: Bool, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult {
 
         guard podState.unacknowledgedCommand == nil else {
             return .certainFailure(error: .unacknowledgedCommandPending)
@@ -607,6 +618,9 @@ public class PodCommsSession {
             var podSuspendedReminderAlert: PodAlert? = nil
             var suspendTimeExpiredAlert: PodAlert? = nil
             let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0
+            let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0)
+            let podTime = podState.podTime + elapsed
+            log.debug("suspendDelivery: podState.podTime=%@, elapsed=%.2fs, computed timeActive %@", podState.podTime.timeIntervalStr, elapsed, podTime.timeIntervalStr)
 
             let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)
             var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
@@ -614,14 +628,14 @@ public class PodCommsSession {
             // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
             if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
                 // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders
-                podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, suspendTime: suspendTime)
+                podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, offset: podTime, suspendTime: suspendTime, silent: silent)
                 alertConfigurations += [podSuspendedReminderAlert!.configuration]
             }
 
             // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
             if suspendTime > 0 {
                 // a timed suspend using a suspend time expired alert
-                suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(suspendTime: suspendTime)
+                suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(offset: podTime, suspendTime: suspendTime, silent: silent)
                 alertConfigurations += [suspendTimeExpiredAlert!.configuration]
             }
 
@@ -661,8 +675,8 @@ public class PodCommsSession {
     private func cancelSuspendAlerts() throws -> StatusResponse {
 
         do {
-            let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, suspendTime: 0)
-            let suspendTimeExpired = PodAlert.suspendTimeExpired(suspendTime: 0) // A suspendTime of 0 deactivates this alert
+            let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, offset: 0, suspendTime: 0)
+            let suspendTimeExpired = PodAlert.suspendTimeExpired(offset: 0, suspendTime: 0) // A suspendTime of 0 deactivates this alert
 
             let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired])
             return status
@@ -727,6 +741,11 @@ public class PodCommsSession {
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
 
         do {
+            if !(podState.lastCommsOK && podState.deliveryStatusVerified) {
+                // Can't trust the current delivery state -- do a cancel all
+                // to be sure that setting a basal program won't fault the pod.
+                let _: StatusResponse = try send([CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)])
+            }
             var status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
             let now = currentDate
             podState.suspendState = .resumed(now)
@@ -911,11 +930,11 @@ public class PodCommsSession {
         }
     }
     
-    public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> [AlertSlot: PodAlert] {
+    public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet {
         let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts)
         let status: StatusResponse = try send([cmd], beepBlock: beepBlock)
         podState.updateFromStatusResponse(status, at: currentDate)
-        return podState.activeAlerts
+        return podState.activeAlertSlots
     }
 
     func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) {

+ 72 - 34
Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift

@@ -1,6 +1,6 @@
 //
 //  PodState.swift
-//  OmnipodKit
+//  OmniBLE
 //
 //  Based on OmniKit/PumpManager/PodState.swift
 //  Created by Pete Schwamb on 10/13/17.
@@ -58,9 +58,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public var bleIdentifier: String
     
     public var activatedAt: Date?
-    public var expiresAt: Date?  // set based on StatusResponse timeActive and can change with Pod clock drift and/or system time change
+    public var expiresAt: Date? // set based on timeActive and can change with Pod clock drift and/or system time change
     public var activeTime: TimeInterval? // Useful after pod deactivated or faulted.
 
+    public var podTime: TimeInterval // pod time from the last response, always whole minute values
+    public var podTimeUpdated: Date? // time that the podTime value was last updated
+
     public var setupUnitsDelivered: Double?
 
     public let firmwareVersion: String
@@ -68,7 +71,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public let lotNo: UInt32
     public let lotSeq: UInt32
     public let productId: UInt8
-    var activeAlertSlots: AlertSet
+    public var activeAlertSlots: AlertSet
     public var lastInsulinMeasurements: PodInsulinMeasurements?
 
     public var unacknowledgedCommand: PendingCommand?
@@ -100,16 +103,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public var configuredAlerts: [AlertSlot: PodAlert]
     public var insulinType: InsulinType
 
-    public var activeAlerts: [AlertSlot: PodAlert] {
-        var active = [AlertSlot: PodAlert]()
-        for slot in activeAlertSlots {
-            if let alert = configuredAlerts[slot] {
-                active[slot] = alert
-            }
-        }
-        return active
-    }
-
     // Allow a grace period while the unacknowledged command is first being sent.
     public var needsCommsRecovery: Bool {
         if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight {
@@ -117,7 +110,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         }
         return false
     }
-    
+
+    // the following two vars are not persistent across app restarts
+    public var deliveryStatusVerified: Bool
+    public var lastCommsOK: Bool
+
     public init(address: UInt32, ltk: Data, firmwareVersion: String, bleFirmwareVersion: String, lotNo: UInt32, lotSeq: UInt32, productId: UInt8, messageTransportState: MessageTransportState? = nil, bleIdentifier: String, insulinType: InsulinType) {
         self.address = address
         self.ltk = ltk
@@ -134,9 +131,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.messageTransportState = messageTransportState ?? MessageTransportState(ck: nil, noncePrefix: nil)
         self.primeFinishTime = nil
         self.setupProgress = .addressAssigned
-        self.configuredAlerts = [.slot7: .waitingForPairingReminder]
+        self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder]
         self.bleIdentifier = bleIdentifier
         self.insulinType = insulinType
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
+        self.podTime = 0
     }
     
     public var unfinishedSetup: Bool {
@@ -171,18 +171,30 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public mutating func advanceToNextNonce() {
         // Dash nonce is a fixed value and is never advanced
     }
-    
+
     public var currentNonce: UInt32 {
-        let fixedNonceValue: UInt32 = 0x494E532E // Dash uses a fixed value for pod nonce
-        return fixedNonceValue // not clear if the actual value even matters
+        let fixedNonceValue: UInt32 = 0x494E532E // Dash pods require this particular fixed value
+        return fixedNonceValue
     }
-    
+
     public mutating func resyncNonce(syncWord: UInt16, sentNonce: UInt32, messageSequenceNum: Int) {
-        assert(false) // XXX ?should never be called for Dash?
+        print("resyncNonce expectedly called!") // Should never be called for Dash!
     }
-    
+
+    // Saves the current pod timeActive and will initialize the activatedAtComputed at
+    // pod startup and updates the expiresAt value to account for pod clock differences.
     private mutating func updatePodTimes(timeActive: TimeInterval) -> Date {
         let now = Date()
+
+        guard timeActive >= self.podTime else {
+            // The pod active time went backwards and thus we have an apparent reset fault.
+            // Don't update any times or displayed expiresAt time will expectedly jump.
+            return now
+        }
+
+        self.podTime = timeActive
+        self.podTimeUpdated = now
+
         let activatedAtComputed = now - timeActive
         if activatedAt == nil {
             self.activatedAt = activatedAtComputed
@@ -194,7 +206,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             // The computed expiresAt time is earlier than or more than a minute later than the current expiresAt time,
             // so use the computed expiresAt time instead to handle Pod clock drift and/or system time changes issues.
             // The more than a minute later test prevents oscillation of expiresAt based on the timing of the responses.
-            // TODO: A significant deviation expiresAt from activatedAt + nominalPodLife should generate a critical alert
             self.expiresAt = expiresAtComputed
         }
         return now
@@ -285,13 +296,25 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     
     private mutating func updateDeliveryStatus(deliveryStatus: DeliveryStatus, podProgressStatus: PodProgressStatus, bolusNotDelivered: Double, at date: Date) {
 
+        deliveryStatusVerified = true
         // See if the pod deliveryStatus indicates an active bolus or temp basal that the PodState isn't tracking (possible Loop restart)
         if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that Loop doesn't know about?
             if podProgressStatus.readyForDelivery {
+                deliveryStatusVerified = false // remember that we had inconsistent (bolus) delivery status
                 // Create an unfinalizedBolus with the remaining bolus amount to capture what we can.
                 unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusNotDelivered, startTime: date, scheduledCertainty: .certain, insulinType: insulinType, automatic: false)
             }
         }
+        if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that app isn't tracking
+            deliveryStatusVerified = false // remember that we had inconsistent (temp basal) delivery status
+            // unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: 0, startTime: Date(), duration: .minutes(30), isHighTemp: false, scheduledCertainty: .certain, insulinType: insulinType)
+        }
+        if deliveryStatus != .suspended && isSuspended { // active basal that app isn't tracking
+            deliveryStatusVerified = false // remember that we had inconsistent (basal) delivery status
+            let resumeStartTime = Date()
+            suspendState = .resumed(resumeStartTime)
+            unfinalizedResume = UnfinalizedDose(resumeStartTime: resumeStartTime, scheduledCertainty: .certain, insulinType: insulinType)
+        }
 
         if var bolus = unfinalizedBolus, !deliveryStatus.bolusing {
             // Due to clock drift or comms delays, boluses can finish earlier than we expect
@@ -362,6 +385,16 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             }
         }
 
+        if let podTime = rawValue["podTime"] as? TimeInterval,
+            let podTimeUpdated = rawValue["podTimeUpdated"] as? Date
+        {
+            self.podTime = podTime
+            self.podTimeUpdated = podTimeUpdated
+        } else {
+            self.podTime = 0
+            self.podTimeUpdated = Date()
+        }
+
         if let setupUnitsDelivered = rawValue["setupUnitsDelivered"] as? Double {
             self.setupUnitsDelivered = setupUnitsDelivered
         }
@@ -461,12 +494,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         } else {
             // Assume migration, and set up with alerts that are normally configured
             self.configuredAlerts = [
-                .slot2: .shutdownImminent(0),
-                .slot3: .expirationReminder(0),
-                .slot4: .lowReservoir(0),
-                .slot5: .podSuspendedReminder(active: false, suspendTime: 0),
-                .slot6: .suspendTimeExpired(suspendTime: 0),
-                .slot7: .expired(alertTime: 0, duration: 0)
+                .slot2ShutdownImminent: .shutdownImminent(offset: 0, absAlertTime: 0),
+                .slot3ExpirationReminder: .expirationReminder(offset: 0, absAlertTime: 0),
+                .slot4LowReservoir: .lowReservoir(units: 0),
+                .slot5SuspendedReminder: .podSuspendedReminder(active: false, offset: 0, suspendTime: 0),
+                .slot6SuspendTimeExpired: .suspendTimeExpired(offset: 0, suspendTime: 0),
+                .slot7Expired: .expired(offset: 0, absAlertTime: 0, duration: 0)
             ]
         }
         
@@ -477,6 +510,9 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         } else {
             self.insulinType = .novolog
         }
+
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
     }
     
     public var rawValue: RawValue {
@@ -508,6 +544,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         rawValue["primeFinishTime"] = primeFinishTime
         rawValue["activatedAt"] = activatedAt
         rawValue["expiresAt"] = expiresAt
+        rawValue["podTime"] = podTime
+        rawValue["podTimeUpdated"] = podTimeUpdated
         rawValue["setupUnitsDelivered"] = setupUnitsDelivered
         rawValue["activeTime"] = activeTime
 
@@ -529,6 +567,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* bleIdentifier: \(bleIdentifier)",
             "* activatedAt: \(String(reflecting: activatedAt))",
             "* expiresAt: \(String(reflecting: expiresAt))",
+            "* podTime: \(podTime.timeIntervalStr)",
+            "* podTimeUpdated: \(String(reflecting: podTimeUpdated))",
             "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))",
             "* firmwareVersion: \(firmwareVersion)",
             "* bleFirmwareVersion: \(bleFirmwareVersion)",
@@ -541,16 +581,14 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))",
             "* unfinalizedResume: \(String(describing: unfinalizedResume))",
             "* finalizedDoses: \(String(describing: finalizedDoses))",
-            "* activeAlerts: \(String(describing: activeAlerts))",
+            "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))",
             "* messageTransportState: \(String(describing: messageTransportState))",
             "* setupProgress: \(setupProgress)",
             "* primeFinishTime: \(String(describing: primeFinishTime))",
-            "* configuredAlerts: \(String(describing: configuredAlerts))",
+            "* configuredAlerts: \(configuredAlertsString(configuredAlerts: configuredAlerts))",
             "* insulinType: \(String(describing: insulinType))",
-            "* pdmRef: \(String(describing: fault?.pdmRef))",
-            "",
-            fault != nil ? String(reflecting: fault!) : "fault: nil",
-            "",
+            "* pdmRef: " + (fault?.pdmRef == nil ? "nil" : String(describing: fault!.pdmRef!)),
+            "* Fault: " + (fault == nil ? "nil" : String(describing: fault!)),
         ].joined(separator: "\n")
     }
 }

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

@@ -149,7 +149,7 @@ class DashUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
             hostedView.navigationItem.title = LocalizedString("Insulin Type", comment: "Title for insulin type selection screen")
             return hostedView
         case .deactivate:
-            let viewModel = DeactivatePodViewModel(podDeactivator: pumpManager, podAttachedToBody: pumpManager.podAttachmentConfirmed)
+            let viewModel = DeactivatePodViewModel(podDeactivator: pumpManager, podAttachedToBody: pumpManager.podAttachmentConfirmed, fault: pumpManager.state.podState?.fault)
 
             viewModel.didFinish = { [weak self] in
                 self?.stepFinished()

+ 36 - 17
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/DeactivatePodViewModel.swift

@@ -10,8 +10,8 @@ import Foundation
 import LoopKitUI
 
 public protocol PodDeactivater {
-    func deactivatePod(completion: @escaping (OmniBLEPumpManagerError?) -> ())
-    func forgetPod(completion: @escaping () -> ())
+    func deactivatePod(completion: @escaping (OmniBLEPumpManagerError?) -> Void)
+    func forgetPod(completion: @escaping () -> Void)
 }
 
 extension OmniBLEPumpManager: PodDeactivater {}
@@ -19,16 +19,6 @@ extension OmniBLEPumpManager: PodDeactivater {}
 
 class DeactivatePodViewModel: ObservableObject, Identifiable {
     
-    public var podAttachedToBody: Bool
-    
-    var instructionText: String {
-        if podAttachedToBody {
-            return LocalizedString("Please deactivate the pod. When deactivation is complete, you may remove it and pair a new pod.", comment: "Instructions for deactivate pod when pod is on body")
-        } else {
-            return LocalizedString("Please deactivate the pod. When deactivation is complete, you may pair a new pod.", comment: "Instructions for deactivate pod when pod not on body")
-        }
-    }
-    
     enum DeactivatePodViewModelState {
         case active
         case deactivating
@@ -124,9 +114,38 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
     
     var podDeactivator: PodDeactivater
 
-    init(podDeactivator: PodDeactivater, podAttachedToBody: Bool) {
+    var podAttachedToBody: Bool
+
+    var instructionText: String
+
+    init(podDeactivator: PodDeactivater, podAttachedToBody: Bool, fault: DetailedStatus?) {
+
+        var text: String = ""
+        if let faultEventCode = fault?.faultEventCode {
+            let notificationString = faultEventCode.notificationTitle
+            switch faultEventCode.faultType {
+            case .exceededMaximumPodLife80Hrs, .reservoirEmpty, .occluded:
+                // Just prepend a simple sentence with the notification string for these faults.
+                // Other occluded related 0x6? faults will be treated as a general pod error as per the PDM.
+                text = String(format: "%@. ", notificationString)
+            default:
+                // Display the fault code in decimal and hex, the fault description and the pdmRef string for other errors.
+                text = String(format: "⚠️ %1$@ (0x%2$02X)\n%3$@\n", notificationString, faultEventCode.rawValue, faultEventCode.faultDescription)
+                if let pdmRef = fault?.pdmRef {
+                    text += LocalizedString("Ref: ", comment: "PDM Ref string line") + pdmRef + "\n\n"
+                }
+            }
+        }
+
+        if podAttachedToBody {
+            text += LocalizedString("Please deactivate the pod. When deactivation is complete, you may remove it and pair a new pod.", comment: "Instructions for deactivate pod when pod is on body")
+        } else {
+            text += LocalizedString("Please deactivate the pod. When deactivation is complete, you may pair a new pod.", comment: "Instructions for deactivate pod when pod not on body")
+        }
+
         self.podDeactivator = podDeactivator
         self.podAttachedToBody = podAttachedToBody
+        self.instructionText = text
     }
     
     public func continueButtonTapped() {
@@ -137,7 +156,7 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
             podDeactivator.deactivatePod { (error) in
                 DispatchQueue.main.async {
                     if let error = error {
-                        self.state = .resultError(DeactivationError.OmnipodPumpManagerError(error))
+                        self.state = .resultError(DeactivationError.OmniBLEPumpManagerError(error))
                     } else {
                         self.discardPod(navigateOnCompletion: false)
                     }
@@ -160,18 +179,18 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
 }
 
 enum DeactivationError : LocalizedError {
-    case OmnipodPumpManagerError(OmniBLEPumpManagerError)
+    case OmniBLEPumpManagerError(OmniBLEPumpManagerError)
     
     var recoverySuggestion: String? {
         switch self {
-        case .OmnipodPumpManagerError:
+        case .OmniBLEPumpManagerError:
             return LocalizedString("There was a problem communicating with the pod. If this problem persists, tap Discard Pod. You can then activate a new Pod.", comment: "Format string for recovery suggestion during deactivate pod.")
         }
     }
     
     var errorDescription: String? {
         switch self {
-        case .OmnipodPumpManagerError(let error):
+        case .OmniBLEPumpManagerError(let error):
             return error.errorDescription
         }
     }

+ 43 - 5
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift

@@ -1,5 +1,5 @@
 //
-//  DashSettingsViewModel.swift
+//  OmniBLESettingsViewModel.swift
 //  OmniBLE
 //
 //  Created by Pete Schwamb on 3/8/20.
@@ -40,6 +40,8 @@ class OmniBLESettingsViewModel: ObservableObject {
 
     @Published var beepPreference: BeepPreference
 
+    @Published var silencePodPreference: SilencePodPreference
+
     @Published var podConnected: Bool
 
     var activatedAtString: String {
@@ -136,7 +138,7 @@ class OmniBLESettingsViewModel: ObservableObject {
 
     var recoveryText: String? {
         if case .fault = podCommState {
-            return LocalizedString("Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted")
+            return LocalizedString("⚠️ Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted")
         } else if podOk && isPodDataStale {
             return LocalizedString("Make sure your phone and pod are close to each other. If communication issues persist, move to a new area.", comment: "The action string on pod status page when pod data is stale")
         } else if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, serviceTimeRemaining <= Pod.serviceDuration - Pod.nominalPodLife {
@@ -233,6 +235,7 @@ class OmniBLESettingsViewModel: ObservableObject {
         lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
         podCommState = self.pumpManager.podCommState
         beepPreference = self.pumpManager.beepPreference
+        silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled
         podConnected = self.pumpManager.isConnected
         insulinType = self.pumpManager.insulinType
         podDetails = self.pumpManager.podDetails
@@ -262,7 +265,7 @@ class OmniBLESettingsViewModel: ObservableObject {
     }
     
     func stopUsingOmnipodDashTapped() {
-        self.pumpManager.notifyDelegateOfDeactivation {
+        pumpManager.notifyDelegateOfDeactivation {
             DispatchQueue.main.async {
                 self.didFinish?()
             }
@@ -321,10 +324,30 @@ class OmniBLESettingsViewModel: ObservableObject {
         }
     }
 
+    func readPodStatus(_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) {
+        pumpManager.getDetailedStatus() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
+    func readPulseLog(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readPulseLog() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
     func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
         pumpManager.playTestBeeps(completion: completion)
     }
 
+    func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) {
+        completion(pumpManager.debugDescription)
+    }
+
     func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
         pumpManager.setConfirmationBeeps(newPreference: preference) { error in
             DispatchQueue.main.async {
@@ -336,6 +359,17 @@ class OmniBLESettingsViewModel: ObservableObject {
         }
     }
 
+    func setSilencePod(_ silencePodPreference: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
+        pumpManager.setSilencePod(silencePod: silencePodPreference == .enabled) { error in
+            DispatchQueue.main.async {
+                if error == nil {
+                    self.silencePodPreference = silencePodPreference
+                }
+                completion(error)
+            }
+        }
+    }
+
     func didChangeInsulinType(_ newType: InsulinType?) {
         self.pumpManager.insulinType = newType
     }
@@ -351,6 +385,10 @@ class OmniBLESettingsViewModel: ObservableObject {
         }
     }
 
+    var noPod: Bool {
+        return podCommState == .noPod
+    }
+
     var podError: String? {
         switch podCommState {
         case .fault(let status):
@@ -362,11 +400,11 @@ class OmniBLESettingsViewModel: ObservableObject {
             case .occluded, .occlusionCheckStartup1, .occlusionCheckStartup2, .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold, .occlusionCheckValueTooHigh:
                 return LocalizedString("Pod Occlusion", comment: "Error message for reservoir view when pod occlusion checks failed")
             default:
-                return LocalizedString("Pod Error", comment: "Error message for reservoir view during general pod fault")
+                return String(format: LocalizedString("Pod Fault %1$03d", comment: "Error message for reservoir view during general pod fault: (1: fault code value)"), status.faultEventCode.rawValue)
             }
         case .active:
             if isPodDataStale {
-                return LocalizedString("Signal Loss", comment: "Error message for reservoir view during general pod fault")
+                return LocalizedString("Signal Loss", comment: "Error message for reservoir view during signal loss")
             } else {
                 return nil
             }

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

@@ -85,7 +85,7 @@ enum PodLifeState {
         case .podDeactivating:
             return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation")
         default:
-            return LocalizedString("Replace Pod", comment: "Settings page link description when next lifecycle action is to replace pod")
+            return LocalizedString("Deactivate Pod", comment: "Settings page link description when next lifecycle action is to deactivate pod")
         }
     }
     

+ 36 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ActivityView.swift

@@ -0,0 +1,36 @@
+//
+//  ActivityView.swift
+//  OmniBLE
+//
+//  Created by Joe Moran on 9/17/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+
+struct ActivityView: UIViewControllerRepresentable {
+    @Binding var isPresented: Bool
+    let activityItems: [Any]
+
+    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
+        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
+        controller.completionWithItemsHandler = { (_, _, _, _) in
+            self.isPresented = false
+        }
+        return controller
+    }
+
+    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
+    }
+}
+
+fileprivate struct ActivityViewController: UIViewControllerRepresentable {
+    var activityItems: [Any]
+    var applicationActivities: [UIActivity]? = nil
+
+    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
+        return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
+    }
+
+    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
+}

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/AttachPodView.swift

@@ -33,7 +33,7 @@ struct AttachPodView: View {
                 HStack {
                     InstructionList(instructions: [
                         LocalizedString("Prepare site.", comment: "Label text for step one of attach pod instructions"),
-                        LocalizedString("Remove blue Pod needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"),
+                        LocalizedString("Remove the Pod's blue needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"),
                         LocalizedString("Check Pod, apply to site, then confirm pod attachment.", comment: "Label text for step three of attach pod instructions")
                     ])
                 }

+ 5 - 5
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/BeepPreferenceSelectionView.swift

@@ -38,7 +38,7 @@ struct BeepPreferenceSelectionView: View {
         VStack {
             List {
                 Section {
-                    Text(LocalizedString("Confidence reminders are beeps from the pod which can be used to acknowledge selected commands.", comment: "Help text for BeepPreferenceSelectionView")).fixedSize(horizontal: false, vertical: true)
+                    Text(LocalizedString("Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced.", comment: "Help text for BeepPreferenceSelectionView")).fixedSize(horizontal: false, vertical: true)
                         .padding(.vertical, 10)
                 }
 
@@ -109,15 +109,15 @@ struct BeepPreferenceSelectionView: View {
 
     private var cancelButton: some View {
         Button(action: { self.presentationMode.wrappedValue.dismiss() } ) {
-            Text(LocalizedString("Cancel", comment: "Button title for cancelling low reservoir reminder edit"))
+            Text(LocalizedString("Cancel", comment: "Button title for cancelling confidence reminders edit"))
         }
     }
 
     var saveButtonText: String {
         if saving {
-            return LocalizedString("Saving...", comment: "button title for saving low reservoir reminder while saving")
+            return LocalizedString("Saving...", comment: "button title for saving confidence reminder while saving")
         } else {
-            return LocalizedString("Save", comment: "button title for saving low reservoir reminder")
+            return LocalizedString("Save", comment: "button title for saving confidence reminder")
         }
     }
 
@@ -134,7 +134,7 @@ struct BeepPreferenceSelectionView: View {
 
 }
 
-struct ContentView_Previews: PreviewProvider {
+struct BeepPreferenceSelectionView_Previews: PreviewProvider {
     static var previews: some View {
         NavigationView {
             BeepPreferenceSelectionView(initialValue: .extended) { selectedValue, completion in

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ExpirationReminderPickerView.swift

@@ -61,6 +61,6 @@ struct ExpirationReminderPickerView: View {
 
 struct ExpirationReminderPickerView_Previews: PreviewProvider {
     static var previews: some View {
-        ExpirationReminderPickerView(expirationReminderDefault: .constant(2))
+        ExpirationReminderPickerView(expirationReminderDefault: .constant(2), showingHourPicker: true)
     }
 }

+ 30 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/FirstAppear.swift

@@ -0,0 +1,30 @@
+//
+//  FirstAppear.swift
+//  OmniBLE
+//
+//  Created by Joe Moran on 9/24/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+
+extension View {
+    func onFirstAppear(_ action: @escaping () -> ()) -> some View {
+        modifier(FirstAppear(action: action))
+    }
+}
+
+private struct FirstAppear: ViewModifier {
+    let action: () -> ()
+
+    // State used to insure action is invoked here only once
+    @State private var hasAppeared = false
+
+    func body(content: Content) -> some View {
+        content.onAppear {
+            guard !hasAppeared else { return }
+            hasAppeared = true
+            action()
+        }
+    }
+}

+ 2 - 5
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ManualTempBasalEntryView.swift

@@ -95,7 +95,7 @@ struct ManualTempBasalEntryView: View {
                     .frame(maxHeight: 162.0)
                     .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert })
                     Section {
-                        Text(LocalizedString("Loop will not automatically adjust your insulin delivery until the temporary basal rate finishes or is canceled.", comment: "Description text on manual temp basal action sheet"))
+                        Text(LocalizedString("Your insulin delivery will not be automatically adjusted until the temporary basal rate finishes or is canceled.", comment: "Description text on manual temp basal action sheet"))
                             .font(.footnote)
                             .foregroundColor(.secondary)
                             .fixedSize(horizontal: false, vertical: true)
@@ -147,7 +147,7 @@ struct ManualTempBasalEntryView: View {
     var missingConfigAlert: SwiftUI.Alert {
         return SwiftUI.Alert(
             title: Text(LocalizedString("Missing Config", comment: "Alert title for missing temp basal configuration")),
-            message: Text(LocalizedString("This PumpManager has not been configured with a maximum basal rate because it was added before manual temp basal was a feature. Please go to therapy settings -> delivery limits and set a new maximum basal rate.", comment: "Alert format string for missing temp basal configuration."))
+            message: Text(LocalizedString("This Pump has not been configured with a maximum basal rate because it was added before manual temp basal was a feature. Please go to Pump Settings in the settings CONFIGURATION section to set a new Max Basal.", comment: "Alert format string for missing temp basal configuration."))
         )
     }
 
@@ -158,7 +158,4 @@ struct ManualTempBasalEntryView: View {
         }
         .accessibility(identifier: "button_cancel")
     }
-
 }
-
-

+ 11 - 8
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/NotificationSettingsView.swift

@@ -34,15 +34,15 @@ struct NotificationSettingsView: View {
     var body: some View {
         RoundedCardScrollView {
             RoundedCard(
-                title: LocalizedString("Omnipod Reminders", comment: "Title for omnipod reminders section"),
-                footer: LocalizedString("The app configures a reminder on the pod to notify you in advance of Pod expiration. Set the number of hours advance notice you would like to configure when pairing a new Pod.", comment: "Footer text for omnipod reminders section")
+                title: LocalizedString("Pod Reminders", comment: "Title for pod reminders section"),
+                footer: LocalizedString("The app configures a reminder on the Pod to notify you in advance of Pod expiration. Set the number of hours advance notice you would like to configure by default when pairing a new Pod.", comment: "Footer text for pod reminders section")
             ) {
                 ExpirationReminderPickerView(expirationReminderDefault: $expirationReminderDefault)
             }
 
             if let allowedDates = allowedScheduledReminderDates {
                 RoundedCard(
-                    footer: LocalizedString("This is a reminder that you scheduled when you paired your current Pod.", comment: "Footer text for scheduled reminder area"))
+                    footer: LocalizedString("The expiration reminder time for the current Pod.", comment: "Footer text for scheduled reminder area"))
                 {
                     Text(LocalizedString("Scheduled Reminder", comment: "Title of scheduled reminder card on NotificationSettingsView"))
                     Divider()
@@ -50,13 +50,13 @@ struct NotificationSettingsView: View {
                 }
             }
 
-            RoundedCard(footer: LocalizedString("The App notifies you when the amount of insulin in the Pod reaches this level.", comment: "Footer text for low reservoir value row")) {
+            RoundedCard(footer: LocalizedString("The app notifies you when the amount of insulin in the Pod reaches this level.", comment: "Footer text for low reservoir value row")) {
                 lowReservoirValueRow
             }
 
             RoundedCard<EmptyView>(
                 title: LocalizedString("Critical Alerts", comment: "Title for critical alerts description"),
-                footer: LocalizedString("The reminders above will not sound if your device is in Silent or Do Not Disturb mode.\n\nThere are other critical Pod alerts and alarms that will sound even if your device is set to Silent or Do Not Disturb mode.", comment: "Description text for critical alerts")
+                footer: LocalizedString("The above reminders will not sound in the app if your device is in Silent or Do Not Disturb mode. There are other critical Pod alerts that will sound in the app even if your device is set to Silent or Do Not Disturb mode.\n\nThe Pod will also use audible beeps for all Pod reminders and alerts except when the Pod is Silenced.", comment: "Description text for critical alerts")
             )
         }
         .navigationBarTitle(LocalizedString("Notification Settings", comment: "navigation title for notification settings"))
@@ -66,7 +66,8 @@ struct NotificationSettingsView: View {
     
     private func scheduledReminderRow(scheduledDate: Date?, allowedDates: [Date]) -> some View {
         Group {
-            if let scheduledDate = scheduledDate, scheduledDate <= Date() {
+            // Make the expiration reminder time read-only if there aren't any more available times.
+            if allowedDates.isEmpty {
                 scheduledReminderRowContents(disclosure: false)
             } else {
                 NavigationLink(
@@ -124,14 +125,16 @@ struct NotificationSettingsView: View {
 struct NotificationSettingsView_Previews: PreviewProvider {
     static var previews: some View {
         return Group {
+            let now = Date()
             NavigationView {
-                NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: Date(), allowedScheduledReminderDates: [Date()], lowReservoirReminderValue: 20)
+                NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: now + TimeInterval(hours: 1), allowedScheduledReminderDates: [now, now - TimeInterval(hours: 2), now - TimeInterval(hours: 3)], lowReservoirReminderValue: 20)
                     .previewDevice(PreviewDevice(rawValue:"iPod touch (7th generation)"))
                     .previewDisplayName("iPod touch (7th generation)")
             }
 
             NavigationView {
-                NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: Date(), allowedScheduledReminderDates: [Date()], lowReservoirReminderValue: 20)
+                let now = Date()
+                NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: now + TimeInterval(hours: 1), allowedScheduledReminderDates: [now, now - TimeInterval(hours: 2), now - TimeInterval(hours: 3)], lowReservoirReminderValue: 20)
                     .colorScheme(.dark)
                     .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
                     .previewDisplayName("iPhone XS Max - Dark")

+ 54 - 24
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift

@@ -12,11 +12,11 @@ import LoopKitUI
 import HealthKit
 
 struct OmniBLESettingsView: View  {
-    
+
     @ObservedObject var viewModel: OmniBLESettingsViewModel
-    
+
     @State private var showingDeleteConfirmation = false
-    
+
     @State private var showSuspendOptions = false
 
     @State private var showManualTempBasalOptions = false
@@ -28,7 +28,7 @@ struct OmniBLESettingsView: View  {
     @State private var cancelingTempBasal = false
 
     var supportedInsulinTypes: [InsulinType]
-    
+
     @Environment(\.guidanceColors) var guidanceColors
     @Environment(\.insulinTintColor) var insulinTintColor
     
@@ -243,7 +243,7 @@ struct OmniBLESettingsView: View  {
     }
     
     private var doneButton: some View {
-        Button("Done", action: {
+        Button(LocalizedString("Done", comment: "Title of done button on OmniBLESettingsView"), action: {
             self.viewModel.doneTapped()
         })
     }
@@ -279,7 +279,7 @@ struct OmniBLESettingsView: View  {
                     headerImage
 
                     lifecycleProgress
-                    
+
                     HStack(alignment: .top) {
                         deliveryStatus
                         Spacer()
@@ -302,7 +302,7 @@ struct OmniBLESettingsView: View  {
                     }.padding(.vertical, 8)
                 }
             }
-            
+
             Section(header: SectionHeader(label: LocalizedString("Activity", comment: "Section header for activity section"))) {
                 suspendResumeRow()
                     .disabled(!self.viewModel.podOk)
@@ -343,8 +343,8 @@ struct OmniBLESettingsView: View  {
                     manualTempBasalRow
                 }
             }
-            .disabled(cancelingTempBasal)
-            
+            .disabled(cancelingTempBasal || !self.viewModel.podOk)
+
             Section() {
                 HStack {
                     FrameworkLocalText("Pod Activated", comment: "Label for pod insertion row")
@@ -352,7 +352,7 @@ struct OmniBLESettingsView: View  {
                     Text(self.viewModel.activatedAtString)
                         .foregroundColor(Color.secondary)
                 }
-                
+
                 HStack {
                     if let expiresAt = viewModel.expiresAt, expiresAt < Date() {
                         FrameworkLocalText("Pod Expired", comment: "Label for pod expiration row, past tense")
@@ -363,21 +363,34 @@ struct OmniBLESettingsView: View  {
                     Text(self.viewModel.expiresAtString)
                         .foregroundColor(Color.secondary)
                 }
-                
+
                 if let podDetails = self.viewModel.podDetails {
-                    NavigationLink(destination: PodDetailsView(podDetails: podDetails, title: LocalizedString("Device Details", comment: "title for device details page"))) {
-                        FrameworkLocalText("Device Details", comment: "Text for device details disclosure row").foregroundColor(Color.primary)
+                    NavigationLink(destination: PodDetailsView(podDetails: podDetails, title: LocalizedString("Pod Details", comment: "title for pod details page"))) {
+                        FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row").foregroundColor(Color.primary)
+                    }
+                } else {
+                    HStack {
+                        FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row")
+                        Spacer()
+                        Text("—")
+                            .foregroundColor(Color.secondary)
+                    }
+                }
+
+                if let previousPodDetails = viewModel.previousPodDetails {
+                    NavigationLink(destination: PodDetailsView(podDetails: previousPodDetails, title: LocalizedString("Previous Pod", comment: "title for previous pod page"))) {
+                        FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row").foregroundColor(Color.primary)
                     }
                 } else {
                     HStack {
-                        FrameworkLocalText("Device Details", comment: "Text for device details disclosure row")
+                        FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row")
                         Spacer()
                         Text("—")
                             .foregroundColor(Color.secondary)
                     }
                 }
             }
-            
+
             Section() {
                 Button(action: {
                     self.viewModel.navigateTo?(self.viewModel.lifeState.nextPodLifecycleAction)
@@ -386,7 +399,7 @@ struct OmniBLESettingsView: View  {
                         .foregroundColor(self.viewModel.lifeState.nextPodLifecycleActionColor)
                 }
             }
-            
+
             Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "Section header for configuration section")))
             {
                 NavigationLink(destination:
@@ -409,9 +422,17 @@ struct OmniBLESettingsView: View  {
                             .foregroundColor(.secondary)
                     }
                 }
+                NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) {
+                    HStack {
+                        FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link").foregroundColor(Color.primary)
+                        Spacer()
+                        Text(viewModel.silencePodPreference.title)
+                            .foregroundColor(.secondary)
+                    }
+                }
                 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)
+                        FrameworkLocalText("Insulin Type", comment: "Text for insulin type navigation link").foregroundColor(Color.primary)
                         if let currentTitle = viewModel.insulinType?.brandName {
                             Spacer()
                             Text(currentTitle)
@@ -420,7 +441,7 @@ struct OmniBLESettingsView: View  {
                     }
                 }
             }
-            
+
             Section() {
                 HStack {
                     FrameworkLocalText("Pump Time", comment: "The title of the command to change pump time zone")
@@ -451,14 +472,23 @@ struct OmniBLESettingsView: View  {
                 }
             }
 
-            if let previousPodDetails = viewModel.previousPodDetails {
-                Section() {
-                    NavigationLink(destination: PodDetailsView(podDetails: previousPodDetails, title: LocalizedString("Previous Pod", comment: "title for previous pod page"))) {
-                        FrameworkLocalText("Previous Pod Information", comment: "Text for previous pod information row").foregroundColor(Color.primary)
-                    }
+            Section(header: SectionHeader(label: LocalizedString("Diagnostics", comment: "Section header for diagnostic section"))) {
+                NavigationLink(destination: ReadPodStatusView(toRun: viewModel.readPodStatus)) {
+                    FrameworkLocalText("Read Pod Status", comment: "Text for read pod status navigation link").foregroundColor(Color.primary)
+                }
+                .disabled(self.viewModel.noPod)
+                NavigationLink(destination: ReadPulseLogView(toRun: viewModel.readPulseLog)) {
+                    FrameworkLocalText("Read Pulse Log", comment: "Text for read pulse log navigation link").foregroundColor(Color.primary)
+                }
+                .disabled(self.viewModel.noPod)
+                NavigationLink(destination: PlayTestBeepsView(toRun: viewModel.playTestBeeps)) {
+                    FrameworkLocalText("Play Test Beeps", comment: "Text for play test beeps navigation link").foregroundColor(Color.primary)
+                }
+                .disabled(!self.viewModel.podOk)
+                NavigationLink(destination: PumpManagerDetailsView(toRun: viewModel.pumpManagerDetails)) {
+                    FrameworkLocalText("Pump Manager Details", comment: "Text for pump manager details navigation link").foregroundColor(Color.primary)
                 }
             }
-            
 
             if self.viewModel.lifeState.allowsPumpManagerRemoval {
                 Section() {

+ 101 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PlayTestBeepsView.swift

@@ -0,0 +1,101 @@
+//
+//  PlayTestBeepsView.swift
+//  OmniBLE
+//
+//  Created by Joe Moran on 9/1/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+import LoopKit
+
+
+struct PlayTestBeepsView: View {
+    @Environment(\.horizontalSizeClass) var horizontalSizeClass
+    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
+
+    private var toRun: ((_ completion: @escaping (_ result: Error?) -> Void) -> Void)?
+
+    @State private var alertIsPresented: Bool = false
+    @State private var displayString: String = ""
+    @State private var successMessage = LocalizedString("Play test beeps command sent successfully.\n\nIf you did not hear any beeps from your Pod, the piezo speaker in your Pod may be broken or disabled.", comment: "Success message for play test beeps")
+    @State private var error: Error? = nil
+    @State private var executing: Bool = false
+    @State private var showActivityView = false
+
+    init(toRun: @escaping (_ completion: @escaping (_ result: Error?) -> Void) -> Void) {
+        self.toRun = toRun
+    }
+
+    var body: some View {
+        VStack {
+            List {
+                Section {
+                    Text(self.displayString).fixedSize(horizontal: false, vertical: true)
+                }
+            }
+            VStack {
+                Button(action: {
+                    asyncAction()
+                }) {
+                    Text(buttonText)
+                        .actionButtonStyle(.primary)
+                }
+                .padding()
+                .disabled(executing)
+            }
+            .padding(self.horizontalSizeClass == .regular ? .bottom : [])
+            .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
+        }
+        .insetGroupedListStyle()
+        .navigationTitle(LocalizedString("Play Test Beeps", comment: "navigation title for play test beeps"))
+        .navigationBarTitleDisplayMode(.inline)
+        .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
+        .onFirstAppear {
+            asyncAction()
+        }
+    }
+
+    private func asyncAction () {
+        DispatchQueue.global(qos: .utility).async {
+            executing = true
+            self.displayString = ""
+            toRun?() { (error) in
+                if let error = error {
+                    self.displayString = ""
+                    self.error = error
+                    self.alertIsPresented = true
+                } else {
+                    self.displayString = successMessage
+                }
+                executing = false
+            }
+        }
+    }
+
+    private var buttonText: String {
+        if executing {
+            return LocalizedString("Playing Test Beeps...", comment: "button title when executing play test beeps")
+        } else {
+            return LocalizedString("Play Test Beeps", comment: "button title to play test beeps")
+        }
+    }
+
+    private func alert(error: Error?) -> SwiftUI.Alert {
+        return SwiftUI.Alert(
+            title: Text(LocalizedString("Failed to play test beeps.", comment: "Alert title for error when playing test beeps")),
+            message: Text(error?.localizedDescription ?? "No Error")
+        )
+    }
+
+}
+
+struct PlayTestBeepsView_Previews: PreviewProvider {
+    static var previews: some View {
+        NavigationView {
+            PlayTestBeepsView() { completion in
+                completion(nil)
+            }
+        }
+    }
+}

+ 4 - 7
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PodDetailsView.swift

@@ -92,12 +92,12 @@ struct PodDetailsView: View {
                 row(LocalizedString("Device Name", comment: "description label for device name pod details row"), value: deviceName)
             }
             row(LocalizedString("Lot Number", comment: "description label for lot number pod details row"), value: String(describing: podDetails.lotNumber))
-            row(LocalizedString("Sequence Number", comment: "description label for sequence number pod details row"), value: String(describing: podDetails.sequenceNumber))
+            row(LocalizedString("Sequence Number", comment: "description label for sequence number pod details row"), value: String(format: "%07d", podDetails.sequenceNumber))
             row(LocalizedString("Firmware Version", comment: "description label for firmware version pod details row"), value: podDetails.firmwareVersion)
             row(LocalizedString("BLE Firmware Version", comment: "description label for ble firmware version pod details row"), value: podDetails.bleFirmwareVersion)
             row(LocalizedString("Total Delivery", comment: "description label for total delivery pod details row"), value: totalDeliveryText)
             if let activeTime = podDetails.activeTime, let activatedAt = podDetails.activatedAt {
-                row(LocalizedString("Pod Activated", comment: "description label for activated at timne pod details row"), value: dateFormatter.string(from: activatedAt))
+                row(LocalizedString("Pod Activated", comment: "description label for activated at time pod details row"), value: dateFormatter.string(from: activatedAt))
                 row(LocalizedString("Active Time", comment: "description label for active time pod details row"), value: activeTimeText(activeTime))
             } else {
                 row(LocalizedString("Last Status", comment: "description label for last status date pod details row"), value: lastStatusText)
@@ -111,10 +111,7 @@ struct PodDetailsView: View {
                             Text(LocalizedString("Pod Fault Details", comment: "description label for pod fault details"))
                                 .fontWeight(.semibold)
                         }.padding(.vertical, 4)
-                        Text(String(describing: fault))
-                            .fixedSize(horizontal: false, vertical: true)
-                            .foregroundColor(.secondary)
-                        Text("Ref: " + pdmRef)
+                        Text(String(format: LocalizedString("Internal Pod fault code %1$03d\n%2$@\nRef: %3$@\n", comment: "The format string for the pod fault info: (1: fault code) (2: fault description) (3: pdm ref string)"), fault.rawValue, fault.faultDescription, pdmRef))
                             .fixedSize(horizontal: false, vertical: true)
                             .foregroundColor(.secondary)
                     }
@@ -127,6 +124,6 @@ struct PodDetailsView: View {
 
 struct PodDetailsView_Previews: PreviewProvider {
     static var previews: some View {
-        PodDetailsView(podDetails: PodDetails(lotNumber: 0x1234, sequenceNumber: 0x1234, firmwareVersion: "1.1.1", bleFirmwareVersion: "2.2.2", deviceName: "PreviewPod", totalDelivery: 10, lastStatus: Date(), fault: FaultEventCode(rawValue: 0x67), activatedAt: Date().addingTimeInterval(.days(1))), title: "Device Details")
+        PodDetailsView(podDetails: PodDetails(lotNumber: 123456789, sequenceNumber: 1234567, firmwareVersion: "4.3.2", bleFirmwareVersion: "1.2.3", deviceName: "DashPreviewPod", totalDelivery: 99, lastStatus: Date(), fault: FaultEventCode(rawValue: 064), activatedAt: Date().addingTimeInterval(.days(2)), pdmRef: "19-02448-09951-064"), title: "Device Details")
     }
 }

Разница между файлами не показана из-за своего большого размера
+ 100 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PumpManagerDetailsView.swift


+ 167 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPodStatusView.swift

@@ -0,0 +1,167 @@
+//
+//  ReadPodStatusView.swift
+//  OmniBLE
+//
+//  Created by Joe Moran on 8/15/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+import LoopKit
+
+private func podStatusString(status: DetailedStatus) -> String {
+    var result, str: String
+
+    let formatter = DateComponentsFormatter()
+    formatter.unitsStyle = .full
+    formatter.allowedUnits = [.hour, .minute]
+    formatter.unitsStyle = .short
+    if let timeStr = formatter.string(from: status.timeActive) {
+        str = timeStr
+    } else {
+        str = String(format: LocalizedString("%1$@ minutes", comment: "The format string for minutes (1: number of minutes string)"), String(describing: Int(status.timeActive / 60)))
+    }
+    result = String(format: LocalizedString("Pod Active: %1$@", comment: "The format string for Pod Active: (1: formatted time)"), str)
+
+    result += String(format: LocalizedString("\nPod Progress: %1$@", comment: "The format string for Pod Progress: (1: pod progress string)"), String(describing: status.podProgressStatus))
+
+    result += String(format: LocalizedString("\nDelivery Status: %1$@", comment: "The format string for Delivery Status: (1: delivery status string)"), String(describing: status.deliveryStatus))
+
+    result += String(format: LocalizedString("\nLast Programming Seq Num: %1$@", comment: "The format string for last programming sequence number: (1: last programming sequence number)"), String(describing: status.lastProgrammingMessageSeqNum))
+
+    result += String(format: LocalizedString("\nBolus Not Delivered: %1$@ U", comment: "The format string for Bolus Not Delivered: (1: bolus not delivered string)"), status.bolusNotDelivered.twoDecimals)
+
+    result += String(format: LocalizedString("\nPulse Count: %1$d", comment: "The format string for Pulse Count (1: pulse count)"), Int(round(status.totalInsulinDelivered / Pod.pulseSize)))
+
+    result += String(format: LocalizedString("\nReservoir Level: %1$@ U", comment: "The format string for Reservoir Level: (1: reservoir level string)"), status.reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : status.reservoirLevel.twoDecimals)
+
+    result += String(format: LocalizedString("\nAlerts: %1$@", comment: "The format string for Alerts: (1: the alerts string)"), alertSetString(alertSet: status.unacknowledgedAlerts))
+
+    if status.radioRSSI != 0 {
+        result += String(format: LocalizedString("\nRSSI: %1$@", comment: "The format string for RSSI: (1: RSSI value)"), String(describing: status.radioRSSI))
+        result += String(format: LocalizedString("\nReceiver Low Gain: %1$@", comment: "The format string for receiverLowGain: (1: receiverLowGain)"), String(describing: status.receiverLowGain))
+    }
+
+    if status.faultEventCode.faultType != .noFaults {
+        // report the additional fault related information in a separate section
+        result += String(format: LocalizedString("\n\n⚠️ Critical Pod Fault %1$03d (0x%2$02X)", comment: "The format string for fault code in decimal and hex: (1: fault code for decimal display) (2: fault code for hex display)"), status.faultEventCode.rawValue, status.faultEventCode.rawValue)
+        result += String(format: "\n%1$@", status.faultEventCode.faultDescription)
+        if let faultEventTimeSinceActivation = status.faultEventTimeSinceActivation,
+           let faultTimeStr = formatter.string(from: faultEventTimeSinceActivation)
+        {
+            result += String(format: LocalizedString("\nFault Time: %1$@", comment: "The format string for fault time: (1: fault time string)"), faultTimeStr)
+        }
+        if let errorEventInfo = status.errorEventInfo {
+            result += String(format: LocalizedString("\nFault Event Info: %1$03d (0x%2$02X),", comment: "The format string for fault event info: (1: fault event info)"), errorEventInfo.rawValue, errorEventInfo.rawValue)
+            result += String(format: LocalizedString("\n  Insulin State Table Corrupted: %@", comment: "The format string for insulin state table corrupted: (1: insulin state corrupted)"), String(describing: errorEventInfo.insulinStateTableCorruption))
+            result += String(format: LocalizedString("\n  Occlusion Type: %1$@", comment: "The format string for occlusion type: (1: occlusion type)"), String(describing: errorEventInfo.occlusionType))
+            result += String(format: LocalizedString("\n  Immediate Bolus In Progress: %1$@", comment: "The format string for immediate bolus in progress: (1: immediate bolus in progress)"), String(describing: errorEventInfo.immediateBolusInProgress))
+            result += String(format: LocalizedString("\n  Previous Pod Progress: %1$@", comment: "The format string for previous pod progress: (1: previous pod progress string)"), String(describing: errorEventInfo.podProgressStatus))
+        }
+        if let pdmRef = status.pdmRef {
+            result += String(format: LocalizedString("\nRef: %@", comment: "The Ref format string (1: pdm ref string)"), pdmRef)
+        }
+    }
+
+    return result
+}
+
+struct ReadPodStatusView: View {
+    @Environment(\.horizontalSizeClass) var horizontalSizeClass
+    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
+
+    private var toRun: ((_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) -> Void)?
+
+    @State private var alertIsPresented: Bool = false
+    @State private var displayString: String = ""
+    @State private var error: LocalizedError? = nil
+    @State private var executing: Bool = false
+    @State private var showActivityView: Bool = false
+
+    init(toRun: @escaping (_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) -> Void) {
+        self.toRun = toRun
+    }
+
+    var body: some View {
+        VStack {
+            List {
+                Section {
+                    Text(self.displayString).fixedSize(horizontal: false, vertical: true)
+                }
+            }
+            .toolbar {
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button(action: {
+                        self.showActivityView = true
+                    }) {
+                        Image(systemName: "square.and.arrow.up")
+                    }
+                }
+            }.sheet(isPresented: $showActivityView) {
+                ActivityView(isPresented: $showActivityView, activityItems: [self.displayString])
+            }
+            VStack {
+                Button(action: {
+                    asyncAction()
+                }) {
+                    Text(buttonText)
+                        .actionButtonStyle(.primary)
+                }
+                .padding()
+                .disabled(executing)
+            }
+            .padding(self.horizontalSizeClass == .regular ? .bottom : [])
+            .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
+        }
+        .insetGroupedListStyle()
+        .navigationTitle(LocalizedString("Read Pod Status", comment: "navigation title for read pod status"))
+        .navigationBarTitleDisplayMode(.inline)
+        .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
+        .onFirstAppear {
+            asyncAction()
+        }
+    }
+
+    private func asyncAction () {
+        DispatchQueue.global(qos: .utility).async {
+            executing = true
+            self.displayString = ""
+            toRun?() { (result) in
+                switch result {
+                case .success(let detailedStatus):
+                    self.displayString = podStatusString(status: detailedStatus)
+                case .failure(let error):
+                    self.error = error
+                    self.alertIsPresented = true
+                }
+                executing = false
+            }
+        }
+    }
+
+    private var buttonText: String {
+        if executing {
+            return LocalizedString("Reading Pod Status...", comment: "button title when executing read pod status")
+        } else {
+            return LocalizedString("Read Pod Status", comment: "button title to read pod status")
+        }
+    }
+
+    private func alert(error: Error?) -> SwiftUI.Alert {
+        return SwiftUI.Alert(
+            title: Text(LocalizedString("Failed to read pod status.", comment: "Alert title for error when reading pod status")),
+            message: Text(error?.localizedDescription ?? "No Error")
+        )
+    }
+}
+
+struct ReadPodStatusView_Previews: PreviewProvider {
+    static var previews: some View {
+        NavigationView {
+            let detailedStatus = try! DetailedStatus(encodedData: Data([0x02, 0x0d, 0x00, 0x00, 0x00, 0x0e, 0x00, 0xc3, 0x6a, 0x02, 0x07, 0x03, 0xff, 0x02, 0x09, 0x20, 0x00, 0x28, 0x00, 0x08, 0x00, 0x82]))
+            ReadPodStatusView() { completion in
+                completion(.success(detailedStatus))
+            }
+        }
+    }
+ }

+ 128 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPulseLogView.swift

@@ -0,0 +1,128 @@
+//
+//  ReadPulseLogView.swift
+//  OmniBLE
+//
+//  Created by Joe Moran on 9/1/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+import LoopKit
+
+
+struct ReadPulseLogView: View {
+    @Environment(\.horizontalSizeClass) var horizontalSizeClass
+    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
+
+    private var toRun: ((_ completion: @escaping (_ result: Result<String, Error>) -> Void) -> Void)?
+
+    @State private var alertIsPresented: Bool = false
+    @State private var displayString: String = ""
+    @State private var error: Error? = nil
+    @State private var executing: Bool = false
+    @State private var showActivityView: Bool = false
+
+    init(toRun: @escaping (_ completion: @escaping (_ result: Result<String, Error>) -> Void) -> Void) {
+        self.toRun = toRun
+    }
+
+    var body: some View {
+        VStack {
+            List {
+                Section {
+                    let myFont = Font
+                        .system(size: 12)
+                        .monospaced()
+                    Text(self.displayString)
+                        .font(myFont)
+                }
+            }
+            .toolbar {
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button(action: {
+                        self.showActivityView = true
+                    }) {
+                        Image(systemName: "square.and.arrow.up")
+                    }
+                }
+            }.sheet(isPresented: $showActivityView) {
+                ActivityView(isPresented: $showActivityView, activityItems: [self.displayString])
+            }
+            VStack {
+                Button(action: {
+                    asyncAction()
+                }) {
+                    Text(buttonText)
+                        .actionButtonStyle(.primary)
+                }
+                .padding()
+                .disabled(executing)
+            }
+            .padding(self.horizontalSizeClass == .regular ? .bottom : [])
+            .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
+        }
+        .insetGroupedListStyle()
+        .navigationTitle(LocalizedString("Read Pulse Log", comment: "navigation title for read pulse log"))
+        .navigationBarTitleDisplayMode(.inline)
+        .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
+        .onFirstAppear {
+            asyncAction()
+        }
+    }
+
+    private func asyncAction () {
+        DispatchQueue.global(qos: .utility).async {
+            executing = true
+            self.displayString = ""
+            toRun?() { (result) in
+                switch result {
+                case .success(let pulseLogString):
+                    self.displayString = pulseLogString
+                case .failure(let error):
+                    self.displayString = ""
+                    self.error = error
+                    self.alertIsPresented = true
+                }
+                executing = false
+            }
+        }
+    }
+
+    private var buttonText: String {
+        if executing {
+            return LocalizedString("Reading Pulse Log...", comment: "button title when executing read pulse log")
+        } else {
+            return LocalizedString("Read Pulse Log", comment: "button title to read pulse log")
+        }
+    }
+
+    private func alert(error: Error?) -> SwiftUI.Alert {
+        return SwiftUI.Alert(
+            title: Text(LocalizedString("Failed to read pulse log.", comment: "Alert title for error when reading pulse log")),
+            message: Text(error?.localizedDescription ?? "No Error")
+        )
+    }
+}
+
+struct ReadPulsePodLogView_Previews: PreviewProvider {
+    static var previews: some View {
+        ReadPulseLogView() { completion in
+            let podInfoPulseLogRecent = try! PodInfoPulseLogRecent(encodedData: Data([0x50, 0x03, 0x17,
+                0x39, 0x72, 0x58, 0x01,  0x3c, 0x72, 0x43, 0x01,  0x41, 0x72, 0x5a, 0x01,  0x44, 0x71, 0x47, 0x01,
+                0x49, 0x51, 0x59, 0x01,  0x4c, 0x51, 0x44, 0x01,  0x51, 0x73, 0x59, 0x01,  0x54, 0x50, 0x43, 0x01,
+                0x59, 0x50, 0x5a, 0x81,  0x5c, 0x51, 0x42, 0x81,  0x61, 0x73, 0x59, 0x81,  0x00, 0x75, 0x43, 0x80,
+                0x05, 0x70, 0x5a, 0x80,  0x08, 0x50, 0x44, 0x80,  0x0d, 0x50, 0x5b, 0x80,  0x10, 0x75, 0x43, 0x80,
+                0x15, 0x72, 0x5e, 0x80,  0x18, 0x73, 0x45, 0x80,  0x1d, 0x72, 0x5b, 0x00,  0x20, 0x70, 0x43, 0x00,
+                0x25, 0x50, 0x5c, 0x00,  0x28, 0x50, 0x46, 0x00,  0x2d, 0x50, 0x5a, 0x00,  0x30, 0x75, 0x47, 0x00,
+                0x35, 0x72, 0x59, 0x00,  0x38, 0x70, 0x46, 0x00,  0x3d, 0x75, 0x57, 0x00,  0x40, 0x72, 0x43, 0x00,
+                0x45, 0x73, 0x55, 0x00,  0x48, 0x73, 0x41, 0x00,  0x4d, 0x70, 0x52, 0x00,  0x50, 0x73, 0x3f, 0x00,
+                0x55, 0x74, 0x4d, 0x00,  0x58, 0x72, 0x3d, 0x80,  0x5d, 0x73, 0x4d, 0x80,  0x60, 0x71, 0x3d, 0x80,
+                0x01, 0x51, 0x50, 0x80,  0x04, 0x72, 0x3d, 0x80,  0x09, 0x50, 0x4e, 0x80,  0x0c, 0x51, 0x40, 0x80,
+                0x11, 0x74, 0x50, 0x80,  0x14, 0x71, 0x40, 0x80,  0x19, 0x50, 0x4d, 0x80,  0x1c, 0x75, 0x3f, 0x00,
+                0x21, 0x72, 0x52, 0x00,  0x24, 0x72, 0x40, 0x00,  0x29, 0x71, 0x53, 0x00,  0x2c, 0x50, 0x42, 0x00,
+                0x31, 0x51, 0x55, 0x00,  0x34, 0x50, 0x42, 0x00   ]))
+            let lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry)
+            completion(.success(pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber)))
+        }
+    }
+}

+ 143 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/SilencePodSelectionView.swift

@@ -0,0 +1,143 @@
+//
+//  SilencePodSelectionView.swift
+//  OmniBLE
+//
+//  Created by Joe Moran 8/30/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+import LoopKit
+import LoopKitUI
+
+struct SilencePodSelectionView: View {
+
+    @Environment(\.horizontalSizeClass) var horizontalSizeClass
+    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
+
+    private var initialValue: SilencePodPreference
+    @State private var preference: SilencePodPreference
+    private var onSave: ((_ selectedValue: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) -> Void)?
+
+    @State private var alertIsPresented: Bool = false
+    @State private var error: LocalizedError?
+    @State private var saving: Bool = false
+
+
+    init(initialValue: SilencePodPreference, onSave: @escaping (_ selectedValue: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) -> Void) {
+        self.initialValue = initialValue
+        self._preference = State(initialValue: initialValue)
+        self.onSave = onSave
+    }
+
+    var body: some View {
+        contentWithCancel
+    }
+
+    var content: some View {
+        VStack {
+            List {
+                Section {
+                    Text(LocalizedString("Silence Pod mode suppresses all Pod alert and confirmation reminder beeping.", comment: "Help text for Silence Pod view")).fixedSize(horizontal: false, vertical: true)
+                        .padding(.vertical, 10)
+                }
+                Section {
+                    ForEach(SilencePodPreference.allCases, id: \.self) { preference in
+                        HStack {
+                            CheckmarkListItem(
+                                title: Text(preference.title),
+                                description: Text(preference.description),
+                                isSelected: Binding(
+                                    get: { self.preference == preference },
+                                    set: { isSelected in
+                                        if isSelected {
+                                            self.preference = preference
+                                        }
+                                    }
+                                )
+                            )
+                        }
+                        .padding(.vertical, 10)
+                    }
+                }
+                .buttonStyle(PlainButtonStyle()) // Disable row highlighting on selection
+            }
+            VStack {
+                Button(action: {
+                    saving = true
+                    onSave?(preference) { (error) in
+                        saving = false
+                        if let error = error {
+                            self.error = error
+                            self.alertIsPresented = true
+                        } else {
+                            self.presentationMode.wrappedValue.dismiss()
+                        }
+                    }
+                }) {
+                    Text(saveButtonText)
+                        .actionButtonStyle(.primary)
+                }
+                .padding()
+                .disabled(saving || !valueChanged)
+            }
+            .padding(self.horizontalSizeClass == .regular ? .bottom : [])
+            .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
+        }
+        .insetGroupedListStyle()
+        .navigationTitle(LocalizedString("Silence Pod", comment: "navigation title for Silnce Pod"))
+        .navigationBarTitleDisplayMode(.inline)
+        .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
+    }
+
+    private var contentWithCancel: some View {
+        if saving {
+            return AnyView(content
+                .navigationBarBackButtonHidden(true)
+            )
+        } else if valueChanged {
+            return AnyView(content
+                .navigationBarBackButtonHidden(true)
+                .navigationBarItems(leading: cancelButton)
+            )
+        } else {
+            return AnyView(content)
+        }
+    }
+
+    private var cancelButton: some View {
+        Button(action: { self.presentationMode.wrappedValue.dismiss() } ) {
+            Text(LocalizedString("Cancel", comment: "Button title for cancelling silence pod edit"))
+        }
+    }
+
+    var saveButtonText: String {
+        if saving {
+            return LocalizedString("Saving...", comment: "button title for saving silence pod preference while saving")
+        } else {
+            return LocalizedString("Save", comment: "button title for saving silence pod preference")
+        }
+    }
+
+    private var valueChanged: Bool {
+        return preference != initialValue
+    }
+
+    private func alert(error: Error?) -> SwiftUI.Alert {
+        return SwiftUI.Alert(
+            title: Text(LocalizedString("Failed to update silence pod preference.", comment: "Alert title for error when updating silence pod preference")),
+            message: Text(error?.localizedDescription ?? "No Error")
+        )
+    }
+}
+
+struct SilencePodSelectionView_Previews: PreviewProvider {
+    static var previews: some View {
+        NavigationView {
+            SilencePodSelectionView(initialValue: .disabled) { selectedValue, completion in
+                print("Selected: \(selectedValue)")
+                completion(nil)
+            }
+        }
+    }
+}

+ 2 - 2
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/UncertaintyRecoveredView.swift

@@ -16,8 +16,8 @@ struct UncertaintyRecoveredView: View {
     
     var body: some View {
         GuidePage(content: {
-            Text("\(self.appName) has recovered communication with the pod on your body.\n\nInsulin delivery records have been updated and should match what has actually been delivered.\n\nYou may continue to use \(self.appName) normally now.")
-                .padding([.top, .bottom])
+            Text(String(format: LocalizedString("%1$@ has recovered communication with the pod on your body.\n\nInsulin delivery records have been updated and should match what has actually been delivered.\n\nYou may continue to use %2$@ normally now.", comment: "Text body for page showing insulin uncertainty has been recovered (1: appName) (2: appName)"), self.appName, self.appName))
+               .padding([.top, .bottom])
         }) {
             VStack {
                 Button(action: {

+ 65 - 33
Dependencies/OmniKit/OmniKit.xcodeproj/project.pbxproj

@@ -157,6 +157,14 @@
 		CEC751DF29D8834B006E9D24 /* RileyLinkKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1229C1929C7E5BC0066A89C /* RileyLinkKitUI.framework */; };
 		CEC751E329D88392006E9D24 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEC751E229D88392006E9D24 /* LoopKitUI.framework */; };
 		CEF2639B29D88516009921F1 /* OmniKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C124016C29C7D87A00B32844 /* OmniKit.framework */; };
+		D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */; };
+		D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1452AF8A4DA00EA0853 /* ActivityView.swift */; };
+		D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1472AF8A4E400EA0853 /* FirstAppear.swift */; };
+		D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */; };
+		D845A14E2AF8A4FB00EA0853 /* ReadPodStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */; };
+		D845A14F2AF8A4FB00EA0853 /* ReadPulseLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A14C2AF8A4FB00EA0853 /* ReadPulseLogView.swift */; };
+		D845A1502AF8A4FB00EA0853 /* PumpManagerDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */; };
+		D845A1522AF8A51000EA0853 /* SilencePodSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1512AF8A51000EA0853 /* SilencePodSelectionView.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -402,6 +410,14 @@
 		C12EDA1729C7E01800435701 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = "<group>"; };
 		C12EDA1A29C7E06900435701 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
 		CEC751E229D88392006E9D24 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodPreference.swift; sourceTree = "<group>"; };
+		D845A1452AF8A4DA00EA0853 /* ActivityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
+		D845A1472AF8A4E400EA0853 /* FirstAppear.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstAppear.swift; sourceTree = "<group>"; };
+		D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayTestBeepsView.swift; sourceTree = "<group>"; };
+		D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodStatusView.swift; sourceTree = "<group>"; };
+		D845A14C2AF8A4FB00EA0853 /* ReadPulseLogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPulseLogView.swift; sourceTree = "<group>"; };
+		D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpManagerDetailsView.swift; sourceTree = "<group>"; };
+		D845A1512AF8A51000EA0853 /* SilencePodSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodSelectionView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -520,25 +536,26 @@
 		C124017A29C7D8E900B32844 /* OmnipodCommon */ = {
 			isa = PBXGroup;
 			children = (
+				C12401A129C7D8E900B32844 /* AlertSlot.swift */,
+				C124019D29C7D8E900B32844 /* BasalDeliveryTable.swift */,
+				C124019C29C7D8E900B32844 /* BasalSchedule+LoopKit.swift */,
+				C12401A529C7D8E900B32844 /* BasalSchedule.swift */,
+				C124019A29C7D8E900B32844 /* BeepPreference.swift */,
 				C124017B29C7D8E900B32844 /* BeepType.swift */,
-				C124017C29C7D8E900B32844 /* PumpManagerAlert.swift */,
-				C124017D29C7D8E900B32844 /* MessageBlocks */,
-				C124019729C7D8E900B32844 /* PodDoseProgressEstimator.swift */,
+				C12401A629C7D8E900B32844 /* BolusDeliveryTable.swift */,
+				C12401A429C7D8E900B32844 /* CRC16.swift */,
 				C124019829C7D8E900B32844 /* FaultEventCode.swift */,
+				C12401A229C7D8E900B32844 /* InsulinTableEntry.swift */,
 				C124019929C7D8E900B32844 /* Message.swift */,
-				C124019A29C7D8E900B32844 /* BeepPreference.swift */,
-				C124019B29C7D8E900B32844 /* Pod.swift */,
-				C124019C29C7D8E900B32844 /* BasalSchedule+LoopKit.swift */,
-				C124019D29C7D8E900B32844 /* BasalDeliveryTable.swift */,
+				C124017D29C7D8E900B32844 /* MessageBlocks */,
 				C124019E29C7D8E900B32844 /* PendingCommand.swift */,
+				C124019B29C7D8E900B32844 /* Pod.swift */,
+				C124019729C7D8E900B32844 /* PodDoseProgressEstimator.swift */,
 				C124019F29C7D8E900B32844 /* PodInsulinMeasurements.swift */,
-				C12401A029C7D8E900B32844 /* ReservoirLevel.swift */,
-				C12401A129C7D8E900B32844 /* AlertSlot.swift */,
-				C12401A229C7D8E900B32844 /* InsulinTableEntry.swift */,
 				C12401A329C7D8E900B32844 /* PodProgressStatus.swift */,
-				C12401A429C7D8E900B32844 /* CRC16.swift */,
-				C12401A529C7D8E900B32844 /* BasalSchedule.swift */,
-				C12401A629C7D8E900B32844 /* BolusDeliveryTable.swift */,
+				C124017C29C7D8E900B32844 /* PumpManagerAlert.swift */,
+				C12401A029C7D8E900B32844 /* ReservoirLevel.swift */,
+				D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */,
 				C12401A729C7D8E900B32844 /* UnfinalizedDose.swift */,
 			);
 			path = OmnipodCommon;
@@ -669,13 +686,13 @@
 		C124022529C7DA9700B32844 /* ViewModels */ = {
 			isa = PBXGroup;
 			children = (
-				C124022629C7DA9700B32844 /* PairPodViewModel.swift */,
+				C124022929C7DA9700B32844 /* DeactivatePodViewModel.swift */,
 				C124022729C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift */,
 				C124022829C7DA9700B32844 /* InsertCannulaViewModel.swift */,
-				C124022929C7DA9700B32844 /* DeactivatePodViewModel.swift */,
-				C124022A29C7DA9700B32844 /* RileyLinkListDataSource.swift */,
-				C124022B29C7DA9700B32844 /* PodLifeState.swift */,
 				C124022C29C7DA9700B32844 /* OmnipodSettingsViewModel.swift */,
+				C124022629C7DA9700B32844 /* PairPodViewModel.swift */,
+				C124022B29C7DA9700B32844 /* PodLifeState.swift */,
+				C124022A29C7DA9700B32844 /* RileyLinkListDataSource.swift */,
 			);
 			path = ViewModels;
 			sourceTree = "<group>";
@@ -683,32 +700,39 @@
 		C124022D29C7DA9700B32844 /* Views */ = {
 			isa = PBXGroup;
 			children = (
-				C124022E29C7DA9700B32844 /* PodLifeHUDView.swift */,
+				D845A1452AF8A4DA00EA0853 /* ActivityView.swift */,
+				C124024629C7DA9700B32844 /* AttachPodView.swift */,
+				C124024C29C7DA9700B32844 /* BasalStateView.swift */,
+				C124024429C7DA9700B32844 /* BeepPreferenceSelectionView.swift */,
 				C124022F29C7DA9700B32844 /* CheckInsertedCannulaView.swift */,
+				C124024B29C7DA9700B32844 /* DeactivatePodView.swift */,
+				C124024A29C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift */,
 				C124023129C7DA9700B32844 /* DesignElements */,
-				C124023529C7DA9700B32844 /* LowReservoirReminderEditView.swift */,
-				C124023629C7DA9700B32844 /* InsertCannulaView.swift */,
-				C124023729C7DA9700B32844 /* RileyLinkSetupView.swift */,
-				C124023829C7DA9700B32844 /* SetupCompleteView.swift */,
 				C124023A29C7DA9700B32844 /* ExpirationReminderPickerView.swift */,
-				C124023B29C7DA9700B32844 /* UncertaintyRecoveredView.swift */,
+				C124024529C7DA9700B32844 /* ExpirationReminderSetupView.swift */,
+				D845A1472AF8A4E400EA0853 /* FirstAppear.swift */,
+				C124023629C7DA9700B32844 /* InsertCannulaView.swift */,
 				C124023C29C7DA9700B32844 /* InsulinTypeConfirmation.swift */,
+				C124023529C7DA9700B32844 /* LowReservoirReminderEditView.swift */,
+				C124024229C7DA9700B32844 /* LowReservoirReminderSetupView.swift */,
 				C124023D29C7DA9700B32844 /* ManualTempBasalEntryView.swift */,
 				C124023F29C7DA9700B32844 /* NotificationSettingsView.swift */,
+				C124024929C7DA9700B32844 /* OmnipodReservoirView.swift */,
 				C124024029C7DA9700B32844 /* OmnipodSettingsView.swift */,
-				C124024129C7DA9700B32844 /* PodSetupView.swift */,
-				C124024229C7DA9700B32844 /* LowReservoirReminderSetupView.swift */,
 				C124024329C7DA9700B32844 /* PairPodView.swift */,
-				C124024429C7DA9700B32844 /* BeepPreferenceSelectionView.swift */,
-				C124024529C7DA9700B32844 /* ExpirationReminderSetupView.swift */,
-				C124024629C7DA9700B32844 /* AttachPodView.swift */,
-				C124024729C7DA9700B32844 /* ScheduledExpirationReminderEditView.swift */,
+				D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */,
 				C124024829C7DA9700B32844 /* PodDetailsView.swift */,
-				C124024929C7DA9700B32844 /* OmnipodReservoirView.swift */,
-				C124024A29C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift */,
-				C124024B29C7DA9700B32844 /* DeactivatePodView.swift */,
-				C124024C29C7DA9700B32844 /* BasalStateView.swift */,
+				C124022E29C7DA9700B32844 /* PodLifeHUDView.swift */,
+				C124024129C7DA9700B32844 /* PodSetupView.swift */,
+				D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */,
+				D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */,
+				D845A14C2AF8A4FB00EA0853 /* ReadPulseLogView.swift */,
+				C124023729C7DA9700B32844 /* RileyLinkSetupView.swift */,
+				C124024729C7DA9700B32844 /* ScheduledExpirationReminderEditView.swift */,
+				C124023829C7DA9700B32844 /* SetupCompleteView.swift */,
+				D845A1512AF8A51000EA0853 /* SilencePodSelectionView.swift */,
 				C124024D29C7DA9700B32844 /* TimeView.swift */,
+				C124023B29C7DA9700B32844 /* UncertaintyRecoveredView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -1056,6 +1080,7 @@
 				C12401E329C7D8E900B32844 /* PodComms.swift in Sources */,
 				C12401D429C7D8E900B32844 /* BeepPreference.swift in Sources */,
 				C12401B829C7D8E900B32844 /* PodInfoPulseLog.swift in Sources */,
+				D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */,
 				C12401BE29C7D8E900B32844 /* PodInfoConfiguredAlerts.swift in Sources */,
 				C12401E529C7D8E900B32844 /* PodCommsSession.swift in Sources */,
 				C12401DE29C7D8E900B32844 /* CRC16.swift in Sources */,
@@ -1102,13 +1127,16 @@
 				C124028B29C7DA9700B32844 /* BeepPreferenceSelectionView.swift in Sources */,
 				C12EDA1429C7DFBF00435701 /* TimeInterval.swift in Sources */,
 				C124028D29C7DA9700B32844 /* AttachPodView.swift in Sources */,
+				D845A14F2AF8A4FB00EA0853 /* ReadPulseLogView.swift in Sources */,
 				C124027229C7DA9700B32844 /* DeactivatePodViewModel.swift in Sources */,
 				C124028C29C7DA9700B32844 /* ExpirationReminderSetupView.swift in Sources */,
+				D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */,
 				C124029029C7DA9700B32844 /* OmnipodReservoirView.swift in Sources */,
 				C124027C29C7DA9700B32844 /* LowReservoirReminderEditView.swift in Sources */,
 				C124028129C7DA9700B32844 /* ExpirationReminderPickerView.swift in Sources */,
 				C124028329C7DA9700B32844 /* InsulinTypeConfirmation.swift in Sources */,
 				C124029129C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift in Sources */,
+				D845A1502AF8A4FB00EA0853 /* PumpManagerDetailsView.swift in Sources */,
 				C124028229C7DA9700B32844 /* UncertaintyRecoveredView.swift in Sources */,
 				C124027E29C7DA9700B32844 /* RileyLinkSetupView.swift in Sources */,
 				C124027429C7DA9700B32844 /* PodLifeState.swift in Sources */,
@@ -1116,11 +1144,14 @@
 				C124027329C7DA9700B32844 /* RileyLinkListDataSource.swift in Sources */,
 				C124029329C7DA9700B32844 /* BasalStateView.swift in Sources */,
 				C124027A29C7DA9700B32844 /* LeadingImage.swift in Sources */,
+				D845A14E2AF8A4FB00EA0853 /* ReadPodStatusView.swift in Sources */,
 				C124028729C7DA9700B32844 /* OmnipodSettingsView.swift in Sources */,
 				C124028929C7DA9700B32844 /* LowReservoirReminderSetupView.swift in Sources */,
 				C124027029C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */,
 				C12EDA0E29C7DEFD00435701 /* NumberFormatter.swift in Sources */,
+				D845A1522AF8A51000EA0853 /* SilencePodSelectionView.swift in Sources */,
 				C12EDA1229C7DF4B00435701 /* IdentifiableClass.swift in Sources */,
+				D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */,
 				C124027529C7DA9700B32844 /* OmnipodSettingsViewModel.swift in Sources */,
 				C124027629C7DA9700B32844 /* PodLifeHUDView.swift in Sources */,
 				C124029729C7DA9700B32844 /* OmnipodUICoordinator.swift in Sources */,
@@ -1131,6 +1162,7 @@
 				C124028A29C7DA9700B32844 /* PairPodView.swift in Sources */,
 				C124029229C7DA9700B32844 /* DeactivatePodView.swift in Sources */,
 				C124027929C7DA9700B32844 /* RoundedCard.swift in Sources */,
+				D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */,
 				C124026F29C7DA9700B32844 /* PairPodViewModel.swift in Sources */,
 				C124026E29C7DA9700B32844 /* FrameworkLocalText.swift in Sources */,
 				C124028F29C7DA9700B32844 /* PodDetailsView.swift in Sources */,

+ 473 - 136
Dependencies/OmniKit/OmniKit/OmnipodCommon/AlertSlot.swift

@@ -8,11 +8,29 @@
 
 import Foundation
 
+fileprivate let defaultShutdownImminentTime = Pod.serviceDuration - Pod.endOfServiceImminentWindow
+fileprivate let defaultExpirationReminderTime = Pod.nominalPodLife - Pod.defaultExpirationReminderOffset
+fileprivate let defaultExpiredTime = Pod.nominalPodLife
+
+// PDM and pre-SwiftUI use every1MinuteFor3MinutesAndRepeatEvery15Minutes, but with SwiftUI use every15Minutes
+fileprivate let suspendTimeExpiredBeepRepeat = BeepRepeat.every15Minutes
+
 public enum AlertTrigger {
     case unitsRemaining(Double)
     case timeUntilAlert(TimeInterval)
 }
 
+extension AlertTrigger: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch self {
+        case .unitsRemaining(let units):
+            return "\(Int(units))U"
+        case .timeUntilAlert(let triggerTime):
+            return "triggerTime=\(triggerTime.timeIntervalStr)"
+        }
+    }
+}
+
 public enum BeepRepeat: UInt8 {
     case once = 0
     case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1
@@ -29,29 +47,48 @@ public enum BeepRepeat: UInt8 {
 public struct AlertConfiguration {
 
     let slot: AlertSlot
-    let trigger: AlertTrigger
     let active: Bool
     let duration: TimeInterval
+    let trigger: AlertTrigger
     let beepRepeat: BeepRepeat
     let beepType: BeepType
+    let silent: Bool
     let autoOffModifier: Bool
 
     static let length = 6
 
-    public init(alertType: AlertSlot, active: Bool = true, autoOffModifier: Bool = false, duration: TimeInterval, trigger: AlertTrigger, beepRepeat: BeepRepeat, beepType: BeepType) {
+    public init(alertType: AlertSlot, active: Bool = true, duration: TimeInterval = 0, trigger: AlertTrigger, beepRepeat: BeepRepeat, beepType: BeepType, silent: Bool = false, autoOffModifier: Bool = false)
+    {
         self.slot = alertType
         self.active = active
-        self.autoOffModifier = autoOffModifier
         self.duration = duration
         self.trigger = trigger
         self.beepRepeat = beepRepeat
         self.beepType = beepType
+        self.silent = silent
+        self.autoOffModifier = autoOffModifier
     }
 }
 
 extension AlertConfiguration: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "AlertConfiguration(slot:\(slot), active:\(active), autoOffModifier:\(autoOffModifier), duration:\(duration), trigger:\(trigger), beepRepeat:\(beepRepeat), beepType:\(beepType))"
+        var str = "slot:\(slot)"
+        if !active {
+            str += ", active:\(active)"
+        }
+        if duration != 0 {
+            str += ", duration:\(duration.timeIntervalStr)"
+        }
+        str += ", trigger:\(trigger), beepRepeat:\(beepRepeat)"
+        if beepType != .noBeepNonCancel {
+            str += ", beepType:\(beepType)"
+        } else {
+            str += ", silent:\(silent)"
+        }
+        if autoOffModifier {
+            str += ", autoOffModifier:\(autoOffModifier)"
+        }
+        return "\nAlertConfiguration(\(str))"
     }
 }
 
@@ -60,54 +97,73 @@ extension AlertConfiguration: CustomDebugStringConvertible {
 public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
     public typealias RawValue = [String: Any]
 
-    // 2 hours long, time for user to start pairing process
-    case waitingForPairingReminder
+    // slot0AutoOff: auto-off timer; requires user input every x minutes -- NOT IMPLEMENTED
+    case autoOff(active: Bool, offset: TimeInterval, countdownDuration: TimeInterval, silent: Bool = false)
 
-    // 1 hour long, time for user to finish priming, cannula insertion
-    case finishSetupReminder
+    // slot1NotUsed
+    case notUsed
 
-    // User configurable with PDM (1-24 hours before 72 hour expiration) "Change Pod Soon"
-    case expirationReminder(TimeInterval)
+    // slot2ShutdownImminent: 79 hour alarm (1 hour before shutdown)
+    // 2 sets of beeps every 15 minutes for 1 hour
+    case shutdownImminent(offset: TimeInterval, absAlertTime: TimeInterval, silent: Bool = false)
 
-    // 72 hour alarm
-    case expired(alertTime: TimeInterval, duration: TimeInterval)
+    // slot3ExpirationReminder: User configurable with PDM (1-24 hours before 72 hour expiration)
+    // 2 sets of beeps every minute for 3 minutes and repeat every 15 minutes
+    // The PDM doesn't use a duration for this alert (presumably because it is limited to 2^9-1 minutes or 8h31m)
+    case expirationReminder(offset: TimeInterval, absAlertTime: TimeInterval, duration: TimeInterval = 0, silent: Bool = false)
 
-    // 79 hour alarm (1 hour before shutdown)
-    case shutdownImminent(TimeInterval)
+    // slot4LowReservoir: reservoir below configured value alert
+    case lowReservoir(units: Double, silent: Bool = false)
 
-    // reservoir below configured value alert
-    case lowReservoir(Double)
+    // slot5SuspendedReminder: pod suspended reminder, before suspendTime;
+    // short beep every 15 minutes if > 30 min, else short beep every 5 minutes
+    case podSuspendedReminder(active: Bool, offset: TimeInterval, suspendTime: TimeInterval, timePassed: TimeInterval = 0, silent: Bool = false)
 
-    // auto-off timer; requires user input every x minutes
-    case autoOff(active: Bool, countdownDuration: TimeInterval)
+    // slot6SuspendTimeExpired: pod suspend time expired alarm, after suspendTime;
+    // 2 sets of beeps every minute for 3 minutes repeated every 15 minutes (PDM & pre-SwiftUI implementations)
+    // 2 sets of beeps every 15 minutes (for SwiftUI PumpManagerAlerts implementations)
+    case suspendTimeExpired(offset: TimeInterval, suspendTime: TimeInterval, silent: Bool = false)
 
-    // pod suspended reminder, before suspendTime; short beep every 15 minutes if > 30 min, else every 5 minutes
-    case podSuspendedReminder(active: Bool, suspendTime: TimeInterval)
+    // slot7Expired: 2 hours long, time for user to start pairing process
+    case waitingForPairingReminder
+
+    // slot7Expired: 1 hour long, time for user to finish priming, cannula insertion
+    case finishSetupReminder
 
-    // pod suspend time expired alarm, after suspendTime; 2 sets of beeps every min for 3 minutes repeated every 15 minutes
-    case suspendTimeExpired(suspendTime: TimeInterval)
+    // slot7Expired: 72 hour alarm
+    case expired(offset: TimeInterval, absAlertTime: TimeInterval, duration: TimeInterval, silent: Bool = false)
 
     public var description: String {
         var alertName: String
         switch self {
-        case .waitingForPairingReminder:
-            return LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder")
-        case .finishSetupReminder:
-            return LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder")
-        case .expirationReminder:
-            alertName = LocalizedString("Expiration alert", comment: "Description for expiration alert")
-        case .expired:
-            alertName = LocalizedString("Expiration advisory", comment: "Description for expiration advisory")
-        case .shutdownImminent:
-            alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent")
-        case .lowReservoir(let units):
-            alertName = String(format: LocalizedString("Low reservoir advisory (%1$gU)", comment: "Format string for description for low reservoir advisory (1: reminder units)"), units)
+        // slot0AutoOff
         case .autoOff:
-            alertName = LocalizedString("Auto-off", comment: "Description for auto-off")
+            alertName = LocalizedString("Auto-off", comment: "Description for auto-off alert")
+        // slot1NotUsed
+        case .notUsed:
+            alertName = LocalizedString("Not used", comment: "Description for not used slot alert")
+        // slot2ShutdownImminent
+        case .shutdownImminent:
+            alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent alert")
+        // slot3ExpirationReminder
+        case .expirationReminder:
+            alertName = LocalizedString("Expiration reminder", comment: "Description for expiration reminder alert")
+        // slot4LowReservoir
+        case .lowReservoir:
+            alertName = LocalizedString("Low reservoir", comment: "Format string for description for low reservoir alert")
+        // slot5SuspendedReminder
         case .podSuspendedReminder:
-            alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder")
+            alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder alert")
+        // slot6SuspendTimeExpired
         case .suspendTimeExpired:
-            alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired")
+            alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired alert")
+        // slot7Expired
+        case .waitingForPairingReminder:
+            alertName = LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder alert")
+        case .finishSetupReminder:
+            alertName = LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder alert")
+        case .expired:
+            alertName = LocalizedString("Pod expired", comment: "Description for pod expired alert")
         }
         if self.configuration.active == false {
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
@@ -117,71 +173,126 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
     public var configuration: AlertConfiguration {
         switch self {
-        case .waitingForPairingReminder:
-            return AlertConfiguration(alertType: .slot7, duration: .minutes(110), trigger: .timeUntilAlert(.minutes(10)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .finishSetupReminder:
-            return AlertConfiguration(alertType: .slot7, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .expirationReminder(let alertTime):
-            let active = alertTime != 0 // disable if alertTime is 0
-            return AlertConfiguration(alertType: .slot3, active: active, duration: 0, trigger: .timeUntilAlert(alertTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .expired(let alarmTime, let duration):
-            let active = alarmTime != 0 // disable if alarmTime is 0
-            return AlertConfiguration(alertType: .slot7, active: active, duration: duration, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .shutdownImminent(let alarmTime):
-            let active = alarmTime != 0 // disable if alarmTime is 0
-            return AlertConfiguration(alertType: .slot2, active: active, duration: 0, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .lowReservoir(let units):
+        // slot0AutoOff
+        case .autoOff(let active, _, let countdownDuration, let silent):
+            return AlertConfiguration(alertType: .slot0AutoOff, active: active, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent, autoOffModifier: true)
+
+        // slot1NotUsed
+        case .notUsed:
+            return AlertConfiguration(alertType: .slot1NotUsed, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .noBeepNonCancel)
+
+        // slot2ShutdownImminent
+        case .shutdownImminent(let offset, let absAlertTime, let silent):
+            let active = absAlertTime != 0 // disable if absAlertTime is 0
+            let triggerTime: TimeInterval
+            if active {
+                triggerTime = absAlertTime - offset
+            } else {
+                triggerTime = 0
+            }
+            return AlertConfiguration(alertType: .slot2ShutdownImminent, active: active, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
+
+        // slot3ExpirationReminder
+        case .expirationReminder(let offset, let absAlertTime, let duration, let silent):
+            let active = absAlertTime != 0 // disable if absAlertTime is 0
+            let triggerTime: TimeInterval
+            if active {
+                triggerTime = absAlertTime - offset
+            } else {
+                triggerTime = 0
+            }
+            return AlertConfiguration(alertType: .slot3ExpirationReminder, active: active, duration: duration, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
+
+        // slot4LowReservoir
+        case .lowReservoir(let units, let silent):
             let active = units != 0 // disable if units is 0
-            return AlertConfiguration(alertType: .slot4, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .autoOff(let active, let countdownDuration):
-            return AlertConfiguration(alertType: .slot0, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
-        case .podSuspendedReminder(let active, let suspendTime):
-            // A suspendTime of 0 is an untimed suspend
+            return AlertConfiguration(alertType: .slot4LowReservoir, active: active, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
+
+        // slot5SuspendedReminder
+        // A suspendTime of 0 is an untimed suspend
+        // timePassed will be > 0 for an existing pod suspended reminder changing its silent state
+        case .podSuspendedReminder(let active, _, let suspendTime, let timePassed, let silent):
             let reminderInterval, duration: TimeInterval
-            let trigger: AlertTrigger
-            let beepRepeat: BeepRepeat
+            var beepRepeat: BeepRepeat
             let beepType: BeepType
-            if active {
-                if suspendTime >= TimeInterval(minutes :30) {
-                    // Use 15-minute pod suspended reminder beeps for longer scheduled suspend times as per PDM.
-                    reminderInterval = TimeInterval(minutes: 15)
-                    beepRepeat = .every15Minutes
-                } else {
-                    // Use 5-minute pod suspended reminder beeps for shorter scheduled suspend times.
-                    reminderInterval = TimeInterval(minutes: 5)
-                    beepRepeat = .every5Minutes
-                }
+            let trigger: AlertTrigger
+            var isActive: Bool = active
+
+            if suspendTime == 0 || suspendTime >= TimeInterval(minutes: 30) {
+                // Use 15-minute pod suspended reminder beeps for untimed or longer scheduled suspend times.
+                reminderInterval = TimeInterval(minutes: 15)
+                beepRepeat = .every15Minutes
+            } else {
+                // Use 5-minute pod suspended reminder beeps for shorter scheduled suspend times.
+                reminderInterval = TimeInterval(minutes: 5)
+                beepRepeat = .every5Minutes
+            }
+
+            // Make alert inactive if there isn't enough remaining in suspend time for a reminder beep.
+            let suspendTimeRemaining = suspendTime - timePassed
+            if suspendTime != 0 && suspendTimeRemaining <= reminderInterval {
+                isActive = false
+            }
+
+            if isActive {
+                // Compute the alert trigger time as the interval until the next upcoming reminder interval
+                let triggerTime: TimeInterval = .seconds(reminderInterval - Double((Int(timePassed) % Int(reminderInterval))))
+
                 if suspendTime == 0 {
                     duration = 0 // Untimed suspend, no duration
-                } else if suspendTime > reminderInterval {
-                    duration = suspendTime - reminderInterval // End after suspendTime total time
                 } else {
-                    duration = .minutes(1) // Degenerate case, end ASAP
+                    // duration is from triggerTime to suspend time remaining
+                    duration = suspendTimeRemaining - triggerTime
                 }
-                trigger = .timeUntilAlert(reminderInterval) // Start after reminderInterval has passed
+                trigger = .timeUntilAlert(triggerTime) // time to next reminder interval with the suspend time
                 beepType = .beep
             } else {
+                beepRepeat = .once
                 duration = 0
                 trigger = .timeUntilAlert(.minutes(0))
-                beepRepeat = .once
                 beepType = .noBeepCancel
             }
-            return AlertConfiguration(alertType: .slot5, active: active, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType)
-        case .suspendTimeExpired(let suspendTime):
+            return AlertConfiguration(alertType: .slot5SuspendedReminder, active: isActive, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType, silent: silent)
+
+        // slot6SuspendTimeExpired
+        case .suspendTimeExpired(_, let suspendTime, let silent):
             let active = suspendTime != 0 // disable if suspendTime is 0
             let trigger: AlertTrigger
             let beepRepeat: BeepRepeat
             let beepType: BeepType
             if active {
                 trigger = .timeUntilAlert(suspendTime)
-                beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes
+                beepRepeat = suspendTimeExpiredBeepRepeat
                 beepType = .bipBeepBipBeepBipBeepBipBeep
             } else {
                 trigger = .timeUntilAlert(.minutes(0))
                 beepRepeat = .once
                 beepType = .noBeepCancel
             }
-            return AlertConfiguration(alertType: .slot6, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType)
+            return AlertConfiguration(alertType: .slot6SuspendTimeExpired, active: active, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType, silent: silent)
+
+        // slot7Expired
+        case .waitingForPairingReminder:
+            // After pod is powered up, beep every 10 minutes for up to 2 hours before pairing before failing
+            let totalDuration: TimeInterval = .hours(2)
+            let startOffset: TimeInterval = .minutes(10)
+            return AlertConfiguration(alertType: .slot7Expired, duration: totalDuration - startOffset, trigger: .timeUntilAlert(startOffset), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
+        case .finishSetupReminder:
+            // After pod is paired, beep every 5 minutes for up to 1 hour for pod setup to complete before failing
+            let totalDuration: TimeInterval = .hours(1)
+            let startOffset: TimeInterval = .minutes(5)
+            return AlertConfiguration(alertType: .slot7Expired, duration: totalDuration - startOffset, trigger: .timeUntilAlert(startOffset), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep)
+        case .expired(let offset, let absAlertTime, let duration, let silent):
+            // Normally used to alert at Pod.nominalPodLife (72 hours) for Pod.expirationAdvisoryWindow (7 hours)
+            // 2 sets of beeps repeating every 60 minutes
+            let active = absAlertTime != 0 // disable if absAlertTime is 0
+            let triggerTime: TimeInterval
+            if active {
+                triggerTime = absAlertTime - offset
+            } else {
+                triggerTime = .minutes(0)
+            }
+            return AlertConfiguration(alertType: .slot7Expired, active: active, duration: duration, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent)
         }
     }
 
@@ -194,51 +305,92 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
         }
 
         switch name {
-        case "waitingForPairingReminder":
-            self = .waitingForPairingReminder
-        case "finishSetupReminder":
-            self = .finishSetupReminder
-        case "expirationReminder":
-            guard let alertTime = rawValue["alertTime"] as? Double else {
-                return nil
-            }
-            self = .expirationReminder(TimeInterval(alertTime))
-        case "expired":
-            guard let alarmTime = rawValue["alarmTime"] as? Double,
-                let duration = rawValue["duration"] as? Double else
+        case "autoOff":
+            guard let active = rawValue["active"] as? Bool,
+                let countdownDuration = rawValue["countdownDuration"] as? TimeInterval else
             {
                 return nil
             }
-            self = .expired(alertTime: TimeInterval(alarmTime), duration: TimeInterval(duration))
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .autoOff(active: active, offset: offset, countdownDuration: countdownDuration, silent: silent)
         case "shutdownImminent":
-            guard let alarmTime = rawValue["alarmTime"] as? Double else {
+            guard let alarmTime = rawValue["alarmTime"] as? TimeInterval else {
                 return nil
             }
-            self = .shutdownImminent(alarmTime)
-        case "lowReservoir":
-            guard let units = rawValue["units"] as? Double else {
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let offsetToUse, absAlertTime: TimeInterval
+            if offset == 0 {
+                // use default values as no offset value was found
+                absAlertTime = defaultShutdownImminentTime
+                offsetToUse = absAlertTime - alarmTime
+            } else {
+                absAlertTime = offset + alarmTime
+                offsetToUse = offset
+            }
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .shutdownImminent(offset: offsetToUse, absAlertTime: absAlertTime, silent: silent)
+        case "expirationReminder":
+            guard let alertTime = rawValue["alertTime"] as? TimeInterval else {
                 return nil
             }
-            self = .lowReservoir(units)
-        case "autoOff":
-            guard let active = rawValue["active"] as? Bool,
-                let countdownDuration = rawValue["countdownDuration"] as? Double else
-            {
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let offsetToUse, absAlertTime: TimeInterval
+            if offset == 0 {
+                // use default values as no offset value was found
+                absAlertTime = defaultExpirationReminderTime
+                offsetToUse = absAlertTime - alertTime
+            } else {
+                absAlertTime = offset + alertTime
+                offsetToUse = offset
+            }
+            let duration = rawValue["duration"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .expirationReminder(offset: offsetToUse, absAlertTime: absAlertTime, duration: duration,  silent: silent)
+        case "lowReservoir":
+            guard let units = rawValue["units"] as? Double else {
                 return nil
             }
-            self = .autoOff(active: active, countdownDuration: TimeInterval(countdownDuration))
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .lowReservoir(units: units, silent: silent)
         case "podSuspendedReminder":
             guard let active = rawValue["active"] as? Bool,
-                let suspendTime = rawValue["suspendTime"] as? Double else
+                let suspendTime = rawValue["suspendTime"] as? TimeInterval else
             {
                 return nil
             }
-            self = .podSuspendedReminder(active: active, suspendTime: suspendTime)
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .podSuspendedReminder(active: active, offset: offset, suspendTime: suspendTime, silent: silent)
         case "suspendTimeExpired":
             guard let suspendTime = rawValue["suspendTime"] as? Double else {
                 return nil
             }
-            self = .suspendTimeExpired(suspendTime: suspendTime)
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .suspendTimeExpired(offset: offset, suspendTime: suspendTime, silent: silent)
+        case "waitingForPairingReminder":
+            self = .waitingForPairingReminder
+        case "finishSetupReminder":
+            self = .finishSetupReminder
+        case "expired":
+            guard let alarmTime = rawValue["alarmTime"] as? TimeInterval,
+                let duration = rawValue["duration"] as? TimeInterval else
+            {
+                return nil
+            }
+            let offset = rawValue["offset"] as? TimeInterval ?? 0
+            let offsetToUse, absAlertTime: TimeInterval
+            if offset == 0 {
+                // use default values as no offset value was found
+                absAlertTime = defaultExpiredTime
+                offsetToUse = absAlertTime - alarmTime
+            } else {
+                absAlertTime = offset + alarmTime
+                offsetToUse = offset
+            }
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .expired(offset: offsetToUse, absAlertTime: absAlertTime, duration: duration, silent: silent)
         default:
             return nil
         }
@@ -248,50 +400,65 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
         let name: String = {
             switch self {
-            case .waitingForPairingReminder:
-                return "waitingForPairingReminder"
-            case .finishSetupReminder:
-                return "finishSetupReminder"
-            case .expirationReminder:
-                return "expirationReminder"
-            case .expired:
-                return "expired"
+            case .autoOff:
+                return "autoOff"
+            case .notUsed:
+                return "notUsed"
             case .shutdownImminent:
                 return "shutdownImminent"
+            case .expirationReminder:
+                return "expirationReminder"
             case .lowReservoir:
                 return "lowReservoir"
-            case .autoOff:
-                return "autoOff"
             case .podSuspendedReminder:
                 return "podSuspendedReminder"
             case .suspendTimeExpired:
                 return "suspendTimeExpired"
+            case .waitingForPairingReminder:
+                return "waitingForPairingReminder"
+            case .finishSetupReminder:
+                return "finishSetupReminder"
+            case .expired:
+                return "expired"
             }
         }()
 
-
         var rawValue: RawValue = [
             "name": name,
         ]
 
         switch self {
-        case .expirationReminder(let alertTime):
-            rawValue["alertTime"] = alertTime
-        case .expired(let alarmTime, let duration):
-            rawValue["alarmTime"] = alarmTime
-            rawValue["duration"] = duration
-        case .shutdownImminent(let alarmTime):
-            rawValue["alarmTime"] = alarmTime
-        case .lowReservoir(let units):
-            rawValue["units"] = units
-        case .autoOff(let active, let countdownDuration):
+        case .autoOff(let active, let offset, let countdownDuration, let silent):
             rawValue["active"] = active
+            rawValue["offset"] = offset
             rawValue["countdownDuration"] = countdownDuration
-        case .podSuspendedReminder(let active, let suspendTime):
+            rawValue["silent"] = silent
+        case .shutdownImminent(let offset, let absAlertTime, let silent):
+            rawValue["offset"] = offset
+            rawValue["alarmTime"] = absAlertTime - offset
+            rawValue["silent"] = silent
+        case .expirationReminder(let offset, let absAlertTime, let duration, let silent):
+            rawValue["offset"] = offset
+            rawValue["alertTime"] = absAlertTime - offset
+            rawValue["duration"] = duration
+            rawValue["silent"] = silent
+        case .lowReservoir(let units, let silent):
+            rawValue["units"] = units
+            rawValue["silent"] = silent
+        case .podSuspendedReminder(let active, let offset, let suspendTime, _, let silent):
             rawValue["active"] = active
+            rawValue["offset"] = offset
             rawValue["suspendTime"] = suspendTime
-        case .suspendTimeExpired(let suspendTime):
+            rawValue["silent"] = silent
+        case .suspendTimeExpired(let offset, let suspendTime, let silent):
+            rawValue["offset"] = offset
             rawValue["suspendTime"] = suspendTime
+            rawValue["silent"] = silent
+        case .expired(let offset, let absAlertTime, let duration, let silent):
+            rawValue["offset"] = offset
+            rawValue["alarmTime"] = absAlertTime - offset
+            rawValue["duration"] = duration
+            rawValue["silent"] = silent
         default:
             break
         }
@@ -301,14 +468,14 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 }
 
 public enum AlertSlot: UInt8 {
-    case slot0 = 0x00
-    case slot1 = 0x01
-    case slot2 = 0x02
-    case slot3 = 0x03
-    case slot4 = 0x04
-    case slot5 = 0x05
-    case slot6 = 0x06
-    case slot7 = 0x07
+    case slot0AutoOff = 0x00
+    case slot1NotUsed = 0x01
+    case slot2ShutdownImminent = 0x02
+    case slot3ExpirationReminder = 0x03
+    case slot4LowReservoir = 0x04
+    case slot5SuspendedReminder = 0x05
+    case slot6SuspendTimeExpired = 0x06
+    case slot7Expired = 0x07
 
     public var bitMaskValue: UInt8 {
         return 1<<rawValue
@@ -373,9 +540,179 @@ public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, E
 
 // Returns true if there are any active suspend related alerts
 public func hasActiveSuspendAlert(configuredAlerts: [AlertSlot : PodAlert]) -> Bool {
-    // slot5 is for podSuspendedReminder and slot6 is for suspendTimeExpired
-    if configuredAlerts.contains(where: { ($0.key == .slot5 || $0.key == .slot6) && $0.value.configuration.active }) {
+    if configuredAlerts.contains(where: { ($0.key == .slot5SuspendedReminder || $0.key == .slot6SuspendTimeExpired) && $0.value.configuration.active })
+    {
         return true
     }
     return false
 }
+
+// Returns a descriptive string for all the alerts in alertSet
+public func alertSetString(alertSet: AlertSet) -> String {
+
+    if alertSet.isEmpty {
+        // Don't bother displaying any additional info for an inactive alert
+        return String(describing: alertSet)
+    }
+
+    let alertDescription = alertSet.map { (slot) -> String in
+        switch slot {
+        case .slot0AutoOff:
+            return PodAlert.autoOff(active: true, offset: 0, countdownDuration: 0).description
+        case .slot1NotUsed:
+            return PodAlert.notUsed.description
+        case .slot2ShutdownImminent:
+            return PodAlert.shutdownImminent(offset: 0, absAlertTime: defaultShutdownImminentTime).description
+        case .slot3ExpirationReminder:
+            return PodAlert.expirationReminder(offset: 0, absAlertTime: defaultExpirationReminderTime).description
+        case .slot4LowReservoir:
+            return PodAlert.lowReservoir(units: Pod.maximumReservoirReading).description
+        case .slot5SuspendedReminder:
+            return PodAlert.podSuspendedReminder(active: true, offset: 0, suspendTime: .minutes(30)).description
+        case .slot6SuspendTimeExpired:
+            return PodAlert.suspendTimeExpired(offset: 0, suspendTime: .minutes(30)).description
+        case .slot7Expired:
+            return PodAlert.expired(offset: 0, absAlertTime: defaultExpiredTime, duration: Pod.expirationAdvisoryWindow).description
+        }
+    }
+
+    return alertDescription.joined(separator: ", ")
+}
+
+func configuredAlertsString(configuredAlerts: [AlertSlot : PodAlert]) -> String {
+
+    if configuredAlerts.isEmpty {
+        return String(describing: configuredAlerts)
+    }
+
+    let configuredAlertString = configuredAlerts.map { (configuredAlert) -> String in
+
+        let podAlert = configuredAlert.value
+        let description = podAlert.description
+        guard podAlert.configuration.active else {
+            return description
+        }
+
+        switch podAlert {
+        case .shutdownImminent(_, let absAlertTime, _):
+            return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr)
+        case .expirationReminder(_, let absAlertTime, _, _):
+            return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr)
+        case .lowReservoir(let unitTrigger, _):
+            return String(format: "%@ @ %dU", description, Int(unitTrigger))
+        case .podSuspendedReminder(_, let offset, let suspendTime, _, _):
+            return String(format: "%@ ending @ %@ after %@", description, (offset + suspendTime).timeIntervalStr, suspendTime.timeIntervalStr)
+        case .suspendTimeExpired(let offset, let suspendTime, _):
+            return String(format: "%@ @ %@ after %@", description, (offset + suspendTime).timeIntervalStr, suspendTime.timeIntervalStr)
+        case .expired(_, let absAlertTime, _, _):
+            return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr)
+        default:
+            return ""
+        }
+    }
+
+    return configuredAlertString.joined(separator: ", ")
+}
+
+// Returns an array of appropriate PodAlerts with the specified silent value
+// for all the configuredAlerts given all the current pod conditions.
+func regeneratePodAlerts(silent: Bool, configuredAlerts: [AlertSlot: PodAlert], activeAlertSlots: AlertSet, currentPodTime: TimeInterval, currentReservoirLevel: Double) -> [PodAlert] {
+    var podAlerts: [PodAlert] = []
+
+    for alert in configuredAlerts {
+        // Just skip this alert if not previously active
+        guard alert.value.configuration.active else {
+            continue
+        }
+
+        // Map alerts to corresponding appropriate new ones at the current pod time using the specified silent value.
+        switch alert.value {
+
+        case .shutdownImminent(let offset, let alertTime, _):
+            // alertTime is absolute when offset is non-zero, otherwise use  default value
+            var absAlertTime = offset != 0 ? alertTime : defaultShutdownImminentTime
+            if currentPodTime >= absAlertTime {
+                // alert trigger is not in the future, make inactive using a 0 value
+                absAlertTime = 0
+            }
+            // create new shutdownImminent podAlert using the current timeActive and the original absolute alert time
+            podAlerts.append(PodAlert.shutdownImminent(offset: currentPodTime, absAlertTime: absAlertTime, silent: silent))
+
+        case .expirationReminder(let offset, let alertTime, let alertDuration, _):
+            let duration: TimeInterval
+
+            // alertTime is absolute when offset is non-zero, otherwise use default value
+            var absAlertTime = offset != 0 ? alertTime : defaultExpirationReminderTime
+            if currentPodTime >= absAlertTime {
+                // alert trigger is not in the future, make inactive using a 0 value
+                absAlertTime = 0
+                duration = 0
+            } else {
+                duration = alertDuration
+            }
+            // create new expirationReminder podAlert using the current active time and the original absolute alert time and duration
+            podAlerts.append(PodAlert.expirationReminder(offset: currentPodTime, absAlertTime: absAlertTime, duration: duration, silent: silent))
+
+        case .lowReservoir(let unitTrigger, _):
+            let units: Double
+            if currentReservoirLevel > unitTrigger {
+                units = unitTrigger
+            } else {
+                // reservoir is no longer more than the unitTrigger, make inactive using a 0 value
+                units = 0
+            }
+            podAlerts.append(PodAlert.lowReservoir(units: units, silent: silent))
+
+        case .podSuspendedReminder(let active, let offset, let suspendTime, _, _):
+            let timePassed: TimeInterval = min(currentPodTime - offset, .hours(2))
+            // Pass along the computed time passed since alert was originally set so creation routine can
+            // do all the grunt work dealing with varying reminder intervals and time passing scenarios.
+            podAlerts.append(PodAlert.podSuspendedReminder(active: active, offset: offset, suspendTime: suspendTime, timePassed: timePassed, silent: silent))
+
+        case .suspendTimeExpired(let lastOffset, let lastSuspendTime, _):
+            let absAlertTime = lastOffset + lastSuspendTime
+            let suspendTime: TimeInterval
+            if currentPodTime >= absAlertTime {
+                // alert trigger is no longer in the future
+                if activeAlertSlots.contains(where: { $0 == .slot6SuspendTimeExpired } ) {
+                    // The suspendTimeExpired alert has yet been acknowledged,
+                    // set up a suspendTimeExpired alert for the next 15m interval.
+                    // Compute a new suspendTime that is a multiple of 15 minutes
+                    // from lastOffset which is at least one minute in the future.
+                    let newOffsetSuspendTime = ceil((currentPodTime - lastOffset) / .minutes(15)) * .minutes(15)
+                    let newAbsAlertTime = lastOffset + newOffsetSuspendTime
+                    suspendTime = max(newAbsAlertTime - currentPodTime, .minutes(1))
+                } else {
+                    // The suspendTimeExpired alert was already been acknowledged,
+                    // so now make this alert inactive by using a 0 suspendTime.
+                    suspendTime = 0
+                }
+            } else {
+                // recompute a new suspendTime based on the current pod time
+                suspendTime = absAlertTime - currentPodTime
+                print("setting new suspendTimeExpired suspendTime of \(suspendTime) with currentPodTime\(currentPodTime) and absAlertTime=\(absAlertTime)")
+            }
+            // create a new suspendTimeExpired PodAlert using the current active time and the computed suspendTime (if any)
+            podAlerts.append(PodAlert.suspendTimeExpired(offset: currentPodTime, suspendTime: suspendTime, silent: silent))
+
+        case .expired(let offset, let alertTime, let alertDuration, _):
+            let duration: TimeInterval
+
+            // alertTime is absolute when offset is non-zero, otherwise use default value
+            var absAlertTime = offset != 0 ? alertTime : defaultExpiredTime
+            if currentPodTime >= absAlertTime {
+                // alert trigger is not in the future, make inactive using a 0 value
+                absAlertTime = 0
+                duration = 0
+            } else {
+                duration = alertDuration
+            }
+            // create new expired podAlert using the current active time and the original absolute alert time and duration
+            podAlerts.append(PodAlert.expired(offset: currentPodTime, absAlertTime: absAlertTime, duration: duration, silent: silent))
+
+        default:
+            break
+        }
+    }
+    return podAlerts
+}

+ 7 - 5
Dependencies/OmniKit/OmniKit/OmnipodCommon/BasalDeliveryTable.swift

@@ -163,8 +163,9 @@ public struct RateEntry {
             // Eros zero TB is the only case not using pulses
             return 0
         } else {
-            // Use delayBetweenPulses to compute rate, works for non-Eros near zero rates
-            return (.hours(1) / delayBetweenPulses) / Pod.pulsesPerUnit
+            // Use delayBetweenPulses to compute rate which will also work for non-Eros near zero rates.
+            // Round the rate calculation to a two digit value to avoid slightly off values for some cases.
+            return round(((.hours(1) / delayBetweenPulses) / Pod.pulsesPerUnit) * 100) / 100.0
         }
     }
     
@@ -173,8 +174,9 @@ public struct RateEntry {
             // Eros zero TB case uses fixed 30 minute rate entries
             return TimeInterval(minutes: 30)
         } else {
-            // Use delayBetweenPulses to compute duration, works for non-Eros near zero rates
-            return delayBetweenPulses * totalPulses
+            // Use delayBetweenPulses to compute duration which will also work for non-Eros near zero rates.
+            // Round to nearest second to not be slightly off of the 30 minute rate entry boundary for some cases.
+            return round(delayBetweenPulses * totalPulses)
         }
     }
     
@@ -230,6 +232,6 @@ public struct RateEntry {
 
 extension RateEntry: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "RateEntry(rate:\(rate) duration:\(duration.stringValue))"
+        return "RateEntry(rate:\(rate), duration:\(duration.timeIntervalStr))"
     }
 }

+ 2 - 2
Dependencies/OmniKit/OmniKit/OmnipodCommon/BeepPreference.swift

@@ -29,9 +29,9 @@ public enum BeepPreference: Int, CaseIterable {
         case .silent:
             return LocalizedString("No confidence reminders are used.", comment: "Description for BeepPreference.silent")
         case .manualCommands:
-            return LocalizedString("Confidence reminders will sound for commands you initiate, like bolus, cancel bolus, suspend, resume, save notification reminders, etc. When Loop automatically adjusts delivery, no confidence reminders are used.", comment: "Description for BeepPreference.manualCommands")
+            return LocalizedString("Confidence reminders will sound for commands you initiate, like bolus, cancel bolus, suspend, resume, save notification reminders, etc. When the app automatically adjusts delivery, no confidence reminders are used.", comment: "Description for BeepPreference.manualCommands")
         case .extended:
-            return LocalizedString("Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.", comment: "Description for BeepPreference.extended")
+            return LocalizedString("Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.", comment: "Description for BeepPreference.extended")
         }
     }
 

+ 263 - 263
Dependencies/OmniKit/OmniKit/OmnipodCommon/FaultEventCode.swift

@@ -25,6 +25,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case invalidBeepRepeatPattern             = 0x09
         case bf0notEqualToBF1                     = 0x0A
         case tableCorruptionTempBasalSubcommand   = 0x0B
+
         case resetDueToCOP                        = 0x0D
         case resetDueToIllegalOpcode              = 0x0E
         case resetDueToIllegalAddress             = 0x0F
@@ -75,6 +76,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case testInProgress                       = 0x3C
         case problemWithPumpAnchor                = 0x3D
         case errorFlashWrite                      = 0x3E
+
         case encoderCountTooHigh                  = 0x40
         case encoderCountExcessiveVariance        = 0x41
         case encoderCountTooLow                   = 0x42
@@ -89,6 +91,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case trimICSTooCloseTo0x1FF               = 0x4B
         case problemFindingBestTrimValue          = 0x4C
         case badSetTPM1MultiCasesValue            = 0x4D
+        case sawTrimError                         = 0x4E
         case unexpectedRFErrorFlagDuringReset     = 0x4F
         case timerPulseWidthModulatorOverflow     = 0x50
         case tickcntError                         = 0x51
@@ -109,11 +112,13 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case occlusionCheckStartup1               = 0x60
         case occlusionCheckStartup2               = 0x61
         case occlusionCheckTimeouts1              = 0x62
+
         case occlusionCheckTimeouts2              = 0x66
         case occlusionCheckTimeouts3              = 0x67
         case occlusionCheckPulseIssue             = 0x68
         case occlusionCheckBolusProblem           = 0x69
         case occlusionCheckAboveThreshold         = 0x6A
+
         case basalUnderInfusion                   = 0x80
         case basalOverInfusion                    = 0x81
         case tempBasalUnderInfusion               = 0x82
@@ -137,9 +142,9 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case illegalInterLockChan                 = 0x95
         case badStateInClearBolusIST2AndVars      = 0x96
         case badStateInMaybeInc33D                = 0x97
-        case valuesDoNotMatchOrAreGreaterThan0x97 = 0x98
+        case valuesDoNotMatch                     = 0xFF
     }
-    
+
     public var faultType: FaultEventType? {
         return FaultEventType(rawValue: rawValue)
     }
@@ -147,268 +152,263 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
     public init(rawValue: UInt8) {
         self.rawValue = rawValue
     }
-    
-    public var description: String {
-        let faultDescription: String
-        
-        if let faultType = faultType {
-            faultDescription = {
-                switch faultType {
-                case .noFaults:
-                    return "No fault"
-                case .failedFlashErase:
-                    return "Flash erase failed"
-                case .failedFlashStore:
-                    return "Flash store failed"
-                case .tableCorruptionBasalSubcommand:
-                    return "Basal subcommand table corruption"
-                case .basalPulseTableCorruption:
-                    return "Basal pulse table corruption"
-                case .corruptionByte720:
-                    return "Corruption in byte_720"
-                case .dataCorruptionInTestRTCInterrupt:
-                    return "Data corruption error in test_RTC_interrupt"
-                case .rtcInterruptHandlerInconsistentState:
-                    return "RTC interrupt handler called with inconstent state"
-                case .valueGreaterThan8:
-                    return "Value > 8"
-                case .invalidBeepRepeatPattern:
-                    return "Invalid beep repeat pattern"
-                case .bf0notEqualToBF1:
-                    return "Corruption in byte_BF0"
-                case .tableCorruptionTempBasalSubcommand:
-                    return "Temp basal subcommand table corruption"
-                case .resetDueToCOP:
-                    return "Reset due to COP"
-                case .resetDueToIllegalOpcode:
-                    return "Reset due to illegal opcode"
-                case .resetDueToIllegalAddress:
-                    return "Reset due to illegal address"
-                case .resetDueToSAWCOP:
-                    return "Reset due to SAWCOP"
-                case .corruptionInByte_866:
-                    return "Corruption in byte_866"
-                case .resetDueToLVD:
-                    return "Reset due to LVD"
-                case .messageLengthTooLong:
-                    return "Message length too long"
-                case .occluded:
-                    return "Occluded"
-                case .corruptionInWord129:
-                    return "Corruption in word_129 table/word_86A/dword_86E"
-                case .corruptionInByte868:
-                    return "Corruption in byte_868"
-                case .corruptionInAValidatedTable:
-                    return "Corruption in a validated table"
-                case .reservoirEmpty:
-                    return "Reservoir empty or exceeded maximum pulse delivery"
-                case .badPowerSwitchArrayValue1:
-                    return "Bad Power Switch Array Status and Control Register value 1 before starting pump"
-                case .badPowerSwitchArrayValue2:
-                    return "Bad Power Switch Array Status and Control Register value 2 before starting pump"
-                case .badLoadCnthValue:
-                    return "Bad LOADCNTH value when running pump"
-                case .exceededMaximumPodLife80Hrs:
-                    return "Exceeded maximum Pod life of 80 hours"
-                case .badStateCommand1AScheduleParse:
-                    return "Unexpected internal state in command_1A_schedule_parse_routine_wrapper"
-                case .unexpectedStateInRegisterUponReset:
-                    return "Unexpected commissioned state in status and control register upon reset"
-                case .wrongSummaryForTable129:
-                    return "Sum mismatch for word_129 table"
-                case .validateCountErrorWhenBolusing:
-                    return "Validate encoder count error when bolusing"
-                case .badTimerVariableState:
-                    return "Bad timer variable state"
-                case .unexpectedRTCModuleValueDuringReset:
-                    return "Unexpected RTC Modulo Register value during reset"
-                case .problemCalibrateTimer:
-                    return "Problem in calibrate_timer_case_3"
-                case .tickcntErrorRTC:
-                    return "Tick count error RTC"
-                case .tickFailure:
-                    return "Tick failure"
-                case .rtcInterruptHandlerUnexpectedCall:
-                    return "RTC interrupt handler unexpectedly called"
-                case .missing2hourAlertToFillTank:
-                    return "Failed to set up 2 hour alert for tank fill operation"
-                case .faultEventSetupPod:
-                    return "Bad arg or state in update_insulin_variables, verify_and_start_pump or main_loop_control_pump"
-                case .autoOff0:
-                    return "Alert #0 auto-off timeout"
-                case .autoOff1:
-                    return "Alert #1 auto-off timeout"
-                case .autoOff2:
-                    return "Alert #2 auto-off timeout"
-                case .autoOff3:
-                    return "Alert #3 auto-off timeout"
-                case .autoOff4:
-                    return "Alert #4 auto-off timeout"
-                case .autoOff5:
-                    return "Alert #5 auto-off timeout"
-                case .autoOff6:
-                    return "Alert #6 auto-off timeout"
-                case .autoOff7:
-                    return "Alert #7 auto-off timeout"
-                case .insulinDeliveryCommandError:
-                    return "Incorrect pod state for command or error during insulin command setup"
-                case .badValueStartupTest:
-                    return "Bad value during startup testing"
-                case .connectedPodCommandTimeout:
-                    return "Connected Pod command timeout"
-                case .resetFromUnknownCause:
-                    return "Reset from unknown cause"
-                case .vetoNotSet:
-                    return "Veto not set"
-                case .errorFlashInitialization:
-                    return "Flash initialization error"
-                case .badPiezoValue:
-                    return "Bad piezo value"
-                case .unexpectedValueByte358:
-                    return "Unexpected byte_358 value"
-                case .problemWithLoad1and2:
-                    return "Problem with LOAD1/LOAD2"
-                case .aGreaterThan7inMessage:
-                    return "A > 7 in message processing"
-                case .failedTestSawReset:
-                    return "SAW reset testing fail"
-                case .testInProgress:
-                    return "402D is 'Z' - test in progress"
-                case .problemWithPumpAnchor:
-                    return "Problem with pump anchor"
-                case .errorFlashWrite:
-                    return "Flash initialization or write error"
-                case .encoderCountTooHigh:
-                    return "Encoder count too high"
-                case .encoderCountExcessiveVariance:
-                    return "Encoder count excessive variance"
-                case .encoderCountTooLow:
-                    return "Encoder count too low"
-                case .encoderCountProblem:
-                    return "Encoder count problem"
-                case .checkVoltageOpenWire1:
-                    return "Check voltage open wire 1 problem"
-                case .checkVoltageOpenWire2:
-                    return "Check voltage open wire 2 problem"
-                case .problemWithLoad1and2type46:
-                    return "Problem with LOAD1/LOAD2"
-                case .problemWithLoad1and2type47:
-                    return "Problem with LOAD1/LOAD2"
-                case .badTimerCalibration:
-                    return "Bad timer calibration"
-                case .badTimerRatios:
-                    return "Bad timer values: COP timer ratio bad"
-                case .badTimerValues:
-                    return "Bad timer values"
-                case .trimICSTooCloseTo0x1FF:
-                    return "ICS trim too close to 0x1FF"
-                case .problemFindingBestTrimValue:
-                    return "find_best_trim_value problem"
-                case .badSetTPM1MultiCasesValue:
-                    return "Bad set_TPM1_multi_cases value"
-                case .unexpectedRFErrorFlagDuringReset:
-                    return "Unexpected TXSCR2 RF Tranmission Error Flag set during reset"
-                case .timerPulseWidthModulatorOverflow:
-                    return "Timer pulse-width modulator overflow"
-                case .tickcntError:
-                    return "Bad tick count state before starting pump"
-                case .badRfmXtalStart:
-                    return "TXOK issue in process_input_buffer"
-                case .badRxSensitivity:
-                    return "Bad Rx word_107 sensitivity value during input message processing"
-                case .packetFrameLengthTooLong:
-                    return "Packet frame length too long"
-                case .unexpectedIRQHighinTimerTick:
-                    return "Unexpected IRQ high in timer_tick"
-                case .unexpectedIRQLowinTimerTick:
-                    return "Unexpected IRQ low in timer_tick"
-                case .badArgToGetEntry:
-                    return "Corrupt constants table at byte_37A[] or flash byte_4036[]"
-                case .badArgToUpdate37ATable:
-                    return "Bad argument to update_37A_table"
-                case .errorUpdating37ATable:
-                    return "Error updating constants byte_37A table"
-                case .occlusionCheckValueTooHigh:
-                    return "Occlusion check value too high for detection"
-                case .loadTableCorruption:
-                    return "Load table corruption"
-                case .primeOpenCountTooLow:
-                    return "Prime open count too low"
-                case .badValueByte109:
-                    return "Bad byte_109 value"
-                case .disableFlashSecurityFailed:
-                    return "Write flash byte to disable flash security failed"
-                case .checkVoltageFailure:
-                    return "Two check voltage failures before starting pump"
-                case .occlusionCheckStartup1:
-                    return "Occlusion check startup problem 1"
-                case .occlusionCheckStartup2:
-                    return "Occlusion check startup problem 2"
-                case .occlusionCheckTimeouts1:
-                    return "Occlusion check excess timeouts 1"
-                case .occlusionCheckTimeouts2:
-                    return "Occlusion check excess timeouts 2"
-                case .occlusionCheckTimeouts3:
-                    return "Occlusion check excess timeouts 3"
-                case .occlusionCheckPulseIssue:
-                    return "Occlusion check pulse issue"
-                case .occlusionCheckBolusProblem:
-                    return "Occlusion check bolus problem"
-                case .occlusionCheckAboveThreshold:
-                    return "Occlusion check above threshold"
-                case .basalUnderInfusion:
-                    return "Basal under infusion"
-                case .basalOverInfusion:
-                    return "Basal over infusion"
-                case .tempBasalUnderInfusion:
-                    return "Temp basal under infusion"
-                case .tempBasalOverInfusion:
-                    return "Temp basal over infusion"
-                case .bolusUnderInfusion:
-                    return "Bolus under infusion"
-                case .bolusOverInfusion:
-                    return "Bolus over infusion"
-                case .basalOverInfusionPulse:
-                    return "Basal over infusion pulse"
-                case .tempBasalOverInfusionPulse:
-                    return "Temp basal over infusion pulse"
-                case .bolusOverInfusionPulse:
-                    return "Bolus over infusion pulse"
-                case .immediateBolusOverInfusionPulse:
-                    return "Immediate bolus under infusion pulse"
-                case .extendedBolusOverInfusionPulse:
-                    return "Extended bolus over infusion pulse"
-                case .corruptionOfTables:
-                    return "Corruption of $283/$2E3/$315 tables"
-                case .unrecognizedPulse:
-                    return "Bad pulse value to verify_and_start_pump"
-                case .syncWithoutTempActive:
-                    return "Pump sync req 5 with no temp basal active"
-                case .command1AParseUnexpectedFailed:
-                    return "Command 1A parse routine unexpected failed"
-                case .illegalChanParam:
-                    return "Bad parameter for $283/$2E3/$315 channel table specification"
-                case .basalPulseChanInactive:
-                    return "Pump basal request with basal IST not set"
-                case .tempPulseChanInactive:
-                    return "Pump temp basal request with temp basal IST not set"
-                case .bolusPulseChanInactive:
-                    return "Pump bolus request and bolus IST not set"
-                case .intSemaphoreNotSet:
-                    return "Bad table specifier field6 in 1A command"
-                case .illegalInterLockChan:
-                    return "Illegal interlock channel"
-                case .badStateInClearBolusIST2AndVars:
-                    return "Bad variable state in clear_Bolus_IST2_and_vars"
-                case .badStateInMaybeInc33D:
-                    return "Bad variable state in maybe_inc_33D"
-                case .valuesDoNotMatchOrAreGreaterThan0x97:
-                    return "Unknown fault code"
-                }
-            }()
-        } else {
-            faultDescription = "Unknown Fault"
+
+    public var faultDescription: String {
+        switch faultType {
+        case .noFaults:
+            return "No fault"
+        case .failedFlashErase:
+            return "Flash erase failed"
+        case .failedFlashStore:
+            return "Flash store failed"
+        case .tableCorruptionBasalSubcommand:
+            return "Basal subcommand table corruption"
+        case .basalPulseTableCorruption:
+            return "Basal pulse table corruption"
+        case .corruptionByte720:
+            return "Corruption in byte_720"
+        case .dataCorruptionInTestRTCInterrupt:
+            return "Data corruption error in test_RTC_interrupt"
+        case .rtcInterruptHandlerInconsistentState:
+            return "RTC interrupt handler called with inconstent state"
+        case .valueGreaterThan8:
+            return "Value > 8"
+        case .invalidBeepRepeatPattern:
+            return "Invalid beep repeat pattern"
+        case .bf0notEqualToBF1:
+            return "Corruption in byte_BF0"
+        case .tableCorruptionTempBasalSubcommand:
+            return "Temp basal subcommand table corruption"
+        case .resetDueToCOP:
+            return "Reset due to COP"
+        case .resetDueToIllegalOpcode:
+            return "Reset due to illegal opcode"
+        case .resetDueToIllegalAddress:
+            return "Reset due to illegal address"
+        case .resetDueToSAWCOP:
+            return "Reset due to SAWCOP"
+        case .corruptionInByte_866:
+            return "Corruption in byte_866"
+        case .resetDueToLVD:
+            return "Reset due to LVD"
+        case .messageLengthTooLong:
+            return "Message length too long"
+        case .occluded:
+            return "Occluded"
+        case .corruptionInWord129:
+            return "Corruption in word_129 table/word_86A/dword_86E"
+        case .corruptionInByte868:
+            return "Corruption in byte_868"
+        case .corruptionInAValidatedTable:
+            return "Corruption in a validated table"
+        case .reservoirEmpty:
+            return "Reservoir empty or exceeded maximum pulse delivery"
+        case .badPowerSwitchArrayValue1:
+            return "Bad Power Switch Array Status and Control Register value 1 before starting pump"
+        case .badPowerSwitchArrayValue2:
+            return "Bad Power Switch Array Status and Control Register value 2 before starting pump"
+        case .badLoadCnthValue:
+            return "Bad LOADCNTH value when running pump"
+        case .exceededMaximumPodLife80Hrs:
+            return "Exceeded maximum Pod life of 80 hours"
+        case .badStateCommand1AScheduleParse:
+            return "Unexpected internal state in command_1A_schedule_parse_routine_wrapper"
+        case .unexpectedStateInRegisterUponReset:
+            return "Unexpected commissioned state in status and control register upon reset"
+        case .wrongSummaryForTable129:
+            return "Sum mismatch for word_129 table"
+        case .validateCountErrorWhenBolusing:
+            return "Validate encoder count error when bolusing"
+        case .badTimerVariableState:
+            return "Bad timer variable state"
+        case .unexpectedRTCModuleValueDuringReset:
+            return "Unexpected RTC Modulo Register value during reset"
+        case .problemCalibrateTimer:
+            return "Problem in calibrate_timer_case_3"
+        case .tickcntErrorRTC:
+            return "Tick count error RTC"
+        case .tickFailure:
+            return "Tick failure"
+        case .rtcInterruptHandlerUnexpectedCall:
+            return "RTC interrupt handler unexpectedly called"
+        case .missing2hourAlertToFillTank:
+            return "Failed to set up 2 hour alert for tank fill operation"
+        case .faultEventSetupPod:
+            return "Bad arg or state in update_insulin_variables, verify_and_start_pump or main_loop_control_pump"
+        case .autoOff0:
+            return "Alert #0 auto-off timeout"
+        case .autoOff1:
+            return "Alert #1 auto-off timeout"
+        case .autoOff2:
+            return "Alert #2 auto-off timeout"
+        case .autoOff3:
+            return "Alert #3 auto-off timeout"
+        case .autoOff4:
+            return "Alert #4 auto-off timeout"
+        case .autoOff5:
+            return "Alert #5 auto-off timeout"
+        case .autoOff6:
+            return "Alert #6 auto-off timeout"
+        case .autoOff7:
+            return "Alert #7 auto-off timeout"
+        case .insulinDeliveryCommandError:
+            return "Incorrect pod state for command or error during insulin command setup"
+        case .badValueStartupTest:
+            return "Bad value during startup testing"
+        case .connectedPodCommandTimeout:
+            return "Connected Pod command timeout"
+        case .resetFromUnknownCause:
+            return "Reset from unknown cause"
+        case .vetoNotSet:
+            return "Veto not set"
+        case .errorFlashInitialization:
+            return "Flash initialization error"
+        case .badPiezoValue:
+            return "Bad piezo value"
+        case .unexpectedValueByte358:
+            return "Unexpected byte_358 value"
+        case .problemWithLoad1and2:
+            return "Problem with LOAD1/LOAD2"
+        case .aGreaterThan7inMessage:
+            return "A > 7 in message processing"
+        case .failedTestSawReset:
+            return "SAW reset testing fail"
+        case .testInProgress:
+            return "test in progress"
+        case .problemWithPumpAnchor:
+            return "Problem with pump anchor"
+        case .errorFlashWrite:
+            return "Flash initialization or write error"
+        case .encoderCountTooHigh:
+            return "Encoder count too high"
+        case .encoderCountExcessiveVariance:
+            return "Encoder count excessive variance"
+        case .encoderCountTooLow:
+            return "Encoder count too low"
+        case .encoderCountProblem:
+            return "Encoder count problem"
+        case .checkVoltageOpenWire1:
+            return "Check voltage open wire 1 problem"
+        case .checkVoltageOpenWire2:
+            return "Check voltage open wire 2 problem"
+        case .problemWithLoad1and2type46:
+            return "Problem with LOAD1/LOAD2"
+        case .problemWithLoad1and2type47:
+            return "Problem with LOAD1/LOAD2"
+        case .badTimerCalibration:
+            return "Bad timer calibration"
+        case .badTimerRatios:
+            return "Bad timer values: COP timer ratio bad"
+        case .badTimerValues:
+            return "Bad timer values"
+        case .trimICSTooCloseTo0x1FF:
+            return "ICS trim too close to 0x1FF"
+        case .problemFindingBestTrimValue:
+            return "find_best_trim_value problem"
+        case .badSetTPM1MultiCasesValue:
+            return "Bad set_TPM1_multi_cases value"
+        case .unexpectedRFErrorFlagDuringReset:
+            return "Unexpected TXSCR2 RF Tranmission Error Flag set during reset"
+        case .timerPulseWidthModulatorOverflow:
+            return "Timer pulse-width modulator overflow"
+        case .tickcntError:
+            return "Bad tick count state before starting pump"
+        case .badRfmXtalStart:
+            return "TXOK issue in process_input_buffer"
+        case .badRxSensitivity:
+            return "Bad Rx word_107 sensitivity value during input message processing"
+        case .packetFrameLengthTooLong:
+            return "Packet frame length too long"
+        case .unexpectedIRQHighinTimerTick:
+            return "Unexpected IRQ high in timer_tick"
+        case .unexpectedIRQLowinTimerTick:
+            return "Unexpected IRQ low in timer_tick"
+        case .badArgToGetEntry:
+            return "Corrupt constants table at byte_37A[] or flash byte_4036[]"
+        case .badArgToUpdate37ATable:
+            return "Bad argument to update_37A_table"
+        case .errorUpdating37ATable:
+            return "Error updating constants byte_37A table"
+        case .occlusionCheckValueTooHigh:
+            return "Occlusion check value too high for detection"
+        case .loadTableCorruption:
+            return "Load table corruption"
+        case .primeOpenCountTooLow:
+            return "Prime open count too low"
+        case .badValueByte109:
+            return "Bad byte_109 value"
+        case .disableFlashSecurityFailed:
+            return "Write flash byte to disable flash security failed"
+        case .checkVoltageFailure:
+            return "Two check voltage failures before starting pump"
+        case .occlusionCheckStartup1:
+            return "Occlusion check startup problem 1"
+        case .occlusionCheckStartup2:
+            return "Occlusion check startup problem 2"
+        case .occlusionCheckTimeouts1:
+            return "Occlusion check excess timeouts 1"
+        case .occlusionCheckTimeouts2:
+            return "Occlusion check excess timeouts 2"
+        case .occlusionCheckTimeouts3:
+            return "Occlusion check excess timeouts 3"
+        case .occlusionCheckPulseIssue:
+            return "Occlusion check pulse issue"
+        case .occlusionCheckBolusProblem:
+            return "Occlusion check bolus problem"
+        case .occlusionCheckAboveThreshold:
+            return "Occlusion check above threshold"
+        case .basalUnderInfusion:
+            return "Basal under infusion"
+        case .basalOverInfusion:
+            return "Basal over infusion"
+        case .tempBasalUnderInfusion:
+            return "Temp basal under infusion"
+        case .tempBasalOverInfusion:
+            return "Temp basal over infusion"
+        case .bolusUnderInfusion:
+            return "Bolus under infusion"
+        case .bolusOverInfusion:
+            return "Bolus over infusion"
+        case .basalOverInfusionPulse:
+            return "Basal over infusion pulse"
+        case .tempBasalOverInfusionPulse:
+            return "Temp basal over infusion pulse"
+        case .bolusOverInfusionPulse:
+            return "Bolus over infusion pulse"
+        case .immediateBolusOverInfusionPulse:
+            return "Immediate bolus under infusion pulse"
+        case .extendedBolusOverInfusionPulse:
+            return "Extended bolus over infusion pulse"
+        case .corruptionOfTables:
+            return "Corruption of $283/$2E3/$315 tables"
+        case .unrecognizedPulse:
+            return "Bad pulse value to verify_and_start_pump"
+        case .syncWithoutTempActive:
+            return "Pump sync req 5 with no temp basal active"
+        case .command1AParseUnexpectedFailed:
+            return "Command 1A parse routine unexpected failed"
+        case .illegalChanParam:
+            return "Bad parameter for $283/$2E3/$315 channel table specification"
+        case .basalPulseChanInactive:
+            return "Pump basal request with basal IST not set"
+        case .tempPulseChanInactive:
+            return "Pump temp basal request with temp basal IST not set"
+        case .bolusPulseChanInactive:
+            return "Pump bolus request and bolus IST not set"
+        case .intSemaphoreNotSet:
+            return "Bad table specifier field6 in 1A command"
+        case .illegalInterLockChan:
+            return "Illegal interlock channel"
+        case .badStateInClearBolusIST2AndVars:
+            return "Bad variable state in clear_Bolus_IST2_and_vars"
+        case .badStateInMaybeInc33D:
+            return "Bad variable state in maybe_inc_33D"
+        default:
+            return "Unknown fault code"
         }
+    }
+
+    public var description: String {
         return String(format: "Fault Event Code 0x%02x: %@", rawValue, faultDescription)
     }
     

+ 33 - 23
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/CancelDeliveryCommand.swift

@@ -9,30 +9,24 @@
 import Foundation
 
 
-
 public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
-    
+
     public let blockType: MessageBlockType = .cancelDelivery
-    
-    // ID1:1f00ee84 PTYPE:PDM SEQ:26 ID2:1f00ee84 B9:ac BLEN:7 MTYPE:1f05 BODY:e1f78752078196 CRC:03
-    
-    // Cancel bolus
-    // 1f 05 be1b741a 64 - 1U
-    // 1f 05 a00a1a95 64 - 1U over 1hr
-    // 1f 05 ff52f6c8 64 - 1U immediate, 1U over 1hr
-    
-    // Cancel temp basal
-    // 1f 05 f76d34c4 62 - 30U/hr
-    // 1f 05 156b93e8 62 - ?
-    // 1f 05 62723698 62 - 0%
-    // 1f 05 2933db73 62 - 03ea
-    
-    // Suspend is a Cancel delivery, followed by a configure alerts command (0x19)
-    // 1f 05 50f02312 03 191050f02312580f000f06046800001e0302
-    
-    // Deactivate pod:
+    // OFF 1  2 3 4 5  6
+    // 1F 05 NNNNNNNN AX
+
+    // Cancel bolus (with confirmation beep)
+    // 1f 05 be1b741a 64
+
+    // Cancel temp basal (with confirmation beep)
+    // 1f 05 f76d34c4 62
+
+    // Cancel all (before deactivate pod)
     // 1f 05 e1f78752 07
-    
+
+    // Cancel basal & temp basal for a suspend, followed by a configure alerts command (0x19) for alerts 5 & 6
+    // 1f 05 50f02312 03 19 10 50f02312 580f 000f 0604 6800 001e 0302
+
     public struct DeliveryType: OptionSet, Equatable {
         public let rawValue: UInt8
         
@@ -47,7 +41,23 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
         public init(rawValue: UInt8) {
             self.rawValue = rawValue
         }
-        
+
+        var debugDescription: String {
+            switch self {
+            case .none:
+                return "None"
+            case .basal:
+                return "Basal"
+            case .tempBasal:
+                return "TempBasal"
+            case .all:
+                return "All"
+            case .allButBasal:
+                return "AllButBasal"
+            default:
+                return "\(self.rawValue)"
+            }
+        }
     }
     
     public let deliveryType: DeliveryType
@@ -84,6 +94,6 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
 
 extension CancelDeliveryCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "CancelDeliveryCommand(nonce:\(Data(bigEndian: nonce).hexadecimalString), deliveryType:\(deliveryType), beepType:\(beepType))"
+        return "CancelDeliveryCommand(nonce:\(Data(bigEndian: nonce).hexadecimalString), deliveryType:\(deliveryType.debugDescription), beepType:\(beepType))"
     }
 }

+ 12 - 4
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift

@@ -21,7 +21,9 @@ public struct ConfigureAlertsCommand : NonceResyncableMessageBlock {
             UInt8(4 + configurations.count * AlertConfiguration.length),
             ])
         data.appendBigEndian(nonce)
-        for config in configurations {
+        // Sorting the alerts not required, but it can be helpful for log analysis
+        let sorted = configurations.sorted { $0.slot.rawValue < $1.slot.rawValue }
+        for config in sorted {
             data.append(contentsOf: config.data)
         }
         return data
@@ -92,6 +94,7 @@ extension AlertConfiguration {
         }
         self.beepType = beepType
 
+        self.silent = (beepType == .noBeepNonCancel)
     }
 
     public var data: Data {
@@ -104,12 +107,16 @@ extension AlertConfiguration {
         if autoOffModifier {
             firstByte += 1 << 1
         }
+
+        // The 9-bit duration is limited to 2^9-1 minutes max value
+        let durationMinutes = min(UInt(duration.minutes), 0x1ff)
+
         // High bit of duration
-        firstByte += UInt8((Int(duration.minutes) >> 8) & 0x1)
+        firstByte += UInt8((durationMinutes >> 8) & 0x1)
 
         var data = Data([
             firstByte,
-            UInt8(Int(duration.minutes) & 0xff)
+            UInt8(durationMinutes & 0xff)
             ])
 
         switch trigger {
@@ -122,7 +129,8 @@ extension AlertConfiguration {
             data.appendBigEndian(minutes)
         }
         data.append(beepRepeat.rawValue)
-        data.append(beepType.rawValue)
+        let beepTypeToSet: BeepType = silent ? .noBeepNonCancel : beepType
+        data.append(beepTypeToSet.rawValue)
 
         return data
     }

+ 2 - 3
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift

@@ -9,14 +9,13 @@
 import Foundation
 
 public struct DeactivatePodCommand : NonceResyncableMessageBlock {
-    
-    // ID1:1f00ee84 PTYPE:PDM SEQ:09 ID2:1f00ee84 B9:34 BLEN:6 MTYPE:1c04 BODY:0f7dc4058344 CRC:f1
+    // OFF 1  2 3 4 5
+    // 1C 04 NNNNNNNN
     
     public let blockType: MessageBlockType = .deactivatePod
     
     public var nonce: UInt32
     
-    // e1f78752 07 8196
     public var data: Data {
         var data = Data([
             blockType.rawValue,

+ 24 - 24
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DetailedStatus.swift

@@ -98,7 +98,7 @@ public struct DetailedStatus : PodInfo, Equatable {
             // subsequent returns will be byte swapped data from previous command/response at the same buffer offset.
             self.possibleFaultCallingAddress = encodedData[20...21].toBigEndian(UInt16.self) // only potentially valid for Dash
         }
-        
+
         self.data = Data(encodedData)
     }
 
@@ -118,9 +118,8 @@ extension DetailedStatus: CustomDebugStringConvertible {
             "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U",
             "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U",
-            "* faultEventCode: \(faultEventCode.description)",
-            "* reservoirLevel: \(reservoirLevel > Pod.maximumReservoirReading ? "50+" : reservoirLevel.twoDecimals) U",
-            "* timeActive: \(timeActive.stringValue)",
+            "* reservoirLevel: \(reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : reservoirLevel.twoDecimals) U",
+            "* timeActive: \(timeActive.timeIntervalStr)",
             "* unacknowledgedAlerts: \(unacknowledgedAlerts)",
             "",
             ].joined(separator: "\n")
@@ -133,8 +132,9 @@ extension DetailedStatus: CustomDebugStringConvertible {
         }
         if faultEventCode.faultType != .noFaults {
             result += [
+                "* faultEventCode: \(faultEventCode.description)",
                 "* faultAccessingTables: \(faultAccessingTables)",
-                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "NA")",
+                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.timeIntervalStr ?? "NA")",
                 "* errorEventInfo: \(errorEventInfo?.description ?? "NA")",
                 "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
                 "* possibleFaultCallingAddress: \(possibleFaultCallingAddress != nil ? String(format: "0x%04x", possibleFaultCallingAddress!) : "NA")",
@@ -160,26 +160,26 @@ extension DetailedStatus: RawRepresentable {
 }
 
 extension TimeInterval {
-    var stringValue: String {
-        let totalSeconds = self
-        let minutes = Int(totalSeconds / 60) % 60
-        let hours = Int(totalSeconds / 3600) - (Int(self / 3600)/24 * 24)
-        let days = Int((totalSeconds / 3600) / 24)
-        var pluralFormOfDays = "days"
-        if days == 1 {
-            pluralFormOfDays = "day"
+    var timeIntervalStr: String {
+        var str: String = ""
+        let hours = UInt(self / 3600)
+        let minutes = UInt(self / 60) % 60
+        let seconds = UInt(self) % 60
+        if hours != 0 {
+            str += String(format: "%uh", hours)
         }
-        let timeComponent = String(format: "%02d:%02d", hours, minutes)
-        if days > 0 {
-            return String(format: "%d \(pluralFormOfDays) plus %@", days, timeComponent)
-        } else {
-            return timeComponent
+        if minutes != 0 || hours != 0 {
+            str += String(format: "%um", minutes)
+        }
+        if seconds != 0 || str.isEmpty {
+            str += String(format: "%us", seconds)
         }
+        return str
     }
 }
 
 extension Double {
-    var twoDecimals: String {
+    public var twoDecimals: String {
         return String(format: "%.2f", self)
     }
 }
@@ -191,11 +191,11 @@ extension Double {
 // dddd: Pod Progress at time of first logged fault event
 //
 public struct ErrorEventInfo: CustomStringConvertible, Equatable {
-    let rawValue: UInt8
-    let insulinStateTableCorruption: Bool // 'a' bit
-    let occlusionType: Int // 'bb' 2-bit occlusion type
-    let immediateBolusInProgress: Bool // 'c' bit
-    let podProgressStatus: PodProgressStatus // 'dddd' bits
+    public let rawValue: UInt8
+    public let insulinStateTableCorruption: Bool // 'a' bit
+    public let occlusionType: Int // 'bb' 2-bit occlusion type
+    public let immediateBolusInProgress: Bool // 'c' bit
+    public let podProgressStatus: PodProgressStatus // 'dddd' bits
 
     public var errorEventInfo: ErrorEventInfo? {
         return ErrorEventInfo(rawValue: rawValue)

+ 4 - 4
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift

@@ -89,14 +89,14 @@ extension BinaryInteger {
     }
 }
 
-func pulseLogString(pulseLogEntries: [UInt32], lastPulseNumber: Int) -> String {
-    var str: String = "Pulse eeeeee0a pppliiib cccccccc dfgggggg\n"
+public func pulseLogString(pulseLogEntries: [UInt32], lastPulseNumber: Int) -> String {
+    var str: String = "Pulse eeeeee0a pppliiib cccccccc dfgggggg"
     var index = pulseLogEntries.count - 1
     var pulseNumber = lastPulseNumber
     while index >= 0 {
-        str += String(format: "%04d:", pulseNumber) + UInt32(pulseLogEntries[index]).binaryDescription + "\n"
+        str += String(format: "\n%04d:", pulseNumber) + UInt32(pulseLogEntries[index]).binaryDescription
         index -= 1
         pulseNumber -= 1
     }
-    return str + "\n"
+    return str
 }

+ 1 - 1
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/StatusResponse.swift

@@ -94,7 +94,7 @@ public struct StatusResponse : MessageBlock {
 
 extension StatusResponse: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulinDelivered), bolusNotDelivered:\(bolusNotDelivered), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))"
+        return "StatusResponse(deliveryStatus:\(deliveryStatus.description), progressStatus:\(podProgressStatus), timeActive:\(timeActive.timeIntervalStr), reservoirLevel:\(reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : reservoirLevel.twoDecimals), insulinDelivered:\(insulinDelivered.twoDecimals), bolusNotDelivered:\(bolusNotDelivered.twoDecimals), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))"
     }
 }
 

+ 1 - 1
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/TempBasalExtraCommand.swift

@@ -74,6 +74,6 @@ public struct TempBasalExtraCommand : MessageBlock {
 
 extension TempBasalExtraCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
-        return "TempBasalExtraCommand(completionBeep:\(completionBeep), programReminderInterval:\(programReminderInterval.stringValue) remainingPulses:\(remainingPulses), delayUntilFirstPulse:\(delayUntilFirstPulse.stringValue), rateEntries:\(rateEntries))"
+        return "TempBasalExtraCommand(completionBeep:\(completionBeep), programReminderInterval:\(programReminderInterval.timeIntervalStr), remainingPulses:\(remainingPulses), delayUntilFirstPulse:\(delayUntilFirstPulse.timeIntervalStr), rateEntries:\(rateEntries))"
     }
 }

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

@@ -30,9 +30,6 @@ public struct Pod {
     // Units per second for priming/cannula insertion
     public static let primeDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerPrimePulse
 
-    // User configured time before expiration advisory (PDM allows 1-24 hours)
-    public static let expirationAlertWindow = TimeInterval(hours: 2)
-
     // Expiration advisory window: time after expiration alert, and end of service imminent alarm
     public static let expirationAdvisoryWindow = TimeInterval(hours: 7)
 

+ 15 - 25
Dependencies/OmniKit/OmniKit/OmnipodCommon/PumpManagerAlert.swift

@@ -1,5 +1,5 @@
 //
-//  PodAlert.swift
+//  PumpManagerAlert.swift
 //  OmniKit
 //
 //  Created by Pete Schwamb on 7/9/20.
@@ -11,7 +11,6 @@ import LoopKit
 import HealthKit
 
 public enum PumpManagerAlert: Hashable {
-    case multiCommand(triggeringSlot: AlertSlot?)
     case podExpireImminent(triggeringSlot: AlertSlot?)
     case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval)
     case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double)
@@ -19,6 +18,7 @@ public enum PumpManagerAlert: Hashable {
     case suspendEnded(triggeringSlot: AlertSlot?)
     case podExpiring(triggeringSlot: AlertSlot?)
     case finishSetupReminder(triggeringSlot: AlertSlot?)
+    case unexpectedAlert(triggeringSlot: AlertSlot?)
     case timeOffsetChangeDetected
 
     var isRepeating: Bool {
@@ -36,8 +36,6 @@ public enum PumpManagerAlert: Hashable {
 
     var contentTitle: String {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content title for multiCommand pod alert")
         case .userPodExpiration:
             return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert")
         case .podExpiring:
@@ -52,6 +50,8 @@ public enum PumpManagerAlert: Hashable {
             return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert")
         case .finishSetupReminder:
             return LocalizedString("Pod Pairing Incomplete", comment: "Alert content title for finishSetupReminder pod alert")
+        case .unexpectedAlert:
+            return LocalizedString("Unexpected Alert", comment: "Alert content title for unexpected pod alert")
         case .timeOffsetChangeDetected:
             return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert")
         }
@@ -59,8 +59,6 @@ public enum PumpManagerAlert: Hashable {
 
     var contentBody: String {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content body for multiCommand pod alert")
         case .userPodExpiration(_, let offset):
             let formatter = DateComponentsFormatter()
             formatter.allowedUnits = [.hour]
@@ -81,6 +79,9 @@ public enum PumpManagerAlert: Hashable {
             return LocalizedString("The insulin suspension period has ended.\n\nYou can resume delivery from the banner on the home screen or from your pump settings screen. You will be reminded again in 15 minutes.", comment: "Alert content body for suspendEnded pod alert")
         case .finishSetupReminder:
             return LocalizedString("Please finish pairing your pod.", comment: "Alert content body for finishSetupReminder pod alert")
+        case .unexpectedAlert(let triggeringSlot):
+            let slotNumberString = triggeringSlot != nil ? String(describing: triggeringSlot!.rawValue) : "?"
+            return String(format: LocalizedString("Unexpected Pod Alert #%1@!", comment: "Alert content body for unexpected pod alert (1: slotNumberString)"), slotNumberString)
         case .timeOffsetChangeDetected:
             return LocalizedString("The time on your pump is different from the current time. You can review the pump time and and sync to current time in settings.", comment: "Alert content body for timeOffsetChangeDetected pod alert")
         }
@@ -88,8 +89,6 @@ public enum PumpManagerAlert: Hashable {
 
     var triggeringSlot: AlertSlot? {
         switch self {
-        case .multiCommand(let slot):
-            return slot
         case .userPodExpiration(let slot, _):
             return slot
         case .podExpiring(let slot):
@@ -104,6 +103,8 @@ public enum PumpManagerAlert: Hashable {
             return slot
         case .finishSetupReminder(let slot):
             return slot
+        case .unexpectedAlert(let slot):
+            return slot
         case .timeOffsetChangeDetected:
             return nil
         }
@@ -139,8 +140,6 @@ public enum PumpManagerAlert: Hashable {
 
     var alertIdentifier: String {
         switch self {
-        case .multiCommand:
-            return "multiCommand"
         case .userPodExpiration:
             return "userPodExpiration"
         case .podExpiring:
@@ -153,10 +152,12 @@ public enum PumpManagerAlert: Hashable {
             return "suspendInProgress"
         case .suspendEnded:
             return "suspendEnded"
-        case .timeOffsetChangeDetected:
-            return "timeOffsetChangeDetected"
         case .finishSetupReminder:
             return "finishSetupReminder"
+        case .unexpectedAlert:
+            return "unexpectedAlert"
+        case .timeOffsetChangeDetected:
+            return "timeOffsetChangeDetected"
         }
     }
 
@@ -183,8 +184,6 @@ extension PumpManagerAlert: RawRepresentable {
         }
 
         switch identifier {
-        case "multiCommand":
-            self = .multiCommand(triggeringSlot: slot)
         case "userPodExpiration":
             guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else {
                 return nil
@@ -203,6 +202,8 @@ extension PumpManagerAlert: RawRepresentable {
             self = .suspendInProgress(triggeringSlot: slot)
         case "suspendEnded":
             self = .suspendEnded(triggeringSlot: slot)
+        case "unexpectedAlert":
+            self = .unexpectedAlert(triggeringSlot: slot)
         case "timeOffsetChangeDetected":
             self = .timeOffsetChangeDetected
         default:
@@ -229,14 +230,3 @@ extension PumpManagerAlert: RawRepresentable {
         return rawValue
     }
 }
-
-extension PodAlert {
-    var isIgnored: Bool {
-        switch self {
-        case .podSuspendedReminder, .finishSetupReminder:
-            return true
-        default:
-            return false
-        }
-    }
-}

+ 32 - 0
Dependencies/OmniKit/OmniKit/OmnipodCommon/SilencePodPreference.swift

@@ -0,0 +1,32 @@
+//
+//  SilencePodPreference.swift
+//  OmniKit
+//
+//  Created by Joe Moran on 8/30/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import Foundation
+
+public enum SilencePodPreference: Int, CaseIterable {
+    case disabled
+    case enabled
+
+    public var title: String {
+        switch self {
+        case .disabled:
+            return LocalizedString("Disabled", comment: "Title string for SilencePodPreference.disabled")
+        case .enabled:
+            return LocalizedString("Silenced", comment: "Title string for SilencePodPreference.enabled")
+        }
+    }
+
+    public var description: String {
+        switch self {
+        case .disabled:
+            return LocalizedString("Normal operation mode where audible Pod beeps are used for all Pod alerts and when confidence reminders are enabled.", comment: "Description for SilencePodPreference.disabled")
+        case .enabled:
+            return LocalizedString("All Pod alerts use no beeps and confirmation reminder beeps are suppressed. The Pod will only beep for fatal Pod faults and when playing test beeps.\n\n⚠️Warning - Whenever the Pod is silenced it must be kept within Bluetooth range of this device to receive notifications for Pod alerts.", comment: "Description for SilencePodPreference.enabled")
+        }
+    }
+}

+ 259 - 80
Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -143,14 +143,6 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
         return setStateWithResult(changes)
     }
 
-    @discardableResult
-    private func mutateState(_ changes: (_ state: inout OmnipodPumpManagerState) -> Void) -> OmnipodPumpManagerState {
-        return setStateWithResult({ (state) -> OmnipodPumpManagerState in
-            changes(&state)
-            return state
-        })
-    }
-
     private func setStateWithResult<ReturnType>(_ changes: (_ state: inout OmnipodPumpManagerState) -> ReturnType) -> ReturnType {
         var oldValue: OmnipodPumpManagerState!
         var returnType: ReturnType!
@@ -282,12 +274,14 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
     override public var debugDescription: String {
         let lines = [
             "## OmnipodPumpManager",
+            "",
+            super.debugDescription,
             "podComms: \(String(reflecting: podComms))",
-            "state: \(String(reflecting: state))",
+            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
             "status: \(String(describing: status))",
+            "",
             "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)",
-            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
-            super.debugDescription,
+            "state: \(String(reflecting: state))",
         ]
         return lines.joined(separator: "\n")
     }
@@ -548,10 +542,21 @@ extension OmnipodPumpManager {
         return false
     }
 
+    private var podTime: TimeInterval {
+        get {
+            guard let podState = state.podState else {
+                return 0
+            }
+            let elapsed = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0)
+            let podActiveTime = podState.podTime + elapsed
+            return podActiveTime
+        }
+    }
+
     // Returns a suitable beep command MessageBlock based the current beep preferences and
     // whether there is an unfinializedDose for a manual temp basal &/or a manual bolus.
     private func beepMessageBlock(beepType: BeepType) -> MessageBlock? {
-        guard self.beepPreference.shouldBeepForManualCommand else {
+        guard self.beepPreference.shouldBeepForManualCommand && !self.silencePod else {
             return nil
         }
 
@@ -635,6 +640,13 @@ extension OmnipodPumpManager {
         }
     }
 
+    // Thread-safe
+    public var silencePod: Bool {
+        get {
+            return state.silencePod
+        }
+    }
+
     // From last status response
     public var reservoirLevel: ReservoirLevel? {
         return state.reservoirLevel
@@ -876,15 +888,13 @@ extension OmnipodPumpManager {
                         }
                     }
 
-                    let expiration = self.podExpiresAt ?? Date().addingTimeInterval(Pod.nominalPodLife)
-                    let timeUntilExpirationReminder = expiration.addingTimeInterval(-self.state.defaultExpirationReminderOffset).timeIntervalSince(self.dateGenerator())
-
+                    let expirationReminderTime = Pod.nominalPodLife - self.state.defaultExpirationReminderOffset
                     let alerts: [PodAlert] = [
-                        .expirationReminder(self.state.defaultExpirationReminderOffset > 0 ? timeUntilExpirationReminder : 0),
-                        .lowReservoir(self.state.lowReservoirReminderValue)
+                        .expirationReminder(offset: self.podTime, absAlertTime: self.state.defaultExpirationReminderOffset > 0 ? expirationReminderTime : 0),
+                        .lowReservoir(units: self.state.lowReservoirReminderValue)
                     ]
 
-                    let finishWait = try messageSender.insertCannula(optionalAlerts: alerts)
+                    let finishWait = try messageSender.insertCannula(optionalAlerts: alerts, silent: self.silencePod)
                     completion(.success(finishWait))
                 } catch let error {
                     completion(.failure(.communication(error)))
@@ -948,6 +958,62 @@ extension OmnipodPumpManager {
         }
     }
 
+    public func getDetailedStatus(completion: ((_ result: PumpManagerResult<DetailedStatus>) -> Void)? = nil) {
+
+        // use hasSetupPod here instead of hasActivePod as DetailedStatus can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion?(.failure(PumpManagerError.configuration(OmnipodPumpManagerError.noPodPaired)))
+            return
+        }
+
+        let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
+        podComms.runSession(withName: "Get detailed status", using: rileyLinkSelector) { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .bipBip)
+                    let detailedStatus = try session.getDetailedStatus(beepBlock: beepBlock)
+                    session.dosesForStorage({ (doses) -> Bool in
+                        self.store(doses: doses, in: session)
+                    })
+                    completion?(.success(detailedStatus))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion?(.failure(.communication(error as? LocalizedError)))
+                self.log.error("Failed to fetch detailed status: %{public}@", String(describing: error))
+            }
+        }
+    }
+
+    public func acknowledgePodAlerts(_ alertsToAcknowledge: AlertSet, completion: @escaping (_ alerts: AlertSet?) -> Void) {
+        guard self.hasActivePod else {
+            completion(nil)
+            return
+        }
+
+        let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
+        self.podComms.runSession(withName: "Acknowledge Alerts", using: rileyLinkSelector) { (result) in
+            let session: PodCommsSession
+            switch result {
+            case .success(let s):
+                session = s
+            case .failure:
+                completion(nil)
+                return
+            }
+
+            do {
+                let beepBlock = self.beepMessageBlock(beepType: .bipBip)
+                let alerts = try session.acknowledgeAlerts(alerts: alertsToAcknowledge, beepBlock: beepBlock)
+                completion(alerts)
+            } catch {
+                completion(nil)
+            }
+        }
+    }
+
     public func setTime(completion: @escaping (OmnipodPumpManagerError?) -> Void) {
         
         guard state.hasActivePod else {
@@ -966,7 +1032,7 @@ extension OmnipodPumpManager {
             switch result {
             case .success(let session):
                 do {
-                    let beep = self.beepPreference.shouldBeepForManualCommand
+                    let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                     let _ = try session.setTime(timeZone: timeZone, basalSchedule: self.state.basalSchedule, date: Date(), acknowledgementBeep: beep)
                     self.setState { (state) in
                         state.timeZone = timeZone
@@ -1023,7 +1089,7 @@ extension OmnipodPumpManager {
                     case .success:
                         break
                     }
-                    let beep = self.beepPreference.shouldBeepForManualCommand
+                    let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                     let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
 
                     self.setState { (state) in
@@ -1086,12 +1152,12 @@ extension OmnipodPumpManager {
         self.podComms.runSession(withName: "Play Test Beeps", using: rileyLinkSelector) { (result) in
             switch result {
             case .success(let session):
-                // preserve Pod completion beep state for any unfinalized manual insulin delivery
-                let beep = self.beepPreference.shouldBeepForManualCommand
+                // preserve the pod's completion beep state which gets reset playing beeps
+                let enabled: Bool = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                 let result = session.beepConfig(
                     beepType: .bipBeepBipBeepBipBeepBipBeep,
-                    tempBasalCompletionBeep: beep && self.hasUnfinalizedManualTempBasal,
-                    bolusCompletionBeep: beep && self.hasUnfinalizedManualBolus
+                    tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal,
+                    bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus
                 )
 
                 switch result {
@@ -1145,17 +1211,21 @@ extension OmnipodPumpManager {
     }
 
     public func setConfirmationBeeps(newPreference: BeepPreference, completion: @escaping (OmnipodPumpManagerError?) -> Void) {
-        self.log.default("Set Confirmation Beeps to %s", String(describing: newPreference))
-        guard self.hasActivePod else {
+
+        // If there isn't an active pod or the pod is currently silenced,
+        // just need to update the internal state without any pod commands.
+        let name = String(format: "Set Beep Preference to %@", String(describing: newPreference))
+        if !self.hasActivePod || self.silencePod {
+            self.log.default("%{public}@ for internal state only", name)
             self.setState { state in
-                state.confirmationBeeps = newPreference // set here to allow changes on a faulted Pod
+                state.confirmationBeeps = newPreference
             }
             completion(nil)
             return
         }
 
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
-        self.podComms.runSession(withName: "Set Confirmation Beeps Preference", using: rileyLinkSelector) { (result) in
+        self.podComms.runSession(withName: name, using: rileyLinkSelector) { (result) in
             switch result {
             case .success(let session):
                 // enable/disable Pod completion beep state for any unfinalized manual insulin delivery
@@ -1181,6 +1251,76 @@ extension OmnipodPumpManager {
             }
         }
     }
+
+    // Reconfigures all active alerts in pod to be silent or not as well as sets/clears the
+    // self.silencePod state variable which silences all confirmation beeping when enabled.
+    public func setSilencePod(silencePod: Bool, completion: @escaping (OmnipodPumpManagerError?) -> Void) {
+
+        let name = String(format: "%@ Pod", silencePod ? "Silence" : "Unsilence")
+        // allow Silence Pod changes without an active Pod
+        guard self.hasActivePod else {
+            self.log.default("%{public}@", name)
+            self.setState { state in
+                state.silencePod = silencePod
+            }
+            completion(nil)
+            return
+        }
+
+        let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
+        self.podComms.runSession(withName: name, using: rileyLinkSelector) { (result) in
+
+            let session: PodCommsSession
+            switch result {
+            case .success(let s):
+                session = s
+            case .failure(let error):
+                completion(.communication(error))
+                return
+            }
+
+            guard let configuredAlerts = self.state.podState?.configuredAlerts,
+                  let activeAlertSlots = self.state.podState?.activeAlertSlots,
+                  let reservoirLevel = self.state.podState?.lastInsulinMeasurements?.reservoirLevel?.rawValue else
+            {
+                self.log.error("Missing podState") // should never happen
+                completion(OmnipodPumpManagerError.noPodPaired)
+                return
+            }
+
+            let beepBlock: MessageBlock?
+            if !self.beepPreference.shouldBeepForManualCommand {
+                // No enabled completion beeps to worry about for any in-progress manual delivery
+                beepBlock = nil
+            } else if silencePod {
+                // Disable completion beeps for any in-progress manual delivery w/o beeping
+                beepBlock = BeepConfigCommand(beepType: .noBeepNonCancel)
+            } else {
+                // Emit a confirmation beep and enable completion beeps for any in-progress manual delivery
+                beepBlock = BeepConfigCommand(
+                    beepType: .bipBip,
+                    tempBasalCompletionBeep: self.hasUnfinalizedManualTempBasal,
+                    bolusCompletionBeep: self.hasUnfinalizedManualBolus
+                )
+            }
+
+            let podAlerts = regeneratePodAlerts(silent: silencePod, configuredAlerts: configuredAlerts, activeAlertSlots: activeAlertSlots, currentPodTime: self.podTime, currentReservoirLevel: reservoirLevel)
+            do {
+                // Since non-responsive pod comms are currently only resolved for insulin related commands,
+                // it's possible that a previous pod alert was successfully configured will lose its response
+                // and thus the alert won't get reset when reconfiguring pod alerts with a new silence pod state.
+                // So acknowledge all alerts now to be absolutely sure that no triggered alert will be forgotten.
+                try session.configureAlerts(podAlerts, acknowledgeAll: true, beepBlock: beepBlock)
+                self.setState { (state) in
+                    state.silencePod = silencePod
+                }
+                completion(nil)
+            } catch {
+                self.log.error("Configure alerts %{public}@ failed: %{public}@", String(describing: podAlerts), String(describing: error))
+                completion(.communication(error))
+            }
+        }
+    }
 }
 
 // MARK: - PumpManager
@@ -1273,7 +1413,7 @@ extension OmnipodPumpManager: PumpManager {
 
     public var defaultExpirationReminderOffset: TimeInterval {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.defaultExpirationReminderOffset = newValue
             }
         }
@@ -1284,7 +1424,7 @@ extension OmnipodPumpManager: PumpManager {
 
     public var lowReservoirReminderValue: Double {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.lowReservoirReminderValue = newValue
             }
         }
@@ -1295,7 +1435,7 @@ extension OmnipodPumpManager: PumpManager {
 
     public var podAttachmentConfirmed: Bool {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.podAttachmentConfirmed = newValue
             }
         }
@@ -1306,7 +1446,7 @@ extension OmnipodPumpManager: PumpManager {
 
     public var initialConfigurationCompleted: Bool {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.initialConfigurationCompleted = newValue
             }
         }
@@ -1391,7 +1531,7 @@ extension OmnipodPumpManager: PumpManager {
 
             // Use a beepBlock for the confirmation beep to avoid getting 3 beeps using cancel command beeps!
             let beepBlock = self.beepMessageBlock(beepType: .beeeeeep)
-            let result = session.suspendDelivery(suspendReminder: suspendReminder, beepBlock: beepBlock)
+            let result = session.suspendDelivery(suspendReminder: suspendReminder, silent: self.silencePod, beepBlock: beepBlock)
             switch result {
             case .certainFailure(let error):
                 self.log.error("Failed to suspend: %{public}@", String(describing: error))
@@ -1437,8 +1577,8 @@ extension OmnipodPumpManager: PumpManager {
 
             do {
                 let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
-                let beep = self.beepPreference.shouldBeepForManualCommand
-                let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep, completionBeep: beep)
+                let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
+                let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                 self.clearSuspendReminder()
                 session.dosesForStorage() { (doses) -> Bool in
                     return self.store(doses: doses, in: session)
@@ -1514,8 +1654,14 @@ extension OmnipodPumpManager: PumpManager {
         // Round to nearest supported volume
         let enactUnits = roundToSupportedBolusVolume(units: units)
 
-        let acknowledgementBeep = self.beepPreference.shouldBeepForCommand(automatic: activationType.isAutomatic)
-        let completionBeep = beepPreference.shouldBeepForManualCommand && !activationType.isAutomatic
+        let acknowledgementBeep, completionBeep: Bool
+        if self.silencePod {
+            acknowledgementBeep = false
+            completionBeep = false
+        } else {
+            acknowledgementBeep = self.beepPreference.shouldBeepForCommand(automatic: activationType.isAutomatic)
+            completionBeep = beepPreference.shouldBeepForManualCommand && !activationType.isAutomatic
+        }
 
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName: "Bolus", using: rileyLinkSelector) { (result) in
@@ -1601,7 +1747,7 @@ extension OmnipodPumpManager: PumpManager {
                 }
 
                 // when cancelling a bolus use the built-in type 6 beeeeeep to match PDM if confirmation beeps are enabled
-                let beepType: BeepType = self.beepPreference.shouldBeepForManualCommand ? .beeeeeep : .noBeepCancel
+                let beepType: BeepType = self.beepPreference.shouldBeepForManualCommand && !self.silencePod ? .beeeeeep : .noBeepCancel
                 let result = session.cancelDelivery(deliveryType: .bolus, beepType: beepType)
                 switch result {
                 case .certainFailure(let error):
@@ -1637,8 +1783,14 @@ extension OmnipodPumpManager: PumpManager {
         // Round to nearest supported rate
         let rate = roundToSupportedBasalRate(unitsPerHour: unitsPerHour)
 
-        let acknowledgementBeep = beepPreference.shouldBeepForCommand(automatic: automatic)
-        let completionBeep = beepPreference.shouldBeepForManualCommand && !automatic
+        let acknowledgementBeep, completionBeep: Bool
+        if self.silencePod {
+            acknowledgementBeep = false
+            completionBeep = false
+        } else {
+            acknowledgementBeep = beepPreference.shouldBeepForCommand(automatic: automatic)
+            completionBeep = beepPreference.shouldBeepForManualCommand && !automatic
+        }
 
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName: "Enact Temp Basal", using: rileyLinkSelector) { (result) in
@@ -1668,9 +1820,12 @@ extension OmnipodPumpManager: PumpManager {
                 return
             }
 
+            // Did the last message have comms issues or is the last delivery status not verified correctly?
+            let uncertainDeliveryStatus = self.state.podState?.lastCommsOK == false || self.state.podState?.deliveryStatusVerified == false
+
             // Do the cancel temp basal command if currently running a temp basal OR
-            // if resuming scheduled basal delivery.
-            if self.state.podState?.unfinalizedTempBasal != nil || resumingScheduledBasal {
+            // if resuming scheduled basal delivery OR if the delivery status is uncertain.
+            if self.state.podState?.unfinalizedTempBasal != nil || resumingScheduledBasal || uncertainDeliveryStatus {
                 let status: StatusResponse
 
                 // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
@@ -1768,7 +1923,7 @@ extension OmnipodPumpManager: PumpManager {
     }
 
     public func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (Result<DeliveryLimits, Error>) -> Void) {
-        mutateState { state in
+        setState { state in
             if let rate = deliveryLimits.maximumBasalRate?.doubleValue(for: .internationalUnitsPerHour) {
                 state.maximumTempBasalRate = rate
                 completion(.success(deliveryLimits))
@@ -1814,16 +1969,25 @@ extension OmnipodPumpManager: PumpManager {
                 return
             }
 
-            var timeUntilReminder : TimeInterval = 0
+            let podTime = self.podTime
+            var expirationReminderPodTime: TimeInterval = 0 // default to expiration reminder alert inactive
+
+            // If the interval before expiration is not a positive value (e.g., it's in the past),
+            // then the pod alert will get the default alert time of 0 making this alert inactive.
             if let intervalBeforeExpiration = intervalBeforeExpiration, intervalBeforeExpiration > 0 {
-                timeUntilReminder = expiresAt.addingTimeInterval(-intervalBeforeExpiration).timeIntervalSince(self.dateGenerator())
+                let timeUntilReminder = expiresAt.addingTimeInterval(-intervalBeforeExpiration).timeIntervalSince(self.dateGenerator())
+                // Only bother to set an expiration reminder pod alert if it is still at least a couple of minutes in the future
+                if timeUntilReminder > .minutes(2) {
+                    expirationReminderPodTime = podTime + timeUntilReminder
+                    self.log.debug("Update Expiration timeUntilReminder=%@, podTime=%@, expirationReminderPodTime=%@", timeUntilReminder.timeIntervalStr, podTime.timeIntervalStr, expirationReminderPodTime.timeIntervalStr)
+                }
             }
 
-            let expirationReminder = PodAlert.expirationReminder(timeUntilReminder)
+            let expirationReminder = PodAlert.expirationReminder(offset: podTime, absAlertTime: expirationReminderPodTime, silent: self.silencePod)
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([expirationReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
+                self.setState({ (state) in
                     state.scheduledExpirationReminderOffset = intervalBeforeExpiration
                 })
                 completion(nil)
@@ -1847,7 +2011,8 @@ extension OmnipodPumpManager: PumpManager {
             expiration.addingTimeInterval(.hours(Double(i)))
         }
         let now = dateGenerator()
-        return allDates.filter { $0.timeIntervalSince(now) > 0 }
+        // Have a couple minutes of slop to avoid confusion trying to set an expiration reminder too close to now
+        return allDates.filter { $0.timeIntervalSince(now) > .minutes(2) }
     }
 
     public var scheduledExpirationReminder: Date? {
@@ -1860,9 +2025,26 @@ extension OmnipodPumpManager: PumpManager {
         return expiration.addingTimeInterval(-.hours(round(offset.hours)))
     }
 
+    // Updates the low reservior reminder value both for the current pod (when applicable) and for future pods
     public func updateLowReservoirReminder(_ value: Int, completion: @escaping (OmnipodPumpManagerError?) -> Void) {
+
+        let supportedValue = min(max(0, Double(value)), Pod.maximumReservoirReading)
+        let setLowReservoirReminderValue = {
+            self.log.default("Set Low Reservoir Reminder to %d U", value)
+            self.lowReservoirReminderValue = supportedValue
+            completion(nil)
+        }
+
         guard self.hasActivePod else {
-            completion(OmnipodPumpManagerError.noPodPaired)
+            // no active pod, just set the internal state for the next pod
+            setLowReservoirReminderValue()
+            return
+        }
+
+        guard let currentReservoirLevel = self.reservoirLevel?.rawValue, currentReservoirLevel > supportedValue else {
+            // Since the new low reservoir alert level is not below the current reservoir value,
+            // just set the internal state for the next pod to prevent an immediate low reservoir alert.
+            setLowReservoirReminderValue()
             return
         }
 
@@ -1878,13 +2060,11 @@ extension OmnipodPumpManager: PumpManager {
                 return
             }
 
-            let lowReservoirReminder = PodAlert.lowReservoir(Double(value))
+            let lowReservoirReminder = PodAlert.lowReservoir(units: supportedValue, silent: self.silencePod)
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
-                    state.lowReservoirReminderValue = Double(value)
-                })
+                self.lowReservoirReminderValue = supportedValue
                 completion(nil)
             } catch {
                 completion(.communication(error))
@@ -1909,7 +2089,7 @@ extension OmnipodPumpManager: PumpManager {
             }
         }
 
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.insert(alert)
         }
     }
@@ -1925,7 +2105,7 @@ extension OmnipodPumpManager: PumpManager {
                 delegate?.retractAlert(identifier: repeatingIdentifier)
             }
         }
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.remove(alert)
         }
     }
@@ -1946,6 +2126,8 @@ extension OmnipodPumpManager: PumpManager {
                 }
             } else {
                 log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot))
+                let pumpManagerAlert = PumpManagerAlert.unexpectedAlert(triggeringSlot: slot)
+                issueAlert(alert: pumpManagerAlert)
             }
         }
         for alert in removed {
@@ -1954,34 +2136,24 @@ extension OmnipodPumpManager: PumpManager {
     }
 
     private func getPumpManagerAlert(for podAlert: PodAlert, slot: AlertSlot) -> PumpManagerAlert? {
-        guard let podState = state.podState, let expiresAt = podState.expiresAt else {
-            preconditionFailure("trying to lookup alert info without podState")
-        }
-
-        guard !podAlert.isIgnored else {
-            return nil
-        }
 
         switch podAlert {
-        case .podSuspendedReminder:
-            return PumpManagerAlert.suspendInProgress(triggeringSlot: slot)
+        case .shutdownImminent:
+            return PumpManagerAlert.podExpireImminent(triggeringSlot: slot)
         case .expirationReminder:
-            guard let offset = state.scheduledExpirationReminderOffset, offset > 0 else {
-                return nil
+            guard let podState = state.podState, let expiresAt = podState.expiresAt else {
+                preconditionFailure("trying to lookup expiresAt")
             }
             let timeToExpiry = TimeInterval(hours: expiresAt.timeIntervalSince(dateGenerator()).hours.rounded())
             return PumpManagerAlert.userPodExpiration(triggeringSlot: slot, scheduledExpirationReminderOffset: timeToExpiry)
-        case .expired:
-            return PumpManagerAlert.podExpiring(triggeringSlot: slot)
-        case .shutdownImminent:
-            return PumpManagerAlert.podExpireImminent(triggeringSlot: slot)
-        case .lowReservoir(let units):
+        case .lowReservoir(let units, _):
             return PumpManagerAlert.lowReservoir(triggeringSlot: slot, lowReservoirReminderValue: units)
-        case .finishSetupReminder, .waitingForPairingReminder:
-            return PumpManagerAlert.finishSetupReminder(triggeringSlot: slot)
         case .suspendTimeExpired:
             return PumpManagerAlert.suspendEnded(triggeringSlot: slot)
+        case .expired:
+            return PumpManagerAlert.podExpiring(triggeringSlot: slot)
         default:
+            // No PumpManagerAlerts are used for any other pod alerts (including suspendInProgress).
             return nil
         }
     }
@@ -1999,7 +2171,7 @@ extension OmnipodPumpManager: PumpManager {
                         } catch {
                             return
                         }
-                        self.mutateState { state in
+                        self.setState { state in
                             state.activeAlerts.remove(alert)
                             state.alertsWithPendingAcknowledgment.remove(alert)
                         }
@@ -2123,7 +2295,7 @@ extension OmnipodPumpManager: PodCommsDelegate {
             }
         } else {
             // Resetting podState
-            mutateState { state in
+            setState { state in
                 state.updatePodStateFromPodComms(podState)
             }
         }
@@ -2142,6 +2314,13 @@ extension OmnipodPumpManager {
             if alert.alertIdentifier == alertIdentifier {
                 // If this alert was triggered by the pod find the slot to clear it.
                 if let slot = alert.triggeringSlot {
+                    if case .some(.suspended) = self.state.podState?.suspendState, slot == .slot6SuspendTimeExpired {
+                        // Don't clear this pod alert here with the pod still suspended so that the suspend time expired
+                        // pod alert beeping will continue until the pod is resumed which will then deactivate this alert.
+                        log.default("Skipping acknowledgement of suspend time expired alert with a suspended pod")
+                        completion(nil)
+                        return
+                    }
                     let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
                     self.podComms.runSession(withName: "Acknowledge Alert", using: rileyLinkSelector) { (result) in
                         switch result {
@@ -2150,18 +2329,18 @@ extension OmnipodPumpManager {
                                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                                 let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock)
                             } catch {
-                                self.mutateState { state in
+                                self.setState { state in
                                     state.alertsWithPendingAcknowledgment.insert(alert)
                                 }
                                 completion(error)
                                 return
                             }
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.activeAlerts.remove(alert)
                             }
                             completion(nil)
                         case .failure(let error):
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.alertsWithPendingAcknowledgment.insert(alert)
                             }
                             completion(error)
@@ -2170,7 +2349,7 @@ extension OmnipodPumpManager {
                     }
                 } else {
                     // Non-pod alert
-                    self.mutateState { state in
+                    self.setState { state in
                         state.activeAlerts.remove(alert)
                         if alert == .timeOffsetChangeDetected {
                             state.acknowledgedTimeOffsetAlert = true
@@ -2199,7 +2378,7 @@ extension FaultEventCode {
         case .exceededMaximumPodLife80Hrs:
             return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification")
         default:
-            return LocalizedString("Critical Pod Error", comment: "The title for AlarmCode.other notification")
+            return String(format: LocalizedString("Critical Pod Fault %1$03d", comment: "The title for AlarmCode.other notification: (1: fault code value)"), self.rawValue)
         }
     }
 

+ 18 - 5
Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManagerState.swift

@@ -35,6 +35,8 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
 
     public var unstoredDoses: [UnfinalizedDose]
 
+    public var silencePod: Bool
+
     public var confirmationBeeps: BeepPreference
 
     public var scheduledExpirationReminderOffset: TimeInterval?
@@ -100,6 +102,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
         self.basalSchedule = basalSchedule
         self.rileyLinkConnectionManagerState = rileyLinkConnectionManagerState
         self.unstoredDoses = []
+        self.silencePod = false
         self.confirmationBeeps = .manualCommands
         self.insulinType = insulinType
         self.lowReservoirReminderValue = Pod.defaultLowReservoirReminder
@@ -186,6 +189,8 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
             self.unstoredDoses = []
         }
 
+        self.silencePod = rawValue["silencePod"] as? Bool ?? false
+
         if let oldAutomaticBolusBeeps = rawValue["automaticBolusBeeps"] as? Bool, oldAutomaticBolusBeeps {
             self.confirmationBeeps = .extended
         } else if let oldConfirmationBeeps = rawValue["confirmationBeeps"] as? Bool, oldConfirmationBeeps {
@@ -253,6 +258,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
             "timeZone": timeZone.secondsFromGMT(),
             "basalSchedule": basalSchedule.rawValue,
             "unstoredDoses": unstoredDoses.map { $0.rawValue },
+            "silencePod": silencePod,
             "confirmationBeeps": confirmationBeeps.rawValue,
             "activeAlerts": activeAlerts.map { $0.rawValue },
             "podAttachmentConfirmed": podAttachmentConfirmed,
@@ -303,8 +309,8 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible {
             "* timeZone: \(timeZone)",
             "* basalSchedule: \(String(describing: basalSchedule))",
             "* maximumTempBasalRate: \(maximumTempBasalRate)",
-            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset))",
-            "* defaultExpirationReminderOffset: \(String(describing: defaultExpirationReminderOffset))",
+            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))",
+            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)",
             "* lowReservoirReminderValue: \(String(describing: lowReservoirReminderValue))",
             "* podAttachmentConfirmed: \(podAttachmentConfirmed)",
             "* activeAlerts: \(activeAlerts)",
@@ -317,14 +323,21 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible {
             "* tempBasalEngageState: \(String(describing: tempBasalEngageState))",
             "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))",
             "* isPumpDataStale: \(String(describing: isPumpDataStale))",
+            "* silencePod: \(String(describing: silencePod))",
             "* confirmationBeeps: \(String(describing: confirmationBeeps))",
             "* pairingAttemptAddress: \(String(describing: pairingAttemptAddress))",
             "* insulinType: \(String(describing: insulinType))",
+            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))",
+            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)",
             "* rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))",
             "* lastRileyLinkBatteryAlertDate \(String(describing: lastRileyLinkBatteryAlertDate))",
-            String(reflecting: podState),
-            "* PreviousPodState: \(String(reflecting: previousPodState))",
-            String(reflecting: rileyLinkConnectionManagerState),
+            "",
+            "* RileyLinkConnectionManagerState: " + (rileyLinkConnectionManagerState == nil ? "nil" : String(describing: rileyLinkConnectionManagerState!)),
+            "",
+            "* PodState: " + (podState == nil ? "nil" : String(describing: podState!)),
+            "",
+            "* PreviousPodState: " + (previousPodState == nil ? "nil" : String(describing: previousPodState!)),
+            "",
         ].joined(separator: "\n")
     }
 }

+ 0 - 1
Dependencies/OmniKit/OmniKit/PumpManager/PodComms.swift

@@ -454,7 +454,6 @@ class PodComms: CustomDebugStringConvertible {
     var debugDescription: String {
         return [
             "## PodComms",
-            "podState: \(String(reflecting: podState))",
             "configuredDevices: \(configuredDevices.value.map { $0.uuidString })",
             "delegate: \(String(describing: delegate != nil))",
             ""

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

@@ -279,6 +279,7 @@ public class PodCommsSession {
 
             let message = Message(address: podState.address, messageBlocks: blocksToSend, sequenceNum: messageNumber, expectFollowOnMessage: expectFollowOnMessage)
 
+            self.podState.lastCommsOK = false // mark last comms as not OK until we get the expected response
             let response = try transport.sendMessage(message)
 
             // Simulate fault
@@ -287,6 +288,7 @@ public class PodCommsSession {
 
             if let responseMessageBlock = response.messageBlocks[0] as? T {
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
+                self.podState.lastCommsOK = true // message successfully sent and expected response received
                 return responseMessageBlock
             }
 
@@ -385,10 +387,16 @@ public class PodCommsSession {
     }
 
     @discardableResult
-    func configureAlerts(_ alerts: [PodAlert], beepBlock: MessageBlock? = nil) throws -> StatusResponse {
+    func configureAlerts(_ alerts: [PodAlert], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse {
         let configurations = alerts.map { $0.configuration }
         let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations)
-        let status: StatusResponse = try send([configureAlerts], beepBlock: beepBlock)
+        var blocksToSend: [MessageBlock] = [configureAlerts]
+        if acknowledgeAll {
+            // requested to acknowledge any possible pending pod alerts out of an abundnace of caution
+            let acknowledgeAll = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: AlertSet(rawValue: ~0))
+            blocksToSend += [acknowledgeAll]
+        }
+        let status: StatusResponse = try send(blocksToSend, beepBlock: beepBlock)
         for alert in alerts {
             podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
         }
@@ -421,11 +429,11 @@ public class PodCommsSession {
         }
     }
 
-    public func insertCannula(optionalAlerts: [PodAlert] = []) throws -> TimeInterval {
+    public func insertCannula(optionalAlerts: [PodAlert] = [], silent: Bool) throws -> TimeInterval {
         let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
         let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
-        guard let activatedAt = podState.activatedAt else {
+        guard podState.activatedAt != nil else {
             throw PodCommsError.noPodPaired
         }
 
@@ -444,12 +452,12 @@ public class PodCommsSession {
             }
             podState.updateFromStatusResponse(status, at: currentDate)
         } else {
-            // Configure all the non-optional Pod Alarms
-            let expirationTime = activatedAt + Pod.nominalPodLife
-            let timeUntilExpirationAdvisory = expirationTime.timeIntervalSinceNow
-            let expirationAdvisoryAlarm = PodAlert.expired(alertTime: timeUntilExpirationAdvisory, duration: Pod.expirationAdvisoryWindow)
-            let endOfServiceTime = activatedAt + Pod.serviceDuration
-            let shutdownImminentAlarm = PodAlert.shutdownImminent((endOfServiceTime - Pod.endOfServiceImminentWindow).timeIntervalSinceNow)
+            let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0)
+            let podTime = podState.podTime + elapsed
+
+            // Configure the mandatory Pod Alerts for shutdown imminent alert (79 hours) and pod expiration alert (72 hours) along with any optional alerts
+            let shutdownImminentAlarm = PodAlert.shutdownImminent(offset: podTime, absAlertTime: Pod.serviceDuration - Pod.endOfServiceImminentWindow, silent: silent)
+            let expirationAdvisoryAlarm = PodAlert.expired(offset: podTime, absAlertTime: Pod.nominalPodLife, duration: Pod.expirationAdvisoryWindow, silent: silent)
             try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm] + optionalAlerts)
         }
 
@@ -500,7 +508,9 @@ public class PodCommsSession {
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, units: units, timeBetweenPulses: timeBetweenPulses, extendedUnits: extendedUnits, extendedDuration: extendedDuration)
 
-        if podState.unfinalizedBolus != nil {
+        // Do a getstatus to verify that there isn't an on-going bolus in progress if the last bolus command is still
+        // finalized, if the last delivery status wasn't successfully verified or the last comms attempt wasn't OK
+        if podState.unfinalizedBolus != nil || !podState.deliveryStatusVerified || !podState.lastCommsOK {
             var ongoingBolus = true
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
                 podState.updateFromStatusResponse(statusResponse, at: currentDate)
@@ -602,7 +612,8 @@ public class PodCommsSession {
     // A suspendReminder of 0 is an untimed suspend which only uses podSuspendedReminder alert beeps.
     // A suspendReminder of 1-5 minutes will only use suspendTimeExpired alert beeps.
     // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts.
-    public func suspendDelivery(suspendReminder: TimeInterval? = nil, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult {
+    // The configured alerts will set up as silent pod alerts if silent is true.
+    public func suspendDelivery(suspendReminder: TimeInterval? = nil, silent: Bool, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult {
 
         guard podState.unacknowledgedCommand == nil else {
             return .certainFailure(error: .unacknowledgedCommandPending)
@@ -613,6 +624,9 @@ public class PodCommsSession {
             var podSuspendedReminderAlert: PodAlert? = nil
             var suspendTimeExpiredAlert: PodAlert? = nil
             let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0
+            let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0)
+            let podTime = podState.podTime + elapsed
+            log.debug("suspendDelivery: podState.podTime=%@, elapsed=%.2fs, computed timeActive %@", podState.podTime.timeIntervalStr, elapsed, podTime.timeIntervalStr)
 
             let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)
             var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
@@ -620,14 +634,14 @@ public class PodCommsSession {
             // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
             if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
                 // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders
-                podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, suspendTime: suspendTime)
+                podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, offset: podTime, suspendTime: suspendTime, silent: silent)
                 alertConfigurations += [podSuspendedReminderAlert!.configuration]
             }
 
             // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
             if suspendTime > 0 {
                 // a timed suspend using a suspend time expired alert
-                suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(suspendTime: suspendTime)
+                suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(offset: podTime, suspendTime: suspendTime, silent: silent)
                 alertConfigurations += [suspendTimeExpiredAlert!.configuration]
             }
 
@@ -667,8 +681,8 @@ public class PodCommsSession {
     private func cancelSuspendAlerts() throws -> StatusResponse {
 
         do {
-            let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, suspendTime: 0)
-            let suspendTimeExpired = PodAlert.suspendTimeExpired(suspendTime: 0) // A suspendTime of 0 deactivates this alert
+            let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, offset: 0, suspendTime: 0)
+            let suspendTimeExpired = PodAlert.suspendTimeExpired(offset: 0, suspendTime: 0) // A suspendTime of 0 deactivates this alert
 
             let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired])
             return status
@@ -733,6 +747,11 @@ public class PodCommsSession {
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
 
         do {
+            if !(podState.lastCommsOK && podState.deliveryStatusVerified) {
+                // Can't trust the current delivery state -- do a cancel all
+                // to be sure that setting a basal program won't fault the pod.
+                let _: StatusResponse = try send([CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)])
+            }
             var status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
             let now = currentDate
             podState.suspendState = .resumed(now)
@@ -918,11 +937,11 @@ public class PodCommsSession {
         }
     }
 
-    public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> [AlertSlot: PodAlert] {
+    public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet {
         let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts)
         let status: StatusResponse = try send([cmd], beepBlock: beepBlock)
         podState.updateFromStatusResponse(status, at: currentDate)
-        return podState.activeAlerts
+        return podState.activeAlertSlots
     }
 
     func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) {

+ 66 - 27
Dependencies/OmniKit/OmniKit/PumpManager/PodState.swift

@@ -55,16 +55,19 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     fileprivate var nonceState: NonceState
 
     public var activatedAt: Date?
-    public var expiresAt: Date?  // set based on StatusResponse timeActive and can change with Pod clock drift and/or system time change
+    public var expiresAt: Date? // set based on timeActive and can change with Pod clock drift and/or system time change
     public var activeTime: TimeInterval? // Useful after pod deactivated or faulted.
 
+    public var podTime: TimeInterval // pod time from the last response, always whole minute values
+    public var podTimeUpdated: Date? // time that the podTime value was last updated
+
     public var setupUnitsDelivered: Double?
 
     public let pmVersion: String
     public let piVersion: String
     public let lot: UInt32
     public let tid: UInt32
-    var activeAlertSlots: AlertSet
+    public var activeAlertSlots: AlertSet
     public var lastInsulinMeasurements: PodInsulinMeasurements?
 
     public var unacknowledgedCommand: PendingCommand?
@@ -96,16 +99,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public var configuredAlerts: [AlertSlot: PodAlert]
     public var insulinType: InsulinType
 
-    public var activeAlerts: [AlertSlot: PodAlert] {
-        var active = [AlertSlot: PodAlert]()
-        for slot in activeAlertSlots {
-            if let alert = configuredAlerts[slot] {
-                active[slot] = alert
-            }
-        }
-        return active
-    }
-
     // Allow a grace period while the unacknowledged command is first being sent.
     public var needsCommsRecovery: Bool {
         if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight {
@@ -114,6 +107,10 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         return false
     }
 
+    // the following two vars are not persistent across app restarts
+    public var deliveryStatusVerified: Bool
+    public var lastCommsOK: Bool
+
     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)
@@ -129,8 +126,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.messageTransportState = MessageTransportState(packetNumber: packetNumber, messageNumber: messageNumber)
         self.primeFinishTime = nil
         self.setupProgress = .addressAssigned
-        self.configuredAlerts = [.slot7: .waitingForPairingReminder]
+        self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder]
         self.insulinType = insulinType
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
+        self.podTime = 0
     }
     
     public var unfinishedSetup: Bool {
@@ -170,9 +170,21 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         let seed = UInt16(sum & 0xffff) ^ syncWord
         nonceState = NonceState(lot: lot, tid: tid, seed: seed)
     }
-    
+
+    // Saves the current pod timeActive and will initialize the activatedAtComputed at
+    // pod startup and updates the expiresAt value to account for pod clock differences.
     private mutating func updatePodTimes(timeActive: TimeInterval) -> Date {
         let now = Date()
+
+        guard timeActive >= self.podTime else {
+            // The pod active time went backwards and thus we have an apparent reset fault.
+            // Don't update any times or displayed expiresAt time will expectedly jump.
+            return now
+        }
+
+        self.podTime = timeActive
+        self.podTimeUpdated = now
+
         let activatedAtComputed = now - timeActive
         if activatedAt == nil {
             self.activatedAt = activatedAtComputed
@@ -273,13 +285,25 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     
     private mutating func updateDeliveryStatus(deliveryStatus: DeliveryStatus, podProgressStatus: PodProgressStatus, bolusNotDelivered: Double, at date: Date) {
 
+        deliveryStatusVerified = true
         // See if the pod deliveryStatus indicates an active bolus or temp basal that the PodState isn't tracking (possible Loop restart)
         if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that Loop doesn't know about?
             if podProgressStatus.readyForDelivery {
+                deliveryStatusVerified = false // remember that we had inconsistent (bolus) delivery status
                 // Create an unfinalizedBolus with the remaining bolus amount to capture what we can.
                 unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusNotDelivered, startTime: date, scheduledCertainty: .certain, insulinType: insulinType, automatic: false)
             }
         }
+        if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that app isn't tracking
+            deliveryStatusVerified = false // remember that we had inconsistent (temp basal) delivery status
+            // unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: 0, startTime: Date(), duration: .minutes(30), isHighTemp: false, scheduledCertainty: .certain, insulinType: insulinType)
+        }
+        if deliveryStatus != .suspended && isSuspended { // active basal that app isn't tracking
+            deliveryStatusVerified = false // remember that we had inconsistent (basal) delivery status
+            let resumeStartTime = Date()
+            suspendState = .resumed(resumeStartTime)
+            unfinalizedResume = UnfinalizedDose(resumeStartTime: resumeStartTime, scheduledCertainty: .certain, insulinType: insulinType)
+        }
 
         if var bolus = unfinalizedBolus, !deliveryStatus.bolusing {
             // Due to clock drift or comms delays, boluses can finish earlier than we expect
@@ -413,6 +437,16 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             self.activeAlertSlots = .none
         }
         
+        if let podTime = rawValue["podTime"] as? TimeInterval,
+            let podTimeUpdated = rawValue["podTimeUpdated"] as? Date
+        {
+            self.podTime = podTime
+            self.podTimeUpdated = podTimeUpdated
+        } else {
+            self.podTime = 0
+            self.podTimeUpdated = nil
+        }
+
         if let setupProgressRaw = rawValue["setupProgress"] as? Int,
             let setupProgress = SetupProgress(rawValue: setupProgressRaw)
         {
@@ -441,12 +475,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         } else {
             // Assume migration, and set up with alerts that are normally configured
             self.configuredAlerts = [
-                .slot2: .shutdownImminent(0),
-                .slot3: .expirationReminder(0),
-                .slot4: .lowReservoir(0),
-                .slot5: .podSuspendedReminder(active: false, suspendTime: 0),
-                .slot6: .suspendTimeExpired(suspendTime: 0),
-                .slot7: .expired(alertTime: 0, duration: 0)
+                .slot2ShutdownImminent: .shutdownImminent(offset: 0, absAlertTime: 0),
+                .slot3ExpirationReminder: .expirationReminder(offset: 0, absAlertTime: 0),
+                .slot4LowReservoir: .lowReservoir(units: 0),
+                .slot5SuspendedReminder: .podSuspendedReminder(active: false, offset: 0, suspendTime: 0),
+                .slot6SuspendTimeExpired: .suspendTimeExpired(offset: 0, suspendTime: 0),
+                .slot7Expired: .expired(offset: 0, absAlertTime: 0, duration: 0)
             ]
         }
         
@@ -455,8 +489,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
             self.insulinType = insulinType
         } else {
-            insulinType = .novolog
+            self.insulinType = .novolog
         }
+
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
     }
     
     public var rawValue: RawValue {
@@ -494,6 +531,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         rawValue["activeTime"] = activeTime
         rawValue["activatedAt"] = activatedAt
         rawValue["expiresAt"] = expiresAt
+        rawValue["podTime"] = podTime
+        rawValue["podTimeUpdated"] = podTimeUpdated
 
         rawValue["setupUnitsDelivered"] = setupUnitsDelivered
 
@@ -514,6 +553,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* address: \(String(format: "%04X", address))",
             "* activatedAt: \(String(reflecting: activatedAt))",
             "* expiresAt: \(String(reflecting: expiresAt))",
+            "* podTime: \(podTime.timeIntervalStr)",
+            "* podTimeUpdated: \(String(reflecting: podTimeUpdated))",
             "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))",
             "* piVersion: \(piVersion)",
             "* pmVersion: \(pmVersion)",
@@ -526,16 +567,14 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))",
             "* unfinalizedResume: \(String(describing: unfinalizedResume))",
             "* finalizedDoses: \(String(describing: finalizedDoses))",
-            "* activeAlerts: \(String(describing: activeAlerts))",
+            "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))",
             "* messageTransportState: \(String(describing: messageTransportState))",
             "* setupProgress: \(setupProgress)",
             "* primeFinishTime: \(String(describing: primeFinishTime))",
-            "* configuredAlerts: \(String(describing: configuredAlerts))",
+            "* configuredAlerts: \(configuredAlertsString(configuredAlerts: configuredAlerts))",
             "* insulinType: \(String(describing: insulinType))",
-            "* pdmRef: \(String(describing: fault?.pdmRef))",
-            "",
-            fault != nil ? String(reflecting: fault!) : "fault: nil",
-            "",
+            "* pdmRef: " + (fault?.pdmRef == nil ? "nil" : String(describing: fault!.pdmRef!)),
+            "* Fault: " + (fault == nil ? "nil" : String(describing: fault!)),
         ].joined(separator: "\n")
     }
 }

+ 4 - 4
Dependencies/OmniKit/OmniKitTests/PodInfoTests.swift

@@ -133,7 +133,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(8100, decoded.timeActive)
             XCTAssertEqual(TimeInterval(minutes: 0x0087), decoded.timeActive)
-            XCTAssertEqual("02:15", decoded.timeActive.stringValue)
+            XCTAssertEqual("02:15", decoded.timeActive.timeIntervalStr)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertNil(decoded.errorEventInfo)
@@ -223,7 +223,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.basalOverInfusionPulse, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(TimeInterval(minutes: 0x09ff), decoded.faultEventTimeSinceActivation)
-            XCTAssertEqual("1 day plus 18:39", decoded.faultEventTimeSinceActivation?.stringValue)
+            XCTAssertEqual("1 day plus 18:39", decoded.faultEventTimeSinceActivation?.timeIntervalStr)
             XCTAssertEqual(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(TimeInterval(minutes: 0x0a02), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
@@ -255,7 +255,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(TimeInterval(minutes: 0x0e0c), decoded.faultEventTimeSinceActivation)
-            XCTAssertEqual("2 days plus 11:56", decoded.faultEventTimeSinceActivation?.stringValue)
+            XCTAssertEqual("2 days plus 11:56", decoded.faultEventTimeSinceActivation?.timeIntervalStr)
             XCTAssertEqual(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(TimeInterval(minutes: 0x0e14), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
@@ -287,7 +287,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(TimeInterval(minutes: 0x0268), decoded.faultEventTimeSinceActivation)
-            XCTAssertEqual("10:16", decoded.faultEventTimeSinceActivation?.stringValue)
+            XCTAssertEqual("10:16", decoded.faultEventTimeSinceActivation?.timeIntervalStr)
             XCTAssertEqual(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(TimeInterval(minutes: 0x026b), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)

BIN
Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/Cannula Inserted.imageset/CannulaInserted.png


+ 21 - 0
Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/Cannula Inserted.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "CannulaInserted.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 16 - 0
Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_mask_swiftui.imageset/Contents.json

@@ -0,0 +1,16 @@
+{
+  "images" : [
+    {
+      "filename" : "pod_reservoir_mask.svg",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true,
+    "template-rendering-intent" : "template"
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 55 - 0
Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_mask_swiftui.imageset/pod_reservoir_mask.svg


+ 15 - 0
Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_swiftui.imageset/Contents.json

@@ -0,0 +1,15 @@
+{
+  "images" : [
+    {
+      "filename" : "pod_reservoir.svg",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "preserves-vector-representation" : true
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 59 - 0
Dependencies/OmniKit/OmniKitUI/Resources/OmniKitUI.xcassets/pod_reservoir_swiftui.imageset/pod_reservoir.svg


+ 1 - 1
Dependencies/OmniKit/OmniKitUI/Resources/nl.lproj/Localizable.strings

@@ -681,7 +681,7 @@
 "The App notifies you when the amount of insulin in the Pod reaches this level." = "iAPS geeft een melding wanneer de hoeveelheid insuline in de Pod dit niveau bereikt.";
 
 /* Description text for critical alerts */
-"The reminders above will not sound if your device is in Silent or Do Not Disturb mode.\n\nThere are other critical Pod alerts and alarms that will sound even if your device is set to Silent or Do Not Disturb mode." = "Bovenstaande herinneringen zijn niet hoorbaar wanneer je apparaat in de modus Stil of Niet Storen staat.\n\nAndere kritieke Podmeldingen en Podalarmen gaan wel af, zelfs als je apparaat op de modus Stil of Niet Storen staat.";
+"The reminders above will not sound if your device is in Silent or Do Not Disturb mode.\n\nThere are other critical Pod alerts and alarms that will sound even if your device is set to Silent or Do Not Disturb mode." = "Bovenstaande herinneringen zijn niet hoorbaar wanneer je apparaat in de modus 'Stil' of 'Niet storen' staat.\n\nAndere kritieke Podmeldingen en Podalarmen gaan wel af, zelfs als je apparaat op de modus 'Stil' of 'Niet storen' staat.";
 
 /* Message for pod sync time action sheet */
 "The time on your pump is different from the current time. Do you want to update the time on your pump to the current time?" = "De tijd op je pomp is anders dan de huidige tijd. Wil je de tijd op je pomp bijwerken naar de huidige tijd?";

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

@@ -162,7 +162,7 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl
             hostedView.navigationItem.title = LocalizedString("Insulin Type", comment: "Title for insulin type selection screen")
             return hostedView
         case .deactivate:
-            let viewModel = DeactivatePodViewModel(podDeactivator: pumpManager, podAttachedToBody: pumpManager.podAttachmentConfirmed)
+            let viewModel = DeactivatePodViewModel(podDeactivator: pumpManager, podAttachedToBody: pumpManager.podAttachmentConfirmed, fault: pumpManager.state.podState?.fault)
 
             viewModel.didFinish = { [weak self] in
                 self?.stepFinished()

+ 30 - 11
Dependencies/OmniKit/OmniKitUI/ViewModels/DeactivatePodViewModel.swift

@@ -20,16 +20,6 @@ extension OmnipodPumpManager: PodDeactivater {}
 
 class DeactivatePodViewModel: ObservableObject, Identifiable {
     
-    public var podAttachedToBody: Bool
-    
-    var instructionText: String {
-        if podAttachedToBody {
-            return LocalizedString("Please deactivate the pod. When deactivation is complete, you may remove it and pair a new pod.", comment: "Instructions for deactivate pod when pod is on body")
-        } else {
-            return LocalizedString("Please deactivate the pod. When deactivation is complete, you may pair a new pod.", comment: "Instructions for deactivate pod when pod not on body")
-        }
-    }
-    
     enum DeactivatePodViewModelState {
         case active
         case deactivating
@@ -125,9 +115,38 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
     
     var podDeactivator: PodDeactivater
 
-    init(podDeactivator: PodDeactivater, podAttachedToBody: Bool) {
+    var podAttachedToBody: Bool
+
+    var instructionText: String
+
+    init(podDeactivator: PodDeactivater, podAttachedToBody: Bool, fault: DetailedStatus?) {
+
+        var text: String = ""
+        if let faultEventCode = fault?.faultEventCode {
+            let notificationString = faultEventCode.notificationTitle
+            switch faultEventCode.faultType {
+            case .exceededMaximumPodLife80Hrs, .reservoirEmpty, .occluded:
+                // Just prepend a simple sentence with the notification string for these faults.
+                // Other occluded related 0x6? faults will be treated as a general pod error as per the PDM.
+                text = String(format: "%@. ", notificationString)
+            default:
+                // Display the fault code in decimal and hex, the fault description and the pdmRef string for other errors.
+                text = String(format: "⚠️ %1$@ (0x%2$02X)\n%3$@\n", notificationString, faultEventCode.rawValue, faultEventCode.faultDescription)
+                if let pdmRef = fault?.pdmRef {
+                    text += LocalizedString("Ref: ", comment: "PDM Ref string line") + pdmRef + "\n\n"
+                }
+            }
+        }
+
+        if podAttachedToBody {
+            text += LocalizedString("Please deactivate the pod. When deactivation is complete, you may remove it and pair a new pod.", comment: "Instructions for deactivate pod when pod is on body")
+        } else {
+            text += LocalizedString("Please deactivate the pod. When deactivation is complete, you may pair a new pod.", comment: "Instructions for deactivate pod when pod not on body")
+        }
+
         self.podDeactivator = podDeactivator
         self.podAttachedToBody = podAttachedToBody
+        self.instructionText = text
     }
     
     public func continueButtonTapped() {

+ 41 - 3
Dependencies/OmniKit/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift

@@ -35,6 +35,8 @@ class OmnipodSettingsViewModel: ObservableObject {
 
     @Published var beepPreference: BeepPreference
 
+    @Published var silencePodPreference: SilencePodPreference
+
     @Published var rileylinkConnected: Bool
 
     var activatedAtString: String {
@@ -131,7 +133,7 @@ class OmnipodSettingsViewModel: ObservableObject {
 
     var recoveryText: String? {
         if case .fault = podCommState {
-            return LocalizedString("Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted")
+            return LocalizedString("⚠️ Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted")
         } else if podOk && isPodDataStale {
             return LocalizedString("Make sure your phone and pod are close to each other. If communication issues persist, move to a new area.", comment: "The action string on pod status page when pod data is stale")
         } else if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, serviceTimeRemaining <= Pod.serviceDuration - Pod.nominalPodLife {
@@ -230,6 +232,7 @@ class OmnipodSettingsViewModel: ObservableObject {
         lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
         podCommState = self.pumpManager.podCommState
         beepPreference = self.pumpManager.beepPreference
+        silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled
         insulinType = self.pumpManager.insulinType
         podDetails = self.pumpManager.podDetails
         previousPodDetails = self.pumpManager.previousPodDetails
@@ -278,7 +281,7 @@ class OmnipodSettingsViewModel: ObservableObject {
     }
     
     func stopUsingOmnipodTapped() {
-        self.pumpManager.notifyDelegateOfDeactivation {
+        pumpManager.notifyDelegateOfDeactivation {
             DispatchQueue.main.async {
                 self.didFinish?()
             }
@@ -337,10 +340,30 @@ class OmnipodSettingsViewModel: ObservableObject {
         }
     }
 
+    func readPodStatus(_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) {
+        pumpManager.getDetailedStatus() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
+    func readPulseLog(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readPulseLog() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
     func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
         pumpManager.playTestBeeps(completion: completion)
     }
 
+    func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) {
+        completion(pumpManager.debugDescription)
+    }
+
     func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
         pumpManager.setConfirmationBeeps(newPreference: preference) { error in
             DispatchQueue.main.async {
@@ -352,6 +375,17 @@ class OmnipodSettingsViewModel: ObservableObject {
         }
     }
 
+    func setSilencePod(_ silencePodPreference: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
+        pumpManager.setSilencePod(silencePod: silencePodPreference == .enabled) { error in
+            DispatchQueue.main.async {
+                if error == nil {
+                    self.silencePodPreference = silencePodPreference
+                }
+                completion(error)
+            }
+        }
+    }
+
     func didChangeInsulinType(_ newType: InsulinType?) {
         self.pumpManager.insulinType = newType
     }
@@ -367,6 +401,10 @@ class OmnipodSettingsViewModel: ObservableObject {
         }
     }
 
+    var noPod: Bool {
+        return podCommState == .noPod
+    }
+
     var podError: String? {
         switch podCommState {
         case .fault(let status):
@@ -378,7 +416,7 @@ class OmnipodSettingsViewModel: ObservableObject {
             case .occluded, .occlusionCheckStartup1, .occlusionCheckStartup2, .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold, .occlusionCheckValueTooHigh:
                 return LocalizedString("Pod Occlusion", comment: "Error message for reservoir view when pod occlusion checks failed")
             default:
-                return LocalizedString("Pod Error", comment: "Error message for reservoir view during general pod fault")
+                return String(format: LocalizedString("Pod Fault %1$03d", comment: "Error message for reservoir view during general pod fault: (1: fault code value)"), status.faultEventCode.rawValue)
             }
         case .active:
             if isPodDataStale {

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

@@ -85,7 +85,7 @@ enum PodLifeState {
         case .podDeactivating:
             return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation")
         default:
-            return LocalizedString("Replace Pod", comment: "Settings page link description when next lifecycle action is to replace pod")
+            return LocalizedString("Deactivate Pod", comment: "Settings page link description when next lifecycle action is to deactivate pod")
         }
     }
     

+ 36 - 0
Dependencies/OmniKit/OmniKitUI/Views/ActivityView.swift

@@ -0,0 +1,36 @@
+//
+//  ActivityView.swift
+//  OmniKit
+//
+//  Created by Joe Moran on 9/17/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+
+struct ActivityView: UIViewControllerRepresentable {
+    @Binding var isPresented: Bool
+    let activityItems: [Any]
+
+    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
+        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
+        controller.completionWithItemsHandler = { (_, _, _, _) in
+            self.isPresented = false
+        }
+        return controller
+    }
+
+    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityView>) {
+    }
+}
+
+fileprivate struct ActivityViewController: UIViewControllerRepresentable {
+    var activityItems: [Any]
+    var applicationActivities: [UIActivity]? = nil
+
+    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
+        return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
+    }
+
+    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
+}

+ 2 - 2
Dependencies/OmniKit/OmniKitUI/Views/AttachPodView.swift

@@ -33,7 +33,7 @@ struct AttachPodView: View {
                 HStack {
                     InstructionList(instructions: [
                         LocalizedString("Prepare site.", comment: "Label text for step one of attach pod instructions"),
-                        LocalizedString("Remove the pod's needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"),
+                        LocalizedString("Remove the Pod's clear needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"),
                         LocalizedString("Check Pod, apply to site, then confirm pod attachment.", comment: "Label text for step three of attach pod instructions")
                     ])
                 }
@@ -55,7 +55,7 @@ struct AttachPodView: View {
         }
         .animation(.default)
         .alert(item: $activeModal, content: self.alert(for:))
-        .navigationBarTitle("Attach Pod", displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Attach Pod", comment: "navigation bar title attach pod"), displayMode: .automatic)
         .navigationBarItems(trailing: cancelButton)
         .navigationBarBackButtonHidden(true)
     }

+ 6 - 6
Dependencies/OmniKit/OmniKitUI/Views/BeepPreferenceSelectionView.swift

@@ -39,7 +39,7 @@ struct BeepPreferenceSelectionView: View {
         VStack {
             List {
                 Section {
-                    Text(LocalizedString("Confidence reminders are beeps from the pod which can be used to acknowledge selected commands.", comment: "Help text for BeepPreferenceSelectionView")).fixedSize(horizontal: false, vertical: true)
+                    Text(LocalizedString("Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced.", comment: "Help text for BeepPreferenceSelectionView")).fixedSize(horizontal: false, vertical: true)
                         .padding(.vertical, 10)
                 }
 
@@ -88,7 +88,7 @@ struct BeepPreferenceSelectionView: View {
 
         }
         .insetGroupedListStyle()
-        .navigationTitle("Confidence Reminders")
+        .navigationTitle(LocalizedString("Confidence Reminders", comment: "navigation title for confidence reminders"))
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
     }
@@ -110,15 +110,15 @@ struct BeepPreferenceSelectionView: View {
 
     private var cancelButton: some View {
         Button(action: { self.presentationMode.wrappedValue.dismiss() } ) {
-            Text(LocalizedString("Cancel", comment: "Button title for cancelling low reservoir reminder edit"))
+            Text(LocalizedString("Cancel", comment: "Button title for cancelling confidence reminders edit"))
         }
     }
 
     var saveButtonText: String {
         if saving {
-            return LocalizedString("Saving...", comment: "button title for saving low reservoir reminder while saving")
+            return LocalizedString("Saving...", comment: "button title for saving confidence reminder while saving")
         } else {
-            return LocalizedString("Save", comment: "button title for saving low reservoir reminder")
+            return LocalizedString("Save", comment: "button title for saving confidence reminder")
         }
     }
 
@@ -135,7 +135,7 @@ struct BeepPreferenceSelectionView: View {
 
 }
 
-struct ContentView_Previews: PreviewProvider {
+struct BeepPreferenceSelectionView_Previews: PreviewProvider {
     static var previews: some View {
         NavigationView {
             BeepPreferenceSelectionView(initialValue: .extended) { selectedValue, completion in

+ 1 - 1
Dependencies/OmniKit/OmniKitUI/Views/CheckInsertedCannulaView.swift

@@ -55,7 +55,7 @@ struct CheckInsertedCannulaView: View {
         }
         .animation(.default)
         .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal }
-        .navigationBarTitle("Check Cannula", displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Check Cannula", comment: "navigation bar title for check cannula"), displayMode: .automatic)
         .navigationBarItems(trailing: cancelButton)
         .navigationBarBackButtonHidden(true)
     }

+ 1 - 1
Dependencies/OmniKit/OmniKitUI/Views/DeactivatePodView.swift

@@ -78,7 +78,7 @@ struct DeactivatePodView: View {
             .padding()
         }
         .alert(isPresented: $removePodModalIsPresented) { removePodModal }
-        .navigationBarTitle("Deactivate Pod", displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Deactivate Pod", comment: "navigation bar title for deactivate pod"), displayMode: .automatic)
         .navigationBarItems(trailing:
             Button("Cancel") {
                 viewModel.didCancel?()

+ 1 - 1
Dependencies/OmniKit/OmniKitUI/Views/ExpirationReminderSetupView.swift

@@ -38,7 +38,7 @@ struct ExpirationReminderSetupView: View {
             }
             .padding()
         }
-        .navigationBarTitle(LocalizedString("Expiration Reminder", comment: "Title for ExpirationReminderSetupView"), displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Expiration Reminder", comment: "navigation bar title for expiration reminder"), displayMode: .automatic)
         .navigationBarHidden(false)
         .toolbar {
             ToolbarItem(placement: .navigationBarTrailing) {

+ 30 - 0
Dependencies/OmniKit/OmniKitUI/Views/FirstAppear.swift

@@ -0,0 +1,30 @@
+//
+//  FirstAppear.swift
+//  Omnipod
+//
+//  Created by Joe Moran on 9/24/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+
+extension View {
+    func onFirstAppear(_ action: @escaping () -> ()) -> some View {
+        modifier(FirstAppear(action: action))
+    }
+}
+
+private struct FirstAppear: ViewModifier {
+    let action: () -> ()
+
+    // State used to insure action is invoked here only once
+    @State private var hasAppeared = false
+
+    func body(content: Content) -> some View {
+        content.onAppear {
+            guard !hasAppeared else { return }
+            hasAppeared = true
+            action()
+        }
+    }
+}

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

@@ -83,7 +83,7 @@ struct InsertCannulaView: View {
         }
         .animation(.default)
         .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal }
-        .navigationBarTitle("Insert Cannula", displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Insert Cannula", comment: "navigation bar title for insert cannula"), displayMode: .automatic)
         .navigationBarBackButtonHidden(true)
         .navigationBarItems(trailing: cancelButton)
     }

+ 1 - 1
Dependencies/OmniKit/OmniKitUI/Views/InsulinTypeConfirmation.swift

@@ -63,6 +63,6 @@ struct InsulinTypeConfirmation: View {
 
 struct InsulinTypeConfirmation_Previews: PreviewProvider {
     static var previews: some View {
-        InsulinTypeConfirmation(initialValue: .humalog, supportedInsulinTypes: InsulinType.allCases, didConfirm: { (newType) in }, didCancel: {})
+        InsulinTypeConfirmation(initialValue: .humalog, supportedInsulinTypes: InsulinType.allCases, didConfirm: { (newType) in }, didCancel: { })
     }
 }

+ 1 - 1
Dependencies/OmniKit/OmniKitUI/Views/LowReservoirReminderSetupView.swift

@@ -50,7 +50,7 @@ struct LowReservoirReminderSetupView: View {
             }
             .padding()
         }
-        .navigationBarTitle(LocalizedString("Low Reservoir", comment: "Navigation bar title for LowReservoirReminderSetupView"), displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Low Reservoir", comment: "navigation bar title for low reservoir"), displayMode: .automatic)
         .toolbar {
             ToolbarItem(placement: .navigationBarTrailing) {
                 Button(LocalizedString("Cancel", comment: "Cancel button title"), action: {

+ 2 - 5
Dependencies/OmniKit/OmniKitUI/Views/ManualTempBasalEntryView.swift

@@ -95,7 +95,7 @@ struct ManualTempBasalEntryView: View {
                     .frame(maxHeight: 162.0)
                     .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert })
                     Section {
-                        Text(LocalizedString("Loop will not automatically adjust your insulin delivery until the temporary basal rate finishes or is canceled.", comment: "Description text on manual temp basal action sheet"))
+                        Text(LocalizedString("Your insulin delivery will not be automatically adjusted until the temporary basal rate finishes or is canceled.", comment: "Description text on manual temp basal action sheet"))
                             .font(.footnote)
                             .foregroundColor(.secondary)
                             .fixedSize(horizontal: false, vertical: true)
@@ -147,7 +147,7 @@ struct ManualTempBasalEntryView: View {
     var missingConfigAlert: SwiftUI.Alert {
         return SwiftUI.Alert(
             title: Text(LocalizedString("Missing Config", comment: "Alert title for missing temp basal configuration")),
-            message: Text(LocalizedString("This Pump has not been configured with a maximum basal rate because it was added before manual temp basal was a feature. Please go to Therapy Settings -> Delivery Limits and set a new Maximum Basal Rate.", comment: "Alert format string for missing temp basal configuration."))
+            message: Text(LocalizedString("This Pump has not been configured with a maximum basal rate because it was added before manual temp basal was a feature. Please go to Pump Settings in the settings CONFIGURATION section to set a new Max Basal.", comment: "Alert format string for missing temp basal configuration."))
         )
     }
 
@@ -158,7 +158,4 @@ struct ManualTempBasalEntryView: View {
         }
         .accessibility(identifier: "button_cancel")
     }
-
 }
-
-

+ 0 - 0
Dependencies/OmniKit/OmniKitUI/Views/NotificationSettingsView.swift


Некоторые файлы не были показаны из-за большого количества измененных файлов