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

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:
     steps:
       # Uncomment to manually select Xcode version if needed
       # Uncomment to manually select Xcode version if needed
       - name: Select Xcode version
       - 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
       # Checks-out the repo
       - name: Checkout Repo
       - name: Checkout Repo
@@ -64,4 +64,4 @@ jobs:
           name: build-artifacts
           name: build-artifacts
           path: |
           path: |
             artifacts
             artifacts
-            buildlog
+            buildlog

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.2.7
+APP_VERSION = 2.2.9
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##
 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"?>
 <?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="">
 <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">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -118,6 +129,7 @@
     </entity>
     </entity>
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
     <entity name="Readings" representedClassName="Readings" syncable="YES" codeGenerationType="class">
         <attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <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="glucose" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
         <attribute name="id" optional="YES" attributeType="String"/>
         <attribute name="id" optional="YES" attributeType="String"/>
     </entity>
     </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";
 "Remote Data Synchronization" = "Remote Daten Synchronisation";
 
 
 /* Title describing sensor expiration */
 /* Title describing sensor expiration */
-"Sensor Expires" = "Sensor-Ablaufzeitpunkt";
+"Sensor Expires" = "Sensor läuft ab";
 
 
 /* Title describing past sensor expiration */
 /* Title describing past sensor expiration */
 "Sensor Expired" = "Sensor abgelaufen";
 "Sensor Expired" = "Sensor abgelaufen";

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

@@ -54,7 +54,7 @@
 "Last Reading" = "Letzte Messung";
 "Last Reading" = "Letzte Messung";
 
 
 /* Descriptive text on G7StartupView */
 /* 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 */
 /* String displayed instead of a glucose value below the CGM range */
 "LOW" = "NIEDRIG";
 "LOW" = "NIEDRIG";

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

@@ -2,7 +2,7 @@
 "Dexcom G7" = "Dexcom G7";
 "Dexcom G7" = "Dexcom G7";
 
 
 /* Descriptive text on G7StartupView */
 /* 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 */
 /* Button title for starting setup */
 "Continue" = "Fortsetzen";
 "Continue" = "Fortsetzen";
@@ -11,7 +11,7 @@
 "Cancel" = "Abbrechen";
 "Cancel" = "Abbrechen";
 
 
 /* Error description for unreliable state */
 /* 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. */
 /* The description of sensor algorithm state when sensor is ok. */
 "Sensor is OK" = "Sensor ist OK";
 "Sensor is OK" = "Sensor ist OK";
@@ -70,7 +70,7 @@
 "Configuration" = "Konfiguration";
 "Configuration" = "Konfiguration";
 
 
 /* title for g7 config settings to upload readings */
 /* title for g7 config settings to upload readings */
-"Upload Readings" = "Upload von Messwerten";
+"Upload Readings" = "Werte hochladen";
 
 
 /* Button */
 /* Button */
 "Scan for new sensor" = "Nach neuem Sensor suchen";
 "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";
 "Critical Alerts" = "Kritieke waarschuwingen";
 
 
 /* Description text for critical alerts */
 /* 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 */
 /* navigation title for notification settings */
 "Notification Settings" = "Instellingen voor meldingen";
 "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 */; };
 		10389A3F26FF7841002115E9 /* CRC16.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A2026FF7841002115E9 /* CRC16.swift */; };
 		10389A4126FF7841002115E9 /* MessageTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A2226FF7841002115E9 /* MessageTransport.swift */; };
 		10389A4126FF7841002115E9 /* MessageTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A2226FF7841002115E9 /* MessageTransport.swift */; };
 		191DB66D2A06F17800212AC9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 191DB6522A06F17800212AC9 /* Localizable.strings */; };
 		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, ); }; };
 		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 */; };
 		84752ED626ED13F5009FD801 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84752EBF26ED13F5009FD801 /* Id.swift */; };
 		84752ED726ED13F5009FD801 /* X25519KeyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84752EC126ED13F5009FD801 /* X25519KeyGenerator.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 */; };
 		D802CD0A27DD98C10072E3A1 /* TempBasalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD0527DD98C10072E3A1 /* TempBasalTests.swift */; };
 		D802CD1027DD99AB0072E3A1 /* CRC16Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD0F27DD99AB0072E3A1 /* CRC16Tests.swift */; };
 		D802CD1027DD99AB0072E3A1 /* CRC16Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD0F27DD99AB0072E3A1 /* CRC16Tests.swift */; };
 		D802CD1227DD9AE10072E3A1 /* BasalScheduleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802CD1127DD9AE10072E3A1 /* BasalScheduleTests.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 */; };
 		D8896C6227890E6B00E09A96 /* DetailedStatus+OmniBLE.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8896C6127890E6B00E09A96 /* DetailedStatus+OmniBLE.swift */; };
 		D895BF5B275DE64000D51FC7 /* StringLengthPrefixEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D895BF5A275DE64000D51FC7 /* StringLengthPrefixEncoding.swift */; };
 		D895BF5B275DE64000D51FC7 /* StringLengthPrefixEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D895BF5A275DE64000D51FC7 /* StringLengthPrefixEncoding.swift */; };
 		D897B06B29347ED500FDB009 /* BolusDeliveryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D897B06A29347ED500FDB009 /* BolusDeliveryTable.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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		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>"; };
 		D897B06A29347ED500FDB009 /* BolusDeliveryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusDeliveryTable.swift; sourceTree = "<group>"; };
@@ -477,6 +493,7 @@
 				1016325627185EE4007A3BC2 /* PodProgressStatus.swift */,
 				1016325627185EE4007A3BC2 /* PodProgressStatus.swift */,
 				C1F67ED927979E400017487F /* PumpManagerAlert.swift */,
 				C1F67ED927979E400017487F /* PumpManagerAlert.swift */,
 				C1F67EE127985F580017487F /* ReservoirLevel.swift */,
 				C1F67EE127985F580017487F /* ReservoirLevel.swift */,
+				196A6F222AFFFD1200E3C089 /* SilencePodPreference.swift */,
 				1016325827185EE4007A3BC2 /* UnfinalizedDose.swift */,
 				1016325827185EE4007A3BC2 /* UnfinalizedDose.swift */,
 			);
 			);
 			path = OmnipodCommon;
 			path = OmnipodCommon;
@@ -705,12 +722,12 @@
 		8475311D26ED8246009FD801 /* PumpManagerUI */ = {
 		8475311D26ED8246009FD801 /* PumpManagerUI */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				C1F67EBB27975F060017487F /* ViewModels */,
+				C1F67EDF2797B1EF0017487F /* OmniBLEHUDProvider.swift */,
 				1029AE4A27094DDC00B7F5B6 /* OmniBLEPumpManager+UI.swift */,
 				1029AE4A27094DDC00B7F5B6 /* OmniBLEPumpManager+UI.swift */,
 				1029AE4E27094E1900B7F5B6 /* OmniBLEUI.xcassets */,
 				1029AE4E27094E1900B7F5B6 /* OmniBLEUI.xcassets */,
 				8475312626ED838A009FD801 /* ViewControllers */,
 				8475312626ED838A009FD801 /* ViewControllers */,
+				C1F67EBB27975F060017487F /* ViewModels */,
 				8475311E26ED838A009FD801 /* Views */,
 				8475311E26ED838A009FD801 /* Views */,
-				C1F67EDF2797B1EF0017487F /* OmniBLEHUDProvider.swift */,
 			);
 			);
 			path = PumpManagerUI;
 			path = PumpManagerUI;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -718,34 +735,41 @@
 		8475311E26ED838A009FD801 /* Views */ = {
 		8475311E26ED838A009FD801 /* Views */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				C1C001C327A2351D00533D35 /* OmniBLEReservoirView.swift */,
-				C1C001C227A2351D00533D35 /* OmniBLEReservoirView.xib */,
-				C1F67EB227975E710017487F /* DesignElements */,
+				D845A1362AF89F5500EA0853 /* ActivityView.swift */,
 				C1F67E7327975B830017487F /* AttachPodView.swift */,
 				C1F67E7327975B830017487F /* AttachPodView.swift */,
 				C1F67E7A27975B830017487F /* BasalStateView.swift */,
 				C1F67E7A27975B830017487F /* BasalStateView.swift */,
+				C1ED1E7127BAE44E00FED71C /* BeepPreferenceSelectionView.swift */,
 				C1F67E8027975B830017487F /* CheckInsertedCannulaView.swift */,
 				C1F67E8027975B830017487F /* CheckInsertedCannulaView.swift */,
-				C1F67E7727975B830017487F /* OmniBLESettingsView.swift */,
 				C1F67E7927975B830017487F /* DeactivatePodView.swift */,
 				C1F67E7927975B830017487F /* DeactivatePodView.swift */,
 				C1F67E8227975B830017487F /* DeliveryUncertaintyRecoveryView.swift */,
 				C1F67E8227975B830017487F /* DeliveryUncertaintyRecoveryView.swift */,
+				C1F67EB227975E710017487F /* DesignElements */,
 				C1F67E7B27975B830017487F /* ExpirationReminderPickerView.swift */,
 				C1F67E7B27975B830017487F /* ExpirationReminderPickerView.swift */,
 				C1F67E7827975B830017487F /* ExpirationReminderSetupView.swift */,
 				C1F67E7827975B830017487F /* ExpirationReminderSetupView.swift */,
+				D845A1382AF89F6300EA0853 /* FirstAppear.swift */,
 				C1F67E7D27975B830017487F /* HUDAssets.xcassets */,
 				C1F67E7D27975B830017487F /* HUDAssets.xcassets */,
 				C1F67E8527975B830017487F /* InsertCannulaView.swift */,
 				C1F67E8527975B830017487F /* InsertCannulaView.swift */,
+				C187C190278FCEC9006E3557 /* InsulinTypeConfirmation.swift */,
 				C1F67E8627975B830017487F /* LowReservoirReminderEditView.swift */,
 				C1F67E8627975B830017487F /* LowReservoirReminderEditView.swift */,
 				C1F67E7C27975B830017487F /* LowReservoirReminderSetupView.swift */,
 				C1F67E7C27975B830017487F /* LowReservoirReminderSetupView.swift */,
+				C1DBD512282FF79D009FCF74 /* ManualTempBasalEntryView.swift */,
 				C1F67E8127975B830017487F /* NotificationSettingsView.swift */,
 				C1F67E8127975B830017487F /* NotificationSettingsView.swift */,
+				C1C001C327A2351D00533D35 /* OmniBLEReservoirView.swift */,
+				C1C001C227A2351D00533D35 /* OmniBLEReservoirView.xib */,
+				C1F67E7727975B830017487F /* OmniBLESettingsView.swift */,
 				C1F67E7F27975B830017487F /* PairPodView.swift */,
 				C1F67E7F27975B830017487F /* PairPodView.swift */,
+				D845A13A2AF89F7100EA0853 /* PlayTestBeepsView.swift */,
 				C1F67E8A27975B830017487F /* PodDetailsView.swift */,
 				C1F67E8A27975B830017487F /* PodDetailsView.swift */,
+				8475311F26ED838A009FD801 /* PodLifeHUDView.swift */,
+				8475312426ED838A009FD801 /* PodLifeHUDView.xib */,
 				C1F67E8827975B830017487F /* PodSetupView.swift */,
 				C1F67E8827975B830017487F /* PodSetupView.swift */,
+				D845A13E2AF89F8400EA0853 /* PumpManagerDetailsView.swift */,
+				D845A13C2AF89F8400EA0853 /* ReadPodStatusView.swift */,
+				D845A13D2AF89F8400EA0853 /* ReadPulseLogView.swift */,
 				C1F67E7427975B830017487F /* ScheduledExpirationReminderEditView.swift */,
 				C1F67E7427975B830017487F /* ScheduledExpirationReminderEditView.swift */,
 				C1F67E8727975B830017487F /* SetupCompleteView.swift */,
 				C1F67E8727975B830017487F /* SetupCompleteView.swift */,
+				D845A1422AF89F9200EA0853 /* SilencePodSelectionView.swift */,
 				C1F67E7627975B830017487F /* TimeView.swift */,
 				C1F67E7627975B830017487F /* TimeView.swift */,
 				C1F67E8427975B830017487F /* UncertaintyRecoveredView.swift */,
 				C1F67E8427975B830017487F /* UncertaintyRecoveredView.swift */,
-				8475311F26ED838A009FD801 /* PodLifeHUDView.swift */,
-				8475312426ED838A009FD801 /* PodLifeHUDView.xib */,
-				C187C190278FCEC9006E3557 /* InsulinTypeConfirmation.swift */,
-				C1ED1E7127BAE44E00FED71C /* BeepPreferenceSelectionView.swift */,
-				C1DBD512282FF79D009FCF74 /* ManualTempBasalEntryView.swift */,
 			);
 			);
 			path = Views;
 			path = Views;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -836,10 +860,10 @@
 		C1F67EBB27975F060017487F /* ViewModels */ = {
 		C1F67EBB27975F060017487F /* ViewModels */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				C1F67EC227975F360017487F /* OmniBLESettingsViewModel.swift */,
 				C1F67EC027975F360017487F /* DeactivatePodViewModel.swift */,
 				C1F67EC027975F360017487F /* DeactivatePodViewModel.swift */,
 				C1F67EC127975F360017487F /* DeliveryUncertaintyRecoveryViewModel.swift */,
 				C1F67EC127975F360017487F /* DeliveryUncertaintyRecoveryViewModel.swift */,
 				C1F67EBD27975F360017487F /* InsertCannulaViewModel.swift */,
 				C1F67EBD27975F360017487F /* InsertCannulaViewModel.swift */,
+				C1F67EC227975F360017487F /* OmniBLESettingsViewModel.swift */,
 				C1F67EBE27975F360017487F /* PairPodViewModel.swift */,
 				C1F67EBE27975F360017487F /* PairPodViewModel.swift */,
 				C1F67EC327975F360017487F /* PodLifeState.swift */,
 				C1F67EC327975F360017487F /* PodLifeState.swift */,
 			);
 			);
@@ -1059,6 +1083,7 @@
 				1029AE4927094D0E00B7F5B6 /* OmniBLEPumpManagerState.swift in Sources */,
 				1029AE4927094D0E00B7F5B6 /* OmniBLEPumpManagerState.swift in Sources */,
 				10289E7D2739F893000339E6 /* Milenage.swift in Sources */,
 				10289E7D2739F893000339E6 /* Milenage.swift in Sources */,
 				10389A3A26FF7841002115E9 /* SetInsulinScheduleCommand.swift in Sources */,
 				10389A3A26FF7841002115E9 /* SetInsulinScheduleCommand.swift in Sources */,
+				D845A13B2AF89F7100EA0853 /* PlayTestBeepsView.swift in Sources */,
 				10389A3826FF7841002115E9 /* DetailedStatus.swift in Sources */,
 				10389A3826FF7841002115E9 /* DetailedStatus.swift in Sources */,
 				C1F67E9927975B830017487F /* NotificationSettingsView.swift in Sources */,
 				C1F67E9927975B830017487F /* NotificationSettingsView.swift in Sources */,
 				10389A2B26FF7841002115E9 /* PlaceholderMessageBlock.swift in Sources */,
 				10389A2B26FF7841002115E9 /* PlaceholderMessageBlock.swift in Sources */,
@@ -1103,6 +1128,7 @@
 				C1ED1E7227BAE44E00FED71C /* BeepPreferenceSelectionView.swift in Sources */,
 				C1ED1E7227BAE44E00FED71C /* BeepPreferenceSelectionView.swift in Sources */,
 				84752EDE26ED13F5009FD801 /* PeripheralManagerError.swift in Sources */,
 				84752EDE26ED13F5009FD801 /* PeripheralManagerError.swift in Sources */,
 				10389A2526FF7841002115E9 /* PodInfoActivationTime.swift in Sources */,
 				10389A2526FF7841002115E9 /* PodInfoActivationTime.swift in Sources */,
+				D845A1432AF89F9200EA0853 /* SilencePodSelectionView.swift in Sources */,
 				C1F67E9A27975B830017487F /* DeliveryUncertaintyRecoveryView.swift in Sources */,
 				C1F67E9A27975B830017487F /* DeliveryUncertaintyRecoveryView.swift in Sources */,
 				1021114D2709467400784F13 /* PodComms.swift in Sources */,
 				1021114D2709467400784F13 /* PodComms.swift in Sources */,
 				10289E6E27309327000339E6 /* CBPeripheral.swift in Sources */,
 				10289E6E27309327000339E6 /* CBPeripheral.swift in Sources */,
@@ -1120,7 +1146,9 @@
 				C1F67EC927975F360017487F /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */,
 				C1F67EC927975F360017487F /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */,
 				1016325C27185EE5007A3BC2 /* BasalDeliveryTable.swift in Sources */,
 				1016325C27185EE5007A3BC2 /* BasalDeliveryTable.swift in Sources */,
 				84752EE326ED13F5009FD801 /* BLEPacket.swift in Sources */,
 				84752EE326ED13F5009FD801 /* BLEPacket.swift in Sources */,
+				D845A1402AF89F8400EA0853 /* ReadPulseLogView.swift in Sources */,
 				102111472709462300784F13 /* PodState.swift in Sources */,
 				102111472709462300784F13 /* PodState.swift in Sources */,
+				196A6F232AFFFD1700E3C089 /* SilencePodPreference.swift in Sources */,
 				1021114B2709462300784F13 /* BasalSchedule+LoopKit.swift in Sources */,
 				1021114B2709462300784F13 /* BasalSchedule+LoopKit.swift in Sources */,
 				84752EE626ED13F5009FD801 /* LTKExchanger.swift in Sources */,
 				84752EE626ED13F5009FD801 /* LTKExchanger.swift in Sources */,
 				10389A2A26FF7841002115E9 /* MessageBlock.swift in Sources */,
 				10389A2A26FF7841002115E9 /* MessageBlock.swift in Sources */,
@@ -1129,6 +1157,7 @@
 				C1F67E9227975B830017487F /* BasalStateView.swift in Sources */,
 				C1F67E9227975B830017487F /* BasalStateView.swift in Sources */,
 				1024E32B27446DB000DE01F2 /* MessagePacket.swift in Sources */,
 				1024E32B27446DB000DE01F2 /* MessagePacket.swift in Sources */,
 				10289E7B2739F886000339E6 /* EapMessage.swift in Sources */,
 				10289E7B2739F886000339E6 /* EapMessage.swift in Sources */,
+				D845A1392AF89F6300EA0853 /* FirstAppear.swift in Sources */,
 				C1C001C127A2349D00533D35 /* OmniBLE.swift in Sources */,
 				C1C001C127A2349D00533D35 /* OmniBLE.swift in Sources */,
 				10389A3326FF7841002115E9 /* CancelDeliveryCommand.swift in Sources */,
 				10389A3326FF7841002115E9 /* CancelDeliveryCommand.swift in Sources */,
 				C1DBD513282FF79D009FCF74 /* ManualTempBasalEntryView.swift in Sources */,
 				C1DBD513282FF79D009FCF74 /* ManualTempBasalEntryView.swift in Sources */,
@@ -1136,6 +1165,7 @@
 				C1F67EA027975B830017487F /* PodSetupView.swift in Sources */,
 				C1F67EA027975B830017487F /* PodSetupView.swift in Sources */,
 				C1ED1E7027BAE1A600FED71C /* BeepPreference.swift in Sources */,
 				C1ED1E7027BAE1A600FED71C /* BeepPreference.swift in Sources */,
 				10389A3B26FF7841002115E9 /* ConfigureAlertsCommand.swift in Sources */,
 				10389A3B26FF7841002115E9 /* ConfigureAlertsCommand.swift in Sources */,
+				D845A13F2AF89F8400EA0853 /* ReadPodStatusView.swift in Sources */,
 				D8896C6227890E6B00E09A96 /* DetailedStatus+OmniBLE.swift in Sources */,
 				D8896C6227890E6B00E09A96 /* DetailedStatus+OmniBLE.swift in Sources */,
 				10389A3926FF7841002115E9 /* PodInfoResponse.swift in Sources */,
 				10389A3926FF7841002115E9 /* PodInfoResponse.swift in Sources */,
 				84752EE226ED13F5009FD801 /* PayloadJoiner.swift in Sources */,
 				84752EE226ED13F5009FD801 /* PayloadJoiner.swift in Sources */,
@@ -1145,6 +1175,7 @@
 				C1F67EC827975F360017487F /* DeactivatePodViewModel.swift in Sources */,
 				C1F67EC827975F360017487F /* DeactivatePodViewModel.swift in Sources */,
 				C1F67E9127975B830017487F /* DeactivatePodView.swift in Sources */,
 				C1F67E9127975B830017487F /* DeactivatePodView.swift in Sources */,
 				84752EE726ED13F5009FD801 /* PairResult.swift in Sources */,
 				84752EE726ED13F5009FD801 /* PairResult.swift in Sources */,
+				D845A1412AF89F8400EA0853 /* PumpManagerDetailsView.swift in Sources */,
 				8475315D26EDA193009FD801 /* Data.swift in Sources */,
 				8475315D26EDA193009FD801 /* Data.swift in Sources */,
 				C1F67E9427975B830017487F /* LowReservoirReminderSetupView.swift in Sources */,
 				C1F67E9427975B830017487F /* LowReservoirReminderSetupView.swift in Sources */,
 				C1F67EB627975E710017487F /* RoundedCard.swift in Sources */,
 				C1F67EB627975E710017487F /* RoundedCard.swift in Sources */,
@@ -1162,6 +1193,7 @@
 				10389A2E26FF7841002115E9 /* FaultConfigCommand.swift in Sources */,
 				10389A2E26FF7841002115E9 /* FaultConfigCommand.swift in Sources */,
 				C1F67E9F27975B830017487F /* SetupCompleteView.swift in Sources */,
 				C1F67E9F27975B830017487F /* SetupCompleteView.swift in Sources */,
 				C1F67EE227985F580017487F /* ReservoirLevel.swift in Sources */,
 				C1F67EE227985F580017487F /* ReservoirLevel.swift in Sources */,
+				D845A1372AF89F5500EA0853 /* ActivityView.swift in Sources */,
 			);
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			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 characteristic = manager.peripheral.getCommandCharacteristic() else { return }
                     guard let value = characteristic.value else { return }
                     guard let value = characteristic.value else { return }
 
 
-                    manager.log.default("CMD <<< %{public}@", value.hexadecimalString)
                     manager.queueLock.lock()
                     manager.queueLock.lock()
                     manager.cmdQueue.append(value)
                     manager.cmdQueue.append(value)
                     manager.queueLock.signal()
                     manager.queueLock.signal()
@@ -66,7 +65,6 @@ extension PeripheralManager.Configuration {
                     guard let characteristic = manager.peripheral.getDataCharacteristic() else { return }
                     guard let characteristic = manager.peripheral.getDataCharacteristic() else { return }
                     guard let value = characteristic.value else { return }
                     guard let value = characteristic.value else { return }
 
 
-                    manager.log.default("DATA <<< %{public}@", value.hexadecimalString)
                     manager.queueLock.lock()
                     manager.queueLock.lock()
                     manager.dataQueue.append(value)
                     manager.dataQueue.append(value)
                     manager.queueLock.signal()
                     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 header = msg.asData(forEncryption: false).subdata(in: 0..<16)
 
 
         let n = nonce.toData(sqn: nonceSeq, podReceiving: false)
         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 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 aes = try AES(key: ck.bytes, blockMode: ccm, padding: .noPadding)
         let decryptedPayload = try aes.decrypt(payload.bytes)
         let decryptedPayload = try aes.decrypt(payload.bytes)
-        log.debug("Decrypted payload %@", Data(decryptedPayload).hexadecimalString)
         
         
         var msgCopy = msg
         var msgCopy = msg
         msgCopy.payload = Data(decryptedPayload)
         msgCopy.payload = Data(decryptedPayload)
@@ -46,15 +40,9 @@ class EnDecrypt {
         let header = headerMessage.asData(forEncryption: true).subdata(in: 0..<16)
         let header = headerMessage.asData(forEncryption: true).subdata(in: 0..<16)
 
 
         let n = nonce.toData(sqn: nonceSeq, podReceiving: true)
         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 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 aes = try AES(key: ck.bytes, blockMode: ccm, padding: .noPadding)
         let encryptedPayload = try aes.encrypt(payload.bytes)
         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
         var msgCopy = headerMessage
         msgCopy.payload = Data(encryptedPayload)
         msgCopy.payload = Data(encryptedPayload)

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

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

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

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

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

@@ -9,11 +9,29 @@
 
 
 import Foundation
 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 {
 public enum AlertTrigger {
     case unitsRemaining(Double)
     case unitsRemaining(Double)
     case timeUntilAlert(TimeInterval)
     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 {
 public enum BeepRepeat: UInt8 {
     case once = 0
     case once = 0
     case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1
     case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1
@@ -30,29 +48,48 @@ public enum BeepRepeat: UInt8 {
 public struct AlertConfiguration {
 public struct AlertConfiguration {
 
 
     let slot: AlertSlot
     let slot: AlertSlot
-    let trigger: AlertTrigger
     let active: Bool
     let active: Bool
     let duration: TimeInterval
     let duration: TimeInterval
+    let trigger: AlertTrigger
     let beepRepeat: BeepRepeat
     let beepRepeat: BeepRepeat
     let beepType: BeepType
     let beepType: BeepType
+    let silent: Bool
     let autoOffModifier: Bool
     let autoOffModifier: Bool
 
 
     static let length = 6
     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.slot = alertType
         self.active = active
         self.active = active
-        self.autoOffModifier = autoOffModifier
         self.duration = duration
         self.duration = duration
         self.trigger = trigger
         self.trigger = trigger
         self.beepRepeat = beepRepeat
         self.beepRepeat = beepRepeat
         self.beepType = beepType
         self.beepType = beepType
+        self.silent = silent
+        self.autoOffModifier = autoOffModifier
     }
     }
 }
 }
 
 
 extension AlertConfiguration: CustomDebugStringConvertible {
 extension AlertConfiguration: CustomDebugStringConvertible {
     public var debugDescription: String {
     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 enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
     public typealias RawValue = [String: Any]
     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 {
     public var description: String {
         var alertName: String
         var alertName: String
         switch self {
         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:
         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:
         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:
         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 {
         if self.configuration.active == false {
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
@@ -118,71 +174,126 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
 
     public var configuration: AlertConfiguration {
     public var configuration: AlertConfiguration {
         switch self {
         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
             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 reminderInterval, duration: TimeInterval
-            let trigger: AlertTrigger
-            let beepRepeat: BeepRepeat
+            var beepRepeat: BeepRepeat
             let beepType: BeepType
             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 {
                 if suspendTime == 0 {
                     duration = 0 // Untimed suspend, no duration
                     duration = 0 // Untimed suspend, no duration
-                } else if suspendTime > reminderInterval {
-                    duration = suspendTime - reminderInterval // End after suspendTime total time
                 } else {
                 } 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
                 beepType = .beep
             } else {
             } else {
+                beepRepeat = .once
                 duration = 0
                 duration = 0
                 trigger = .timeUntilAlert(.minutes(0))
                 trigger = .timeUntilAlert(.minutes(0))
-                beepRepeat = .once
                 beepType = .noBeepCancel
                 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 active = suspendTime != 0 // disable if suspendTime is 0
             let trigger: AlertTrigger
             let trigger: AlertTrigger
             let beepRepeat: BeepRepeat
             let beepRepeat: BeepRepeat
             let beepType: BeepType
             let beepType: BeepType
             if active {
             if active {
                 trigger = .timeUntilAlert(suspendTime)
                 trigger = .timeUntilAlert(suspendTime)
-                beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes
+                beepRepeat = suspendTimeExpiredBeepRepeat
                 beepType = .bipBeepBipBeepBipBeepBipBeep
                 beepType = .bipBeepBipBeepBipBeepBipBeep
             } else {
             } else {
                 trigger = .timeUntilAlert(.minutes(0))
                 trigger = .timeUntilAlert(.minutes(0))
                 beepRepeat = .once
                 beepRepeat = .once
                 beepType = .noBeepCancel
                 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 {
         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
                 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":
         case "shutdownImminent":
-            guard let alarmTime = rawValue["alarmTime"] as? Double else {
+            guard let alarmTime = rawValue["alarmTime"] as? TimeInterval else {
                 return nil
                 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
                 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
                 return nil
             }
             }
-            self = .autoOff(active: active, countdownDuration: TimeInterval(countdownDuration))
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .lowReservoir(units: units, silent: silent)
         case "podSuspendedReminder":
         case "podSuspendedReminder":
             guard let active = rawValue["active"] as? Bool,
             guard let active = rawValue["active"] as? Bool,
-                let suspendTime = rawValue["suspendTime"] as? Double else
+                let suspendTime = rawValue["suspendTime"] as? TimeInterval else
             {
             {
                 return nil
                 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":
         case "suspendTimeExpired":
             guard let suspendTime = rawValue["suspendTime"] as? Double else {
             guard let suspendTime = rawValue["suspendTime"] as? Double else {
                 return nil
                 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:
         default:
             return nil
             return nil
         }
         }
@@ -249,50 +401,65 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
 
         let name: String = {
         let name: String = {
             switch self {
             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:
             case .shutdownImminent:
                 return "shutdownImminent"
                 return "shutdownImminent"
+            case .expirationReminder:
+                return "expirationReminder"
             case .lowReservoir:
             case .lowReservoir:
                 return "lowReservoir"
                 return "lowReservoir"
-            case .autoOff:
-                return "autoOff"
             case .podSuspendedReminder:
             case .podSuspendedReminder:
                 return "podSuspendedReminder"
                 return "podSuspendedReminder"
             case .suspendTimeExpired:
             case .suspendTimeExpired:
                 return "suspendTimeExpired"
                 return "suspendTimeExpired"
+            case .waitingForPairingReminder:
+                return "waitingForPairingReminder"
+            case .finishSetupReminder:
+                return "finishSetupReminder"
+            case .expired:
+                return "expired"
             }
             }
         }()
         }()
 
 
-
         var rawValue: RawValue = [
         var rawValue: RawValue = [
             "name": name,
             "name": name,
         ]
         ]
 
 
         switch self {
         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["active"] = active
+            rawValue["offset"] = offset
             rawValue["countdownDuration"] = countdownDuration
             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["active"] = active
+            rawValue["offset"] = offset
             rawValue["suspendTime"] = suspendTime
             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["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:
         default:
             break
             break
         }
         }
@@ -302,42 +469,42 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 }
 }
 
 
 public enum AlertSlot: UInt8 {
 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 {
     public var bitMaskValue: UInt8 {
         return 1<<rawValue
         return 1<<rawValue
     }
     }
 
 
     public typealias AllCases = [AlertSlot]
     public typealias AllCases = [AlertSlot]
-    
+
     static var allCases: AllCases {
     static var allCases: AllCases {
         return (0..<8).map { AlertSlot(rawValue: $0)! }
         return (0..<8).map { AlertSlot(rawValue: $0)! }
     }
     }
 }
 }
 
 
 public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, Equatable {
 public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, Equatable {
-    
+
     public typealias RawValue = UInt8
     public typealias RawValue = UInt8
     public typealias Index = Int
     public typealias Index = Int
-    
+
     public let startIndex: Int
     public let startIndex: Int
     public let endIndex: Int
     public let endIndex: Int
-    
+
     private let elements: [AlertSlot]
     private let elements: [AlertSlot]
-    
+
     public static let none = AlertSet(rawValue: 0)
     public static let none = AlertSet(rawValue: 0)
-    
+
     public var rawValue: UInt8 {
     public var rawValue: UInt8 {
         return elements.reduce(0) { $0 | $1.bitMaskValue }
         return elements.reduce(0) { $0 | $1.bitMaskValue }
     }
     }
-    
+
     public init(slots: [AlertSlot]) {
     public init(slots: [AlertSlot]) {
         self.elements = slots
         self.elements = slots
         self.startIndex = 0
         self.startIndex = 0
@@ -351,11 +518,11 @@ public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, E
     public subscript(index: Index) -> AlertSlot {
     public subscript(index: Index) -> AlertSlot {
         return elements[index]
         return elements[index]
     }
     }
-    
+
     public func index(after i: Int) -> Int {
     public func index(after i: Int) -> Int {
         return i+1
         return i+1
     }
     }
-    
+
     public var description: String {
     public var description: String {
         if elements.count == 0 {
         if elements.count == 0 {
             return LocalizedString("No alerts", comment: "Pod alert state when no alerts are active")
             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
 // Returns true if there are any active suspend related alerts
 public func hasActiveSuspendAlert(configuredAlerts: [AlertSlot : PodAlert]) -> Bool {
 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 true
     }
     }
     return false
     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
             // Eros zero TB is the only case not using pulses
             return 0
             return 0
         } else {
         } 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
             // Eros zero TB case uses fixed 30 minute rate entries
             return TimeInterval(minutes: 30)
             return TimeInterval(minutes: 30)
         } else {
         } 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 {
 extension RateEntry: CustomDebugStringConvertible {
     public var debugDescription: String {
     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.rate = rrate
         self.startTime = startTime
         self.startTime = startTime
     }
     }
-    
+
     // MARK: - RawRepresentable
     // MARK: - RawRepresentable
     public init?(rawValue: RawValue) {
     public init?(rawValue: RawValue) {
         
         
@@ -61,13 +61,13 @@ public struct BasalSchedule: RawRepresentable, Equatable {
         let (_, entry, _) = lookup(offset: offset)
         let (_, entry, _) = lookup(offset: offset)
         return entry.rate
         return entry.rate
     }
     }
-    
+
     // Only valid for fixed offset timezones
     // Only valid for fixed offset timezones
     public func currentRate(using calendar: Calendar, at date: Date = Date()) -> Double {
     public func currentRate(using calendar: Calendar, at date: Date = Date()) -> Double {
         let midnight = calendar.startOfDay(for: date)
         let midnight = calendar.startOfDay(for: date)
         return rateAt(offset: date.timeIntervalSince(midnight))
         return rateAt(offset: date.timeIntervalSince(midnight))
     }
     }
-    
+
     // Returns index, entry, and time remaining
     // Returns index, entry, and time remaining
     func lookup(offset: TimeInterval) -> (Int, BasalScheduleEntry, TimeInterval) {
     func lookup(offset: TimeInterval) -> (Int, BasalScheduleEntry, TimeInterval) {
         guard offset >= 0 && offset < .hours(24) else {
         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:
         case .silent:
             return LocalizedString("No confidence reminders are used.", comment: "Description for BeepPreference.silent")
             return LocalizedString("No confidence reminders are used.", comment: "Description for BeepPreference.silent")
         case .manualCommands:
         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:
         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
 //  From OmniKit/MessageTransport/CRC16.swift
 //  Created by Pete Schwamb on 10/14/17.
 //  Created by Pete Schwamb on 10/14/17.
-//  Copyright © 2017 Pete Schwambs. All rights reserved.
+//  Copyright © 2017 Pete Schwamb. All rights reserved.
 //
 //
 
 
 import Foundation
 import Foundation

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

@@ -26,6 +26,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case invalidBeepRepeatPattern             = 0x09
         case invalidBeepRepeatPattern             = 0x09
         case bf0notEqualToBF1                     = 0x0A
         case bf0notEqualToBF1                     = 0x0A
         case tableCorruptionTempBasalSubcommand   = 0x0B
         case tableCorruptionTempBasalSubcommand   = 0x0B
+
         case resetDueToCOP                        = 0x0D
         case resetDueToCOP                        = 0x0D
         case resetDueToIllegalOpcode              = 0x0E
         case resetDueToIllegalOpcode              = 0x0E
         case resetDueToIllegalAddress             = 0x0F
         case resetDueToIllegalAddress             = 0x0F
@@ -76,6 +77,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case testInProgress                       = 0x3C
         case testInProgress                       = 0x3C
         case problemWithPumpAnchor                = 0x3D
         case problemWithPumpAnchor                = 0x3D
         case errorFlashWrite                      = 0x3E
         case errorFlashWrite                      = 0x3E
+
         case encoderCountTooHigh                  = 0x40
         case encoderCountTooHigh                  = 0x40
         case encoderCountExcessiveVariance        = 0x41
         case encoderCountExcessiveVariance        = 0x41
         case encoderCountTooLow                   = 0x42
         case encoderCountTooLow                   = 0x42
@@ -90,6 +92,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case trimICSTooCloseTo0x1FF               = 0x4B
         case trimICSTooCloseTo0x1FF               = 0x4B
         case problemFindingBestTrimValue          = 0x4C
         case problemFindingBestTrimValue          = 0x4C
         case badSetTPM1MultiCasesValue            = 0x4D
         case badSetTPM1MultiCasesValue            = 0x4D
+        case sawTrimError                         = 0x4E
         case unexpectedRFErrorFlagDuringReset     = 0x4F
         case unexpectedRFErrorFlagDuringReset     = 0x4F
         case timerPulseWidthModulatorOverflow     = 0x50
         case timerPulseWidthModulatorOverflow     = 0x50
         case tickcntError                         = 0x51
         case tickcntError                         = 0x51
@@ -110,11 +113,13 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case occlusionCheckStartup1               = 0x60
         case occlusionCheckStartup1               = 0x60
         case occlusionCheckStartup2               = 0x61
         case occlusionCheckStartup2               = 0x61
         case occlusionCheckTimeouts1              = 0x62
         case occlusionCheckTimeouts1              = 0x62
+
         case occlusionCheckTimeouts2              = 0x66
         case occlusionCheckTimeouts2              = 0x66
         case occlusionCheckTimeouts3              = 0x67
         case occlusionCheckTimeouts3              = 0x67
         case occlusionCheckPulseIssue             = 0x68
         case occlusionCheckPulseIssue             = 0x68
         case occlusionCheckBolusProblem           = 0x69
         case occlusionCheckBolusProblem           = 0x69
         case occlusionCheckAboveThreshold         = 0x6A
         case occlusionCheckAboveThreshold         = 0x6A
+
         case basalUnderInfusion                   = 0x80
         case basalUnderInfusion                   = 0x80
         case basalOverInfusion                    = 0x81
         case basalOverInfusion                    = 0x81
         case tempBasalUnderInfusion               = 0x82
         case tempBasalUnderInfusion               = 0x82
@@ -138,9 +143,11 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case illegalInterLockChan                 = 0x95
         case illegalInterLockChan                 = 0x95
         case badStateInClearBolusIST2AndVars      = 0x96
         case badStateInClearBolusIST2AndVars      = 0x96
         case badStateInMaybeInc33D                = 0x97
         case badStateInMaybeInc33D                = 0x97
+
         case bleTimeout                           = 0xA0
         case bleTimeout                           = 0xA0
         case bleInitiated                         = 0xA1
         case bleInitiated                         = 0xA1
         case bleUnkAlarm                          = 0xA2
         case bleUnkAlarm                          = 0xA2
+
         case bleIaas                              = 0xA6
         case bleIaas                              = 0xA6
         case crcFailure                           = 0xA8
         case crcFailure                           = 0xA8
         case bleWdPingTimeout                     = 0xA9
         case bleWdPingTimeout                     = 0xA9
@@ -148,6 +155,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case bleNakError                          = 0xAB
         case bleNakError                          = 0xAB
         case bleReqHighTimeout                    = 0xAC
         case bleReqHighTimeout                    = 0xAC
         case bleUnknownResp                       = 0xAD
         case bleUnknownResp                       = 0xAD
+
         case bleReqStuckHigh                      = 0xAF
         case bleReqStuckHigh                      = 0xAF
         case bleStateMachine1                     = 0xB1
         case bleStateMachine1                     = 0xB1
         case bleStateMachine2                     = 0xB2
         case bleStateMachine2                     = 0xB2
@@ -155,7 +163,17 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case bleEr48DualNack                      = 0xC0
         case bleEr48DualNack                      = 0xC0
         case bleQnExceedMaxRetry                  = 0xC1
         case bleQnExceedMaxRetry                  = 0xC1
         case bleQnCritVarFail                     = 0xC2
         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? {
     public var faultType: FaultEventType? {
@@ -165,302 +183,297 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
     init(rawValue: UInt8) {
     init(rawValue: UInt8) {
         self.rawValue = rawValue
         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)
         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
 import Foundation
 
 
 
 
-
 public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
 public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
-    
+
     public let blockType: MessageBlockType = .cancelDelivery
     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
     // 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 struct DeliveryType: OptionSet, Equatable {
         public let rawValue: UInt8
         public let rawValue: UInt8
         
         
@@ -48,7 +42,23 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
         public init(rawValue: UInt8) {
         public init(rawValue: UInt8) {
             self.rawValue = rawValue
             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
     public let deliveryType: DeliveryType
@@ -85,6 +95,6 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
 
 
 extension CancelDeliveryCommand: CustomDebugStringConvertible {
 extension CancelDeliveryCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
     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),
             UInt8(4 + configurations.count * AlertConfiguration.length),
             ])
             ])
         data.appendBigEndian(nonce)
         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)
             data.append(contentsOf: config.data)
         }
         }
         return data
         return data
@@ -93,6 +95,7 @@ extension AlertConfiguration {
         }
         }
         self.beepType = beepType
         self.beepType = beepType
 
 
+        self.silent = (beepType == .noBeepNonCancel)
     }
     }
 
 
     public var data: Data {
     public var data: Data {
@@ -105,12 +108,16 @@ extension AlertConfiguration {
         if autoOffModifier {
         if autoOffModifier {
             firstByte += 1 << 1
             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
         // High bit of duration
-        firstByte += UInt8((Int(duration.minutes) >> 8) & 0x1)
+        firstByte += UInt8((durationMinutes >> 8) & 0x1)
 
 
         var data = Data([
         var data = Data([
             firstByte,
             firstByte,
-            UInt8(Int(duration.minutes) & 0xff)
+            UInt8(durationMinutes & 0xff)
             ])
             ])
 
 
         switch trigger {
         switch trigger {
@@ -123,7 +130,8 @@ extension AlertConfiguration {
             data.appendBigEndian(minutes)
             data.appendBigEndian(minutes)
         }
         }
         data.append(beepRepeat.rawValue)
         data.append(beepRepeat.rawValue)
-        data.append(beepType.rawValue)
+        let beepTypeToSet: BeepType = silent ? .noBeepNonCancel : beepType
+        data.append(beepTypeToSet.rawValue)
 
 
         return data
         return data
     }
     }

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

@@ -10,14 +10,13 @@
 import Foundation
 import Foundation
 
 
 public struct DeactivatePodCommand : NonceResyncableMessageBlock {
 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 let blockType: MessageBlockType = .deactivatePod
     
     
     public var nonce: UInt32
     public var nonce: UInt32
     
     
-    // e1f78752 07 8196
     public var data: Data {
     public var data: Data {
         var data = Data([
         var data = Data([
             blockType.rawValue,
             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 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.
         // 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
         // Decode YYYY based on whether there was a pod fault
         if encodedData[8] == 0 {
         if encodedData[8] == 0 {
             // For non-faults, YYYY contents not valid (either uninitialized data for Eros or some unknown content for Dash).
             // 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",
             "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U",
             "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U",
             "* 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)",
             "* unacknowledgedAlerts: \(unacknowledgedAlerts)",
             "",
             "",
             ].joined(separator: "\n")
             ].joined(separator: "\n")
@@ -134,8 +133,9 @@ extension DetailedStatus: CustomDebugStringConvertible {
         }
         }
         if faultEventCode.faultType != .noFaults {
         if faultEventCode.faultType != .noFaults {
             result += [
             result += [
+                "* faultEventCode: \(faultEventCode.description)",
                 "* faultAccessingTables: \(faultAccessingTables)",
                 "* faultAccessingTables: \(faultAccessingTables)",
-                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "NA")",
+                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.timeIntervalStr ?? "NA")",
                 "* errorEventInfo: \(errorEventInfo?.description ?? "NA")",
                 "* errorEventInfo: \(errorEventInfo?.description ?? "NA")",
                 "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
                 "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
                 "* possibleFaultCallingAddress: \(possibleFaultCallingAddress != nil ? String(format: "0x%04x", possibleFaultCallingAddress!) : "NA")",
                 "* possibleFaultCallingAddress: \(possibleFaultCallingAddress != nil ? String(format: "0x%04x", possibleFaultCallingAddress!) : "NA")",
@@ -161,21 +161,21 @@ extension DetailedStatus: RawRepresentable {
 }
 }
 
 
 extension TimeInterval {
 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
 // dddd: Pod Progress at time of first logged fault event
 //
 //
 public struct ErrorEventInfo: CustomStringConvertible, Equatable {
 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? {
     public var errorEventInfo: ErrorEventInfo? {
         return ErrorEventInfo(rawValue: rawValue)
         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 {
 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 index = pulseLogEntries.count - 1
     var pulseNumber = lastPulseNumber
     var pulseNumber = lastPulseNumber
     while index >= 0 {
     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
         index -= 1
         pulseNumber -= 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.lastProgrammingMessageSeqNum = (encodedData[4] >> 3) & 0xf
         
         
         self.bolusNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit
         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.alerts = AlertSet(rawValue: ((encodedData[6] & 0x7f) << 1) | (encodedData[7] >> 7))
-        
+
         self.reservoirLevel = Double((Int(encodedData[8] & 0x3) << 8) + Int(encodedData[9])) / Pod.pulsesPerUnit
         self.reservoirLevel = Double((Int(encodedData[8] & 0x3) << 8) + Int(encodedData[9])) / Pod.pulsesPerUnit
     }
     }
 
 
@@ -95,7 +95,7 @@ public struct StatusResponse : MessageBlock {
 
 
 extension StatusResponse: CustomDebugStringConvertible {
 extension StatusResponse: CustomDebugStringConvertible {
     public var debugDescription: String {
     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 {
 extension TempBasalExtraCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
     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 bolus(volume: Double, automatic: Bool)
     case basalProgram(schedule: BasalSchedule)
     case basalProgram(schedule: BasalSchedule)
     case tempBasal(unitsPerHour: Double, duration: TimeInterval, isHighTemp: Bool, automatic: Bool)
     case tempBasal(unitsPerHour: Double, duration: TimeInterval, isHighTemp: Bool, automatic: Bool)
-    
+
     private enum StartProgramType: Int {
     private enum StartProgramType: Int {
         case bolus, basalProgram, tempBasal
         case bolus, basalProgram, tempBasal
     }
     }
-    
+
     public var rawValue: RawValue {
     public var rawValue: RawValue {
         switch self {
         switch self {
         case .bolus(let volume, let automatic):
         case .bolus(let volume, let automatic):
@@ -77,7 +77,7 @@ public enum StartProgram: RawRepresentable {
             self = .tempBasal(unitsPerHour: unitsPerHour, duration: duration, isHighTemp: isHighTemp, automatic: automatic)
             self = .tempBasal(unitsPerHour: unitsPerHour, duration: duration, isHighTemp: isHighTemp, automatic: automatic)
         }
         }
     }
     }
-    
+
     public static func == (lhs: StartProgram, rhs: StartProgram) -> Bool {
     public static func == (lhs: StartProgram, rhs: StartProgram) -> Bool {
         switch(lhs, rhs) {
         switch(lhs, rhs) {
         case (.bolus(let lhsVolume, let lhsAutomatic), .bolus(let rhsVolume, let rhsAutomatic)):
         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 program(StartProgram, Int, Date, Bool = true)
     case stopProgram(CancelDeliveryCommand.DeliveryType, Int, Date, Bool = true)
     case stopProgram(CancelDeliveryCommand.DeliveryType, Int, Date, Bool = true)
-    
+
     private enum PendingCommandType: Int {
     private enum PendingCommandType: Int {
         case startProgram, stopProgram
         case startProgram, stopProgram
     }
     }
@@ -142,7 +142,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
         guard let rawPendingCommandType = rawValue["type"] as? PendingCommandType.RawValue else {
         guard let rawPendingCommandType = rawValue["type"] as? PendingCommandType.RawValue else {
             return nil
             return nil
         }
         }
-        
+
         guard let commandDate = rawValue["date"] as? Date else {
         guard let commandDate = rawValue["date"] as? Date else {
             return nil
             return nil
         }
         }
@@ -176,7 +176,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
 
 
     public var rawValue: RawValue {
     public var rawValue: RawValue {
         var rawValue: RawValue = [:]
         var rawValue: RawValue = [:]
-        
+
         switch self {
         switch self {
         case .program(let program, let sequence, let date, let inflight):
         case .program(let program, let sequence, let date, let inflight):
             rawValue["type"] = PendingCommandType.startProgram.rawValue
             rawValue["type"] = PendingCommandType.startProgram.rawValue
@@ -193,7 +193,7 @@ public enum PendingCommand: RawRepresentable, Equatable {
         }
         }
         return rawValue
         return rawValue
     }
     }
-    
+
     public static func == (lhs: PendingCommand, rhs: PendingCommand) -> Bool {
     public static func == (lhs: PendingCommand, rhs: PendingCommand) -> Bool {
         switch(lhs, rhs) {
         switch(lhs, rhs) {
         case (.program(let lhsProgram, let lhsSequence, let lhsDate, let lhsInflight), .program(let rhsProgram, let rhsSequence, let rhsDate, let rhsInflight)):
         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 defaultExpirationReminderOffset = TimeInterval(hours: 2)
     public static let expirationReminderAlertMinHoursBeforeExpiration = 1
     public static let expirationReminderAlertMinHoursBeforeExpiration = 1
     public static let expirationReminderAlertMaxHoursBeforeExpiration = 24
     public static let expirationReminderAlertMaxHoursBeforeExpiration = 24
-    
+
     // Threshold used to display pod end of life warnings
     // Threshold used to display pod end of life warnings
     public static let timeRemainingWarningThreshold = TimeInterval(days: 1)
     public static let timeRemainingWarningThreshold = TimeInterval(days: 1)
-    
+
     // Default low reservoir alert limit in Units
     // Default low reservoir alert limit in Units
     public static let defaultLowReservoirReminder: Double = 10
     public static let defaultLowReservoirReminder: Double = 10
-    
+
     // Allowed Low Reservoir reminder values
     // Allowed Low Reservoir reminder values
     public static let allowedLowReservoirReminderValues = Array(stride(from: 10, through: 50, by: 1))
     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
 //  OmniBLE
 //
 //
 //  Created by Pete Schwamb on 7/9/20.
 //  Created by Pete Schwamb on 7/9/20.
@@ -11,7 +11,6 @@ import LoopKit
 import HealthKit
 import HealthKit
 
 
 public enum PumpManagerAlert: Hashable {
 public enum PumpManagerAlert: Hashable {
-    case multiCommand(triggeringSlot: AlertSlot?)
     case podExpireImminent(triggeringSlot: AlertSlot?)
     case podExpireImminent(triggeringSlot: AlertSlot?)
     case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval)
     case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval)
     case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double)
     case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double)
@@ -19,12 +18,13 @@ public enum PumpManagerAlert: Hashable {
     case suspendEnded(triggeringSlot: AlertSlot?)
     case suspendEnded(triggeringSlot: AlertSlot?)
     case podExpiring(triggeringSlot: AlertSlot?)
     case podExpiring(triggeringSlot: AlertSlot?)
     case finishSetupReminder(triggeringSlot: AlertSlot?)
     case finishSetupReminder(triggeringSlot: AlertSlot?)
+    case unexpectedAlert(triggeringSlot: AlertSlot?)
     case timeOffsetChangeDetected
     case timeOffsetChangeDetected
-    
+
     var isRepeating: Bool {
     var isRepeating: Bool {
         return repeatInterval != nil
         return repeatInterval != nil
     }
     }
-    
+
     var repeatInterval: TimeInterval? {
     var repeatInterval: TimeInterval? {
         switch self {
         switch self {
         case .suspendEnded:
         case .suspendEnded:
@@ -33,11 +33,9 @@ public enum PumpManagerAlert: Hashable {
             return nil
             return nil
         }
         }
     }
     }
-        
+
     var contentTitle: String {
     var contentTitle: String {
         switch self {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content title for multiCommand pod alert")
         case .userPodExpiration:
         case .userPodExpiration:
             return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert")
             return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert")
         case .podExpiring:
         case .podExpiring:
@@ -52,15 +50,15 @@ public enum PumpManagerAlert: Hashable {
             return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert")
             return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert")
         case .finishSetupReminder:
         case .finishSetupReminder:
             return LocalizedString("Pod Pairing Incomplete", comment: "Alert content title for finishSetupReminder pod alert")
             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:
         case .timeOffsetChangeDetected:
             return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert")
             return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert")
         }
         }
     }
     }
-    
+
     var contentBody: String {
     var contentBody: String {
         switch self {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content body for multiCommand pod alert")
         case .userPodExpiration(_, let offset):
         case .userPodExpiration(_, let offset):
             let formatter = DateComponentsFormatter()
             let formatter = DateComponentsFormatter()
             formatter.allowedUnits = [.hour]
             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")
             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:
         case .finishSetupReminder:
             return LocalizedString("Please finish pairing your pod.", comment: "Alert content body for finishSetupReminder pod alert")
             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:
         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")
             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? {
     var triggeringSlot: AlertSlot? {
         switch self {
         switch self {
-        case .multiCommand(let slot):
-            return slot
         case .userPodExpiration(let slot, _):
         case .userPodExpiration(let slot, _):
             return slot
             return slot
         case .podExpiring(let slot):
         case .podExpiring(let slot):
@@ -104,17 +103,19 @@ public enum PumpManagerAlert: Hashable {
             return slot
             return slot
         case .finishSetupReminder(let slot):
         case .finishSetupReminder(let slot):
             return slot
             return slot
+        case .unexpectedAlert(let slot):
+            return slot
         case .timeOffsetChangeDetected:
         case .timeOffsetChangeDetected:
             return nil
             return nil
         }
         }
     }
     }
-    
+
     // Override background (UserNotification) content
     // Override background (UserNotification) content
-    
+
     var backgroundContentTitle: String {
     var backgroundContentTitle: String {
         return contentTitle
         return contentTitle
     }
     }
-    
+
     var backgroundContentBody: String {
     var backgroundContentBody: String {
         switch self {
         switch self {
         case .suspendEnded:
         case .suspendEnded:
@@ -124,23 +125,21 @@ public enum PumpManagerAlert: Hashable {
         }
         }
     }
     }
 
 
-    
+
     var actionButtonLabel: String {
     var actionButtonLabel: String {
         return LocalizedString("Ok", comment: "Action button default text for PodAlerts")
         return LocalizedString("Ok", comment: "Action button default text for PodAlerts")
     }
     }
-    
+
     var foregroundContent: Alert.Content {
     var foregroundContent: Alert.Content {
         return Alert.Content(title: contentTitle, body: contentBody, acknowledgeActionButtonLabel: actionButtonLabel)
         return Alert.Content(title: contentTitle, body: contentBody, acknowledgeActionButtonLabel: actionButtonLabel)
     }
     }
-    
+
     var backgroundContent: Alert.Content {
     var backgroundContent: Alert.Content {
         return Alert.Content(title: backgroundContentTitle, body: backgroundContentBody, acknowledgeActionButtonLabel: actionButtonLabel)
         return Alert.Content(title: backgroundContentTitle, body: backgroundContentBody, acknowledgeActionButtonLabel: actionButtonLabel)
     }
     }
-    
+
     var alertIdentifier: String {
     var alertIdentifier: String {
         switch self {
         switch self {
-        case .multiCommand:
-            return "multiCommand"
         case .userPodExpiration:
         case .userPodExpiration:
             return "userPodExpiration"
             return "userPodExpiration"
         case .podExpiring:
         case .podExpiring:
@@ -153,38 +152,38 @@ public enum PumpManagerAlert: Hashable {
             return "suspendInProgress"
             return "suspendInProgress"
         case .suspendEnded:
         case .suspendEnded:
             return "suspendEnded"
             return "suspendEnded"
-        case .timeOffsetChangeDetected:
-            return "timeOffsetChangeDetected"
         case .finishSetupReminder:
         case .finishSetupReminder:
             return "finishSetupReminder"
             return "finishSetupReminder"
+        case .unexpectedAlert:
+            return "unexpectedAlert"
+        case .timeOffsetChangeDetected:
+            return "timeOffsetChangeDetected"
         }
         }
     }
     }
-        
+
     var repeatingAlertIdentifier: String {
     var repeatingAlertIdentifier: String {
         return alertIdentifier + "-repeating"
         return alertIdentifier + "-repeating"
     }
     }
 }
 }
 
 
 extension PumpManagerAlert: RawRepresentable {
 extension PumpManagerAlert: RawRepresentable {
-    
+
     public typealias RawValue = [String: Any]
     public typealias RawValue = [String: Any]
-    
+
     public init?(rawValue: RawValue) {
     public init?(rawValue: RawValue) {
         guard let identifier = rawValue["identifier"] as? String else {
         guard let identifier = rawValue["identifier"] as? String else {
             return nil
             return nil
         }
         }
-        
+
         let slot: AlertSlot?
         let slot: AlertSlot?
-        
+
         if let rawSlot = rawValue["slot"] as? AlertSlot.RawValue {
         if let rawSlot = rawValue["slot"] as? AlertSlot.RawValue {
             slot = AlertSlot(rawValue: rawSlot)
             slot = AlertSlot(rawValue: rawSlot)
         } else {
         } else {
             slot = nil
             slot = nil
         }
         }
-        
+
         switch identifier {
         switch identifier {
-        case "multiCommand":
-            self = .multiCommand(triggeringSlot: slot)
         case "userPodExpiration":
         case "userPodExpiration":
             guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else {
             guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else {
                 return nil
                 return nil
@@ -203,6 +202,8 @@ extension PumpManagerAlert: RawRepresentable {
             self = .suspendInProgress(triggeringSlot: slot)
             self = .suspendInProgress(triggeringSlot: slot)
         case "suspendEnded":
         case "suspendEnded":
             self = .suspendEnded(triggeringSlot: slot)
             self = .suspendEnded(triggeringSlot: slot)
+        case "unexpectedAlert":
+            self = .unexpectedAlert(triggeringSlot: slot)
         case "timeOffsetChangeDetected":
         case "timeOffsetChangeDetected":
             self = .timeOffsetChangeDetected
             self = .timeOffsetChangeDetected
         default:
         default:
@@ -214,9 +215,9 @@ extension PumpManagerAlert: RawRepresentable {
         var rawValue: RawValue = [
         var rawValue: RawValue = [
             "identifier": alertIdentifier
             "identifier": alertIdentifier
         ]
         ]
-        
+
         rawValue["slot"] = triggeringSlot?.rawValue
         rawValue["slot"] = triggeringSlot?.rawValue
-        
+
         switch self {
         switch self {
         case .lowReservoir(_, lowReservoirReminderValue: let value):
         case .lowReservoir(_, lowReservoirReminderValue: let value):
             rawValue["value"] = value
             rawValue["value"] = value
@@ -225,18 +226,7 @@ extension PumpManagerAlert: RawRepresentable {
         default:
         default:
             break
             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 {
     public var eventTitle: String {
         switch doseType {
         switch doseType {
         case .bolus:
         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:
         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:
         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:
         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()]
             payloads: [cmd.encoded(), Data()]
         )
         )
 
 
-        log.debug("Sending command: %@", wrapped.hexadecimalString)
-
         let msg = MessagePacket(
         let msg = MessagePacket(
             type: MessageType.ENCRYPTED,
             type: MessageType.ENCRYPTED,
             source: self.myId,
             source: self.myId,
@@ -272,14 +270,11 @@ class PodMessageTransport: MessageTransport {
         incrementNonceSeq()
         incrementNonceSeq()
         let decrypted = try enDecrypt.decrypt(readMessage, nonceSeq)
         let decrypted = try enDecrypt.decrypt(readMessage, nonceSeq)
 
 
-        log.debug("Received response: %@", decrypted.payload.hexadecimalString)
-
         let response = try parseResponse(decrypted: decrypted)
         let response = try parseResponse(decrypted: decrypted)
 
 
         incrementMsgSeq()
         incrementMsgSeq()
         incrementNonceSeq()
         incrementNonceSeq()
         let ack = try getAck(response: decrypted)
         let ack = try getAck(response: decrypted)
-        log.debug("Sending ACK: %@ in packet $ack", ack.payload.hexadecimalString)
         let ackResult = manager.sendMessagePacket(ack)
         let ackResult = manager.sendMessagePacket(ack)
         guard case .sentWithAcknowledgment = ackResult else {
         guard case .sentWithAcknowledgment = ackResult else {
             throw PodProtocolError.messageIOException("Could not write $msgType: \(ackResult)")
             throw PodProtocolError.messageIOException("Could not write $msgType: \(ackResult)")
@@ -296,14 +291,14 @@ class PodMessageTransport: MessageTransport {
     private func parseResponse(decrypted: MessagePacket) throws -> Message {
     private func parseResponse(decrypted: MessagePacket) throws -> Message {
 
 
         let data = try StringLengthPrefixEncoding.parseKeys([RESPONSE_PREFIX], decrypted.payload)[0]
         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
         // 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,
         // 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.
         // 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)
         let response = try Message(encodedData: data, checkCRC: false)
 
 
-        log.default("Recv(Hex): %@", data.hexadecimalString)
+        log.default("Recv(Hex): %{public}@", data.hexadecimalString)
         messageLogger?.didReceive(data)
         messageLogger?.didReceive(data)
 
 
         return response
         return response

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

@@ -135,14 +135,6 @@ public class OmniBLEPumpManager: DeviceManager {
         return setStateWithResult(changes)
         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
     // Status can change even when state does not, because some status changes
     // purely based on time. This provides a mechanism to evaluate status changes
     // purely based on time. This provides a mechanism to evaluate status changes
     // as time progresses and trigger status updates to clients.
     // as time progresses and trigger status updates to clients.
@@ -285,13 +277,15 @@ public class OmniBLEPumpManager: DeviceManager {
     public var debugDescription: String {
     public var debugDescription: String {
         let lines = [
         let lines = [
             "## OmniBLEPumpManager",
             "## OmniBLEPumpManager",
-            "podComms: \(String(reflecting: podComms))",
             "provideHeartbeat: \(provideHeartbeat)",
             "provideHeartbeat: \(provideHeartbeat)",
             "connected: \(isConnected)",
             "connected: \(isConnected)",
-            "state: \(String(reflecting: state))",
+            "",
+            "podComms: \(String(reflecting: podComms))",
+            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
             "status: \(String(describing: status))",
             "status: \(String(describing: status))",
+            "",
             "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)",
             "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)",
-            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
+            "state: \(String(reflecting: state))",
         ]
         ]
         return lines.joined(separator: "\n")
         return lines.joined(separator: "\n")
     }
     }
@@ -419,10 +413,22 @@ extension OmniBLEPumpManager {
         return false
         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
     // 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.
     // whether there is an unfinializedDose for a manual temp basal &/or a manual bolus.
     private func beepMessageBlock(beepType: BeepType) -> MessageBlock? {
     private func beepMessageBlock(beepType: BeepType) -> MessageBlock? {
-        guard self.beepPreference.shouldBeepForManualCommand else {
+        guard self.beepPreference.shouldBeepForManualCommand && !self.silencePod else {
             return nil
             return nil
         }
         }
 
 
@@ -505,6 +511,13 @@ extension OmniBLEPumpManager {
         }
         }
     }
     }
 
 
+    // Thread-safe
+    public var silencePod: Bool {
+        get {
+            return state.silencePod
+        }
+    }
+
     // From last status response
     // From last status response
     public var reservoirLevel: ReservoirLevel? {
     public var reservoirLevel: ReservoirLevel? {
         return state.reservoirLevel
         return state.reservoirLevel
@@ -526,7 +539,7 @@ extension OmniBLEPumpManager {
 
 
     public var defaultExpirationReminderOffset: TimeInterval {
     public var defaultExpirationReminderOffset: TimeInterval {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.defaultExpirationReminderOffset = newValue
                 state.defaultExpirationReminderOffset = newValue
             }
             }
         }
         }
@@ -537,7 +550,7 @@ extension OmniBLEPumpManager {
 
 
     public var lowReservoirReminderValue: Double {
     public var lowReservoirReminderValue: Double {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.lowReservoirReminderValue = newValue
                 state.lowReservoirReminderValue = newValue
             }
             }
         }
         }
@@ -548,7 +561,7 @@ extension OmniBLEPumpManager {
 
 
     public var podAttachmentConfirmed: Bool {
     public var podAttachmentConfirmed: Bool {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.podAttachmentConfirmed = newValue
                 state.podAttachmentConfirmed = newValue
             }
             }
         }
         }
@@ -559,7 +572,7 @@ extension OmniBLEPumpManager {
 
 
     public var initialConfigurationCompleted: Bool {
     public var initialConfigurationCompleted: Bool {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.initialConfigurationCompleted = newValue
                 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] = [
                     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))
                     completion(.success(finishWait))
                 } catch let error {
                 } catch let error {
                     completion(.failure(.communication(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
     // 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 {
         guard self.hasActivePod else {
             completion(nil)
             completion(nil)
             return
             return
         }
         }
 
 
-        self.podComms.runSession(withName: "Acknowledge Alarms") { (result) in
+        self.podComms.runSession(withName: "Acknowledge Alerts") { (result) in
             let session: PodCommsSession
             let session: PodCommsSession
             switch result {
             switch result {
             case .success(let s):
             case .success(let s):
@@ -1049,7 +1089,7 @@ extension OmniBLEPumpManager {
             switch result {
             switch result {
             case .success(let session):
             case .success(let session):
                 do {
                 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)
                     let _ = try session.setTime(timeZone: timeZone, basalSchedule: self.state.basalSchedule, date: Date(), acknowledgementBeep: beep)
                     self.clearSuspendReminder()
                     self.clearSuspendReminder()
                     self.setState { (state) in
                     self.setState { (state) in
@@ -1107,7 +1147,7 @@ extension OmniBLEPumpManager {
                     case .success:
                     case .success:
                         break
                         break
                     }
                     }
-                    let beep = self.beepPreference.shouldBeepForManualCommand
+                    let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                     let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                     let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                     self.clearSuspendReminder()
                     self.clearSuspendReminder()
 
 
@@ -1169,12 +1209,12 @@ extension OmniBLEPumpManager {
         self.podComms.runSession(withName: "Play Test Beeps") { (result) in
         self.podComms.runSession(withName: "Play Test Beeps") { (result) in
             switch result {
             switch result {
             case .success(let session):
             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(
                 let result = session.beepConfig(
                     beepType: .bipBeepBipBeepBipBeepBipBeep,
                     beepType: .bipBeepBipBeepBipBeepBipBeep,
-                    tempBasalCompletionBeep: beep && self.hasUnfinalizedManualTempBasal,
-                    bolusCompletionBeep: beep && self.hasUnfinalizedManualBolus
+                    tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal,
+                    bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus
                 )
                 )
 
 
                 switch result {
                 switch result {
@@ -1227,16 +1267,20 @@ extension OmniBLEPumpManager {
     }
     }
 
 
     public func setConfirmationBeeps(newPreference: BeepPreference, completion: @escaping (OmniBLEPumpManagerError?) -> Void) {
     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
             self.setState { state in
-                state.confirmationBeeps = newPreference // set here to allow changes on a faulted Pod
+                state.confirmationBeeps = newPreference
             }
             }
             completion(nil)
             completion(nil)
             return
             return
         }
         }
 
 
-        self.podComms.runSession(withName: "Set Confirmation Beeps Preference") { (result) in
+        self.podComms.runSession(withName: name) { (result) in
             switch result {
             switch result {
             case .success(let session):
             case .success(let session):
                 // enable/disable Pod completion beep state for any unfinalized manual insulin delivery
                 // 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
 // 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!
             // Use a beepBlock for the confirmation beep to avoid getting 3 beeps using cancel command beeps!
             let beepBlock = self.beepMessageBlock(beepType: .beeeeeep)
             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 {
             switch result {
             case .certainFailure(let error):
             case .certainFailure(let error):
                 self.log.error("Failed to suspend: %{public}@", String(describing: error))
                 self.log.error("Failed to suspend: %{public}@", String(describing: error))
@@ -1471,7 +1584,7 @@ extension OmniBLEPumpManager: PumpManager {
 
 
             do {
             do {
                 let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
                 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)
                 let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                 self.clearSuspendReminder()
                 self.clearSuspendReminder()
                 session.dosesForStorage() { (doses) -> Bool in
                 session.dosesForStorage() { (doses) -> Bool in
@@ -1536,8 +1649,14 @@ extension OmniBLEPumpManager: PumpManager {
         // Round to nearest supported volume
         // Round to nearest supported volume
         let enactUnits = roundToSupportedBolusVolume(units: units)
         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
         self.podComms.runSession(withName: "Bolus") { (result) in
             let session: PodCommsSession
             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
                 // 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 {
                 switch result {
                 case .certainFailure(let error):
                 case .certainFailure(let error):
                     throw error
                     throw error
@@ -1660,8 +1779,14 @@ extension OmniBLEPumpManager: PumpManager {
         // Round to nearest supported rate
         // Round to nearest supported rate
         let rate = roundToSupportedBasalRate(unitsPerHour: unitsPerHour)
         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.podComms.runSession(withName: "Enact Temp Basal") { (result) in
             self.log.info("Enact temp basal %.03fU/hr for %ds", rate, Int(duration))
             self.log.info("Enact temp basal %.03fU/hr for %ds", rate, Int(duration))
@@ -1689,28 +1814,37 @@ extension OmniBLEPumpManager: PumpManager {
                     throw PodCommsError.unfinalizedBolus
                     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 {
                 defer {
@@ -1778,7 +1912,7 @@ extension OmniBLEPumpManager: PumpManager {
     }
     }
 
 
     public func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (Result<DeliveryLimits, Error>) -> Void) {
     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) {
             if let rate = deliveryLimits.maximumBasalRate?.doubleValue(for: .internationalUnitsPerHour) {
                 state.maximumTempBasalRate = rate
                 state.maximumTempBasalRate = rate
                 completion(.success(deliveryLimits))
                 completion(.success(deliveryLimits))
@@ -1823,16 +1957,25 @@ extension OmniBLEPumpManager: PumpManager {
                 return
                 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 {
             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 {
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([expirationReminder], beepBlock: beepBlock)
                 try session.configureAlerts([expirationReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
+                self.setState({ (state) in
                     state.scheduledExpirationReminderOffset = intervalBeforeExpiration
                     state.scheduledExpirationReminderOffset = intervalBeforeExpiration
                 })
                 })
                 completion(nil)
                 completion(nil)
@@ -1856,7 +1999,8 @@ extension OmniBLEPumpManager: PumpManager {
             expiration.addingTimeInterval(.hours(Double(i)))
             expiration.addingTimeInterval(.hours(Double(i)))
         }
         }
         let now = dateGenerator()
         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? {
     public var scheduledExpirationReminder: Date? {
@@ -1869,9 +2013,26 @@ extension OmniBLEPumpManager: PumpManager {
         return expiration.addingTimeInterval(-.hours(round(offset.hours)))
         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) {
     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 {
         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
             return
         }
         }
 
 
@@ -1886,13 +2047,11 @@ extension OmniBLEPumpManager: PumpManager {
                 return
                 return
             }
             }
 
 
-            let lowReservoirReminder = PodAlert.lowReservoir(Double(value))
+            let lowReservoirReminder = PodAlert.lowReservoir(units: supportedValue, silent: self.silencePod)
             do {
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock)
                 try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
-                    state.lowReservoirReminderValue = Double(value)
-                })
+                self.lowReservoirReminderValue = supportedValue
                 completion(nil)
                 completion(nil)
             } catch {
             } catch {
                 completion(.communication(error))
                 completion(.communication(error))
@@ -1917,7 +2076,7 @@ extension OmniBLEPumpManager: PumpManager {
             }
             }
         }
         }
 
 
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.insert(alert)
             state.activeAlerts.insert(alert)
         }
         }
     }
     }
@@ -1933,7 +2092,7 @@ extension OmniBLEPumpManager: PumpManager {
                 delegate?.retractAlert(identifier: repeatingIdentifier)
                 delegate?.retractAlert(identifier: repeatingIdentifier)
             }
             }
         }
         }
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.remove(alert)
             state.activeAlerts.remove(alert)
         }
         }
     }
     }
@@ -1954,6 +2113,8 @@ extension OmniBLEPumpManager: PumpManager {
                 }
                 }
             } else {
             } else {
                 log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot))
                 log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot))
+                let pumpManagerAlert = PumpManagerAlert.unexpectedAlert(triggeringSlot: slot)
+                issueAlert(alert: pumpManagerAlert)
             }
             }
         }
         }
         for alert in removed {
         for alert in removed {
@@ -1962,34 +2123,24 @@ extension OmniBLEPumpManager: PumpManager {
     }
     }
 
 
     private func getPumpManagerAlert(for podAlert: PodAlert, slot: AlertSlot) -> PumpManagerAlert? {
     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 {
         switch podAlert {
-        case .podSuspendedReminder:
-            return PumpManagerAlert.suspendInProgress(triggeringSlot: slot)
+        case .shutdownImminent:
+            return PumpManagerAlert.podExpireImminent(triggeringSlot: slot)
         case .expirationReminder:
         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())
             let timeToExpiry = TimeInterval(hours: expiresAt.timeIntervalSince(dateGenerator()).hours.rounded())
             return PumpManagerAlert.userPodExpiration(triggeringSlot: slot, scheduledExpirationReminderOffset: timeToExpiry)
             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)
             return PumpManagerAlert.lowReservoir(triggeringSlot: slot, lowReservoirReminderValue: units)
-        case .finishSetupReminder, .waitingForPairingReminder:
-            return PumpManagerAlert.finishSetupReminder(triggeringSlot: slot)
         case .suspendTimeExpired:
         case .suspendTimeExpired:
             return PumpManagerAlert.suspendEnded(triggeringSlot: slot)
             return PumpManagerAlert.suspendEnded(triggeringSlot: slot)
+        case .expired:
+            return PumpManagerAlert.podExpiring(triggeringSlot: slot)
         default:
         default:
+            // No PumpManagerAlerts are used for any other pod alerts (including suspendInProgress).
             return nil
             return nil
         }
         }
     }
     }
@@ -2006,7 +2157,7 @@ extension OmniBLEPumpManager: PumpManager {
                         } catch {
                         } catch {
                             return
                             return
                         }
                         }
-                        self.mutateState { state in
+                        self.setState { state in
                             state.activeAlerts.remove(alert)
                             state.activeAlerts.remove(alert)
                             state.alertsWithPendingAcknowledgment.remove(alert)
                             state.alertsWithPendingAcknowledgment.remove(alert)
                         }
                         }
@@ -2149,7 +2300,7 @@ extension OmniBLEPumpManager: PodCommsDelegate {
             }
             }
         } else {
         } else {
             // Resetting podState
             // Resetting podState
-            mutateState { state in
+            setState { state in
                 state.updatePodStateFromPodComms(podState)
                 state.updatePodStateFromPodComms(podState)
             }
             }
         }
         }
@@ -2178,6 +2329,13 @@ extension OmniBLEPumpManager {
             if alert.alertIdentifier == alertIdentifier {
             if alert.alertIdentifier == alertIdentifier {
                 // If this alert was triggered by the pod find the slot to clear it.
                 // If this alert was triggered by the pod find the slot to clear it.
                 if let slot = alert.triggeringSlot {
                 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
                     self.podComms.runSession(withName: "Acknowledge Alert") { (result) in
                         switch result {
                         switch result {
                         case .success(let session):
                         case .success(let session):
@@ -2185,27 +2343,26 @@ extension OmniBLEPumpManager {
                                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                                 let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock)
                                 let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock)
                             } catch {
                             } catch {
-                                self.mutateState { state in
+                                self.setState { state in
                                     state.alertsWithPendingAcknowledgment.insert(alert)
                                     state.alertsWithPendingAcknowledgment.insert(alert)
                                 }
                                 }
                                 completion(error)
                                 completion(error)
                                 return
                                 return
                             }
                             }
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.activeAlerts.remove(alert)
                                 state.activeAlerts.remove(alert)
                             }
                             }
                             completion(nil)
                             completion(nil)
                         case .failure(let error):
                         case .failure(let error):
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.alertsWithPendingAcknowledgment.insert(alert)
                                 state.alertsWithPendingAcknowledgment.insert(alert)
                             }
                             }
                             completion(error)
                             completion(error)
-                            return
                         }
                         }
                     }
                     }
                 } else {
                 } else {
                     // Non-pod alert
                     // Non-pod alert
-                    self.mutateState { state in
+                    self.setState { state in
                         state.activeAlerts.remove(alert)
                         state.activeAlerts.remove(alert)
                         if alert == .timeOffsetChangeDetected {
                         if alert == .timeOffsetChangeDetected {
                             state.acknowledgedTimeOffsetAlert = true
                             state.acknowledgedTimeOffsetAlert = true
@@ -2228,7 +2385,7 @@ extension FaultEventCode {
         case .exceededMaximumPodLife80Hrs:
         case .exceededMaximumPodLife80Hrs:
             return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification")
             return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification")
         default:
         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 unstoredDoses: [UnfinalizedDose]
 
 
+    public var silencePod: Bool
+
     public var confirmationBeeps: BeepPreference
     public var confirmationBeeps: BeepPreference
     
     
     public var controllerId: UInt32 = 0
     public var controllerId: UInt32 = 0
@@ -96,6 +98,7 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
         self.timeZone = timeZone
         self.timeZone = timeZone
         self.basalSchedule = basalSchedule
         self.basalSchedule = basalSchedule
         self.unstoredDoses = []
         self.unstoredDoses = []
+        self.silencePod = false
         self.confirmationBeeps = .manualCommands
         self.confirmationBeeps = .manualCommands
         if controllerId != nil && podId != nil {
         if controllerId != nil && podId != nil {
             self.controllerId = controllerId!
             self.controllerId = controllerId!
@@ -192,6 +195,8 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
             self.unstoredDoses = []
             self.unstoredDoses = []
         }
         }
 
 
+        self.silencePod = rawValue["silencePod"] as? Bool ?? false
+
         if let rawBeeps = rawValue["confirmationBeeps"] as? BeepPreference.RawValue, let confirmationBeeps = BeepPreference(rawValue: rawBeeps) {
         if let rawBeeps = rawValue["confirmationBeeps"] as? BeepPreference.RawValue, let confirmationBeeps = BeepPreference(rawValue: rawBeeps) {
             self.confirmationBeeps = confirmationBeeps
             self.confirmationBeeps = confirmationBeeps
         } else {
         } else {
@@ -246,6 +251,7 @@ public struct OmniBLEPumpManagerState: RawRepresentable, Equatable {
             "timeZone": timeZone.secondsFromGMT(),
             "timeZone": timeZone.secondsFromGMT(),
             "basalSchedule": basalSchedule.rawValue,
             "basalSchedule": basalSchedule.rawValue,
             "unstoredDoses": unstoredDoses.map { $0.rawValue },
             "unstoredDoses": unstoredDoses.map { $0.rawValue },
+            "silencePod": silencePod,
             "confirmationBeeps": confirmationBeeps.rawValue,
             "confirmationBeeps": confirmationBeeps.rawValue,
             "activeAlerts": activeAlerts.map { $0.rawValue },
             "activeAlerts": activeAlerts.map { $0.rawValue },
             "podAttachmentConfirmed": podAttachmentConfirmed,
             "podAttachmentConfirmed": podAttachmentConfirmed,
@@ -299,20 +305,24 @@ extension OmniBLEPumpManagerState: CustomDebugStringConvertible {
             "* tempBasalEngageState: \(String(describing: tempBasalEngageState))",
             "* tempBasalEngageState: \(String(describing: tempBasalEngageState))",
             "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))",
             "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))",
             "* isPumpDataStale: \(String(describing: isPumpDataStale))",
             "* isPumpDataStale: \(String(describing: isPumpDataStale))",
+            "* silencePod: \(String(describing: silencePod))",
             "* confirmationBeeps: \(String(describing: confirmationBeeps))",
             "* confirmationBeeps: \(String(describing: confirmationBeeps))",
             "* controllerId: \(String(format: "%08X", controllerId))",
             "* controllerId: \(String(format: "%08X", controllerId))",
             "* podId: \(String(format: "%08X", podId))",
             "* podId: \(String(format: "%08X", podId))",
             "* insulinType: \(String(describing: insulinType))",
             "* insulinType: \(String(describing: insulinType))",
-            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset))",
-            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset)",
+            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))",
+            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)",
             "* lowReservoirReminderValue: \(lowReservoirReminderValue)",
             "* lowReservoirReminderValue: \(lowReservoirReminderValue)",
             "* podAttachmentConfirmed: \(podAttachmentConfirmed)",
             "* podAttachmentConfirmed: \(podAttachmentConfirmed)",
             "* activeAlerts: \(activeAlerts)",
             "* activeAlerts: \(activeAlerts)",
             "* alertsWithPendingAcknowledgment: \(alertsWithPendingAcknowledgment)",
             "* alertsWithPendingAcknowledgment: \(alertsWithPendingAcknowledgment)",
             "* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)",
             "* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)",
             "* initialConfigurationCompleted: \(initialConfigurationCompleted)",
             "* 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")
         ].joined(separator: "\n")
     }
     }
 }
 }

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

@@ -1,6 +1,6 @@
 //
 //
 //  PodCommsSession.swift
 //  PodCommsSession.swift
-//  OmnipodKit
+//  OmniBLE
 //
 //
 //  From OmniKit/PumpManager/PodCommsSession.swift
 //  From OmniKit/PumpManager/PodCommsSession.swift
 //  Created by Pete Schwamb on 10/13/17.
 //  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)
             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)
             let response = try transport.sendMessage(message)
             
             
             // Simulate fault
             // Simulate fault
@@ -282,6 +283,7 @@ public class PodCommsSession {
 
 
             if let responseMessageBlock = response.messageBlocks[0] as? T {
             if let responseMessageBlock = response.messageBlocks[0] as? T {
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
+                self.podState.lastCommsOK = true // message successfully sent and expected response received
                 return responseMessageBlock
                 return responseMessageBlock
             }
             }
 
 
@@ -379,10 +381,16 @@ public class PodCommsSession {
     }
     }
 
 
     @discardableResult
     @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 configurations = alerts.map { $0.configuration }
         let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations)
         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 {
         for alert in alerts {
             podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
             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 cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
         let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
         let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
 
-        guard let activatedAt = podState.activatedAt else {
+        guard podState.activatedAt != nil else {
             throw PodCommsError.noPodPaired
             throw PodCommsError.noPodPaired
         }
         }
 
 
@@ -438,12 +446,12 @@ public class PodCommsSession {
             }
             }
             podState.updateFromStatusResponse(status, at: currentDate)
             podState.updateFromStatusResponse(status, at: currentDate)
         } else {
         } 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)
             try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm] + optionalAlerts)
         }
         }
         
         
@@ -494,7 +502,9 @@ public class PodCommsSession {
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, units: units, timeBetweenPulses: timeBetweenPulses, extendedUnits: extendedUnits, extendedDuration: extendedDuration)
         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
             var ongoingBolus = true
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
                 podState.updateFromStatusResponse(statusResponse, at: currentDate)
                 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 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 1-5 minutes will only use suspendTimeExpired alert beeps.
     // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts.
     // 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 {
         guard podState.unacknowledgedCommand == nil else {
             return .certainFailure(error: .unacknowledgedCommandPending)
             return .certainFailure(error: .unacknowledgedCommandPending)
@@ -607,6 +618,9 @@ public class PodCommsSession {
             var podSuspendedReminderAlert: PodAlert? = nil
             var podSuspendedReminderAlert: PodAlert? = nil
             var suspendTimeExpiredAlert: PodAlert? = nil
             var suspendTimeExpiredAlert: PodAlert? = nil
             let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0
             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)
             let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)
             var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
             var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
@@ -614,14 +628,14 @@ public class PodCommsSession {
             // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
             // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
             if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
             if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
                 // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders
                 // 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]
                 alertConfigurations += [podSuspendedReminderAlert!.configuration]
             }
             }
 
 
             // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
             // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
             if suspendTime > 0 {
             if suspendTime > 0 {
                 // a timed suspend using a suspend time expired alert
                 // 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]
                 alertConfigurations += [suspendTimeExpiredAlert!.configuration]
             }
             }
 
 
@@ -661,8 +675,8 @@ public class PodCommsSession {
     private func cancelSuspendAlerts() throws -> StatusResponse {
     private func cancelSuspendAlerts() throws -> StatusResponse {
 
 
         do {
         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])
             let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired])
             return status
             return status
@@ -727,6 +741,11 @@ public class PodCommsSession {
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
 
 
         do {
         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])
             var status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
             let now = currentDate
             let now = currentDate
             podState.suspendState = .resumed(now)
             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 cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts)
         let status: StatusResponse = try send([cmd], beepBlock: beepBlock)
         let status: StatusResponse = try send([cmd], beepBlock: beepBlock)
         podState.updateFromStatusResponse(status, at: currentDate)
         podState.updateFromStatusResponse(status, at: currentDate)
-        return podState.activeAlerts
+        return podState.activeAlertSlots
     }
     }
 
 
     func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) {
     func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) {

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

@@ -1,6 +1,6 @@
 //
 //
 //  PodState.swift
 //  PodState.swift
-//  OmnipodKit
+//  OmniBLE
 //
 //
 //  Based on OmniKit/PumpManager/PodState.swift
 //  Based on OmniKit/PumpManager/PodState.swift
 //  Created by Pete Schwamb on 10/13/17.
 //  Created by Pete Schwamb on 10/13/17.
@@ -58,9 +58,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public var bleIdentifier: String
     public var bleIdentifier: String
     
     
     public var activatedAt: Date?
     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 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 var setupUnitsDelivered: Double?
 
 
     public let firmwareVersion: String
     public let firmwareVersion: String
@@ -68,7 +71,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public let lotNo: UInt32
     public let lotNo: UInt32
     public let lotSeq: UInt32
     public let lotSeq: UInt32
     public let productId: UInt8
     public let productId: UInt8
-    var activeAlertSlots: AlertSet
+    public var activeAlertSlots: AlertSet
     public var lastInsulinMeasurements: PodInsulinMeasurements?
     public var lastInsulinMeasurements: PodInsulinMeasurements?
 
 
     public var unacknowledgedCommand: PendingCommand?
     public var unacknowledgedCommand: PendingCommand?
@@ -100,16 +103,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public var configuredAlerts: [AlertSlot: PodAlert]
     public var configuredAlerts: [AlertSlot: PodAlert]
     public var insulinType: InsulinType
     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.
     // Allow a grace period while the unacknowledged command is first being sent.
     public var needsCommsRecovery: Bool {
     public var needsCommsRecovery: Bool {
         if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight {
         if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight {
@@ -117,7 +110,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         }
         }
         return false
         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) {
     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.address = address
         self.ltk = ltk
         self.ltk = ltk
@@ -134,9 +131,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         self.messageTransportState = messageTransportState ?? MessageTransportState(ck: nil, noncePrefix: nil)
         self.messageTransportState = messageTransportState ?? MessageTransportState(ck: nil, noncePrefix: nil)
         self.primeFinishTime = nil
         self.primeFinishTime = nil
         self.setupProgress = .addressAssigned
         self.setupProgress = .addressAssigned
-        self.configuredAlerts = [.slot7: .waitingForPairingReminder]
+        self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder]
         self.bleIdentifier = bleIdentifier
         self.bleIdentifier = bleIdentifier
         self.insulinType = insulinType
         self.insulinType = insulinType
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
+        self.podTime = 0
     }
     }
     
     
     public var unfinishedSetup: Bool {
     public var unfinishedSetup: Bool {
@@ -171,18 +171,30 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public mutating func advanceToNextNonce() {
     public mutating func advanceToNextNonce() {
         // Dash nonce is a fixed value and is never advanced
         // Dash nonce is a fixed value and is never advanced
     }
     }
-    
+
     public var currentNonce: UInt32 {
     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) {
     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 {
     private mutating func updatePodTimes(timeActive: TimeInterval) -> Date {
         let now = 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
         let activatedAtComputed = now - timeActive
         if activatedAt == nil {
         if activatedAt == nil {
             self.activatedAt = activatedAtComputed
             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,
             // 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.
             // 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.
             // 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
             self.expiresAt = expiresAtComputed
         }
         }
         return now
         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) {
     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)
         // 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 deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that Loop doesn't know about?
             if podProgressStatus.readyForDelivery {
             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.
                 // 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)
                 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 {
         if var bolus = unfinalizedBolus, !deliveryStatus.bolusing {
             // Due to clock drift or comms delays, boluses can finish earlier than we expect
             // 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 {
         if let setupUnitsDelivered = rawValue["setupUnitsDelivered"] as? Double {
             self.setupUnitsDelivered = setupUnitsDelivered
             self.setupUnitsDelivered = setupUnitsDelivered
         }
         }
@@ -461,12 +494,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         } else {
         } else {
             // Assume migration, and set up with alerts that are normally configured
             // Assume migration, and set up with alerts that are normally configured
             self.configuredAlerts = [
             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 {
         } else {
             self.insulinType = .novolog
             self.insulinType = .novolog
         }
         }
+
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
     }
     }
     
     
     public var rawValue: RawValue {
     public var rawValue: RawValue {
@@ -508,6 +544,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         rawValue["primeFinishTime"] = primeFinishTime
         rawValue["primeFinishTime"] = primeFinishTime
         rawValue["activatedAt"] = activatedAt
         rawValue["activatedAt"] = activatedAt
         rawValue["expiresAt"] = expiresAt
         rawValue["expiresAt"] = expiresAt
+        rawValue["podTime"] = podTime
+        rawValue["podTimeUpdated"] = podTimeUpdated
         rawValue["setupUnitsDelivered"] = setupUnitsDelivered
         rawValue["setupUnitsDelivered"] = setupUnitsDelivered
         rawValue["activeTime"] = activeTime
         rawValue["activeTime"] = activeTime
 
 
@@ -529,6 +567,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* bleIdentifier: \(bleIdentifier)",
             "* bleIdentifier: \(bleIdentifier)",
             "* activatedAt: \(String(reflecting: activatedAt))",
             "* activatedAt: \(String(reflecting: activatedAt))",
             "* expiresAt: \(String(reflecting: expiresAt))",
             "* expiresAt: \(String(reflecting: expiresAt))",
+            "* podTime: \(podTime.timeIntervalStr)",
+            "* podTimeUpdated: \(String(reflecting: podTimeUpdated))",
             "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))",
             "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))",
             "* firmwareVersion: \(firmwareVersion)",
             "* firmwareVersion: \(firmwareVersion)",
             "* bleFirmwareVersion: \(bleFirmwareVersion)",
             "* bleFirmwareVersion: \(bleFirmwareVersion)",
@@ -541,16 +581,14 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))",
             "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))",
             "* unfinalizedResume: \(String(describing: unfinalizedResume))",
             "* unfinalizedResume: \(String(describing: unfinalizedResume))",
             "* finalizedDoses: \(String(describing: finalizedDoses))",
             "* finalizedDoses: \(String(describing: finalizedDoses))",
-            "* activeAlerts: \(String(describing: activeAlerts))",
+            "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))",
             "* messageTransportState: \(String(describing: messageTransportState))",
             "* messageTransportState: \(String(describing: messageTransportState))",
             "* setupProgress: \(setupProgress)",
             "* setupProgress: \(setupProgress)",
             "* primeFinishTime: \(String(describing: primeFinishTime))",
             "* primeFinishTime: \(String(describing: primeFinishTime))",
-            "* configuredAlerts: \(String(describing: configuredAlerts))",
+            "* configuredAlerts: \(configuredAlertsString(configuredAlerts: configuredAlerts))",
             "* insulinType: \(String(describing: insulinType))",
             "* 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")
         ].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")
             hostedView.navigationItem.title = LocalizedString("Insulin Type", comment: "Title for insulin type selection screen")
             return hostedView
             return hostedView
         case .deactivate:
         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
             viewModel.didFinish = { [weak self] in
                 self?.stepFinished()
                 self?.stepFinished()

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

@@ -10,8 +10,8 @@ import Foundation
 import LoopKitUI
 import LoopKitUI
 
 
 public protocol PodDeactivater {
 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 {}
 extension OmniBLEPumpManager: PodDeactivater {}
@@ -19,16 +19,6 @@ extension OmniBLEPumpManager: PodDeactivater {}
 
 
 class DeactivatePodViewModel: ObservableObject, Identifiable {
 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 {
     enum DeactivatePodViewModelState {
         case active
         case active
         case deactivating
         case deactivating
@@ -124,9 +114,38 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
     
     
     var podDeactivator: PodDeactivater
     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.podDeactivator = podDeactivator
         self.podAttachedToBody = podAttachedToBody
         self.podAttachedToBody = podAttachedToBody
+        self.instructionText = text
     }
     }
     
     
     public func continueButtonTapped() {
     public func continueButtonTapped() {
@@ -137,7 +156,7 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
             podDeactivator.deactivatePod { (error) in
             podDeactivator.deactivatePod { (error) in
                 DispatchQueue.main.async {
                 DispatchQueue.main.async {
                     if let error = error {
                     if let error = error {
-                        self.state = .resultError(DeactivationError.OmnipodPumpManagerError(error))
+                        self.state = .resultError(DeactivationError.OmniBLEPumpManagerError(error))
                     } else {
                     } else {
                         self.discardPod(navigateOnCompletion: false)
                         self.discardPod(navigateOnCompletion: false)
                     }
                     }
@@ -160,18 +179,18 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
 }
 }
 
 
 enum DeactivationError : LocalizedError {
 enum DeactivationError : LocalizedError {
-    case OmnipodPumpManagerError(OmniBLEPumpManagerError)
+    case OmniBLEPumpManagerError(OmniBLEPumpManagerError)
     
     
     var recoverySuggestion: String? {
     var recoverySuggestion: String? {
         switch self {
         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.")
             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? {
     var errorDescription: String? {
         switch self {
         switch self {
-        case .OmnipodPumpManagerError(let error):
+        case .OmniBLEPumpManagerError(let error):
             return error.errorDescription
             return error.errorDescription
         }
         }
     }
     }

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

@@ -1,5 +1,5 @@
 //
 //
-//  DashSettingsViewModel.swift
+//  OmniBLESettingsViewModel.swift
 //  OmniBLE
 //  OmniBLE
 //
 //
 //  Created by Pete Schwamb on 3/8/20.
 //  Created by Pete Schwamb on 3/8/20.
@@ -40,6 +40,8 @@ class OmniBLESettingsViewModel: ObservableObject {
 
 
     @Published var beepPreference: BeepPreference
     @Published var beepPreference: BeepPreference
 
 
+    @Published var silencePodPreference: SilencePodPreference
+
     @Published var podConnected: Bool
     @Published var podConnected: Bool
 
 
     var activatedAtString: String {
     var activatedAtString: String {
@@ -136,7 +138,7 @@ class OmniBLESettingsViewModel: ObservableObject {
 
 
     var recoveryText: String? {
     var recoveryText: String? {
         if case .fault = podCommState {
         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 {
         } 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")
             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 {
         } else if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, serviceTimeRemaining <= Pod.serviceDuration - Pod.nominalPodLife {
@@ -233,6 +235,7 @@ class OmniBLESettingsViewModel: ObservableObject {
         lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
         lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
         podCommState = self.pumpManager.podCommState
         podCommState = self.pumpManager.podCommState
         beepPreference = self.pumpManager.beepPreference
         beepPreference = self.pumpManager.beepPreference
+        silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled
         podConnected = self.pumpManager.isConnected
         podConnected = self.pumpManager.isConnected
         insulinType = self.pumpManager.insulinType
         insulinType = self.pumpManager.insulinType
         podDetails = self.pumpManager.podDetails
         podDetails = self.pumpManager.podDetails
@@ -262,7 +265,7 @@ class OmniBLESettingsViewModel: ObservableObject {
     }
     }
     
     
     func stopUsingOmnipodDashTapped() {
     func stopUsingOmnipodDashTapped() {
-        self.pumpManager.notifyDelegateOfDeactivation {
+        pumpManager.notifyDelegateOfDeactivation {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
                 self.didFinish?()
                 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) {
     func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
         pumpManager.playTestBeeps(completion: completion)
         pumpManager.playTestBeeps(completion: completion)
     }
     }
 
 
+    func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) {
+        completion(pumpManager.debugDescription)
+    }
+
     func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
     func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
         pumpManager.setConfirmationBeeps(newPreference: preference) { error in
         pumpManager.setConfirmationBeeps(newPreference: preference) { error in
             DispatchQueue.main.async {
             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?) {
     func didChangeInsulinType(_ newType: InsulinType?) {
         self.pumpManager.insulinType = newType
         self.pumpManager.insulinType = newType
     }
     }
@@ -351,6 +385,10 @@ class OmniBLESettingsViewModel: ObservableObject {
         }
         }
     }
     }
 
 
+    var noPod: Bool {
+        return podCommState == .noPod
+    }
+
     var podError: String? {
     var podError: String? {
         switch podCommState {
         switch podCommState {
         case .fault(let status):
         case .fault(let status):
@@ -362,11 +400,11 @@ class OmniBLESettingsViewModel: ObservableObject {
             case .occluded, .occlusionCheckStartup1, .occlusionCheckStartup2, .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold, .occlusionCheckValueTooHigh:
             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")
                 return LocalizedString("Pod Occlusion", comment: "Error message for reservoir view when pod occlusion checks failed")
             default:
             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:
         case .active:
             if isPodDataStale {
             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 {
             } else {
                 return nil
                 return nil
             }
             }

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

@@ -85,7 +85,7 @@ enum PodLifeState {
         case .podDeactivating:
         case .podDeactivating:
             return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation")
             return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation")
         default:
         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 {
                 HStack {
                     InstructionList(instructions: [
                     InstructionList(instructions: [
                         LocalizedString("Prepare site.", comment: "Label text for step one of attach pod 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")
                         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 {
         VStack {
             List {
             List {
                 Section {
                 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)
                         .padding(.vertical, 10)
                 }
                 }
 
 
@@ -109,15 +109,15 @@ struct BeepPreferenceSelectionView: View {
 
 
     private var cancelButton: some View {
     private var cancelButton: some View {
         Button(action: { self.presentationMode.wrappedValue.dismiss() } ) {
         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 {
     var saveButtonText: String {
         if saving {
         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 {
         } 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 {
     static var previews: some View {
         NavigationView {
         NavigationView {
             BeepPreferenceSelectionView(initialValue: .extended) { selectedValue, completion in
             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 {
 struct ExpirationReminderPickerView_Previews: PreviewProvider {
     static var previews: some View {
     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)
                     .frame(maxHeight: 162.0)
                     .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert })
                     .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert })
                     Section {
                     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)
                             .font(.footnote)
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
                             .fixedSize(horizontal: false, vertical: true)
                             .fixedSize(horizontal: false, vertical: true)
@@ -147,7 +147,7 @@ struct ManualTempBasalEntryView: View {
     var missingConfigAlert: SwiftUI.Alert {
     var missingConfigAlert: SwiftUI.Alert {
         return SwiftUI.Alert(
         return SwiftUI.Alert(
             title: Text(LocalizedString("Missing Config", comment: "Alert title for missing temp basal configuration")),
             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")
         .accessibility(identifier: "button_cancel")
     }
     }
-
 }
 }
-
-

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

@@ -34,15 +34,15 @@ struct NotificationSettingsView: View {
     var body: some View {
     var body: some View {
         RoundedCardScrollView {
         RoundedCardScrollView {
             RoundedCard(
             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)
                 ExpirationReminderPickerView(expirationReminderDefault: $expirationReminderDefault)
             }
             }
 
 
             if let allowedDates = allowedScheduledReminderDates {
             if let allowedDates = allowedScheduledReminderDates {
                 RoundedCard(
                 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"))
                     Text(LocalizedString("Scheduled Reminder", comment: "Title of scheduled reminder card on NotificationSettingsView"))
                     Divider()
                     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
                 lowReservoirValueRow
             }
             }
 
 
             RoundedCard<EmptyView>(
             RoundedCard<EmptyView>(
                 title: LocalizedString("Critical Alerts", comment: "Title for critical alerts description"),
                 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"))
         .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 {
     private func scheduledReminderRow(scheduledDate: Date?, allowedDates: [Date]) -> some View {
         Group {
         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)
                 scheduledReminderRowContents(disclosure: false)
             } else {
             } else {
                 NavigationLink(
                 NavigationLink(
@@ -124,14 +125,16 @@ struct NotificationSettingsView: View {
 struct NotificationSettingsView_Previews: PreviewProvider {
 struct NotificationSettingsView_Previews: PreviewProvider {
     static var previews: some View {
     static var previews: some View {
         return Group {
         return Group {
+            let now = Date()
             NavigationView {
             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)"))
                     .previewDevice(PreviewDevice(rawValue:"iPod touch (7th generation)"))
                     .previewDisplayName("iPod touch (7th generation)")
                     .previewDisplayName("iPod touch (7th generation)")
             }
             }
 
 
             NavigationView {
             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)
                     .colorScheme(.dark)
                     .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
                     .previewDevice(PreviewDevice(rawValue: "iPhone XS Max"))
                     .previewDisplayName("iPhone XS Max - Dark")
                     .previewDisplayName("iPhone XS Max - Dark")

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

@@ -12,11 +12,11 @@ import LoopKitUI
 import HealthKit
 import HealthKit
 
 
 struct OmniBLESettingsView: View  {
 struct OmniBLESettingsView: View  {
-    
+
     @ObservedObject var viewModel: OmniBLESettingsViewModel
     @ObservedObject var viewModel: OmniBLESettingsViewModel
-    
+
     @State private var showingDeleteConfirmation = false
     @State private var showingDeleteConfirmation = false
-    
+
     @State private var showSuspendOptions = false
     @State private var showSuspendOptions = false
 
 
     @State private var showManualTempBasalOptions = false
     @State private var showManualTempBasalOptions = false
@@ -28,7 +28,7 @@ struct OmniBLESettingsView: View  {
     @State private var cancelingTempBasal = false
     @State private var cancelingTempBasal = false
 
 
     var supportedInsulinTypes: [InsulinType]
     var supportedInsulinTypes: [InsulinType]
-    
+
     @Environment(\.guidanceColors) var guidanceColors
     @Environment(\.guidanceColors) var guidanceColors
     @Environment(\.insulinTintColor) var insulinTintColor
     @Environment(\.insulinTintColor) var insulinTintColor
     
     
@@ -243,7 +243,7 @@ struct OmniBLESettingsView: View  {
     }
     }
     
     
     private var doneButton: some View {
     private var doneButton: some View {
-        Button("Done", action: {
+        Button(LocalizedString("Done", comment: "Title of done button on OmniBLESettingsView"), action: {
             self.viewModel.doneTapped()
             self.viewModel.doneTapped()
         })
         })
     }
     }
@@ -279,7 +279,7 @@ struct OmniBLESettingsView: View  {
                     headerImage
                     headerImage
 
 
                     lifecycleProgress
                     lifecycleProgress
-                    
+
                     HStack(alignment: .top) {
                     HStack(alignment: .top) {
                         deliveryStatus
                         deliveryStatus
                         Spacer()
                         Spacer()
@@ -302,7 +302,7 @@ struct OmniBLESettingsView: View  {
                     }.padding(.vertical, 8)
                     }.padding(.vertical, 8)
                 }
                 }
             }
             }
-            
+
             Section(header: SectionHeader(label: LocalizedString("Activity", comment: "Section header for activity section"))) {
             Section(header: SectionHeader(label: LocalizedString("Activity", comment: "Section header for activity section"))) {
                 suspendResumeRow()
                 suspendResumeRow()
                     .disabled(!self.viewModel.podOk)
                     .disabled(!self.viewModel.podOk)
@@ -343,8 +343,8 @@ struct OmniBLESettingsView: View  {
                     manualTempBasalRow
                     manualTempBasalRow
                 }
                 }
             }
             }
-            .disabled(cancelingTempBasal)
-            
+            .disabled(cancelingTempBasal || !self.viewModel.podOk)
+
             Section() {
             Section() {
                 HStack {
                 HStack {
                     FrameworkLocalText("Pod Activated", comment: "Label for pod insertion row")
                     FrameworkLocalText("Pod Activated", comment: "Label for pod insertion row")
@@ -352,7 +352,7 @@ struct OmniBLESettingsView: View  {
                     Text(self.viewModel.activatedAtString)
                     Text(self.viewModel.activatedAtString)
                         .foregroundColor(Color.secondary)
                         .foregroundColor(Color.secondary)
                 }
                 }
-                
+
                 HStack {
                 HStack {
                     if let expiresAt = viewModel.expiresAt, expiresAt < Date() {
                     if let expiresAt = viewModel.expiresAt, expiresAt < Date() {
                         FrameworkLocalText("Pod Expired", comment: "Label for pod expiration row, past tense")
                         FrameworkLocalText("Pod Expired", comment: "Label for pod expiration row, past tense")
@@ -363,21 +363,34 @@ struct OmniBLESettingsView: View  {
                     Text(self.viewModel.expiresAtString)
                     Text(self.viewModel.expiresAtString)
                         .foregroundColor(Color.secondary)
                         .foregroundColor(Color.secondary)
                 }
                 }
-                
+
                 if let podDetails = self.viewModel.podDetails {
                 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 {
                 } else {
                     HStack {
                     HStack {
-                        FrameworkLocalText("Device Details", comment: "Text for device details disclosure row")
+                        FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row")
                         Spacer()
                         Spacer()
                         Text("—")
                         Text("—")
                             .foregroundColor(Color.secondary)
                             .foregroundColor(Color.secondary)
                     }
                     }
                 }
                 }
             }
             }
-            
+
             Section() {
             Section() {
                 Button(action: {
                 Button(action: {
                     self.viewModel.navigateTo?(self.viewModel.lifeState.nextPodLifecycleAction)
                     self.viewModel.navigateTo?(self.viewModel.lifeState.nextPodLifecycleAction)
@@ -386,7 +399,7 @@ struct OmniBLESettingsView: View  {
                         .foregroundColor(self.viewModel.lifeState.nextPodLifecycleActionColor)
                         .foregroundColor(self.viewModel.lifeState.nextPodLifecycleActionColor)
                 }
                 }
             }
             }
-            
+
             Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "Section header for configuration section")))
             Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "Section header for configuration section")))
             {
             {
                 NavigationLink(destination:
                 NavigationLink(destination:
@@ -409,9 +422,17 @@ struct OmniBLESettingsView: View  {
                             .foregroundColor(.secondary)
                             .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)) {
                 NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) {
                     HStack {
                     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 {
                         if let currentTitle = viewModel.insulinType?.brandName {
                             Spacer()
                             Spacer()
                             Text(currentTitle)
                             Text(currentTitle)
@@ -420,7 +441,7 @@ struct OmniBLESettingsView: View  {
                     }
                     }
                 }
                 }
             }
             }
-            
+
             Section() {
             Section() {
                 HStack {
                 HStack {
                     FrameworkLocalText("Pump Time", comment: "The title of the command to change pump time zone")
                     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 {
             if self.viewModel.lifeState.allowsPumpManagerRemoval {
                 Section() {
                 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("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("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("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("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)
             row(LocalizedString("Total Delivery", comment: "description label for total delivery pod details row"), value: totalDeliveryText)
             if let activeTime = podDetails.activeTime, let activatedAt = podDetails.activatedAt {
             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))
                 row(LocalizedString("Active Time", comment: "description label for active time pod details row"), value: activeTimeText(activeTime))
             } else {
             } else {
                 row(LocalizedString("Last Status", comment: "description label for last status date pod details row"), value: lastStatusText)
                 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"))
                             Text(LocalizedString("Pod Fault Details", comment: "description label for pod fault details"))
                                 .fontWeight(.semibold)
                                 .fontWeight(.semibold)
                         }.padding(.vertical, 4)
                         }.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)
                             .fixedSize(horizontal: false, vertical: true)
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
                     }
                     }
@@ -127,6 +124,6 @@ struct PodDetailsView: View {
 
 
 struct PodDetailsView_Previews: PreviewProvider {
 struct PodDetailsView_Previews: PreviewProvider {
     static var previews: some View {
     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 {
     var body: some View {
         GuidePage(content: {
         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 {
             VStack {
                 Button(action: {
                 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 */; };
 		CEC751DF29D8834B006E9D24 /* RileyLinkKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1229C1929C7E5BC0066A89C /* RileyLinkKitUI.framework */; };
 		CEC751E329D88392006E9D24 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEC751E229D88392006E9D24 /* LoopKitUI.framework */; };
 		CEC751E329D88392006E9D24 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEC751E229D88392006E9D24 /* LoopKitUI.framework */; };
 		CEF2639B29D88516009921F1 /* OmniKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C124016C29C7D87A00B32844 /* OmniKit.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 */
 /* End PBXBuildFile section */
 
 
 /* Begin PBXContainerItemProxy section */
 /* Begin PBXContainerItemProxy section */
@@ -402,6 +410,14 @@
 		C12EDA1729C7E01800435701 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = "<group>"; };
 		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>"; };
 		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; };
 		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 */
 /* End PBXFileReference section */
 
 
 /* Begin PBXFrameworksBuildPhase section */
 /* Begin PBXFrameworksBuildPhase section */
@@ -520,25 +536,26 @@
 		C124017A29C7D8E900B32844 /* OmnipodCommon */ = {
 		C124017A29C7D8E900B32844 /* OmnipodCommon */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				C12401A129C7D8E900B32844 /* AlertSlot.swift */,
+				C124019D29C7D8E900B32844 /* BasalDeliveryTable.swift */,
+				C124019C29C7D8E900B32844 /* BasalSchedule+LoopKit.swift */,
+				C12401A529C7D8E900B32844 /* BasalSchedule.swift */,
+				C124019A29C7D8E900B32844 /* BeepPreference.swift */,
 				C124017B29C7D8E900B32844 /* BeepType.swift */,
 				C124017B29C7D8E900B32844 /* BeepType.swift */,
-				C124017C29C7D8E900B32844 /* PumpManagerAlert.swift */,
-				C124017D29C7D8E900B32844 /* MessageBlocks */,
-				C124019729C7D8E900B32844 /* PodDoseProgressEstimator.swift */,
+				C12401A629C7D8E900B32844 /* BolusDeliveryTable.swift */,
+				C12401A429C7D8E900B32844 /* CRC16.swift */,
 				C124019829C7D8E900B32844 /* FaultEventCode.swift */,
 				C124019829C7D8E900B32844 /* FaultEventCode.swift */,
+				C12401A229C7D8E900B32844 /* InsulinTableEntry.swift */,
 				C124019929C7D8E900B32844 /* Message.swift */,
 				C124019929C7D8E900B32844 /* Message.swift */,
-				C124019A29C7D8E900B32844 /* BeepPreference.swift */,
-				C124019B29C7D8E900B32844 /* Pod.swift */,
-				C124019C29C7D8E900B32844 /* BasalSchedule+LoopKit.swift */,
-				C124019D29C7D8E900B32844 /* BasalDeliveryTable.swift */,
+				C124017D29C7D8E900B32844 /* MessageBlocks */,
 				C124019E29C7D8E900B32844 /* PendingCommand.swift */,
 				C124019E29C7D8E900B32844 /* PendingCommand.swift */,
+				C124019B29C7D8E900B32844 /* Pod.swift */,
+				C124019729C7D8E900B32844 /* PodDoseProgressEstimator.swift */,
 				C124019F29C7D8E900B32844 /* PodInsulinMeasurements.swift */,
 				C124019F29C7D8E900B32844 /* PodInsulinMeasurements.swift */,
-				C12401A029C7D8E900B32844 /* ReservoirLevel.swift */,
-				C12401A129C7D8E900B32844 /* AlertSlot.swift */,
-				C12401A229C7D8E900B32844 /* InsulinTableEntry.swift */,
 				C12401A329C7D8E900B32844 /* PodProgressStatus.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 */,
 				C12401A729C7D8E900B32844 /* UnfinalizedDose.swift */,
 			);
 			);
 			path = OmnipodCommon;
 			path = OmnipodCommon;
@@ -669,13 +686,13 @@
 		C124022529C7DA9700B32844 /* ViewModels */ = {
 		C124022529C7DA9700B32844 /* ViewModels */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				C124022629C7DA9700B32844 /* PairPodViewModel.swift */,
+				C124022929C7DA9700B32844 /* DeactivatePodViewModel.swift */,
 				C124022729C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift */,
 				C124022729C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift */,
 				C124022829C7DA9700B32844 /* InsertCannulaViewModel.swift */,
 				C124022829C7DA9700B32844 /* InsertCannulaViewModel.swift */,
-				C124022929C7DA9700B32844 /* DeactivatePodViewModel.swift */,
-				C124022A29C7DA9700B32844 /* RileyLinkListDataSource.swift */,
-				C124022B29C7DA9700B32844 /* PodLifeState.swift */,
 				C124022C29C7DA9700B32844 /* OmnipodSettingsViewModel.swift */,
 				C124022C29C7DA9700B32844 /* OmnipodSettingsViewModel.swift */,
+				C124022629C7DA9700B32844 /* PairPodViewModel.swift */,
+				C124022B29C7DA9700B32844 /* PodLifeState.swift */,
+				C124022A29C7DA9700B32844 /* RileyLinkListDataSource.swift */,
 			);
 			);
 			path = ViewModels;
 			path = ViewModels;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -683,32 +700,39 @@
 		C124022D29C7DA9700B32844 /* Views */ = {
 		C124022D29C7DA9700B32844 /* Views */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
-				C124022E29C7DA9700B32844 /* PodLifeHUDView.swift */,
+				D845A1452AF8A4DA00EA0853 /* ActivityView.swift */,
+				C124024629C7DA9700B32844 /* AttachPodView.swift */,
+				C124024C29C7DA9700B32844 /* BasalStateView.swift */,
+				C124024429C7DA9700B32844 /* BeepPreferenceSelectionView.swift */,
 				C124022F29C7DA9700B32844 /* CheckInsertedCannulaView.swift */,
 				C124022F29C7DA9700B32844 /* CheckInsertedCannulaView.swift */,
+				C124024B29C7DA9700B32844 /* DeactivatePodView.swift */,
+				C124024A29C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift */,
 				C124023129C7DA9700B32844 /* DesignElements */,
 				C124023129C7DA9700B32844 /* DesignElements */,
-				C124023529C7DA9700B32844 /* LowReservoirReminderEditView.swift */,
-				C124023629C7DA9700B32844 /* InsertCannulaView.swift */,
-				C124023729C7DA9700B32844 /* RileyLinkSetupView.swift */,
-				C124023829C7DA9700B32844 /* SetupCompleteView.swift */,
 				C124023A29C7DA9700B32844 /* ExpirationReminderPickerView.swift */,
 				C124023A29C7DA9700B32844 /* ExpirationReminderPickerView.swift */,
-				C124023B29C7DA9700B32844 /* UncertaintyRecoveredView.swift */,
+				C124024529C7DA9700B32844 /* ExpirationReminderSetupView.swift */,
+				D845A1472AF8A4E400EA0853 /* FirstAppear.swift */,
+				C124023629C7DA9700B32844 /* InsertCannulaView.swift */,
 				C124023C29C7DA9700B32844 /* InsulinTypeConfirmation.swift */,
 				C124023C29C7DA9700B32844 /* InsulinTypeConfirmation.swift */,
+				C124023529C7DA9700B32844 /* LowReservoirReminderEditView.swift */,
+				C124024229C7DA9700B32844 /* LowReservoirReminderSetupView.swift */,
 				C124023D29C7DA9700B32844 /* ManualTempBasalEntryView.swift */,
 				C124023D29C7DA9700B32844 /* ManualTempBasalEntryView.swift */,
 				C124023F29C7DA9700B32844 /* NotificationSettingsView.swift */,
 				C124023F29C7DA9700B32844 /* NotificationSettingsView.swift */,
+				C124024929C7DA9700B32844 /* OmnipodReservoirView.swift */,
 				C124024029C7DA9700B32844 /* OmnipodSettingsView.swift */,
 				C124024029C7DA9700B32844 /* OmnipodSettingsView.swift */,
-				C124024129C7DA9700B32844 /* PodSetupView.swift */,
-				C124024229C7DA9700B32844 /* LowReservoirReminderSetupView.swift */,
 				C124024329C7DA9700B32844 /* PairPodView.swift */,
 				C124024329C7DA9700B32844 /* PairPodView.swift */,
-				C124024429C7DA9700B32844 /* BeepPreferenceSelectionView.swift */,
-				C124024529C7DA9700B32844 /* ExpirationReminderSetupView.swift */,
-				C124024629C7DA9700B32844 /* AttachPodView.swift */,
-				C124024729C7DA9700B32844 /* ScheduledExpirationReminderEditView.swift */,
+				D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */,
 				C124024829C7DA9700B32844 /* PodDetailsView.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 */,
 				C124024D29C7DA9700B32844 /* TimeView.swift */,
+				C124023B29C7DA9700B32844 /* UncertaintyRecoveredView.swift */,
 			);
 			);
 			path = Views;
 			path = Views;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1056,6 +1080,7 @@
 				C12401E329C7D8E900B32844 /* PodComms.swift in Sources */,
 				C12401E329C7D8E900B32844 /* PodComms.swift in Sources */,
 				C12401D429C7D8E900B32844 /* BeepPreference.swift in Sources */,
 				C12401D429C7D8E900B32844 /* BeepPreference.swift in Sources */,
 				C12401B829C7D8E900B32844 /* PodInfoPulseLog.swift in Sources */,
 				C12401B829C7D8E900B32844 /* PodInfoPulseLog.swift in Sources */,
+				D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */,
 				C12401BE29C7D8E900B32844 /* PodInfoConfiguredAlerts.swift in Sources */,
 				C12401BE29C7D8E900B32844 /* PodInfoConfiguredAlerts.swift in Sources */,
 				C12401E529C7D8E900B32844 /* PodCommsSession.swift in Sources */,
 				C12401E529C7D8E900B32844 /* PodCommsSession.swift in Sources */,
 				C12401DE29C7D8E900B32844 /* CRC16.swift in Sources */,
 				C12401DE29C7D8E900B32844 /* CRC16.swift in Sources */,
@@ -1102,13 +1127,16 @@
 				C124028B29C7DA9700B32844 /* BeepPreferenceSelectionView.swift in Sources */,
 				C124028B29C7DA9700B32844 /* BeepPreferenceSelectionView.swift in Sources */,
 				C12EDA1429C7DFBF00435701 /* TimeInterval.swift in Sources */,
 				C12EDA1429C7DFBF00435701 /* TimeInterval.swift in Sources */,
 				C124028D29C7DA9700B32844 /* AttachPodView.swift in Sources */,
 				C124028D29C7DA9700B32844 /* AttachPodView.swift in Sources */,
+				D845A14F2AF8A4FB00EA0853 /* ReadPulseLogView.swift in Sources */,
 				C124027229C7DA9700B32844 /* DeactivatePodViewModel.swift in Sources */,
 				C124027229C7DA9700B32844 /* DeactivatePodViewModel.swift in Sources */,
 				C124028C29C7DA9700B32844 /* ExpirationReminderSetupView.swift in Sources */,
 				C124028C29C7DA9700B32844 /* ExpirationReminderSetupView.swift in Sources */,
+				D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */,
 				C124029029C7DA9700B32844 /* OmnipodReservoirView.swift in Sources */,
 				C124029029C7DA9700B32844 /* OmnipodReservoirView.swift in Sources */,
 				C124027C29C7DA9700B32844 /* LowReservoirReminderEditView.swift in Sources */,
 				C124027C29C7DA9700B32844 /* LowReservoirReminderEditView.swift in Sources */,
 				C124028129C7DA9700B32844 /* ExpirationReminderPickerView.swift in Sources */,
 				C124028129C7DA9700B32844 /* ExpirationReminderPickerView.swift in Sources */,
 				C124028329C7DA9700B32844 /* InsulinTypeConfirmation.swift in Sources */,
 				C124028329C7DA9700B32844 /* InsulinTypeConfirmation.swift in Sources */,
 				C124029129C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift in Sources */,
 				C124029129C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift in Sources */,
+				D845A1502AF8A4FB00EA0853 /* PumpManagerDetailsView.swift in Sources */,
 				C124028229C7DA9700B32844 /* UncertaintyRecoveredView.swift in Sources */,
 				C124028229C7DA9700B32844 /* UncertaintyRecoveredView.swift in Sources */,
 				C124027E29C7DA9700B32844 /* RileyLinkSetupView.swift in Sources */,
 				C124027E29C7DA9700B32844 /* RileyLinkSetupView.swift in Sources */,
 				C124027429C7DA9700B32844 /* PodLifeState.swift in Sources */,
 				C124027429C7DA9700B32844 /* PodLifeState.swift in Sources */,
@@ -1116,11 +1144,14 @@
 				C124027329C7DA9700B32844 /* RileyLinkListDataSource.swift in Sources */,
 				C124027329C7DA9700B32844 /* RileyLinkListDataSource.swift in Sources */,
 				C124029329C7DA9700B32844 /* BasalStateView.swift in Sources */,
 				C124029329C7DA9700B32844 /* BasalStateView.swift in Sources */,
 				C124027A29C7DA9700B32844 /* LeadingImage.swift in Sources */,
 				C124027A29C7DA9700B32844 /* LeadingImage.swift in Sources */,
+				D845A14E2AF8A4FB00EA0853 /* ReadPodStatusView.swift in Sources */,
 				C124028729C7DA9700B32844 /* OmnipodSettingsView.swift in Sources */,
 				C124028729C7DA9700B32844 /* OmnipodSettingsView.swift in Sources */,
 				C124028929C7DA9700B32844 /* LowReservoirReminderSetupView.swift in Sources */,
 				C124028929C7DA9700B32844 /* LowReservoirReminderSetupView.swift in Sources */,
 				C124027029C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */,
 				C124027029C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */,
 				C12EDA0E29C7DEFD00435701 /* NumberFormatter.swift in Sources */,
 				C12EDA0E29C7DEFD00435701 /* NumberFormatter.swift in Sources */,
+				D845A1522AF8A51000EA0853 /* SilencePodSelectionView.swift in Sources */,
 				C12EDA1229C7DF4B00435701 /* IdentifiableClass.swift in Sources */,
 				C12EDA1229C7DF4B00435701 /* IdentifiableClass.swift in Sources */,
+				D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */,
 				C124027529C7DA9700B32844 /* OmnipodSettingsViewModel.swift in Sources */,
 				C124027529C7DA9700B32844 /* OmnipodSettingsViewModel.swift in Sources */,
 				C124027629C7DA9700B32844 /* PodLifeHUDView.swift in Sources */,
 				C124027629C7DA9700B32844 /* PodLifeHUDView.swift in Sources */,
 				C124029729C7DA9700B32844 /* OmnipodUICoordinator.swift in Sources */,
 				C124029729C7DA9700B32844 /* OmnipodUICoordinator.swift in Sources */,
@@ -1131,6 +1162,7 @@
 				C124028A29C7DA9700B32844 /* PairPodView.swift in Sources */,
 				C124028A29C7DA9700B32844 /* PairPodView.swift in Sources */,
 				C124029229C7DA9700B32844 /* DeactivatePodView.swift in Sources */,
 				C124029229C7DA9700B32844 /* DeactivatePodView.swift in Sources */,
 				C124027929C7DA9700B32844 /* RoundedCard.swift in Sources */,
 				C124027929C7DA9700B32844 /* RoundedCard.swift in Sources */,
+				D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */,
 				C124026F29C7DA9700B32844 /* PairPodViewModel.swift in Sources */,
 				C124026F29C7DA9700B32844 /* PairPodViewModel.swift in Sources */,
 				C124026E29C7DA9700B32844 /* FrameworkLocalText.swift in Sources */,
 				C124026E29C7DA9700B32844 /* FrameworkLocalText.swift in Sources */,
 				C124028F29C7DA9700B32844 /* PodDetailsView.swift in Sources */,
 				C124028F29C7DA9700B32844 /* PodDetailsView.swift in Sources */,

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

@@ -8,11 +8,29 @@
 
 
 import Foundation
 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 {
 public enum AlertTrigger {
     case unitsRemaining(Double)
     case unitsRemaining(Double)
     case timeUntilAlert(TimeInterval)
     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 {
 public enum BeepRepeat: UInt8 {
     case once = 0
     case once = 0
     case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1
     case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1
@@ -29,29 +47,48 @@ public enum BeepRepeat: UInt8 {
 public struct AlertConfiguration {
 public struct AlertConfiguration {
 
 
     let slot: AlertSlot
     let slot: AlertSlot
-    let trigger: AlertTrigger
     let active: Bool
     let active: Bool
     let duration: TimeInterval
     let duration: TimeInterval
+    let trigger: AlertTrigger
     let beepRepeat: BeepRepeat
     let beepRepeat: BeepRepeat
     let beepType: BeepType
     let beepType: BeepType
+    let silent: Bool
     let autoOffModifier: Bool
     let autoOffModifier: Bool
 
 
     static let length = 6
     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.slot = alertType
         self.active = active
         self.active = active
-        self.autoOffModifier = autoOffModifier
         self.duration = duration
         self.duration = duration
         self.trigger = trigger
         self.trigger = trigger
         self.beepRepeat = beepRepeat
         self.beepRepeat = beepRepeat
         self.beepType = beepType
         self.beepType = beepType
+        self.silent = silent
+        self.autoOffModifier = autoOffModifier
     }
     }
 }
 }
 
 
 extension AlertConfiguration: CustomDebugStringConvertible {
 extension AlertConfiguration: CustomDebugStringConvertible {
     public var debugDescription: String {
     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 enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
     public typealias RawValue = [String: Any]
     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 {
     public var description: String {
         var alertName: String
         var alertName: String
         switch self {
         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:
         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:
         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:
         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 {
         if self.configuration.active == false {
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
             alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier")
@@ -117,71 +173,126 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
 
     public var configuration: AlertConfiguration {
     public var configuration: AlertConfiguration {
         switch self {
         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
             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 reminderInterval, duration: TimeInterval
-            let trigger: AlertTrigger
-            let beepRepeat: BeepRepeat
+            var beepRepeat: BeepRepeat
             let beepType: BeepType
             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 {
                 if suspendTime == 0 {
                     duration = 0 // Untimed suspend, no duration
                     duration = 0 // Untimed suspend, no duration
-                } else if suspendTime > reminderInterval {
-                    duration = suspendTime - reminderInterval // End after suspendTime total time
                 } else {
                 } 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
                 beepType = .beep
             } else {
             } else {
+                beepRepeat = .once
                 duration = 0
                 duration = 0
                 trigger = .timeUntilAlert(.minutes(0))
                 trigger = .timeUntilAlert(.minutes(0))
-                beepRepeat = .once
                 beepType = .noBeepCancel
                 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 active = suspendTime != 0 // disable if suspendTime is 0
             let trigger: AlertTrigger
             let trigger: AlertTrigger
             let beepRepeat: BeepRepeat
             let beepRepeat: BeepRepeat
             let beepType: BeepType
             let beepType: BeepType
             if active {
             if active {
                 trigger = .timeUntilAlert(suspendTime)
                 trigger = .timeUntilAlert(suspendTime)
-                beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes
+                beepRepeat = suspendTimeExpiredBeepRepeat
                 beepType = .bipBeepBipBeepBipBeepBipBeep
                 beepType = .bipBeepBipBeepBipBeepBipBeep
             } else {
             } else {
                 trigger = .timeUntilAlert(.minutes(0))
                 trigger = .timeUntilAlert(.minutes(0))
                 beepRepeat = .once
                 beepRepeat = .once
                 beepType = .noBeepCancel
                 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 {
         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
                 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":
         case "shutdownImminent":
-            guard let alarmTime = rawValue["alarmTime"] as? Double else {
+            guard let alarmTime = rawValue["alarmTime"] as? TimeInterval else {
                 return nil
                 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
                 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
                 return nil
             }
             }
-            self = .autoOff(active: active, countdownDuration: TimeInterval(countdownDuration))
+            let silent = rawValue["silent"] as? Bool ?? false
+            self = .lowReservoir(units: units, silent: silent)
         case "podSuspendedReminder":
         case "podSuspendedReminder":
             guard let active = rawValue["active"] as? Bool,
             guard let active = rawValue["active"] as? Bool,
-                let suspendTime = rawValue["suspendTime"] as? Double else
+                let suspendTime = rawValue["suspendTime"] as? TimeInterval else
             {
             {
                 return nil
                 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":
         case "suspendTimeExpired":
             guard let suspendTime = rawValue["suspendTime"] as? Double else {
             guard let suspendTime = rawValue["suspendTime"] as? Double else {
                 return nil
                 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:
         default:
             return nil
             return nil
         }
         }
@@ -248,50 +400,65 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 
 
         let name: String = {
         let name: String = {
             switch self {
             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:
             case .shutdownImminent:
                 return "shutdownImminent"
                 return "shutdownImminent"
+            case .expirationReminder:
+                return "expirationReminder"
             case .lowReservoir:
             case .lowReservoir:
                 return "lowReservoir"
                 return "lowReservoir"
-            case .autoOff:
-                return "autoOff"
             case .podSuspendedReminder:
             case .podSuspendedReminder:
                 return "podSuspendedReminder"
                 return "podSuspendedReminder"
             case .suspendTimeExpired:
             case .suspendTimeExpired:
                 return "suspendTimeExpired"
                 return "suspendTimeExpired"
+            case .waitingForPairingReminder:
+                return "waitingForPairingReminder"
+            case .finishSetupReminder:
+                return "finishSetupReminder"
+            case .expired:
+                return "expired"
             }
             }
         }()
         }()
 
 
-
         var rawValue: RawValue = [
         var rawValue: RawValue = [
             "name": name,
             "name": name,
         ]
         ]
 
 
         switch self {
         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["active"] = active
+            rawValue["offset"] = offset
             rawValue["countdownDuration"] = countdownDuration
             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["active"] = active
+            rawValue["offset"] = offset
             rawValue["suspendTime"] = suspendTime
             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["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:
         default:
             break
             break
         }
         }
@@ -301,14 +468,14 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable {
 }
 }
 
 
 public enum AlertSlot: UInt8 {
 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 {
     public var bitMaskValue: UInt8 {
         return 1<<rawValue
         return 1<<rawValue
@@ -373,9 +540,179 @@ public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, E
 
 
 // Returns true if there are any active suspend related alerts
 // Returns true if there are any active suspend related alerts
 public func hasActiveSuspendAlert(configuredAlerts: [AlertSlot : PodAlert]) -> Bool {
 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 true
     }
     }
     return false
     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
             // Eros zero TB is the only case not using pulses
             return 0
             return 0
         } else {
         } 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
             // Eros zero TB case uses fixed 30 minute rate entries
             return TimeInterval(minutes: 30)
             return TimeInterval(minutes: 30)
         } else {
         } 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 {
 extension RateEntry: CustomDebugStringConvertible {
     public var debugDescription: String {
     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:
         case .silent:
             return LocalizedString("No confidence reminders are used.", comment: "Description for BeepPreference.silent")
             return LocalizedString("No confidence reminders are used.", comment: "Description for BeepPreference.silent")
         case .manualCommands:
         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:
         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 invalidBeepRepeatPattern             = 0x09
         case bf0notEqualToBF1                     = 0x0A
         case bf0notEqualToBF1                     = 0x0A
         case tableCorruptionTempBasalSubcommand   = 0x0B
         case tableCorruptionTempBasalSubcommand   = 0x0B
+
         case resetDueToCOP                        = 0x0D
         case resetDueToCOP                        = 0x0D
         case resetDueToIllegalOpcode              = 0x0E
         case resetDueToIllegalOpcode              = 0x0E
         case resetDueToIllegalAddress             = 0x0F
         case resetDueToIllegalAddress             = 0x0F
@@ -75,6 +76,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case testInProgress                       = 0x3C
         case testInProgress                       = 0x3C
         case problemWithPumpAnchor                = 0x3D
         case problemWithPumpAnchor                = 0x3D
         case errorFlashWrite                      = 0x3E
         case errorFlashWrite                      = 0x3E
+
         case encoderCountTooHigh                  = 0x40
         case encoderCountTooHigh                  = 0x40
         case encoderCountExcessiveVariance        = 0x41
         case encoderCountExcessiveVariance        = 0x41
         case encoderCountTooLow                   = 0x42
         case encoderCountTooLow                   = 0x42
@@ -89,6 +91,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case trimICSTooCloseTo0x1FF               = 0x4B
         case trimICSTooCloseTo0x1FF               = 0x4B
         case problemFindingBestTrimValue          = 0x4C
         case problemFindingBestTrimValue          = 0x4C
         case badSetTPM1MultiCasesValue            = 0x4D
         case badSetTPM1MultiCasesValue            = 0x4D
+        case sawTrimError                         = 0x4E
         case unexpectedRFErrorFlagDuringReset     = 0x4F
         case unexpectedRFErrorFlagDuringReset     = 0x4F
         case timerPulseWidthModulatorOverflow     = 0x50
         case timerPulseWidthModulatorOverflow     = 0x50
         case tickcntError                         = 0x51
         case tickcntError                         = 0x51
@@ -109,11 +112,13 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case occlusionCheckStartup1               = 0x60
         case occlusionCheckStartup1               = 0x60
         case occlusionCheckStartup2               = 0x61
         case occlusionCheckStartup2               = 0x61
         case occlusionCheckTimeouts1              = 0x62
         case occlusionCheckTimeouts1              = 0x62
+
         case occlusionCheckTimeouts2              = 0x66
         case occlusionCheckTimeouts2              = 0x66
         case occlusionCheckTimeouts3              = 0x67
         case occlusionCheckTimeouts3              = 0x67
         case occlusionCheckPulseIssue             = 0x68
         case occlusionCheckPulseIssue             = 0x68
         case occlusionCheckBolusProblem           = 0x69
         case occlusionCheckBolusProblem           = 0x69
         case occlusionCheckAboveThreshold         = 0x6A
         case occlusionCheckAboveThreshold         = 0x6A
+
         case basalUnderInfusion                   = 0x80
         case basalUnderInfusion                   = 0x80
         case basalOverInfusion                    = 0x81
         case basalOverInfusion                    = 0x81
         case tempBasalUnderInfusion               = 0x82
         case tempBasalUnderInfusion               = 0x82
@@ -137,9 +142,9 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
         case illegalInterLockChan                 = 0x95
         case illegalInterLockChan                 = 0x95
         case badStateInClearBolusIST2AndVars      = 0x96
         case badStateInClearBolusIST2AndVars      = 0x96
         case badStateInMaybeInc33D                = 0x97
         case badStateInMaybeInc33D                = 0x97
-        case valuesDoNotMatchOrAreGreaterThan0x97 = 0x98
+        case valuesDoNotMatch                     = 0xFF
     }
     }
-    
+
     public var faultType: FaultEventType? {
     public var faultType: FaultEventType? {
         return FaultEventType(rawValue: rawValue)
         return FaultEventType(rawValue: rawValue)
     }
     }
@@ -147,268 +152,263 @@ public struct FaultEventCode: CustomStringConvertible, Equatable {
     public init(rawValue: UInt8) {
     public init(rawValue: UInt8) {
         self.rawValue = rawValue
         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)
         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
 import Foundation
 
 
 
 
-
 public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
 public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
-    
+
     public let blockType: MessageBlockType = .cancelDelivery
     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
     // 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 struct DeliveryType: OptionSet, Equatable {
         public let rawValue: UInt8
         public let rawValue: UInt8
         
         
@@ -47,7 +41,23 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
         public init(rawValue: UInt8) {
         public init(rawValue: UInt8) {
             self.rawValue = rawValue
             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
     public let deliveryType: DeliveryType
@@ -84,6 +94,6 @@ public struct CancelDeliveryCommand : NonceResyncableMessageBlock {
 
 
 extension CancelDeliveryCommand: CustomDebugStringConvertible {
 extension CancelDeliveryCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
     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),
             UInt8(4 + configurations.count * AlertConfiguration.length),
             ])
             ])
         data.appendBigEndian(nonce)
         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)
             data.append(contentsOf: config.data)
         }
         }
         return data
         return data
@@ -92,6 +94,7 @@ extension AlertConfiguration {
         }
         }
         self.beepType = beepType
         self.beepType = beepType
 
 
+        self.silent = (beepType == .noBeepNonCancel)
     }
     }
 
 
     public var data: Data {
     public var data: Data {
@@ -104,12 +107,16 @@ extension AlertConfiguration {
         if autoOffModifier {
         if autoOffModifier {
             firstByte += 1 << 1
             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
         // High bit of duration
-        firstByte += UInt8((Int(duration.minutes) >> 8) & 0x1)
+        firstByte += UInt8((durationMinutes >> 8) & 0x1)
 
 
         var data = Data([
         var data = Data([
             firstByte,
             firstByte,
-            UInt8(Int(duration.minutes) & 0xff)
+            UInt8(durationMinutes & 0xff)
             ])
             ])
 
 
         switch trigger {
         switch trigger {
@@ -122,7 +129,8 @@ extension AlertConfiguration {
             data.appendBigEndian(minutes)
             data.appendBigEndian(minutes)
         }
         }
         data.append(beepRepeat.rawValue)
         data.append(beepRepeat.rawValue)
-        data.append(beepType.rawValue)
+        let beepTypeToSet: BeepType = silent ? .noBeepNonCancel : beepType
+        data.append(beepTypeToSet.rawValue)
 
 
         return data
         return data
     }
     }

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

@@ -9,14 +9,13 @@
 import Foundation
 import Foundation
 
 
 public struct DeactivatePodCommand : NonceResyncableMessageBlock {
 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 let blockType: MessageBlockType = .deactivatePod
     
     
     public var nonce: UInt32
     public var nonce: UInt32
     
     
-    // e1f78752 07 8196
     public var data: Data {
     public var data: Data {
         var data = Data([
         var data = Data([
             blockType.rawValue,
             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.
             // 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.possibleFaultCallingAddress = encodedData[20...21].toBigEndian(UInt16.self) // only potentially valid for Dash
         }
         }
-        
+
         self.data = Data(encodedData)
         self.data = Data(encodedData)
     }
     }
 
 
@@ -118,9 +118,8 @@ extension DetailedStatus: CustomDebugStringConvertible {
             "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U",
             "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U",
             "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)",
             "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U",
             "* 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)",
             "* unacknowledgedAlerts: \(unacknowledgedAlerts)",
             "",
             "",
             ].joined(separator: "\n")
             ].joined(separator: "\n")
@@ -133,8 +132,9 @@ extension DetailedStatus: CustomDebugStringConvertible {
         }
         }
         if faultEventCode.faultType != .noFaults {
         if faultEventCode.faultType != .noFaults {
             result += [
             result += [
+                "* faultEventCode: \(faultEventCode.description)",
                 "* faultAccessingTables: \(faultAccessingTables)",
                 "* faultAccessingTables: \(faultAccessingTables)",
-                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "NA")",
+                "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.timeIntervalStr ?? "NA")",
                 "* errorEventInfo: \(errorEventInfo?.description ?? "NA")",
                 "* errorEventInfo: \(errorEventInfo?.description ?? "NA")",
                 "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
                 "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")",
                 "* possibleFaultCallingAddress: \(possibleFaultCallingAddress != nil ? String(format: "0x%04x", possibleFaultCallingAddress!) : "NA")",
                 "* possibleFaultCallingAddress: \(possibleFaultCallingAddress != nil ? String(format: "0x%04x", possibleFaultCallingAddress!) : "NA")",
@@ -160,26 +160,26 @@ extension DetailedStatus: RawRepresentable {
 }
 }
 
 
 extension TimeInterval {
 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 {
 extension Double {
-    var twoDecimals: String {
+    public var twoDecimals: String {
         return String(format: "%.2f", self)
         return String(format: "%.2f", self)
     }
     }
 }
 }
@@ -191,11 +191,11 @@ extension Double {
 // dddd: Pod Progress at time of first logged fault event
 // dddd: Pod Progress at time of first logged fault event
 //
 //
 public struct ErrorEventInfo: CustomStringConvertible, Equatable {
 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? {
     public var errorEventInfo: ErrorEventInfo? {
         return ErrorEventInfo(rawValue: rawValue)
         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 index = pulseLogEntries.count - 1
     var pulseNumber = lastPulseNumber
     var pulseNumber = lastPulseNumber
     while index >= 0 {
     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
         index -= 1
         pulseNumber -= 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 {
 extension StatusResponse: CustomDebugStringConvertible {
     public var debugDescription: String {
     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 {
 extension TempBasalExtraCommand: CustomDebugStringConvertible {
     public var debugDescription: String {
     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
     // Units per second for priming/cannula insertion
     public static let primeDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerPrimePulse
     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
     // Expiration advisory window: time after expiration alert, and end of service imminent alarm
     public static let expirationAdvisoryWindow = TimeInterval(hours: 7)
     public static let expirationAdvisoryWindow = TimeInterval(hours: 7)
 
 

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

@@ -1,5 +1,5 @@
 //
 //
-//  PodAlert.swift
+//  PumpManagerAlert.swift
 //  OmniKit
 //  OmniKit
 //
 //
 //  Created by Pete Schwamb on 7/9/20.
 //  Created by Pete Schwamb on 7/9/20.
@@ -11,7 +11,6 @@ import LoopKit
 import HealthKit
 import HealthKit
 
 
 public enum PumpManagerAlert: Hashable {
 public enum PumpManagerAlert: Hashable {
-    case multiCommand(triggeringSlot: AlertSlot?)
     case podExpireImminent(triggeringSlot: AlertSlot?)
     case podExpireImminent(triggeringSlot: AlertSlot?)
     case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval)
     case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval)
     case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double)
     case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double)
@@ -19,6 +18,7 @@ public enum PumpManagerAlert: Hashable {
     case suspendEnded(triggeringSlot: AlertSlot?)
     case suspendEnded(triggeringSlot: AlertSlot?)
     case podExpiring(triggeringSlot: AlertSlot?)
     case podExpiring(triggeringSlot: AlertSlot?)
     case finishSetupReminder(triggeringSlot: AlertSlot?)
     case finishSetupReminder(triggeringSlot: AlertSlot?)
+    case unexpectedAlert(triggeringSlot: AlertSlot?)
     case timeOffsetChangeDetected
     case timeOffsetChangeDetected
 
 
     var isRepeating: Bool {
     var isRepeating: Bool {
@@ -36,8 +36,6 @@ public enum PumpManagerAlert: Hashable {
 
 
     var contentTitle: String {
     var contentTitle: String {
         switch self {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content title for multiCommand pod alert")
         case .userPodExpiration:
         case .userPodExpiration:
             return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert")
             return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert")
         case .podExpiring:
         case .podExpiring:
@@ -52,6 +50,8 @@ public enum PumpManagerAlert: Hashable {
             return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert")
             return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert")
         case .finishSetupReminder:
         case .finishSetupReminder:
             return LocalizedString("Pod Pairing Incomplete", comment: "Alert content title for finishSetupReminder pod alert")
             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:
         case .timeOffsetChangeDetected:
             return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert")
             return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert")
         }
         }
@@ -59,8 +59,6 @@ public enum PumpManagerAlert: Hashable {
 
 
     var contentBody: String {
     var contentBody: String {
         switch self {
         switch self {
-        case .multiCommand:
-            return LocalizedString("Multiple Command Alert", comment: "Alert content body for multiCommand pod alert")
         case .userPodExpiration(_, let offset):
         case .userPodExpiration(_, let offset):
             let formatter = DateComponentsFormatter()
             let formatter = DateComponentsFormatter()
             formatter.allowedUnits = [.hour]
             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")
             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:
         case .finishSetupReminder:
             return LocalizedString("Please finish pairing your pod.", comment: "Alert content body for finishSetupReminder pod alert")
             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:
         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")
             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? {
     var triggeringSlot: AlertSlot? {
         switch self {
         switch self {
-        case .multiCommand(let slot):
-            return slot
         case .userPodExpiration(let slot, _):
         case .userPodExpiration(let slot, _):
             return slot
             return slot
         case .podExpiring(let slot):
         case .podExpiring(let slot):
@@ -104,6 +103,8 @@ public enum PumpManagerAlert: Hashable {
             return slot
             return slot
         case .finishSetupReminder(let slot):
         case .finishSetupReminder(let slot):
             return slot
             return slot
+        case .unexpectedAlert(let slot):
+            return slot
         case .timeOffsetChangeDetected:
         case .timeOffsetChangeDetected:
             return nil
             return nil
         }
         }
@@ -139,8 +140,6 @@ public enum PumpManagerAlert: Hashable {
 
 
     var alertIdentifier: String {
     var alertIdentifier: String {
         switch self {
         switch self {
-        case .multiCommand:
-            return "multiCommand"
         case .userPodExpiration:
         case .userPodExpiration:
             return "userPodExpiration"
             return "userPodExpiration"
         case .podExpiring:
         case .podExpiring:
@@ -153,10 +152,12 @@ public enum PumpManagerAlert: Hashable {
             return "suspendInProgress"
             return "suspendInProgress"
         case .suspendEnded:
         case .suspendEnded:
             return "suspendEnded"
             return "suspendEnded"
-        case .timeOffsetChangeDetected:
-            return "timeOffsetChangeDetected"
         case .finishSetupReminder:
         case .finishSetupReminder:
             return "finishSetupReminder"
             return "finishSetupReminder"
+        case .unexpectedAlert:
+            return "unexpectedAlert"
+        case .timeOffsetChangeDetected:
+            return "timeOffsetChangeDetected"
         }
         }
     }
     }
 
 
@@ -183,8 +184,6 @@ extension PumpManagerAlert: RawRepresentable {
         }
         }
 
 
         switch identifier {
         switch identifier {
-        case "multiCommand":
-            self = .multiCommand(triggeringSlot: slot)
         case "userPodExpiration":
         case "userPodExpiration":
             guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else {
             guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else {
                 return nil
                 return nil
@@ -203,6 +202,8 @@ extension PumpManagerAlert: RawRepresentable {
             self = .suspendInProgress(triggeringSlot: slot)
             self = .suspendInProgress(triggeringSlot: slot)
         case "suspendEnded":
         case "suspendEnded":
             self = .suspendEnded(triggeringSlot: slot)
             self = .suspendEnded(triggeringSlot: slot)
+        case "unexpectedAlert":
+            self = .unexpectedAlert(triggeringSlot: slot)
         case "timeOffsetChangeDetected":
         case "timeOffsetChangeDetected":
             self = .timeOffsetChangeDetected
             self = .timeOffsetChangeDetected
         default:
         default:
@@ -229,14 +230,3 @@ extension PumpManagerAlert: RawRepresentable {
         return rawValue
         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)
         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 {
     private func setStateWithResult<ReturnType>(_ changes: (_ state: inout OmnipodPumpManagerState) -> ReturnType) -> ReturnType {
         var oldValue: OmnipodPumpManagerState!
         var oldValue: OmnipodPumpManagerState!
         var returnType: ReturnType!
         var returnType: ReturnType!
@@ -282,12 +274,14 @@ public class OmnipodPumpManager: RileyLinkPumpManager {
     override public var debugDescription: String {
     override public var debugDescription: String {
         let lines = [
         let lines = [
             "## OmnipodPumpManager",
             "## OmnipodPumpManager",
+            "",
+            super.debugDescription,
             "podComms: \(String(reflecting: podComms))",
             "podComms: \(String(reflecting: podComms))",
-            "state: \(String(reflecting: state))",
+            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
             "status: \(String(describing: status))",
             "status: \(String(describing: status))",
+            "",
             "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)",
             "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)",
-            "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
-            super.debugDescription,
+            "state: \(String(reflecting: state))",
         ]
         ]
         return lines.joined(separator: "\n")
         return lines.joined(separator: "\n")
     }
     }
@@ -548,10 +542,21 @@ extension OmnipodPumpManager {
         return false
         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
     // 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.
     // whether there is an unfinializedDose for a manual temp basal &/or a manual bolus.
     private func beepMessageBlock(beepType: BeepType) -> MessageBlock? {
     private func beepMessageBlock(beepType: BeepType) -> MessageBlock? {
-        guard self.beepPreference.shouldBeepForManualCommand else {
+        guard self.beepPreference.shouldBeepForManualCommand && !self.silencePod else {
             return nil
             return nil
         }
         }
 
 
@@ -635,6 +640,13 @@ extension OmnipodPumpManager {
         }
         }
     }
     }
 
 
+    // Thread-safe
+    public var silencePod: Bool {
+        get {
+            return state.silencePod
+        }
+    }
+
     // From last status response
     // From last status response
     public var reservoirLevel: ReservoirLevel? {
     public var reservoirLevel: ReservoirLevel? {
         return state.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] = [
                     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))
                     completion(.success(finishWait))
                 } catch let error {
                 } catch let error {
                     completion(.failure(.communication(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) {
     public func setTime(completion: @escaping (OmnipodPumpManagerError?) -> Void) {
         
         
         guard state.hasActivePod else {
         guard state.hasActivePod else {
@@ -966,7 +1032,7 @@ extension OmnipodPumpManager {
             switch result {
             switch result {
             case .success(let session):
             case .success(let session):
                 do {
                 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)
                     let _ = try session.setTime(timeZone: timeZone, basalSchedule: self.state.basalSchedule, date: Date(), acknowledgementBeep: beep)
                     self.setState { (state) in
                     self.setState { (state) in
                         state.timeZone = timeZone
                         state.timeZone = timeZone
@@ -1023,7 +1089,7 @@ extension OmnipodPumpManager {
                     case .success:
                     case .success:
                         break
                         break
                     }
                     }
-                    let beep = self.beepPreference.shouldBeepForManualCommand
+                    let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand
                     let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
                     let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep)
 
 
                     self.setState { (state) in
                     self.setState { (state) in
@@ -1086,12 +1152,12 @@ extension OmnipodPumpManager {
         self.podComms.runSession(withName: "Play Test Beeps", using: rileyLinkSelector) { (result) in
         self.podComms.runSession(withName: "Play Test Beeps", using: rileyLinkSelector) { (result) in
             switch result {
             switch result {
             case .success(let session):
             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(
                 let result = session.beepConfig(
                     beepType: .bipBeepBipBeepBipBeepBipBeep,
                     beepType: .bipBeepBipBeepBipBeepBipBeep,
-                    tempBasalCompletionBeep: beep && self.hasUnfinalizedManualTempBasal,
-                    bolusCompletionBeep: beep && self.hasUnfinalizedManualBolus
+                    tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal,
+                    bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus
                 )
                 )
 
 
                 switch result {
                 switch result {
@@ -1145,17 +1211,21 @@ extension OmnipodPumpManager {
     }
     }
 
 
     public func setConfirmationBeeps(newPreference: BeepPreference, completion: @escaping (OmnipodPumpManagerError?) -> Void) {
     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
             self.setState { state in
-                state.confirmationBeeps = newPreference // set here to allow changes on a faulted Pod
+                state.confirmationBeeps = newPreference
             }
             }
             completion(nil)
             completion(nil)
             return
             return
         }
         }
 
 
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         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 {
             switch result {
             case .success(let session):
             case .success(let session):
                 // enable/disable Pod completion beep state for any unfinalized manual insulin delivery
                 // 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
 // MARK: - PumpManager
@@ -1273,7 +1413,7 @@ extension OmnipodPumpManager: PumpManager {
 
 
     public var defaultExpirationReminderOffset: TimeInterval {
     public var defaultExpirationReminderOffset: TimeInterval {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.defaultExpirationReminderOffset = newValue
                 state.defaultExpirationReminderOffset = newValue
             }
             }
         }
         }
@@ -1284,7 +1424,7 @@ extension OmnipodPumpManager: PumpManager {
 
 
     public var lowReservoirReminderValue: Double {
     public var lowReservoirReminderValue: Double {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.lowReservoirReminderValue = newValue
                 state.lowReservoirReminderValue = newValue
             }
             }
         }
         }
@@ -1295,7 +1435,7 @@ extension OmnipodPumpManager: PumpManager {
 
 
     public var podAttachmentConfirmed: Bool {
     public var podAttachmentConfirmed: Bool {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.podAttachmentConfirmed = newValue
                 state.podAttachmentConfirmed = newValue
             }
             }
         }
         }
@@ -1306,7 +1446,7 @@ extension OmnipodPumpManager: PumpManager {
 
 
     public var initialConfigurationCompleted: Bool {
     public var initialConfigurationCompleted: Bool {
         set {
         set {
-            mutateState { (state) in
+            setState { (state) in
                 state.initialConfigurationCompleted = newValue
                 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!
             // Use a beepBlock for the confirmation beep to avoid getting 3 beeps using cancel command beeps!
             let beepBlock = self.beepMessageBlock(beepType: .beeeeeep)
             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 {
             switch result {
             case .certainFailure(let error):
             case .certainFailure(let error):
                 self.log.error("Failed to suspend: %{public}@", String(describing: error))
                 self.log.error("Failed to suspend: %{public}@", String(describing: error))
@@ -1437,8 +1577,8 @@ extension OmnipodPumpManager: PumpManager {
 
 
             do {
             do {
                 let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date())
                 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()
                 self.clearSuspendReminder()
                 session.dosesForStorage() { (doses) -> Bool in
                 session.dosesForStorage() { (doses) -> Bool in
                     return self.store(doses: doses, in: session)
                     return self.store(doses: doses, in: session)
@@ -1514,8 +1654,14 @@ extension OmnipodPumpManager: PumpManager {
         // Round to nearest supported volume
         // Round to nearest supported volume
         let enactUnits = roundToSupportedBolusVolume(units: units)
         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
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName: "Bolus", using: rileyLinkSelector) { (result) in
         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
                 // 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)
                 let result = session.cancelDelivery(deliveryType: .bolus, beepType: beepType)
                 switch result {
                 switch result {
                 case .certainFailure(let error):
                 case .certainFailure(let error):
@@ -1637,8 +1783,14 @@ extension OmnipodPumpManager: PumpManager {
         // Round to nearest supported rate
         // Round to nearest supported rate
         let rate = roundToSupportedBasalRate(unitsPerHour: unitsPerHour)
         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
         let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
         self.podComms.runSession(withName: "Enact Temp Basal", using: rileyLinkSelector) { (result) in
         self.podComms.runSession(withName: "Enact Temp Basal", using: rileyLinkSelector) { (result) in
@@ -1668,9 +1820,12 @@ extension OmnipodPumpManager: PumpManager {
                 return
                 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
             // 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
                 let status: StatusResponse
 
 
                 // if resuming scheduled basal delivery & an acknowledgement beep is needed, use the cancel TB beep
                 // 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) {
     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) {
             if let rate = deliveryLimits.maximumBasalRate?.doubleValue(for: .internationalUnitsPerHour) {
                 state.maximumTempBasalRate = rate
                 state.maximumTempBasalRate = rate
                 completion(.success(deliveryLimits))
                 completion(.success(deliveryLimits))
@@ -1814,16 +1969,25 @@ extension OmnipodPumpManager: PumpManager {
                 return
                 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 {
             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 {
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([expirationReminder], beepBlock: beepBlock)
                 try session.configureAlerts([expirationReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
+                self.setState({ (state) in
                     state.scheduledExpirationReminderOffset = intervalBeforeExpiration
                     state.scheduledExpirationReminderOffset = intervalBeforeExpiration
                 })
                 })
                 completion(nil)
                 completion(nil)
@@ -1847,7 +2011,8 @@ extension OmnipodPumpManager: PumpManager {
             expiration.addingTimeInterval(.hours(Double(i)))
             expiration.addingTimeInterval(.hours(Double(i)))
         }
         }
         let now = dateGenerator()
         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? {
     public var scheduledExpirationReminder: Date? {
@@ -1860,9 +2025,26 @@ extension OmnipodPumpManager: PumpManager {
         return expiration.addingTimeInterval(-.hours(round(offset.hours)))
         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) {
     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 {
         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
             return
         }
         }
 
 
@@ -1878,13 +2060,11 @@ extension OmnipodPumpManager: PumpManager {
                 return
                 return
             }
             }
 
 
-            let lowReservoirReminder = PodAlert.lowReservoir(Double(value))
+            let lowReservoirReminder = PodAlert.lowReservoir(units: supportedValue, silent: self.silencePod)
             do {
             do {
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                 try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock)
                 try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock)
-                self.mutateState({ (state) in
-                    state.lowReservoirReminderValue = Double(value)
-                })
+                self.lowReservoirReminderValue = supportedValue
                 completion(nil)
                 completion(nil)
             } catch {
             } catch {
                 completion(.communication(error))
                 completion(.communication(error))
@@ -1909,7 +2089,7 @@ extension OmnipodPumpManager: PumpManager {
             }
             }
         }
         }
 
 
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.insert(alert)
             state.activeAlerts.insert(alert)
         }
         }
     }
     }
@@ -1925,7 +2105,7 @@ extension OmnipodPumpManager: PumpManager {
                 delegate?.retractAlert(identifier: repeatingIdentifier)
                 delegate?.retractAlert(identifier: repeatingIdentifier)
             }
             }
         }
         }
-        self.mutateState { (state) in
+        self.setState { (state) in
             state.activeAlerts.remove(alert)
             state.activeAlerts.remove(alert)
         }
         }
     }
     }
@@ -1946,6 +2126,8 @@ extension OmnipodPumpManager: PumpManager {
                 }
                 }
             } else {
             } else {
                 log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot))
                 log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot))
+                let pumpManagerAlert = PumpManagerAlert.unexpectedAlert(triggeringSlot: slot)
+                issueAlert(alert: pumpManagerAlert)
             }
             }
         }
         }
         for alert in removed {
         for alert in removed {
@@ -1954,34 +2136,24 @@ extension OmnipodPumpManager: PumpManager {
     }
     }
 
 
     private func getPumpManagerAlert(for podAlert: PodAlert, slot: AlertSlot) -> PumpManagerAlert? {
     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 {
         switch podAlert {
-        case .podSuspendedReminder:
-            return PumpManagerAlert.suspendInProgress(triggeringSlot: slot)
+        case .shutdownImminent:
+            return PumpManagerAlert.podExpireImminent(triggeringSlot: slot)
         case .expirationReminder:
         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())
             let timeToExpiry = TimeInterval(hours: expiresAt.timeIntervalSince(dateGenerator()).hours.rounded())
             return PumpManagerAlert.userPodExpiration(triggeringSlot: slot, scheduledExpirationReminderOffset: timeToExpiry)
             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)
             return PumpManagerAlert.lowReservoir(triggeringSlot: slot, lowReservoirReminderValue: units)
-        case .finishSetupReminder, .waitingForPairingReminder:
-            return PumpManagerAlert.finishSetupReminder(triggeringSlot: slot)
         case .suspendTimeExpired:
         case .suspendTimeExpired:
             return PumpManagerAlert.suspendEnded(triggeringSlot: slot)
             return PumpManagerAlert.suspendEnded(triggeringSlot: slot)
+        case .expired:
+            return PumpManagerAlert.podExpiring(triggeringSlot: slot)
         default:
         default:
+            // No PumpManagerAlerts are used for any other pod alerts (including suspendInProgress).
             return nil
             return nil
         }
         }
     }
     }
@@ -1999,7 +2171,7 @@ extension OmnipodPumpManager: PumpManager {
                         } catch {
                         } catch {
                             return
                             return
                         }
                         }
-                        self.mutateState { state in
+                        self.setState { state in
                             state.activeAlerts.remove(alert)
                             state.activeAlerts.remove(alert)
                             state.alertsWithPendingAcknowledgment.remove(alert)
                             state.alertsWithPendingAcknowledgment.remove(alert)
                         }
                         }
@@ -2123,7 +2295,7 @@ extension OmnipodPumpManager: PodCommsDelegate {
             }
             }
         } else {
         } else {
             // Resetting podState
             // Resetting podState
-            mutateState { state in
+            setState { state in
                 state.updatePodStateFromPodComms(podState)
                 state.updatePodStateFromPodComms(podState)
             }
             }
         }
         }
@@ -2142,6 +2314,13 @@ extension OmnipodPumpManager {
             if alert.alertIdentifier == alertIdentifier {
             if alert.alertIdentifier == alertIdentifier {
                 // If this alert was triggered by the pod find the slot to clear it.
                 // If this alert was triggered by the pod find the slot to clear it.
                 if let slot = alert.triggeringSlot {
                 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
                     let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
                     self.podComms.runSession(withName: "Acknowledge Alert", using: rileyLinkSelector) { (result) in
                     self.podComms.runSession(withName: "Acknowledge Alert", using: rileyLinkSelector) { (result) in
                         switch result {
                         switch result {
@@ -2150,18 +2329,18 @@ extension OmnipodPumpManager {
                                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                                 let beepBlock = self.beepMessageBlock(beepType: .beep)
                                 let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock)
                                 let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock)
                             } catch {
                             } catch {
-                                self.mutateState { state in
+                                self.setState { state in
                                     state.alertsWithPendingAcknowledgment.insert(alert)
                                     state.alertsWithPendingAcknowledgment.insert(alert)
                                 }
                                 }
                                 completion(error)
                                 completion(error)
                                 return
                                 return
                             }
                             }
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.activeAlerts.remove(alert)
                                 state.activeAlerts.remove(alert)
                             }
                             }
                             completion(nil)
                             completion(nil)
                         case .failure(let error):
                         case .failure(let error):
-                            self.mutateState { state in
+                            self.setState { state in
                                 state.alertsWithPendingAcknowledgment.insert(alert)
                                 state.alertsWithPendingAcknowledgment.insert(alert)
                             }
                             }
                             completion(error)
                             completion(error)
@@ -2170,7 +2349,7 @@ extension OmnipodPumpManager {
                     }
                     }
                 } else {
                 } else {
                     // Non-pod alert
                     // Non-pod alert
-                    self.mutateState { state in
+                    self.setState { state in
                         state.activeAlerts.remove(alert)
                         state.activeAlerts.remove(alert)
                         if alert == .timeOffsetChangeDetected {
                         if alert == .timeOffsetChangeDetected {
                             state.acknowledgedTimeOffsetAlert = true
                             state.acknowledgedTimeOffsetAlert = true
@@ -2199,7 +2378,7 @@ extension FaultEventCode {
         case .exceededMaximumPodLife80Hrs:
         case .exceededMaximumPodLife80Hrs:
             return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification")
             return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification")
         default:
         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 unstoredDoses: [UnfinalizedDose]
 
 
+    public var silencePod: Bool
+
     public var confirmationBeeps: BeepPreference
     public var confirmationBeeps: BeepPreference
 
 
     public var scheduledExpirationReminderOffset: TimeInterval?
     public var scheduledExpirationReminderOffset: TimeInterval?
@@ -100,6 +102,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
         self.basalSchedule = basalSchedule
         self.basalSchedule = basalSchedule
         self.rileyLinkConnectionManagerState = rileyLinkConnectionManagerState
         self.rileyLinkConnectionManagerState = rileyLinkConnectionManagerState
         self.unstoredDoses = []
         self.unstoredDoses = []
+        self.silencePod = false
         self.confirmationBeeps = .manualCommands
         self.confirmationBeeps = .manualCommands
         self.insulinType = insulinType
         self.insulinType = insulinType
         self.lowReservoirReminderValue = Pod.defaultLowReservoirReminder
         self.lowReservoirReminderValue = Pod.defaultLowReservoirReminder
@@ -186,6 +189,8 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
             self.unstoredDoses = []
             self.unstoredDoses = []
         }
         }
 
 
+        self.silencePod = rawValue["silencePod"] as? Bool ?? false
+
         if let oldAutomaticBolusBeeps = rawValue["automaticBolusBeeps"] as? Bool, oldAutomaticBolusBeeps {
         if let oldAutomaticBolusBeeps = rawValue["automaticBolusBeeps"] as? Bool, oldAutomaticBolusBeeps {
             self.confirmationBeeps = .extended
             self.confirmationBeeps = .extended
         } else if let oldConfirmationBeeps = rawValue["confirmationBeeps"] as? Bool, oldConfirmationBeeps {
         } else if let oldConfirmationBeeps = rawValue["confirmationBeeps"] as? Bool, oldConfirmationBeeps {
@@ -253,6 +258,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable {
             "timeZone": timeZone.secondsFromGMT(),
             "timeZone": timeZone.secondsFromGMT(),
             "basalSchedule": basalSchedule.rawValue,
             "basalSchedule": basalSchedule.rawValue,
             "unstoredDoses": unstoredDoses.map { $0.rawValue },
             "unstoredDoses": unstoredDoses.map { $0.rawValue },
+            "silencePod": silencePod,
             "confirmationBeeps": confirmationBeeps.rawValue,
             "confirmationBeeps": confirmationBeeps.rawValue,
             "activeAlerts": activeAlerts.map { $0.rawValue },
             "activeAlerts": activeAlerts.map { $0.rawValue },
             "podAttachmentConfirmed": podAttachmentConfirmed,
             "podAttachmentConfirmed": podAttachmentConfirmed,
@@ -303,8 +309,8 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible {
             "* timeZone: \(timeZone)",
             "* timeZone: \(timeZone)",
             "* basalSchedule: \(String(describing: basalSchedule))",
             "* basalSchedule: \(String(describing: basalSchedule))",
             "* maximumTempBasalRate: \(maximumTempBasalRate)",
             "* maximumTempBasalRate: \(maximumTempBasalRate)",
-            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset))",
-            "* defaultExpirationReminderOffset: \(String(describing: defaultExpirationReminderOffset))",
+            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))",
+            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)",
             "* lowReservoirReminderValue: \(String(describing: lowReservoirReminderValue))",
             "* lowReservoirReminderValue: \(String(describing: lowReservoirReminderValue))",
             "* podAttachmentConfirmed: \(podAttachmentConfirmed)",
             "* podAttachmentConfirmed: \(podAttachmentConfirmed)",
             "* activeAlerts: \(activeAlerts)",
             "* activeAlerts: \(activeAlerts)",
@@ -317,14 +323,21 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible {
             "* tempBasalEngageState: \(String(describing: tempBasalEngageState))",
             "* tempBasalEngageState: \(String(describing: tempBasalEngageState))",
             "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))",
             "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))",
             "* isPumpDataStale: \(String(describing: isPumpDataStale))",
             "* isPumpDataStale: \(String(describing: isPumpDataStale))",
+            "* silencePod: \(String(describing: silencePod))",
             "* confirmationBeeps: \(String(describing: confirmationBeeps))",
             "* confirmationBeeps: \(String(describing: confirmationBeeps))",
             "* pairingAttemptAddress: \(String(describing: pairingAttemptAddress))",
             "* pairingAttemptAddress: \(String(describing: pairingAttemptAddress))",
             "* insulinType: \(String(describing: insulinType))",
             "* insulinType: \(String(describing: insulinType))",
+            "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))",
+            "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)",
             "* rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))",
             "* rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))",
             "* lastRileyLinkBatteryAlertDate \(String(describing: lastRileyLinkBatteryAlertDate))",
             "* 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")
         ].joined(separator: "\n")
     }
     }
 }
 }

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

@@ -454,7 +454,6 @@ class PodComms: CustomDebugStringConvertible {
     var debugDescription: String {
     var debugDescription: String {
         return [
         return [
             "## PodComms",
             "## PodComms",
-            "podState: \(String(reflecting: podState))",
             "configuredDevices: \(configuredDevices.value.map { $0.uuidString })",
             "configuredDevices: \(configuredDevices.value.map { $0.uuidString })",
             "delegate: \(String(describing: delegate != nil))",
             "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)
             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)
             let response = try transport.sendMessage(message)
 
 
             // Simulate fault
             // Simulate fault
@@ -287,6 +288,7 @@ public class PodCommsSession {
 
 
             if let responseMessageBlock = response.messageBlocks[0] as? T {
             if let responseMessageBlock = response.messageBlocks[0] as? T {
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
                 log.info("POD Response: %{public}@", String(describing: responseMessageBlock))
+                self.podState.lastCommsOK = true // message successfully sent and expected response received
                 return responseMessageBlock
                 return responseMessageBlock
             }
             }
 
 
@@ -385,10 +387,16 @@ public class PodCommsSession {
     }
     }
 
 
     @discardableResult
     @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 configurations = alerts.map { $0.configuration }
         let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations)
         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 {
         for alert in alerts {
             podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
             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 cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
         let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
         let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
 
 
-        guard let activatedAt = podState.activatedAt else {
+        guard podState.activatedAt != nil else {
             throw PodCommsError.noPodPaired
             throw PodCommsError.noPodPaired
         }
         }
 
 
@@ -444,12 +452,12 @@ public class PodCommsSession {
             }
             }
             podState.updateFromStatusResponse(status, at: currentDate)
             podState.updateFromStatusResponse(status, at: currentDate)
         } else {
         } 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)
             try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm] + optionalAlerts)
         }
         }
 
 
@@ -500,7 +508,9 @@ public class PodCommsSession {
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
         let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, units: units, timeBetweenPulses: timeBetweenPulses, extendedUnits: extendedUnits, extendedDuration: extendedDuration)
         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
             var ongoingBolus = true
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
             if let statusResponse: StatusResponse = try? send([GetStatusCommand()]) {
                 podState.updateFromStatusResponse(statusResponse, at: currentDate)
                 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 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 1-5 minutes will only use suspendTimeExpired alert beeps.
     // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts.
     // 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 {
         guard podState.unacknowledgedCommand == nil else {
             return .certainFailure(error: .unacknowledgedCommandPending)
             return .certainFailure(error: .unacknowledgedCommandPending)
@@ -613,6 +624,9 @@ public class PodCommsSession {
             var podSuspendedReminderAlert: PodAlert? = nil
             var podSuspendedReminderAlert: PodAlert? = nil
             var suspendTimeExpiredAlert: PodAlert? = nil
             var suspendTimeExpiredAlert: PodAlert? = nil
             let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0
             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)
             let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel)
             var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
             var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
@@ -620,14 +634,14 @@ public class PodCommsSession {
             // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
             // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
             if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
             if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
                 // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders
                 // 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]
                 alertConfigurations += [podSuspendedReminderAlert!.configuration]
             }
             }
 
 
             // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
             // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
             if suspendTime > 0 {
             if suspendTime > 0 {
                 // a timed suspend using a suspend time expired alert
                 // 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]
                 alertConfigurations += [suspendTimeExpiredAlert!.configuration]
             }
             }
 
 
@@ -667,8 +681,8 @@ public class PodCommsSession {
     private func cancelSuspendAlerts() throws -> StatusResponse {
     private func cancelSuspendAlerts() throws -> StatusResponse {
 
 
         do {
         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])
             let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired])
             return status
             return status
@@ -733,6 +747,11 @@ public class PodCommsSession {
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
         let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval)
 
 
         do {
         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])
             var status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
             let now = currentDate
             let now = currentDate
             podState.suspendState = .resumed(now)
             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 cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts)
         let status: StatusResponse = try send([cmd], beepBlock: beepBlock)
         let status: StatusResponse = try send([cmd], beepBlock: beepBlock)
         podState.updateFromStatusResponse(status, at: currentDate)
         podState.updateFromStatusResponse(status, at: currentDate)
-        return podState.activeAlerts
+        return podState.activeAlertSlots
     }
     }
 
 
     func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) {
     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
     fileprivate var nonceState: NonceState
 
 
     public var activatedAt: Date?
     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 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 var setupUnitsDelivered: Double?
 
 
     public let pmVersion: String
     public let pmVersion: String
     public let piVersion: String
     public let piVersion: String
     public let lot: UInt32
     public let lot: UInt32
     public let tid: UInt32
     public let tid: UInt32
-    var activeAlertSlots: AlertSet
+    public var activeAlertSlots: AlertSet
     public var lastInsulinMeasurements: PodInsulinMeasurements?
     public var lastInsulinMeasurements: PodInsulinMeasurements?
 
 
     public var unacknowledgedCommand: PendingCommand?
     public var unacknowledgedCommand: PendingCommand?
@@ -96,16 +99,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
     public var configuredAlerts: [AlertSlot: PodAlert]
     public var configuredAlerts: [AlertSlot: PodAlert]
     public var insulinType: InsulinType
     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.
     // Allow a grace period while the unacknowledged command is first being sent.
     public var needsCommsRecovery: Bool {
     public var needsCommsRecovery: Bool {
         if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight {
         if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight {
@@ -114,6 +107,10 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         return false
         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) {
     public init(address: UInt32, pmVersion: String, piVersion: String, lot: UInt32, tid: UInt32, packetNumber: Int = 0, messageNumber: Int = 0, insulinType: InsulinType) {
         self.address = address
         self.address = address
         self.nonceState = NonceState(lot: lot, tid: tid)
         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.messageTransportState = MessageTransportState(packetNumber: packetNumber, messageNumber: messageNumber)
         self.primeFinishTime = nil
         self.primeFinishTime = nil
         self.setupProgress = .addressAssigned
         self.setupProgress = .addressAssigned
-        self.configuredAlerts = [.slot7: .waitingForPairingReminder]
+        self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder]
         self.insulinType = insulinType
         self.insulinType = insulinType
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
+        self.podTime = 0
     }
     }
     
     
     public var unfinishedSetup: Bool {
     public var unfinishedSetup: Bool {
@@ -170,9 +170,21 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         let seed = UInt16(sum & 0xffff) ^ syncWord
         let seed = UInt16(sum & 0xffff) ^ syncWord
         nonceState = NonceState(lot: lot, tid: tid, seed: seed)
         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 {
     private mutating func updatePodTimes(timeActive: TimeInterval) -> Date {
         let now = 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
         let activatedAtComputed = now - timeActive
         if activatedAt == nil {
         if activatedAt == nil {
             self.activatedAt = activatedAtComputed
             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) {
     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)
         // 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 deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that Loop doesn't know about?
             if podProgressStatus.readyForDelivery {
             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.
                 // 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)
                 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 {
         if var bolus = unfinalizedBolus, !deliveryStatus.bolusing {
             // Due to clock drift or comms delays, boluses can finish earlier than we expect
             // 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
             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,
         if let setupProgressRaw = rawValue["setupProgress"] as? Int,
             let setupProgress = SetupProgress(rawValue: setupProgressRaw)
             let setupProgress = SetupProgress(rawValue: setupProgressRaw)
         {
         {
@@ -441,12 +475,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         } else {
         } else {
             // Assume migration, and set up with alerts that are normally configured
             // Assume migration, and set up with alerts that are normally configured
             self.configuredAlerts = [
             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) {
         if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
             self.insulinType = insulinType
             self.insulinType = insulinType
         } else {
         } else {
-            insulinType = .novolog
+            self.insulinType = .novolog
         }
         }
+
+        self.deliveryStatusVerified = false
+        self.lastCommsOK = false
     }
     }
     
     
     public var rawValue: RawValue {
     public var rawValue: RawValue {
@@ -494,6 +531,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
         rawValue["activeTime"] = activeTime
         rawValue["activeTime"] = activeTime
         rawValue["activatedAt"] = activatedAt
         rawValue["activatedAt"] = activatedAt
         rawValue["expiresAt"] = expiresAt
         rawValue["expiresAt"] = expiresAt
+        rawValue["podTime"] = podTime
+        rawValue["podTimeUpdated"] = podTimeUpdated
 
 
         rawValue["setupUnitsDelivered"] = setupUnitsDelivered
         rawValue["setupUnitsDelivered"] = setupUnitsDelivered
 
 
@@ -514,6 +553,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* address: \(String(format: "%04X", address))",
             "* address: \(String(format: "%04X", address))",
             "* activatedAt: \(String(reflecting: activatedAt))",
             "* activatedAt: \(String(reflecting: activatedAt))",
             "* expiresAt: \(String(reflecting: expiresAt))",
             "* expiresAt: \(String(reflecting: expiresAt))",
+            "* podTime: \(podTime.timeIntervalStr)",
+            "* podTimeUpdated: \(String(reflecting: podTimeUpdated))",
             "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))",
             "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))",
             "* piVersion: \(piVersion)",
             "* piVersion: \(piVersion)",
             "* pmVersion: \(pmVersion)",
             "* pmVersion: \(pmVersion)",
@@ -526,16 +567,14 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl
             "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))",
             "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))",
             "* unfinalizedResume: \(String(describing: unfinalizedResume))",
             "* unfinalizedResume: \(String(describing: unfinalizedResume))",
             "* finalizedDoses: \(String(describing: finalizedDoses))",
             "* finalizedDoses: \(String(describing: finalizedDoses))",
-            "* activeAlerts: \(String(describing: activeAlerts))",
+            "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))",
             "* messageTransportState: \(String(describing: messageTransportState))",
             "* messageTransportState: \(String(describing: messageTransportState))",
             "* setupProgress: \(setupProgress)",
             "* setupProgress: \(setupProgress)",
             "* primeFinishTime: \(String(describing: primeFinishTime))",
             "* primeFinishTime: \(String(describing: primeFinishTime))",
-            "* configuredAlerts: \(String(describing: configuredAlerts))",
+            "* configuredAlerts: \(configuredAlertsString(configuredAlerts: configuredAlerts))",
             "* insulinType: \(String(describing: insulinType))",
             "* 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")
         ].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(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(8100, decoded.timeActive)
             XCTAssertEqual(8100, decoded.timeActive)
             XCTAssertEqual(TimeInterval(minutes: 0x0087), 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(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertNil(decoded.errorEventInfo)
             XCTAssertNil(decoded.errorEventInfo)
@@ -223,7 +223,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.basalOverInfusionPulse, decoded.faultEventCode.faultType)
             XCTAssertEqual(.basalOverInfusionPulse, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(TimeInterval(minutes: 0x09ff), decoded.faultEventTimeSinceActivation)
             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(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(TimeInterval(minutes: 0x0a02), decoded.timeActive)
             XCTAssertEqual(TimeInterval(minutes: 0x0a02), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.faultAccessingTables)
@@ -255,7 +255,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(TimeInterval(minutes: 0x0e0c), decoded.faultEventTimeSinceActivation)
             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(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(TimeInterval(minutes: 0x0e14), decoded.timeActive)
             XCTAssertEqual(TimeInterval(minutes: 0x0e14), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             XCTAssertEqual(false, decoded.faultAccessingTables)
@@ -287,7 +287,7 @@ class PodInfoTests: XCTestCase {
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue)
             XCTAssertEqual(TimeInterval(minutes: 0x0268), decoded.faultEventTimeSinceActivation)
             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(Pod.reservoirLevelAboveThresholdMagicNumber, decoded.reservoirLevel, accuracy: 0.01)
             XCTAssertEqual(TimeInterval(minutes: 0x026b), decoded.timeActive)
             XCTAssertEqual(TimeInterval(minutes: 0x026b), decoded.timeActive)
             XCTAssertEqual(false, decoded.faultAccessingTables)
             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.";
 "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 */
 /* 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 */
 /* 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?";
 "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")
             hostedView.navigationItem.title = LocalizedString("Insulin Type", comment: "Title for insulin type selection screen")
             return hostedView
             return hostedView
         case .deactivate:
         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
             viewModel.didFinish = { [weak self] in
                 self?.stepFinished()
                 self?.stepFinished()

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

@@ -20,16 +20,6 @@ extension OmnipodPumpManager: PodDeactivater {}
 
 
 class DeactivatePodViewModel: ObservableObject, Identifiable {
 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 {
     enum DeactivatePodViewModelState {
         case active
         case active
         case deactivating
         case deactivating
@@ -125,9 +115,38 @@ class DeactivatePodViewModel: ObservableObject, Identifiable {
     
     
     var podDeactivator: PodDeactivater
     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.podDeactivator = podDeactivator
         self.podAttachedToBody = podAttachedToBody
         self.podAttachedToBody = podAttachedToBody
+        self.instructionText = text
     }
     }
     
     
     public func continueButtonTapped() {
     public func continueButtonTapped() {

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

@@ -35,6 +35,8 @@ class OmnipodSettingsViewModel: ObservableObject {
 
 
     @Published var beepPreference: BeepPreference
     @Published var beepPreference: BeepPreference
 
 
+    @Published var silencePodPreference: SilencePodPreference
+
     @Published var rileylinkConnected: Bool
     @Published var rileylinkConnected: Bool
 
 
     var activatedAtString: String {
     var activatedAtString: String {
@@ -131,7 +133,7 @@ class OmnipodSettingsViewModel: ObservableObject {
 
 
     var recoveryText: String? {
     var recoveryText: String? {
         if case .fault = podCommState {
         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 {
         } 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")
             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 {
         } else if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, serviceTimeRemaining <= Pod.serviceDuration - Pod.nominalPodLife {
@@ -230,6 +232,7 @@ class OmnipodSettingsViewModel: ObservableObject {
         lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
         lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue)
         podCommState = self.pumpManager.podCommState
         podCommState = self.pumpManager.podCommState
         beepPreference = self.pumpManager.beepPreference
         beepPreference = self.pumpManager.beepPreference
+        silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled
         insulinType = self.pumpManager.insulinType
         insulinType = self.pumpManager.insulinType
         podDetails = self.pumpManager.podDetails
         podDetails = self.pumpManager.podDetails
         previousPodDetails = self.pumpManager.previousPodDetails
         previousPodDetails = self.pumpManager.previousPodDetails
@@ -278,7 +281,7 @@ class OmnipodSettingsViewModel: ObservableObject {
     }
     }
     
     
     func stopUsingOmnipodTapped() {
     func stopUsingOmnipodTapped() {
-        self.pumpManager.notifyDelegateOfDeactivation {
+        pumpManager.notifyDelegateOfDeactivation {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
                 self.didFinish?()
                 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) {
     func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
         pumpManager.playTestBeeps(completion: completion)
         pumpManager.playTestBeeps(completion: completion)
     }
     }
 
 
+    func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) {
+        completion(pumpManager.debugDescription)
+    }
+
     func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
     func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) {
         pumpManager.setConfirmationBeeps(newPreference: preference) { error in
         pumpManager.setConfirmationBeeps(newPreference: preference) { error in
             DispatchQueue.main.async {
             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?) {
     func didChangeInsulinType(_ newType: InsulinType?) {
         self.pumpManager.insulinType = newType
         self.pumpManager.insulinType = newType
     }
     }
@@ -367,6 +401,10 @@ class OmnipodSettingsViewModel: ObservableObject {
         }
         }
     }
     }
 
 
+    var noPod: Bool {
+        return podCommState == .noPod
+    }
+
     var podError: String? {
     var podError: String? {
         switch podCommState {
         switch podCommState {
         case .fault(let status):
         case .fault(let status):
@@ -378,7 +416,7 @@ class OmnipodSettingsViewModel: ObservableObject {
             case .occluded, .occlusionCheckStartup1, .occlusionCheckStartup2, .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold, .occlusionCheckValueTooHigh:
             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")
                 return LocalizedString("Pod Occlusion", comment: "Error message for reservoir view when pod occlusion checks failed")
             default:
             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:
         case .active:
             if isPodDataStale {
             if isPodDataStale {

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

@@ -85,7 +85,7 @@ enum PodLifeState {
         case .podDeactivating:
         case .podDeactivating:
             return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation")
             return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation")
         default:
         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 {
                 HStack {
                     InstructionList(instructions: [
                     InstructionList(instructions: [
                         LocalizedString("Prepare site.", comment: "Label text for step one of attach pod 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")
                         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)
         .animation(.default)
         .alert(item: $activeModal, content: self.alert(for:))
         .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)
         .navigationBarItems(trailing: cancelButton)
         .navigationBarBackButtonHidden(true)
         .navigationBarBackButtonHidden(true)
     }
     }

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

@@ -39,7 +39,7 @@ struct BeepPreferenceSelectionView: View {
         VStack {
         VStack {
             List {
             List {
                 Section {
                 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)
                         .padding(.vertical, 10)
                 }
                 }
 
 
@@ -88,7 +88,7 @@ struct BeepPreferenceSelectionView: View {
 
 
         }
         }
         .insetGroupedListStyle()
         .insetGroupedListStyle()
-        .navigationTitle("Confidence Reminders")
+        .navigationTitle(LocalizedString("Confidence Reminders", comment: "navigation title for confidence reminders"))
         .navigationBarTitleDisplayMode(.inline)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
     }
     }
@@ -110,15 +110,15 @@ struct BeepPreferenceSelectionView: View {
 
 
     private var cancelButton: some View {
     private var cancelButton: some View {
         Button(action: { self.presentationMode.wrappedValue.dismiss() } ) {
         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 {
     var saveButtonText: String {
         if saving {
         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 {
         } 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 {
     static var previews: some View {
         NavigationView {
         NavigationView {
             BeepPreferenceSelectionView(initialValue: .extended) { selectedValue, completion in
             BeepPreferenceSelectionView(initialValue: .extended) { selectedValue, completion in

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

@@ -55,7 +55,7 @@ struct CheckInsertedCannulaView: View {
         }
         }
         .animation(.default)
         .animation(.default)
         .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal }
         .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)
         .navigationBarItems(trailing: cancelButton)
         .navigationBarBackButtonHidden(true)
         .navigationBarBackButtonHidden(true)
     }
     }

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

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

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

@@ -38,7 +38,7 @@ struct ExpirationReminderSetupView: View {
             }
             }
             .padding()
             .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)
         .navigationBarHidden(false)
         .toolbar {
         .toolbar {
             ToolbarItem(placement: .navigationBarTrailing) {
             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)
         .animation(.default)
         .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal }
         .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal }
-        .navigationBarTitle("Insert Cannula", displayMode: .automatic)
+        .navigationBarTitle(LocalizedString("Insert Cannula", comment: "navigation bar title for insert cannula"), displayMode: .automatic)
         .navigationBarBackButtonHidden(true)
         .navigationBarBackButtonHidden(true)
         .navigationBarItems(trailing: cancelButton)
         .navigationBarItems(trailing: cancelButton)
     }
     }

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

@@ -63,6 +63,6 @@ struct InsulinTypeConfirmation: View {
 
 
 struct InsulinTypeConfirmation_Previews: PreviewProvider {
 struct InsulinTypeConfirmation_Previews: PreviewProvider {
     static var previews: some View {
     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()
             .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 {
         .toolbar {
             ToolbarItem(placement: .navigationBarTrailing) {
             ToolbarItem(placement: .navigationBarTrailing) {
                 Button(LocalizedString("Cancel", comment: "Cancel button title"), action: {
                 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)
                     .frame(maxHeight: 162.0)
                     .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert })
                     .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert })
                     Section {
                     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)
                             .font(.footnote)
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
                             .fixedSize(horizontal: false, vertical: true)
                             .fixedSize(horizontal: false, vertical: true)
@@ -147,7 +147,7 @@ struct ManualTempBasalEntryView: View {
     var missingConfigAlert: SwiftUI.Alert {
     var missingConfigAlert: SwiftUI.Alert {
         return SwiftUI.Alert(
         return SwiftUI.Alert(
             title: Text(LocalizedString("Missing Config", comment: "Alert title for missing temp basal configuration")),
             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")
         .accessibility(identifier: "button_cancel")
     }
     }
-
 }
 }
-
-

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


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