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

Release 2.2.7 (#375)

* Omnipod update bundle with Silence Pod mode & Diagnostics support (#324)

* Update bundle with various improvements and new functionality including
Silence Pod mode & Diagnostics support from freeaps_dev to SwiftUI

Various fault event code updates
+ Add missing 0x4E code and description for SAW Trim Error
+ Fix typo in rtcInterruptHandlerInconsistentState string
+ Improved description string for the testInProgress fault
+ Use more generic name for the values do not match case
+ Add codes for the 0xD6 & 0xD7 reset faults of unknown origin
+ Add codes for the mystery 0xCB, 0xD4, 0xD5, 0xD8 & 0xD9 faults

Various Omnipod message decode improvements
+ Round RateEntry rate calculation to 2 digits
+ Round RateEntry duration calculation to nearest second
+ Add missing comma to RateEntry debugDescription
+ New timeIntervalStr TimeInterval var for pod interval debug info
+ Update some command commenting to be more consistent and useful
+ Add debugDescription for CancelDeliveryCommand DeliveryType
+ Add additional PodInfoTest checking and comments
+ Whitespace tweaks for OmniKit & OmniBLE consistency

Remove some overly verbose OmniBLE Bluetooth logging messages
+ Add {public} specifier to some non-debug logging messages

New operation mode that suppresses all pod alerts & beeping
+ New CustomDebugStringConvertible extension for AlertTrigger
+ Add new optional silent Bool parameter to AlertConfiguration struct
+ Updated CustomDebugStringConvertible extension for AlertConfiguration
+ Add new optional silent Bool associated value for most PodAlert enum values
+ Add new offset TimeInterval associated value for time based PodAlert enum values
+ Updated AlertSlot naming to make slot to alert mappings consistent
+ Use consistent ascending alert ordering for all switch statements
+ New alertSetString func to return suitable String for a given AlertSet
+ New func to create corresponding PodAlerts for current pod time and silent values
+ Add new silent Bool to ConfigureAlertsCommand struct
+ Sort alert configurations in ConfigureAlertsCommand for easier analysis
+ Have ConfigureAlertsCommand enforce the max alert duration value
+ Rework pump manager code to use new silencePod var
+ Use { } instead of ({ }) for {set,modify}State for consistency & clarity
+ Add timeActive var to pump manager for more accurate pod active time
+ Have acknowlegePodAlerts() use AlertSet instead of [AlertSlot: PodAlert]
+ New setSilencePod pump manager func to change the pod's silence state
+ Add some additional pump manager error logging statements
+ Add new silencePod var to the pump manager state
+ Rework PodAlerts creation for new podActive time offset and silence values
+ New PodState var's to manage pod time active state
+ Remove no longer needed activeAlerts PodState var func
+ Update updatePodTimes func to manages new time active var's
+ Update all unit tests as needed for new names & members
+ Rework UI to use updated simplied interfaces for alertStrings from AlertSlots
+ Add new Pod Settings button for Silence Pod and Unsilence Pod
+ Update UI to use new new pod active pod state
+ Have Pod Settings UI set the pod's expiration reminder alert
+ Various commenting additions/updates/improvements/corrections

Read Pod Status display and consistency updates
+ Add new faultDescription var for FaultEventCode
+ Display errorEventInfo details for Pod fault details
+ Various AlertSlot comment updates for consistency
+ Make PumpManager silencePod and confirmationBeeps vars read-only
+ Consistency updates for setTempBasal result handling

+ Improved OmniBLE PodCommsError recoverySuggestion for noResponse and podNotConnected
+ Change OmniKit acknowledgeAlerts() to acknowledgePodAlerts() for consistency

+ New Silence Pod option and View
+ Update pulseLogString newlines for SwiftUI output
+ Replace pumpmanager mutateState() with discardableResult with setState()
+ Restore getDetailedStatus() pumpmanager funcs
+ enactTempBasal() func skips unneeded pod cancel if running scheduled basal
+ Restore deliveryStatus PodState variable to verify pod delivery state
+ Restore lastCommsOK PodState variable for additional comms verification
+ Add bulletproofing for all insulin commands to prevent 0x31 faults
+ Updated Notification Settings & Confidence Reminders text for silent pod
+ New Diagnostics command section at end of Pump Settings
> Restore Loop 2.x/FreeAPS Read Pod Status diagnostic command
> Restore Loop 2.x/FreeAPS Read Pulse Log diagnostic command
> Restore Loop 2.x/FreeAPS Play Test Beeps diagnostic command
> New Pump Manager Details diagnostic command
+ Fix some mismatched comments for some copied LocalizedString's
+ Update a couple of messages to avoid using the word "Loop"
+ Add missing LocalizedString funcs for various buttons
+ Print Pod Details View Sequence # as a zero padded 7-digit value
+ Fix typo in Pod Details View LocalizedString comment
+ Updated Pod Fault display with decimal & hex values & separate description line
+ Prevent immediate low reservoir pod alerts if setting a value higher than current
+ Improved debugDescription formatting for various PumpManager related state
+ Allow expiration reminder to be triggered more than once
+ Acknowledge all pod alerts when toggling silence pod state for safety
+ Fix to prevent podState expiresAt jumping after a reset pod fault
+ New PumpManagerAlert to post and clear any unexpected pod alerts
+ Cleaned up some uneeded PumpManagerAlert related cruft
+ Improved SwiftUI previews for many views
+ Fixed various comment typos and errors
+ Convert some inconsistent uses of NSLocalizedString to LocalizedString
+ Remove some uneeded and inconsistent semicolons in OmnipodSettingsView
+ White space cleanup to minimize OmniKit & OmniBLE source file differences
+ New configuredAlertsString func for detailed info on configured alerts
+ Renamed alertString func to alertSetString func for better clarity
+ Clearer PumpManager debug strings for optional current & previous PodState
+ Clearer PodState debug strings for optional Fault and pdmRef
+ Add additional bulletproofing for suspend time expired alert clearing

Reworked and improved pump manager settings layout
+ Device Details -> Pod Details
+ Previous Pod Information -> Previous Pod Details
+ Remove Pod -> Deactivate Pod

Improved alert handling when toggling pod silence state during suspend
+ reset suspendTimeExpired alert if active and alert not acknowledged
+ fix logic error which caused podSuspendedReminder alert not to be reset

Pod suspend alert handling improvements
+ Don't clear suspend time expired pod alert to continue beeping until resume
+ Keep suspend time expired pod beeps on 15m intervals when toggling Silence Pod

Make pod fault code, description and ref string info more available
+ Added the decimal fault # to the reservoir display for a faulted pod
+ Deactivate pod view includes notification string for common faults
+ Deactivate pod includes fault # & Ref string for unexpected pod faults

Disable selective pod settings commands based on pod state
+ Disable Set Temporary Basal Rate when pod is faulted or not active
+ Disable Play Test Beeps Diagnostic when pod is faulted or not active
+ Disable Read Pod Status and Read Pulse Log Diagnostics with no pod

Add missing OmniKitUI.xcassets for Cannula Inserted & reservoir masks

* Fix logic error for fault display text during Pod Deactvation
Improve OmniBLE/PumpManagerUI directory Xcode sorting

* New Crowdin updates (#320)

* Merge fix (#327)

* More pod strings

* Localization work

* Revert "strings"

This reverts commit 0d0acadb28d92265731cd7053fa0e26fdcf9e8c9.

* fix strings

* Crowdin (#334)

* New Silence pod string

* More strings

* Crowdin (#362)

* 2.2.7

---------

Authored-by: Joe Moran <mojo@moranfoundation.org>
and Co-authored (strings and localization) by @Jon-b-m
Jon B Mårtensson 2 лет назад
Родитель
Сommit
a68eb8943a
100 измененных файлов с 3953 добавлено и 1458 удалено
  1. 1 1
      Config.xcconfig
  2. 35 3
      Dependencies/OmniBLE/Localizations/ar.lproj/Localizable.strings
  3. 3 3
      Dependencies/OmniBLE/Localizations/bn.lproj/Localizable.strings
  4. 3 3
      Dependencies/OmniBLE/Localizations/ca.lproj/Localizable.strings
  5. 35 3
      Dependencies/OmniBLE/Localizations/da.lproj/Localizable.strings
  6. 51 2
      Dependencies/OmniBLE/Localizations/de.lproj/Localizable.strings
  7. 39 3
      Dependencies/OmniBLE/Localizations/en.lproj/Localizable.strings
  8. 35 3
      Dependencies/OmniBLE/Localizations/es.lproj/Localizable.strings
  9. 35 3
      Dependencies/OmniBLE/Localizations/fi.lproj/Localizable.strings
  10. 34 2
      Dependencies/OmniBLE/Localizations/fr.lproj/Localizable.strings
  11. 35 3
      Dependencies/OmniBLE/Localizations/he.lproj/Localizable.strings
  12. 34 2
      Dependencies/OmniBLE/Localizations/it.lproj/Localizable.strings
  13. 3 3
      Dependencies/OmniBLE/Localizations/ja.lproj/Localizable.strings
  14. 3 3
      Dependencies/OmniBLE/Localizations/lt.lproj/Localizable.strings
  15. 38 2
      Dependencies/OmniBLE/Localizations/nb.lproj/Localizable.strings
  16. 34 2
      Dependencies/OmniBLE/Localizations/nl.lproj/Localizable.strings
  17. 35 3
      Dependencies/OmniBLE/Localizations/pl.lproj/Localizable.strings
  18. 35 3
      Dependencies/OmniBLE/Localizations/pt-BR.lproj/Localizable.strings
  19. 35 3
      Dependencies/OmniBLE/Localizations/pt-PT.lproj/Localizable.strings
  20. 3 3
      Dependencies/OmniBLE/Localizations/ro.lproj/Localizable.strings
  21. 48 2
      Dependencies/OmniBLE/Localizations/ru.lproj/Localizable.strings
  22. 35 3
      Dependencies/OmniBLE/Localizations/sk.lproj/Localizable.strings
  23. 31 2
      Dependencies/OmniBLE/Localizations/sv.lproj/Localizable.strings
  24. 34 2
      Dependencies/OmniBLE/Localizations/tr.lproj/Localizable.strings
  25. 54 2
      Dependencies/OmniBLE/Localizations/uk.lproj/Localizable.strings
  26. 3 3
      Dependencies/OmniBLE/Localizations/vi.lproj/Localizable.strings
  27. 35 3
      Dependencies/OmniBLE/Localizations/zh-Hans.lproj/Localizable.strings
  28. 44 12
      Dependencies/OmniBLE/OmniBLE.xcodeproj/project.pbxproj
  29. 0 2
      Dependencies/OmniBLE/OmniBLE/Bluetooth/BluetoothServices.swift
  30. 0 12
      Dependencies/OmniBLE/OmniBLE/Bluetooth/EnDecrypt/EnDecrypt.swift
  31. 0 9
      Dependencies/OmniBLE/OmniBLE/Bluetooth/PeripheralManager+OmniBLE.swift
  32. 1 1
      Dependencies/OmniBLE/OmniBLE/Bluetooth/PodProtocolError.swift
  33. 482 145
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/AlertSlot.swift
  34. 7 5
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BasalDeliveryTable.swift
  35. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BasalSchedule.swift
  36. 2 2
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/BeepPreference.swift
  37. 1 1
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/CRC16.swift
  38. 309 296
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/FaultEventCode.swift
  39. 33 23
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/CancelDeliveryCommand.swift
  40. 12 4
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift
  41. 2 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift
  42. 23 23
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  43. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  44. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/StatusResponse.swift
  45. 1 1
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/TempBasalExtraCommand.swift
  46. 8 7
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/PendingCommand.swift
  47. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/Pod.swift
  48. 36 46
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/PumpManagerAlert.swift
  49. 32 0
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/SilencePodPreference.swift
  50. 4 4
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/UnfinalizedDose.swift
  51. 2 7
      Dependencies/OmniBLE/OmniBLE/PumpManager/MessageTransport.swift
  52. 257 100
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift
  53. 14 4
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManagerState.swift
  54. 38 19
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodCommsSession.swift
  55. 72 34
      Dependencies/OmniBLE/OmniBLE/PumpManager/PodState.swift
  56. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewControllers/DashUICoordinator.swift
  57. 36 17
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/DeactivatePodViewModel.swift
  58. 43 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift
  59. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/PodLifeState.swift
  60. 36 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ActivityView.swift
  61. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/AttachPodView.swift
  62. 5 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/BeepPreferenceSelectionView.swift
  63. 1 1
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ExpirationReminderPickerView.swift
  64. 30 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/FirstAppear.swift
  65. 2 5
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ManualTempBasalEntryView.swift
  66. 11 8
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/NotificationSettingsView.swift
  67. 54 24
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift
  68. 101 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PlayTestBeepsView.swift
  69. 4 7
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PodDetailsView.swift
  70. 100 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PumpManagerDetailsView.swift
  71. 167 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPodStatusView.swift
  72. 128 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPulseLogView.swift
  73. 143 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/SilencePodSelectionView.swift
  74. 2 2
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/UncertaintyRecoveredView.swift
  75. 3 3
      Dependencies/OmniBLE/OmniBLE/ca.lproj/Localizable.strings
  76. 1 1
      Dependencies/OmniBLE/OmniBLE/cs.lproj/Localizable.strings
  77. 3 3
      Dependencies/OmniBLE/OmniBLE/da.lproj/Localizable.strings
  78. 5 3
      Dependencies/OmniBLE/OmniBLE/de.lproj/Localizable.strings
  79. 3 3
      Dependencies/OmniBLE/OmniBLE/es.lproj/Localizable.strings
  80. 3 3
      Dependencies/OmniBLE/OmniBLE/fr.lproj/Localizable.strings
  81. 3 3
      Dependencies/OmniBLE/OmniBLE/it.lproj/Localizable.strings
  82. 3 3
      Dependencies/OmniBLE/OmniBLE/nb.lproj/Localizable.strings
  83. 3 3
      Dependencies/OmniBLE/OmniBLE/nl.lproj/Localizable.strings
  84. 3 3
      Dependencies/OmniBLE/OmniBLE/pl.lproj/Localizable.strings
  85. 3 3
      Dependencies/OmniBLE/OmniBLE/pt-PT.lproj/Localizable.strings
  86. 3 3
      Dependencies/OmniBLE/OmniBLE/ro.lproj/Localizable.strings
  87. 3 3
      Dependencies/OmniBLE/OmniBLE/ru.lproj/Localizable.strings
  88. 3 3
      Dependencies/OmniBLE/OmniBLE/sk.lproj/Localizable.strings
  89. 3 3
      Dependencies/OmniBLE/OmniBLE/tr.lproj/Localizable.strings
  90. 3 3
      Dependencies/OmniBLE/OmniBLE/uk.lproj/Localizable.strings
  91. 65 33
      Dependencies/OmniKit/OmniKit.xcodeproj/project.pbxproj
  92. 473 136
      Dependencies/OmniKit/OmniKit/OmnipodCommon/AlertSlot.swift
  93. 7 5
      Dependencies/OmniKit/OmniKit/OmnipodCommon/BasalDeliveryTable.swift
  94. 2 2
      Dependencies/OmniKit/OmniKit/OmnipodCommon/BeepPreference.swift
  95. 263 263
      Dependencies/OmniKit/OmniKit/OmnipodCommon/FaultEventCode.swift
  96. 33 23
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/CancelDeliveryCommand.swift
  97. 12 4
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift
  98. 2 3
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DeactivatePodCommand.swift
  99. 24 24
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  100. 0 0
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift

+ 1 - 1
Config.xcconfig

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

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


+ 3 - 3
Dependencies/OmniBLE/Localizations/bn.lproj/Localizable.strings

@@ -433,7 +433,7 @@
 "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains." = "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.";
 
 /* Label text for step 1 of pair pod instructions */
-"Fill a new pod with U-100 Insulin (leave blue Pod needle cap on)." = "Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).";
+"Remove the Pod's blue needle cap and check cannula. Then remove paper backing." = "Remove the Pod's blue needle cap and check cannula. Then remove paper backing.";
 
 /* Label text for step 2 of pair pod instructions */
 "Listen for 2 beeps." = "Listen for 2 beeps.";
@@ -602,10 +602,10 @@
 "No confidence reminders are used." = "No confidence reminders are used.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "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.";
+"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." = "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.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.";
 
 /* Label text for temporary basal rate summary */
 "Rate" = "Rate";

+ 3 - 3
Dependencies/OmniBLE/Localizations/ca.lproj/Localizable.strings

@@ -433,7 +433,7 @@
 "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains." = "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.";
 
 /* Label text for step 1 of pair pod instructions */
-"Fill a new pod with U-100 Insulin (leave blue Pod needle cap on)." = "Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).";
+"Remove the Pod's blue needle cap and check cannula. Then remove paper backing." = "Remove the Pod's blue needle cap and check cannula. Then remove paper backing.";
 
 /* Label text for step 2 of pair pod instructions */
 "Listen for 2 beeps." = "Listen for 2 beeps.";
@@ -602,10 +602,10 @@
 "No confidence reminders are used." = "No confidence reminders are used.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "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.";
+"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." = "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.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.";
 
 /* Label text for temporary basal rate summary */
 "Rate" = "Rate";

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


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


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


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


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


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


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


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


+ 3 - 3
Dependencies/OmniBLE/Localizations/ja.lproj/Localizable.strings

@@ -435,7 +435,7 @@
 "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains." = "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.";
 
 /* Label text for step 1 of pair pod instructions */
-"Fill a new pod with U-100 Insulin (leave blue Pod needle cap on)." = "Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).";
+"Remove the Pod's blue needle cap and check cannula. Then remove paper backing." = "Remove the Pod's blue needle cap and check cannula. Then remove paper backing.";
 
 /* Label text for step 2 of pair pod instructions */
 "Listen for 2 beeps." = "Listen for 2 beeps.";
@@ -606,10 +606,10 @@
 "No confidence reminders are used." = "No confidence reminders are used.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "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.";
+"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." = "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.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.";
 
 /* Label text for temporary basal rate summary */
 "Rate" = "Rate";

+ 3 - 3
Dependencies/OmniBLE/Localizations/lt.lproj/Localizable.strings

@@ -433,7 +433,7 @@
 "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains." = "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.";
 
 /* Label text for step 1 of pair pod instructions */
-"Fill a new pod with U-100 Insulin (leave blue Pod needle cap on)." = "Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).";
+"Remove the Pod's blue needle cap and check cannula. Then remove paper backing." = "Remove the Pod's blue needle cap and check cannula. Then remove paper backing.";
 
 /* Label text for step 2 of pair pod instructions */
 "Listen for 2 beeps." = "Listen for 2 beeps.";
@@ -602,10 +602,10 @@
 "No confidence reminders are used." = "No confidence reminders are used.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "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.";
+"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." = "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.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.";
 
 /* Label text for temporary basal rate summary */
 "Rate" = "Rate";

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


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


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


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


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


+ 3 - 3
Dependencies/OmniBLE/Localizations/ro.lproj/Localizable.strings

@@ -435,7 +435,7 @@
 "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains." = "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.";
 
 /* Label text for step 1 of pair pod instructions */
-"Fill a new pod with U-100 Insulin (leave blue Pod needle cap on)." = "Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).";
+"Remove the Pod's blue needle cap and check cannula. Then remove paper backing." = "Remove the Pod's blue needle cap and check cannula. Then remove paper backing.";
 
 /* Label text for step 2 of pair pod instructions */
 "Listen for 2 beeps." = "Listen for 2 beeps.";
@@ -606,10 +606,10 @@
 "No confidence reminders are used." = "No confidence reminders are used.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "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.";
+"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." = "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.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.";
 
 /* Label text for temporary basal rate summary */
 "Rate" = "Rate";

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


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


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


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


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


+ 3 - 3
Dependencies/OmniBLE/Localizations/vi.lproj/Localizable.strings

@@ -435,7 +435,7 @@
 "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains." = "Change Pod now. Insulin delivery will stop 8 hours after the Pod has expired or when no more insulin remains.";
 
 /* Label text for step 1 of pair pod instructions */
-"Fill a new pod with U-100 Insulin (leave blue Pod needle cap on)." = "Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).";
+"Remove the Pod's blue needle cap and check cannula. Then remove paper backing." = "Remove the Pod's blue needle cap and check cannula. Then remove paper backing.";
 
 /* Label text for step 2 of pair pod instructions */
 "Listen for 2 beeps." = "Listen for 2 beeps.";
@@ -606,10 +606,10 @@
 "No confidence reminders are used." = "No confidence reminders are used.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "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.";
+"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." = "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.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate.";
 
 /* Label text for temporary basal rate summary */
 "Rate" = "Rate";

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -202,13 +202,13 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti
     public var eventTitle: String {
         switch doseType {
         case .bolus:
-            return NSLocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
+            return LocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
         case .resume:
-            return NSLocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
+            return LocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
         case .suspend:
-            return NSLocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
+            return LocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
         case .tempBasal:
-            return NSLocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
+            return LocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
         }
     }
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/ca.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Sicherheitserinnerung";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
+"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfiguration";

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/cs.lproj/Localizable.strings

@@ -65,7 +65,7 @@
 "Comms Recovered" = "Komunikace obnovena";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Ujištující upozornění jsou pípnutí podu, které lze použít k potvrzení zadaných příkazů.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Ujištující upozornění jsou pípnutí podu, které lze použít k potvrzení zadaných příkazů.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfigurace";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/da.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Påmindelse om succesfulde aktiviteter";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Påmindelse om succesfulde aktiviteter er bip fra Pod'en, som kan bruges til at bekræfte valgte kommandoer.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Påmindelse om succesfulde aktiviteter er bip fra Pod'en, som kan bruges til at bekræfte valgte kommandoer.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Påmindelser om succesfulde handlinger vil lyde for de kommandoer du sætter igang, annulleret, suspenderet, genoptaget bolus, gemme notifikationspåmindelser etc. Når Loop automatisk justerer tilførslen, bliver påmindelser om succesfulde handlinger ikke benyttet.";
+"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." = "Påmindelser om succesfulde handlinger vil lyde for de kommandoer du sætter igang, annulleret, suspenderet, genoptaget bolus, gemme notifikationspåmindelser etc. Når Loop automatisk justerer tilførslen, bliver påmindelser om succesfulde handlinger ikke benyttet.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Påmindelser om succesfulde handlinger vil lyde, når Loop automatisk justerer tilførslen og de kommandoer, du sætter igang.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Påmindelser om succesfulde handlinger vil lyde, når Loop automatisk justerer tilførslen og de kommandoer, du sætter igang.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfiguration";

+ 5 - 3
Dependencies/OmniBLE/OmniBLE/de.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Sicherheitserinnerung";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
+"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfiguration";
@@ -1128,3 +1128,5 @@
 /* Alert message body for confirm pod attachment */
 "Your Pod may still be delivering Insulin.\nRemove it from your body, then tap “Continue.“" = "Ihr Pod gibt möglicherweise immer noch Insulin ab.\nEntfernen Sie ihn vom Körper und tippen dann auf „Weiter“.";
 
+/* Alert title for error when updating silence pod preference */
+"Failed to update silence pod preference." = "Failed to update silence pod preference.";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/es.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Recordatorios de confianza";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Los recordatorios de confianza son pitidos que emite el Pod que pueden utilizarse para tener certeza de que se han seleccionado comandos.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Los recordatorios de confianza son pitidos que emite el Pod que pueden utilizarse para tener certeza de que se han seleccionado comandos.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Los recordatorios de confianza sonarán para los comandos que seleccione, como bolo, cancelar bolo, suspender, reanudar, guardar recordatorios de notificación, etc. Cuando Loop ajusta automáticamente la administración de insulina, no se utilizan recordatorios de confianza.";
+"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." = "Los recordatorios de confianza sonarán para los comandos que seleccione, como bolo, cancelar bolo, suspender, reanudar, guardar recordatorios de notificación, etc. Cuando Loop ajusta automáticamente la administración de insulina, no se utilizan recordatorios de confianza.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Recordatorios de confianza sonarán cuando Loop  automáticamente ajuste la administración de insulina, así como para los comandos que selecciones.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Recordatorios de confianza sonarán cuando Loop  automáticamente ajuste la administración de insulina, así como para los comandos que selecciones.";
 
 /* Section header for configuration section */
 "Configuration" = "Configuración";

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


+ 3 - 3
Dependencies/OmniBLE/OmniBLE/it.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Promemoria di fiducia";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "I promemoria di fiducia sono segnali acustici emessi dal Pod che possono essere utilizzati per confermare i comandi selezionati.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "I promemoria di fiducia sono segnali acustici emessi dal Pod che possono essere utilizzati per confermare i comandi selezionati.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "I promemoria di fiducia suoneranno per i comandi inoltrati, come boli, cancellazione boli, sospensioni, ripristini erogazione, ecc. Quando Loop invece regola in automatico l'erogazione allora non userà alcun promemoria di fiducia.";
+"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." = "I promemoria di fiducia suoneranno per i comandi inoltrati, come boli, cancellazione boli, sospensioni, ripristini erogazione, ecc. Quando Loop invece regola in automatico l'erogazione allora non userà alcun promemoria di fiducia.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "I promemoria di fiducia suonano quando Loop regola automaticamente l'erogazione e per i comandi avviati dall'utente.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "I promemoria di fiducia suonano quando Loop regola automaticamente l'erogazione e per i comandi avviati dall'utente.";
 
 /* Section header for configuration section */
 "Configuration" = "Configurazione";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/nb.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Bekreftelser";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Tillitspåminnelser er pip fra pod som kan brukes til å bekrefte valgte kommandoer.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Tillitspåminnelser er pip fra pod som kan brukes til å bekrefte valgte kommandoer.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Tillitspåminnelser høres for kommandoer du starter, for eksempel bolus, avbryt bolus, suspendere, gjenoppta, lagre varslingspåminnelser osv. Når Loop automatisk justerer leveringen, brukes mistillitspåminnelser.";
+"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." = "Tillitspåminnelser høres for kommandoer du starter, for eksempel bolus, avbryt bolus, suspendere, gjenoppta, lagre varslingspåminnelser osv. Når Loop automatisk justerer leveringen, brukes mistillitspåminnelser.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Tillitspåminnelser vil høres når Loop automatisk justerer leveringen så vel som for kommandoer du starter.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Tillitspåminnelser vil høres når Loop automatisk justerer leveringen så vel som for kommandoer du starter.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfigurasjon";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/nl.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Bevestigingsherinneringen";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Bevestigingsherinneringen zijn piepjes van de pod die kunnen worden gebruikt ter bevestiging van de geselecteerde opdrachten.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Bevestigingsherinneringen zijn piepjes van de pod die kunnen worden gebruikt ter bevestiging van de geselecteerde opdrachten.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Bevestigingsherinneringen zijn hoorbaar voor opdrachten die je start, zoals bolussen, bolus annuleren, onderbreken, hervatten, meldingsherinneringen opslaan, enz. Wanneer Loop de toediening automatisch aanpast, zijn er geen bevestigingsherinneringen hoorbaar.";
+"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." = "Bevestigingsherinneringen zijn hoorbaar voor opdrachten die je start, zoals bolussen, bolus annuleren, onderbreken, hervatten, meldingsherinneringen opslaan, enz. Wanneer Loop de toediening automatisch aanpast, zijn er geen bevestigingsherinneringen hoorbaar.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Bevestigingsherinneringen zijn hoorbaar wanneer Loop de toediening automatisch aanpast, evenals voor opdrachten die je geeft.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Bevestigingsherinneringen zijn hoorbaar wanneer Loop de toediening automatisch aanpast, evenals voor opdrachten die je geeft.";
 
 /* Section header for configuration section */
 "Configuration" = "Configuratie";

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


+ 3 - 3
Dependencies/OmniBLE/OmniBLE/pt-PT.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Sicherheitserinnerung";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
+"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfiguration";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/ro.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Mementouri de confimare";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Memento-urile de confirmare sunt semnale sonore de la Pod care pot fi folosite pentru confirmarea comenzilor selectate.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Memento-urile de confirmare sunt semnale sonore de la Pod care pot fi folosite pentru confirmarea comenzilor selectate.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Mementourile de confirmare vor suna pentru comenzile pe care le inițiați, precum bolus, anualarea bolusului, suspendare, reluare, salvarea mementourilor de confirmare etc. Când Loop ajustează automat administrarea, nu sunt folosite mementouri de confirmare.";
+"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." = "Mementourile de confirmare vor suna pentru comenzile pe care le inițiați, precum bolus, anualarea bolusului, suspendare, reluare, salvarea mementourilor de confirmare etc. Când Loop ajustează automat administrarea, nu sunt folosite mementouri de confirmare.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Mementourile de confirmare vor suna atunci când Loop ajustează automat administrare, precum și pentru comenzile pe care le inițiați.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Mementourile de confirmare vor suna atunci când Loop ajustează automat administrare, precum și pentru comenzile pe care le inițiați.";
 
 /* Section header for configuration section */
 "Configuration" = "Configurare";

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


+ 3 - 3
Dependencies/OmniBLE/OmniBLE/sk.lproj/Localizable.strings

@@ -130,13 +130,13 @@
 "Communication issue: Unacknowledged command pending." = "Problém s komunikáciou: Čaká sa na potvrdenie príkazu.";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Potvrdzovacie pripomienky sú pípnutia z podu, ktoré možno použiť pre potvrdenie vybraných príkazov.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Potvrdzovacie pripomienky sú pípnutia z podu, ktoré možno použiť pre potvrdenie vybraných príkazov.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Pripomienky spoľahlivosti zaznejú pri príkazoch, ktoré spustíte, ako je bolus, zrušenie bolusu, pozastavenie, obnovenie, uloženie upozornení atď. Keď Loop automaticky upraví podanie, nepoužijú sa žiadne pripomenutia spoľahlivosti.";
+"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." = "Pripomienky spoľahlivosti zaznejú pri príkazoch, ktoré spustíte, ako je bolus, zrušenie bolusu, pozastavenie, obnovenie, uloženie upozornení atď. Keď Loop automaticky upraví podanie, nepoužijú sa žiadne pripomenutia spoľahlivosti.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Keď Loop automaticky upraví podávanie, ako aj prevedie príkazy, ktoré iniciujete, zaznejú pripomenutia spoľahlivosti.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Keď Loop automaticky upraví podávanie, ako aj prevedie príkazy, ktoré iniciujete, zaznejú pripomenutia spoľahlivosti.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfigurácia";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/tr.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Emniyet Hatırlatıcıları";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Emniyet hatırlatıcıları, poddan gelen ve seçilen komutları onaylamak için kullanılabilen bip sesleridir.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Emniyet hatırlatıcıları, poddan gelen ve seçilen komutları onaylamak için kullanılabilen bip sesleridir.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Emniyet hatırlatıcıları, bolus, bolus iptali, askıya alma, devam ettirme, bildirim hatırlatıcılarını kaydetme gibi başlattığınız komutlar için çalacaktır. Loop iletimi otomatik olarak ayarladığında emniyet hatırlatıcıları kullanılmaz.";
+"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." = "Emniyet hatırlatıcıları, bolus, bolus iptali, askıya alma, devam ettirme, bildirim hatırlatıcılarını kaydetme gibi başlattığınız komutlar için çalacaktır. Loop iletimi otomatik olarak ayarladığında emniyet hatırlatıcıları kullanılmaz.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Emniyet hatırlatıcıları, Başlattığınız komutların yanı sıra Loop iletimi otomatik olarak ayarladığında çalacaktır.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Emniyet hatırlatıcıları, Başlattığınız komutların yanı sıra Loop iletimi otomatik olarak ayarladığında çalacaktır.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfigürasyon";

+ 3 - 3
Dependencies/OmniBLE/OmniBLE/uk.lproj/Localizable.strings

@@ -191,13 +191,13 @@
 "Confidence Reminders" = "Sicherheitserinnerung";
 
 /* No comment provided by engineer. */
-"Confidence reminders are beeps from the pod which can be used to acknowledge selected commands." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Vertrauenserinnerungen sind Pieptöne vom Pod, die verwendet werden können, um ausgewählte Befehle zu bestätigen.";
 
 /* Description for BeepPreference.manualCommands */
-"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
+"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." = "Vertrauenserinnerungen ertönen für von Ihnen initiierte Befehle, wie Bolus, Bolus abbrechen, Unterbrechen, Fortsetzen, Benachrichtigungserinnerungen speichern usw. Wenn Loop die Abgabe automatisch anpasst, werden keine Vertrauenserinnerungen verwendet.";
 
 /* Description for BeepPreference.extended */
-"Confidence reminders will sound when Loop automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
+"Confidence reminders will sound when the app automatically adjusts delivery as well as for commands you initiate." = "Vertrauenserinnerungen ertönen, wenn Loop die Lieferung automatisch anpasst, sowie für von Ihnen initiierte Befehle.";
 
 /* Section header for configuration section */
 "Configuration" = "Konfiguration";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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