Quellcode durchsuchen

New and updated Pod Diagnostics & miscellaneous fixes (#395)

+ New submenu for all the pod diagnostics
+ New Pulse Log Plus, Activation Time & Triggered Alerts commands
+ Renamed & reworked ReadPulseLogView to a new common ReadPodInfoView
+ Add PumpManager & ViewModel funcs for new PodInfo commands
+ Rename PodInfoConfiguredAlerts to PodInfoTriggeredAlerts
+ Correct PodInfoActivationTime & PodInfoTriggeredAlerts defs
+ Update PodInfoPulseLog & PodInfoPulseLogPlus output
+ Update TimeIntervalStr to only include minutes when non-zero
+ PumpManager fixes needed to handle repeated PumpManagerAlerts
+ Two Bluetooth fixes needed to handle payloads > 255 bytes
Joe Moran vor 2 Jahren
Ursprung
Commit
088dbe021d
34 geänderte Dateien mit 1014 neuen und 485 gelöschten Zeilen
  1. 12 8
      Dependencies/OmniBLE/OmniBLE.xcodeproj/project.pbxproj
  2. 1 1
      Dependencies/OmniBLE/OmniBLE/Bluetooth/Packet/BLEPacket.swift
  3. 1 6
      Dependencies/OmniBLE/OmniBLE/Bluetooth/StringLengthPrefixEncoding.swift
  4. 1 1
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  5. 5 5
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift
  6. 28 17
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoActivationTime.swift
  7. 0 56
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift
  8. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  9. 13 0
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift
  10. 92 0
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoTriggeredAlerts.swift
  11. 87 6
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift
  12. 24 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift
  13. 15 19
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift
  14. 10 11
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PlayTestBeepsView.swift
  15. 89 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PodDiagnostics.swift
  16. 8 4
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PumpManagerDetailsView.swift
  17. 39 33
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPulseLogView.swift
  18. 68 67
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPodStatusView.swift
  19. 30 26
      Dependencies/OmniKit/OmniKit.xcodeproj/project.pbxproj
  20. 1 1
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  21. 5 5
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfo.swift
  22. 28 17
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoActivationTime.swift
  23. 0 55
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift
  24. 3 3
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  25. 13 0
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift
  26. 91 0
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoTriggeredAlerts.swift
  27. 90 6
      Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift
  28. 26 2
      Dependencies/OmniKit/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift
  29. 15 19
      Dependencies/OmniKit/OmniKitUI/Views/OmnipodSettingsView.swift
  30. 10 10
      Dependencies/OmniKit/OmniKitUI/Views/PlayTestBeepsView.swift
  31. 90 0
      Dependencies/OmniKit/OmniKitUI/Views/PodDiagnostics.swift
  32. 8 4
      Dependencies/OmniKit/OmniKitUI/Views/PumpManagerDetailsView.swift
  33. 40 33
      Dependencies/OmniKit/OmniKitUI/Views/ReadPulseLogView.swift
  34. 68 67
      Dependencies/OmniKit/OmniKitUI/Views/ReadPodStatusView.swift

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

@@ -45,7 +45,7 @@
 		10389A2626FF7841002115E9 /* TempBasalExtraCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0726FF7841002115E9 /* TempBasalExtraCommand.swift */; };
 		10389A2726FF7841002115E9 /* DeactivatePodCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0826FF7841002115E9 /* DeactivatePodCommand.swift */; };
 		10389A2826FF7841002115E9 /* AcknowledgeAlertCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0926FF7841002115E9 /* AcknowledgeAlertCommand.swift */; };
-		10389A2926FF7841002115E9 /* PodInfoConfiguredAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0A26FF7841002115E9 /* PodInfoConfiguredAlerts.swift */; };
+		10389A2926FF7841002115E9 /* PodInfoTriggeredAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0A26FF7841002115E9 /* PodInfoTriggeredAlerts.swift */; };
 		10389A2A26FF7841002115E9 /* MessageBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0B26FF7841002115E9 /* MessageBlock.swift */; };
 		10389A2B26FF7841002115E9 /* PlaceholderMessageBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0C26FF7841002115E9 /* PlaceholderMessageBlock.swift */; };
 		10389A2C26FF7841002115E9 /* PodInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10389A0D26FF7841002115E9 /* PodInfo.swift */; };
@@ -173,9 +173,10 @@
 		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 */; };
+		D85AEABC2B12D76F00081044 /* PodDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85AEABB2B12D76F00081044 /* PodDiagnostics.swift */; };
+		D85AEAC42B13083F00081044 /* ReadPodInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85AEAC32B13083F00081044 /* ReadPodInfoView.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 */; };
@@ -251,7 +252,7 @@
 		10389A0726FF7841002115E9 /* TempBasalExtraCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempBasalExtraCommand.swift; sourceTree = "<group>"; };
 		10389A0826FF7841002115E9 /* DeactivatePodCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivatePodCommand.swift; sourceTree = "<group>"; };
 		10389A0926FF7841002115E9 /* AcknowledgeAlertCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgeAlertCommand.swift; sourceTree = "<group>"; };
-		10389A0A26FF7841002115E9 /* PodInfoConfiguredAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfoConfiguredAlerts.swift; sourceTree = "<group>"; };
+		10389A0A26FF7841002115E9 /* PodInfoTriggeredAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfoTriggeredAlerts.swift; sourceTree = "<group>"; };
 		10389A0B26FF7841002115E9 /* MessageBlock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageBlock.swift; sourceTree = "<group>"; };
 		10389A0C26FF7841002115E9 /* PlaceholderMessageBlock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderMessageBlock.swift; sourceTree = "<group>"; };
 		10389A0D26FF7841002115E9 /* PodInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfo.swift; sourceTree = "<group>"; };
@@ -433,9 +434,10 @@
 		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>"; };
+		D85AEABB2B12D76F00081044 /* PodDiagnostics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodDiagnostics.swift; sourceTree = "<group>"; };
+		D85AEAC32B13083F00081044 /* ReadPodInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodInfoView.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>"; };
@@ -540,10 +542,10 @@
 				10389A0C26FF7841002115E9 /* PlaceholderMessageBlock.swift */,
 				10389A0D26FF7841002115E9 /* PodInfo.swift */,
 				10389A0626FF7841002115E9 /* PodInfoActivationTime.swift */,
-				10389A0A26FF7841002115E9 /* PodInfoConfiguredAlerts.swift */,
 				10389A0426FF7841002115E9 /* PodInfoPulseLog.swift */,
 				10389A1026FF7841002115E9 /* PodInfoPulseLogPlus.swift */,
 				10389A1A26FF7841002115E9 /* PodInfoResponse.swift */,
+				10389A0A26FF7841002115E9 /* PodInfoTriggeredAlerts.swift */,
 				10389A1B26FF7841002115E9 /* SetInsulinScheduleCommand.swift */,
 				10389A1826FF7841002115E9 /* SetupPodCommand.swift */,
 				10389A1126FF7841002115E9 /* StatusResponse.swift */,
@@ -759,12 +761,13 @@
 				C1F67E7F27975B830017487F /* PairPodView.swift */,
 				D845A13A2AF89F7100EA0853 /* PlayTestBeepsView.swift */,
 				C1F67E8A27975B830017487F /* PodDetailsView.swift */,
+				D85AEABB2B12D76F00081044 /* PodDiagnostics.swift */,
 				8475311F26ED838A009FD801 /* PodLifeHUDView.swift */,
 				8475312426ED838A009FD801 /* PodLifeHUDView.xib */,
 				C1F67E8827975B830017487F /* PodSetupView.swift */,
 				D845A13E2AF89F8400EA0853 /* PumpManagerDetailsView.swift */,
+				D85AEAC32B13083F00081044 /* ReadPodInfoView.swift */,
 				D845A13C2AF89F8400EA0853 /* ReadPodStatusView.swift */,
-				D845A13D2AF89F8400EA0853 /* ReadPulseLogView.swift */,
 				C1F67E7427975B830017487F /* ScheduledExpirationReminderEditView.swift */,
 				C1F67E8727975B830017487F /* SetupCompleteView.swift */,
 				D845A1422AF89F9200EA0853 /* SilencePodSelectionView.swift */,
@@ -1085,6 +1088,7 @@
 				10389A3A26FF7841002115E9 /* SetInsulinScheduleCommand.swift in Sources */,
 				D845A13B2AF89F7100EA0853 /* PlayTestBeepsView.swift in Sources */,
 				10389A3826FF7841002115E9 /* DetailedStatus.swift in Sources */,
+				D85AEABC2B12D76F00081044 /* PodDiagnostics.swift in Sources */,
 				C1F67E9927975B830017487F /* NotificationSettingsView.swift in Sources */,
 				10389A2B26FF7841002115E9 /* PlaceholderMessageBlock.swift in Sources */,
 				10389A3026FF7841002115E9 /* StatusResponse.swift in Sources */,
@@ -1099,7 +1103,7 @@
 				C1F67EDA27979E400017487F /* PumpManagerAlert.swift in Sources */,
 				10389A2D26FF7841002115E9 /* BolusExtraCommand.swift in Sources */,
 				8475315926EDA193009FD801 /* UIColor.swift in Sources */,
-				10389A2926FF7841002115E9 /* PodInfoConfiguredAlerts.swift in Sources */,
+				10389A2926FF7841002115E9 /* PodInfoTriggeredAlerts.swift in Sources */,
 				C1F67EB727975E710017487F /* LeadingImage.swift in Sources */,
 				C1F67ECB27975F360017487F /* PodLifeState.swift in Sources */,
 				8475315B26EDA193009FD801 /* NibLoadable.swift in Sources */,
@@ -1107,6 +1111,7 @@
 				8475313226ED838B009FD801 /* PodLifeHUDView.swift in Sources */,
 				C1C001BF27A2337B00533D35 /* PeripheralManager+OmniBLE.swift in Sources */,
 				10389A3F26FF7841002115E9 /* CRC16.swift in Sources */,
+				D85AEAC42B13083F00081044 /* ReadPodInfoView.swift in Sources */,
 				10289E68271B2A08000339E6 /* NumberFormatter.swift in Sources */,
 				1016325927185EE4007A3BC2 /* BeepType.swift in Sources */,
 				C1F67E9E27975B830017487F /* LowReservoirReminderEditView.swift in Sources */,
@@ -1146,7 +1151,6 @@
 				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 */,

+ 1 - 1
Dependencies/OmniBLE/OmniBLE/Bluetooth/Packet/BLEPacket.swift

@@ -60,7 +60,7 @@ struct FirstBlePacket: BlePacket {
         }
 
         let fullFragments = Int(payload[1])
-        guard (fullFragments < MAX_FRAGMENTS) else {
+        guard (fullFragments <= MAX_FRAGMENTS) else {
             throw PodProtocolError.messageIOException(String(format: "Received more than %d fragments", MAX_FRAGMENTS))
         }
         guard (fullFragments > 0) else {

+ 1 - 6
Dependencies/OmniBLE/OmniBLE/Bluetooth/StringLengthPrefixEncoding.swift

@@ -30,12 +30,7 @@ final class StringLengthPrefixEncoding {
                 throw PodProtocolError.messageIOException("Payload too short: \(payload)")
             }
             remaining = remaining.subdata(in: key.count..<remaining.count)
-
-            //        let value = data.withUnsafeBytes {
-            //            $0.load(as: Int16.self).bigEndian
-            //        }
-            let ulength = UInt16(remaining[0] << 8) | UInt16(remaining[1])
-            let length = (ulength <= UInt(Int.max)) ? Int(ulength) : Int(ulength - UInt16(Int.max) - 1) + Int.min
+            let length = Int(remaining[0...].toBigEndian(UInt16.self))
             guard remaining.count >= length else {
                 throw PodProtocolError.messageIOException("Payload too short: \(payload)")
             }

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

@@ -169,7 +169,7 @@ extension TimeInterval {
         if hours != 0 {
             str += String(format: "%uh", hours)
         }
-        if minutes != 0 || hours != 0 {
+        if minutes != 0 {
             str += String(format: "%um", minutes)
         }
         if seconds != 0 || str.isEmpty {

+ 5 - 5
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift

@@ -18,10 +18,10 @@ public protocol PodInfo {
 
 public enum PodInfoResponseSubType: UInt8, Equatable {
     case normal                      = 0x00
-    case configuredAlerts            = 0x01 // Returns information on configured alerts
-    case detailedStatus              = 0x02 // Returned on any pod fault
+    case triggeredAlerts             = 0x01 // Returns values for any unacknowledged triggered alerts
+    case detailedStatus              = 0x02 // Returns detailed pod status, returned for most calls after a pod fault
     case pulseLogPlus                = 0x03 // Returns up to the last 60 pulse log entries plus additional info
-    case activationTime              = 0x05 // Returns activation date, elapsed time, and fault code
+    case activationTime              = 0x05 // Returns pod activation time and possible fault code & fault time
     case pulseLogRecent              = 0x50 // Returns the last 50 pulse log entries
     case pulseLogPrevious            = 0x51 // Like 0x50, but returns up to the previous 50 entries before the last 50
     
@@ -29,8 +29,8 @@ public enum PodInfoResponseSubType: UInt8, Equatable {
         switch self {
         case .normal:
             return StatusResponse.self as! PodInfo.Type
-        case .configuredAlerts:
-            return PodInfoConfiguredAlerts.self
+        case .triggeredAlerts:
+            return PodInfoTriggeredAlerts.self
         case .detailedStatus:
             return DetailedStatus.self
         case .pulseLogPlus:

+ 28 - 17
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoActivationTime.swift

@@ -9,7 +9,7 @@
 
 import Foundation
 
-// Type 5 PodInfo returns the pod activation time, time pod alive, and the possible fault code
+// Type 5 PodInfo returns the pod activation time and possible fault code & fault time
 public struct PodInfoActivationTime : PodInfo {
     // OFF 1  2  3  4 5  6 7 8 9 10111213 1415161718
     // DATA   0  1  2 3  4 5 6 7 8 9 1011 1213141516
@@ -17,8 +17,12 @@ public struct PodInfoActivationTime : PodInfo {
 
     public let podInfoType: PodInfoResponseSubType = .activationTime
     public let faultEventCode: FaultEventCode
-    public let timeActivation: TimeInterval
-    public let dateTime: DateComponents
+    public let faultTime: TimeInterval
+    public let year: Int
+    public let month: Int
+    public let day: Int
+    public let hour: Int
+    public let minute: Int
     public let data: Data
     
     public init(encodedData: Data) throws {
@@ -26,22 +30,29 @@ public struct PodInfoActivationTime : PodInfo {
             throw MessageBlockError.notEnoughData
         }
         self.faultEventCode = FaultEventCode(rawValue: encodedData[1])
-        self.timeActivation = TimeInterval(minutes: Double((Int(encodedData[2] & 0b1) << 8) + Int(encodedData[3])))
-        self.dateTime = DateComponents(encodedDateTime: encodedData.subdata(in: 12..<17))
+        self.faultTime = TimeInterval(minutes: Double((Int(encodedData[2]) << 8) + Int(encodedData[3])))
+        self.year   = Int(encodedData[14])
+        self.month  = Int(encodedData[12])
+        self.day    = Int(encodedData[13])
+        self.hour   = Int(encodedData[15])
+        self.minute = Int(encodedData[16])
         self.data = Data(encodedData)
     }
 }
 
-extension DateComponents {
-    init(encodedDateTime: Data) {
-        self.init()
-        
-        year   = Int(encodedDateTime[2]) + 2000
-        month  = Int(encodedDateTime[0])
-        day    = Int(encodedDateTime[1])
-        hour   = Int(encodedDateTime[3])
-        minute = Int(encodedDateTime[4])
-        
-        calendar = Calendar(identifier: .gregorian)
-    }
+func activationTimeString(podInfoActivationTime: PodInfoActivationTime) -> String {
+    var result: [String] = []
+
+    // activation time info
+    result.append(String(format: "Year:   %u", podInfoActivationTime.year))
+    result.append(String(format: "Month:  %u", podInfoActivationTime.month))
+    result.append(String(format: "Day:    %u", podInfoActivationTime.day))
+    result.append(String(format: "Hour:   %u", podInfoActivationTime.hour))
+    result.append(String(format: "Minute: %u", podInfoActivationTime.minute))
+
+    // pod fault info
+    result.append(String(format: "\n%@", String(describing: podInfoActivationTime.faultEventCode)))
+    result.append(String(format: "Fault Time: %@", podInfoActivationTime.faultTime.timeIntervalStr))
+
+    return result.joined(separator: "\n")
 }

+ 0 - 56
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift

@@ -1,56 +0,0 @@
-//
-//  PodInfoConfiguredAlerts.swift
-//  OmniBLE
-//
-//  From OmniKit/MessageTransport/MessageBlocks/PodInfoConfiguredAlerts.swift
-//  Created by Eelke Jager on 16/09/2018.
-//  Copyright © 2018 Pete Schwamb. All rights reserved.
-//
-
-import Foundation
-
-// Type 1 Pod Info returns information about the currently configured alerts
-public struct PodInfoConfiguredAlerts : PodInfo {
-    // CMD 1  2  3 4  5 6  7 8  910 1112 1314 1516 1718 1920
-    // DATA   0  1 2  3 4  5 6  7 8  910 1112 1314 1516 1718
-    // 02 13 01 XXXX VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV
-
-    public let podInfoType : PodInfoResponseSubType = .configuredAlerts
-    public let word_278    : Data
-    public let alertsActivations : [AlertActivation]
-    public let data       : Data
-
-    public struct AlertActivation {
-        let beepType: BeepType
-        let unitsLeft: Double
-        let timeFromPodStart: UInt8
-        
-        public init(beepType: BeepType, timeFromPodStart: UInt8, unitsLeft: Double) {
-            self.beepType = beepType
-            self.timeFromPodStart = timeFromPodStart
-            self.unitsLeft = unitsLeft
-        }
-    }
-    
-    public init(encodedData: Data) throws {
-        guard encodedData.count >= 11 else {
-            throw MessageBlockError.notEnoughData
-        }
-
-        self.word_278 = encodedData[1...2]
-        
-        let numAlertTypes = 8
-        let beepType = BeepType.self
-        
-        var activations = [AlertActivation]()
-
-        for alarmType in (0..<numAlertTypes) {
-            let beepType = beepType.init(rawValue: UInt8(alarmType))
-            let timeFromPodStart = encodedData[(3 + alarmType * 2)] // Double(encodedData[(5 + alarmType)] & 0x3f)
-            let unitsLeft = Double(encodedData[(4 + alarmType * 2)]) / Pod.pulsesPerUnit
-            activations.append(AlertActivation(beepType: beepType!, timeFromPodStart: timeFromPodStart, unitsLeft: unitsLeft))
-        }
-        alertsActivations = activations
-        self.data         = encodedData
-    }
-}

+ 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"
+    var result: [String] = ["Pulse eeeeee0a pppliiib cccccccc dfgggggg"]
     var index = pulseLogEntries.count - 1
     var pulseNumber = lastPulseNumber
     while index >= 0 {
-        str += String(format: "\n%04d:", pulseNumber) + UInt32(pulseLogEntries[index]).binaryDescription
+        result.append(String(format: "%04d:%@", pulseNumber, UInt32(pulseLogEntries[index]).binaryDescription))
         index -= 1
         pulseNumber -= 1
     }
-    return str
+    return result.joined(separator: "\n")
 }

+ 13 - 0
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift

@@ -50,3 +50,16 @@ public struct PodInfoPulseLogPlus : PodInfo {
         self.data = encodedData
     }
 }
+
+func pulseLogPlusString(podInfoPulseLogPlus: PodInfoPulseLogPlus) -> String {
+    var result: [String] = []
+
+    result.append(String(format: "Pod Active: %@", podInfoPulseLogPlus.timeActivation.timeIntervalStr))
+    result.append(String(format: "Fault Time: %@", podInfoPulseLogPlus.timeFaultEvent.timeIntervalStr))
+    result.append(String(format: "%@\n", String(describing: podInfoPulseLogPlus.faultEventCode)))
+
+    let lastPulseNumber = Int(podInfoPulseLogPlus.nEntries)
+    result.append(pulseLogString(pulseLogEntries: podInfoPulseLogPlus.pulseLog, lastPulseNumber: lastPulseNumber))
+
+    return result.joined(separator: "\n")
+}

+ 92 - 0
Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoTriggeredAlerts.swift

@@ -0,0 +1,92 @@
+//
+//  PodInfoTriggeredAlerts.swift
+//  OmniBLE
+//
+//  From OmniKit/MessageTransport/MessageBlocks/PodInfoTriggeredAlerts.swift
+//  Created by Eelke Jager on 16/09/2018.
+//  Copyright © 2018 Pete Schwamb. All rights reserved.
+//
+
+import Foundation
+
+// Type 1 Pod Info returns information about the currently unacknowledged triggered alert values
+public struct PodInfoTriggeredAlerts: PodInfo {
+    // CMD 1  2  3 4  5 6  7 8  910 1112 1314 1516 1718 1920
+    // DATA   0  1 2  3 4  5 6  7 8  910 1112 1314 1516 1718
+    // 02 13 01 XXXX VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV
+
+    public let podInfoType: PodInfoResponseSubType = .triggeredAlerts
+    public let unknown_word: UInt16
+    public let alertsActivations: [AlertActivation]
+    public let data: Data
+
+    public struct AlertActivation {
+        let triggeredAlertValue: TriggeredAlertValue
+
+        public init(triggeredAlertValue: TriggeredAlertValue) {
+            self.triggeredAlertValue = triggeredAlertValue
+        }
+    }
+
+    public init(encodedData: Data) throws {
+        guard encodedData.count >= 11 else {
+            throw MessageBlockError.notEnoughData
+        }
+
+        let numAlerts = 8
+        var activations = [AlertActivation]()
+        var i = 3 // starting data index for first VVVV value
+        for alertNum in (0..<numAlerts) {
+            let val = Double(encodedData[i...].toBigEndian(UInt16.self))
+            if AlertSlot(rawValue: UInt8(alertNum)) == .slot4LowReservoir {
+                let triggeredAlertValue: TriggeredAlertValue = .unitsRemaining(val / Pod.pulsesPerUnit)
+                activations.append(AlertActivation(triggeredAlertValue: triggeredAlertValue))
+            } else {
+                let triggeredAlertValue: TriggeredAlertValue = .podTime(TimeInterval(minutes: val))
+                activations.append(AlertActivation(triggeredAlertValue: triggeredAlertValue))
+            }
+            i += 2
+        }
+        self.unknown_word = encodedData[1...].toBigEndian(UInt16.self)
+        self.alertsActivations = activations
+        self.data = encodedData
+    }
+}
+
+public enum TriggeredAlertValue {
+    case unitsRemaining(Double)
+    case podTime(TimeInterval)
+}
+
+extension TriggeredAlertValue: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch self {
+        case .unitsRemaining(let units):
+            if units != 0 {
+                return "\(Int(units))U"
+            }
+        case .podTime(let triggerTime):
+            if triggerTime != 0 {
+                return "\(triggerTime.timeIntervalStr)"
+            }
+        }
+        return ""
+    }
+}
+
+func triggeredAlertsString(podInfoTriggeredAlerts: PodInfoTriggeredAlerts) -> String {
+    var result: [String] = []
+
+    for index in podInfoTriggeredAlerts.alertsActivations.indices {
+        // extract the alert slot debug description for a more helpful display
+        let description = AlertSlot(rawValue: UInt8(index)).debugDescription
+        let start = description.index(description.startIndex, offsetBy: 27)
+        let end = description.index(description.endIndex, offsetBy: -1)
+        let range = start..<end
+
+        let alert = podInfoTriggeredAlerts.alertsActivations[index]
+        result.append(String(format: "%@: %@", String(description[range]), String(describing: alert.triggeredAlertValue)))
+    }
+
+    return result.joined(separator: "\n")
+}

+ 87 - 6
Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift

@@ -1230,7 +1230,7 @@ extension OmniBLEPumpManager {
     }
 
     public func readPulseLog(completion: @escaping (Result<String, Error>) -> Void) {
-        // use hasSetupPod to be able to read pulse log from a faulted Pod
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
         guard self.hasSetupPod else {
             completion(.failure(OmniBLEPumpManagerError.noPodPaired))
             return
@@ -1266,6 +1266,87 @@ extension OmniBLEPumpManager {
         }
     }
 
+    public func readPulseLogPlus(completion: @escaping (Result<String, Error>) -> Void) {
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion(.failure(OmniBLEPumpManagerError.noPodPaired))
+            return
+        }
+        guard state.podState?.isFaulted == true || state.podState?.unfinalizedBolus?.scheduledCertainty == .uncertain || state.podState?.unfinalizedBolus?.isFinished() != false else
+        {
+            self.log.info("Skipping Read Pulse Log Plus due to bolus still in progress.")
+            completion(.failure(PodCommsError.unfinalizedBolus))
+            return
+        }
+
+        podComms.runSession(withName: "Read Pulse Log Plus") { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .bipBeeeeep)
+                    let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .pulseLogPlus, beepBlock: beepBlock)
+                    let podInfoPulseLogPlus = podInfoResponse.podInfo as! PodInfoPulseLogPlus
+                    let str = pulseLogPlusString(podInfoPulseLogPlus: podInfoPulseLogPlus)
+                    completion(.success(str))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+
+    public func readActivationTime(completion: @escaping (Result<String, Error>) -> Void) {
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion(.failure(OmniBLEPumpManagerError.noPodPaired))
+            return
+        }
+
+        podComms.runSession(withName: "Read Activation Time") { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .beepBeep)
+                    let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .activationTime, beepBlock: beepBlock)
+                    let podInfoActivationTime = podInfoResponse.podInfo as! PodInfoActivationTime
+                    let str = activationTimeString(podInfoActivationTime: podInfoActivationTime)
+                    completion(.success(str))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+
+    public func readTriggeredAlerts(completion: @escaping (Result<String, Error>) -> Void) {
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion(.failure(OmniBLEPumpManagerError.noPodPaired))
+            return
+        }
+
+        podComms.runSession(withName: "Read Triggered Alerts") { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .beepBeep)
+                    let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .triggeredAlerts, beepBlock: beepBlock)
+                    let podInfoTriggeredAlerts = podInfoResponse.podInfo as! PodInfoTriggeredAlerts
+                    let str = triggeredAlertsString(podInfoTriggeredAlerts: podInfoTriggeredAlerts)
+                    completion(.success(str))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+
     public func setConfirmationBeeps(newPreference: BeepPreference, completion: @escaping (OmniBLEPumpManagerError?) -> Void) {
 
         // If there isn't an active pod or the pod is currently silenced,
@@ -1361,10 +1442,10 @@ extension OmniBLEPumpManager {
             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)
+                // it's possible that a response from a previous successful pod alert configuration can be lost
+                // and thus the alert won't get reset here when reconfiguring pod alerts with a new silence pod state.
+                let acknowledgeAll = true   // protect against lost alert configuration response related issues
+                try session.configureAlerts(podAlerts, acknowledgeAll: acknowledgeAll, beepBlock: beepBlock)
                 self.setState { (state) in
                     state.silencePod = silencePod
                 }
@@ -2326,7 +2407,7 @@ extension OmniBLEPumpManager {
         }
 
         for alert in state.activeAlerts {
-            if alert.alertIdentifier == alertIdentifier {
+            if alert.alertIdentifier == alertIdentifier || alert.repeatingAlertIdentifier == 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 {

+ 24 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift

@@ -340,6 +340,30 @@ class OmniBLESettingsViewModel: ObservableObject {
         }
     }
 
+    func readPulseLogPlus(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readPulseLogPlus() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
+    func readActivationTime(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readActivationTime() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
+    func readTriggeredAlerts(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readTriggeredAlerts() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
     func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
         pumpManager.playTestBeeps(completion: completion)
     }

+ 15 - 19
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift

@@ -366,7 +366,8 @@ struct OmniBLESettingsView: View  {
 
                 if let podDetails = self.viewModel.podDetails {
                     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)
+                        FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row")
+                            .foregroundColor(Color.primary)
                     }
                 } else {
                     HStack {
@@ -379,7 +380,8 @@ struct OmniBLESettingsView: View  {
 
                 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)
+                        FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row")
+                            .foregroundColor(Color.primary)
                     }
                 } else {
                     HStack {
@@ -416,7 +418,8 @@ struct OmniBLESettingsView: View  {
                 }
                 NavigationLink(destination: BeepPreferenceSelectionView(initialValue: viewModel.beepPreference, onSave: viewModel.setConfirmationBeeps)) {
                     HStack {
-                        FrameworkLocalText("Confidence Reminders", comment: "Text for confidence reminders navigation link").foregroundColor(Color.primary)
+                        FrameworkLocalText("Confidence Reminders", comment: "Text for confidence reminders navigation link")
+                            .foregroundColor(Color.primary)
                         Spacer()
                         Text(viewModel.beepPreference.title)
                             .foregroundColor(.secondary)
@@ -424,7 +427,8 @@ struct OmniBLESettingsView: View  {
                 }
                 NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) {
                     HStack {
-                        FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link").foregroundColor(Color.primary)
+                        FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link")
+                            .foregroundColor(Color.primary)
                         Spacer()
                         Text(viewModel.silencePodPreference.title)
                             .foregroundColor(.secondary)
@@ -472,21 +476,13 @@ struct OmniBLESettingsView: View  {
                 }
             }
 
-            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)
+            Section() {
+                NavigationLink(destination: PodDiagnosticsView(
+                    title: LocalizedString("Pod Diagnostics", comment: "Title for the pod diagnostic view"),
+                    viewModel: viewModel))
+                {
+                    FrameworkLocalText("Pod Diagnostics", comment: "Text for pod diagnostics row")
+                        .foregroundColor(Color.primary)
                 }
             }
 

+ 10 - 11
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PlayTestBeepsView.swift

@@ -14,7 +14,11 @@ struct PlayTestBeepsView: View {
     @Environment(\.horizontalSizeClass) var horizontalSizeClass
     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
 
-    private var toRun: ((_ completion: @escaping (_ result: Error?) -> Void) -> Void)?
+    var toRun: ((_ completion: @escaping (_ result: Error?) -> Void) -> Void)?
+
+    private let title = LocalizedString("Play Test Beeps", comment: "navigation title for play test beeps")
+    private let actionString = LocalizedString("Playing Test Beeps...", comment: "button title when executing play test beeps")
+    private let failedString: String = LocalizedString("Failed to play test beeps.", comment: "Alert title for error when playing test beeps")
 
     @State private var alertIsPresented: Bool = false
     @State private var displayString: String = ""
@@ -23,10 +27,6 @@ struct PlayTestBeepsView: View {
     @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 {
@@ -48,7 +48,7 @@ struct PlayTestBeepsView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Play Test Beeps", comment: "navigation title for play test beeps"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .onFirstAppear {
@@ -61,6 +61,7 @@ struct PlayTestBeepsView: View {
             executing = true
             self.displayString = ""
             toRun?() { (error) in
+                executing = false
                 if let error = error {
                     self.displayString = ""
                     self.error = error
@@ -68,26 +69,24 @@ struct PlayTestBeepsView: View {
                 } 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")
+            return actionString
         } else {
-            return LocalizedString("Play Test Beeps", comment: "button title to play test beeps")
+            return title
         }
     }
 
     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")),
+            title: Text(failedString),
             message: Text(error?.localizedDescription ?? "No Error")
         )
     }
-
 }
 
 struct PlayTestBeepsView_Previews: PreviewProvider {

+ 89 - 0
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PodDiagnostics.swift

@@ -0,0 +1,89 @@
+//
+//  PodDiagnotics.swift
+//  OmniBLE
+//
+//  Created by Joseph Moran on 11/25/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+import LoopKit
+import LoopKitUI
+import HealthKit
+
+
+struct PodDiagnosticsView: View  {
+
+    var title: String
+    
+    @ObservedObject var viewModel: OmniBLESettingsViewModel
+
+    var body: some View {
+        List {
+            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: 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: ReadPodInfoView(
+                title: LocalizedString("Read Pulse Log", comment: "Text for read pulse log title"),
+                actionString: LocalizedString("Reading Pulse Log...", comment: "Text for read pulse log action"),
+                failedString: LocalizedString("Failed to read pulse log.", comment: "Alert title for error when reading pulse log"),
+                toRun: viewModel.readPulseLog))
+            {
+                FrameworkLocalText("Read Pulse Log", comment: "Text for read pulse log navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: ReadPodInfoView(
+                title: LocalizedString("Read Pulse Log Plus", comment: "Text for read pulse log plus title"),
+                actionString: LocalizedString("Reading Pulse Log Plus...", comment: "Text for read pulse log plus action"),
+                failedString: LocalizedString("Failed to read pulse log plus.", comment: "Alert title for error when reading pulse log plus"),
+                toRun: viewModel.readPulseLogPlus))
+            {
+                FrameworkLocalText("Read Pulse Log Plus", comment: "Text for read pulse log plus navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: ReadPodInfoView(
+                title: LocalizedString("Read Activation Time", comment: "Text for read activation time title"),
+                actionString: LocalizedString("Reading Activation Time...", comment: "Text for read activation time action"),
+                failedString: LocalizedString("Failed to read activation time.", comment: "Alert title for error when reading activation time"),
+                toRun: self.viewModel.readActivationTime))
+            {
+                FrameworkLocalText("Read Activation Time", comment: "Text for read activation time navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: ReadPodInfoView(
+                title: LocalizedString("Read Triggered Alerts", comment: "Text for read triggered alerts title"),
+                actionString: LocalizedString("Reading Triggered Alerts...", comment: "Text for read triggered alerts action"),
+                failedString: LocalizedString("Failed to read triggered alerts.", comment: "Alert title for error when reading triggered alerts"),
+                toRun: self.viewModel.readTriggeredAlerts))
+            {
+                FrameworkLocalText("Read Triggered Alerts", comment: "Text for read triggered alerts navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: PumpManagerDetailsView(
+                toRun: self.viewModel.pumpManagerDetails))
+            {
+                FrameworkLocalText("Pump Manager Details", comment: "Text for pump manager details navigation link")
+                    .foregroundColor(Color.primary)
+            }
+        }
+        .insetGroupedListStyle()
+        .navigationBarTitle(title)
+    }
+}

+ 8 - 4
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PumpManagerDetailsView.swift

@@ -14,7 +14,11 @@ struct PumpManagerDetailsView: View {
     @Environment(\.horizontalSizeClass) var horizontalSizeClass
     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
 
-    private var toRun: ((_ completion: @escaping (_ result: String) -> Void) -> Void)?
+    var toRun: ((_ completion: @escaping (_ result: String) -> Void) -> Void)?
+
+    private let title = LocalizedString("Pump Manager Details", comment: "navigation title for pump manager details")
+    private let actionString = LocalizedString("Retrieving Pump Manager Details...", comment: "button title when retrieving pump manager details")
+    private let buttonTitle = LocalizedString("Refresh Pump Manager Details", comment: "button title to refresh pump manager details")
 
     @State private var displayString: String = ""
     @State private var error: Error? = nil
@@ -61,7 +65,7 @@ struct PumpManagerDetailsView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Pump Manager Details", comment: "navigation title for pump manager details"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .onFirstAppear {
             asyncAction()
@@ -81,9 +85,9 @@ struct PumpManagerDetailsView: View {
 
     private var buttonText: String {
         if executing {
-            return LocalizedString("Retrieving Pump Manager Details...", comment: "button title when retrieving pump manager details")
+            return actionString
         } else {
-            return LocalizedString("Refresh Pump Manager Details", comment: "button title to refresh pump manager details")
+            return buttonTitle
         }
     }
 }

+ 39 - 33
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPulseLogView.swift

@@ -1,8 +1,8 @@
 //
-//  ReadPulseLogView.swift
+//  ReadPodInfoView.swift
 //  OmniBLE
 //
-//  Created by Joe Moran on 9/1/23.
+//  Created by Joe Moran on 11/25/23.
 //  Copyright © 2023 LoopKit Authors. All rights reserved.
 //
 
@@ -10,11 +10,15 @@ import SwiftUI
 import LoopKit
 
 
-struct ReadPulseLogView: View {
+struct ReadPodInfoView: View {
     @Environment(\.horizontalSizeClass) var horizontalSizeClass
     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
 
-    private var toRun: ((_ completion: @escaping (_ result: Result<String, Error>) -> Void) -> Void)?
+    var title: String           // e.g., "Read Pulse Log"
+    var actionString: String    // e.g., "Reading Pulse Log..."
+    var failedString: String    // e.g., "Failed to read pulse log."
+
+    var toRun: ((_ completion: @escaping (_ result: Result<String, Error>) -> Void) -> Void)?
 
     @State private var alertIsPresented: Bool = false
     @State private var displayString: String = ""
@@ -22,10 +26,6 @@ struct ReadPulseLogView: View {
     @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 {
@@ -62,7 +62,7 @@ struct ReadPulseLogView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Read Pulse Log", comment: "navigation title for read pulse log"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .onFirstAppear {
@@ -75,54 +75,60 @@ struct ReadPulseLogView: View {
             executing = true
             self.displayString = ""
             toRun?() { (result) in
+                executing = false
                 switch result {
-                case .success(let pulseLogString):
-                    self.displayString = pulseLogString
+                case .success(let resultString):
+                    self.displayString = resultString
                 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")
+            return actionString
         } else {
-            return LocalizedString("Read Pulse Log", comment: "button title to read pulse log")
+            return title
         }
     }
 
     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")),
+            title: Text(failedString),
             message: Text(error?.localizedDescription ?? "No Error")
         )
     }
 }
 
-struct ReadPulsePodLogView_Previews: PreviewProvider {
+struct ReadPodInfoView_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)))
+        NavigationView {
+            ReadPodInfoView(
+                title: "Read Pulse Log",
+                actionString: "Reading Pulse Log...",
+                failedString: "Failed to read pulse log"
+            ) { 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)))
+            }
         }
     }
 }

+ 68 - 67
Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPodStatusView.swift

@@ -9,68 +9,16 @@
 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)?
+    var toRun: ((_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) -> Void)?
+
+    private let title = LocalizedString("Read Pod Status", comment: "navigation title for read pod status")
+    private let actionString = LocalizedString("Reading Pod Status...", comment: "button title when executing read pod status")
+    private let failedString = LocalizedString("Failed to read pod status.", comment: "Alert title for error when reading pod status")
 
     @State private var alertIsPresented: Bool = false
     @State private var displayString: String = ""
@@ -78,10 +26,6 @@ struct ReadPodStatusView: View {
     @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 {
@@ -114,7 +58,7 @@ struct ReadPodStatusView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Read Pod Status", comment: "navigation title for read pod status"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .onFirstAppear {
@@ -127,6 +71,7 @@ struct ReadPodStatusView: View {
             executing = true
             self.displayString = ""
             toRun?() { (result) in
+                executing = false
                 switch result {
                 case .success(let detailedStatus):
                     self.displayString = podStatusString(status: detailedStatus)
@@ -134,27 +79,83 @@ struct ReadPodStatusView: View {
                     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")
+            return actionString
         } else {
-            return LocalizedString("Read Pod Status", comment: "button title to read pod status")
+            return title
         }
     }
 
     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")),
+            title: Text(failedString),
             message: Text(error?.localizedDescription ?? "No Error")
         )
     }
 }
 
+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_Previews: PreviewProvider {
     static var previews: some View {
         NavigationView {
@@ -164,4 +165,4 @@ struct ReadPodStatusView_Previews: PreviewProvider {
             }
         }
     }
- }
+}

+ 30 - 26
Dependencies/OmniKit/OmniKit.xcodeproj/project.pbxproj

@@ -25,7 +25,7 @@
 		C12401BB29C7D8E900B32844 /* TempBasalExtraCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018129C7D8E900B32844 /* TempBasalExtraCommand.swift */; };
 		C12401BC29C7D8E900B32844 /* DeactivatePodCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018229C7D8E900B32844 /* DeactivatePodCommand.swift */; };
 		C12401BD29C7D8E900B32844 /* AcknowledgeAlertCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018329C7D8E900B32844 /* AcknowledgeAlertCommand.swift */; };
-		C12401BE29C7D8E900B32844 /* PodInfoConfiguredAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018429C7D8E900B32844 /* PodInfoConfiguredAlerts.swift */; };
+		C12401BE29C7D8E900B32844 /* PodInfoTriggeredAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018429C7D8E900B32844 /* PodInfoTriggeredAlerts.swift */; };
 		C12401BF29C7D8E900B32844 /* MessageBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018529C7D8E900B32844 /* MessageBlock.swift */; };
 		C12401C029C7D8E900B32844 /* PlaceholderMessageBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018629C7D8E900B32844 /* PlaceholderMessageBlock.swift */; };
 		C12401C129C7D8E900B32844 /* PodInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C124018729C7D8E900B32844 /* PodInfo.swift */; };
@@ -162,9 +162,10 @@
 		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 */; };
+		D85AEAC82B1403C000081044 /* PodDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85AEAC72B1403C000081044 /* PodDiagnostics.swift */; };
+		D85AEACA2B1403CB00081044 /* ReadPodInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -236,7 +237,7 @@
 		C124018129C7D8E900B32844 /* TempBasalExtraCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempBasalExtraCommand.swift; sourceTree = "<group>"; };
 		C124018229C7D8E900B32844 /* DeactivatePodCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivatePodCommand.swift; sourceTree = "<group>"; };
 		C124018329C7D8E900B32844 /* AcknowledgeAlertCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AcknowledgeAlertCommand.swift; sourceTree = "<group>"; };
-		C124018429C7D8E900B32844 /* PodInfoConfiguredAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfoConfiguredAlerts.swift; sourceTree = "<group>"; };
+		C124018429C7D8E900B32844 /* PodInfoTriggeredAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfoTriggeredAlerts.swift; sourceTree = "<group>"; };
 		C124018529C7D8E900B32844 /* MessageBlock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageBlock.swift; sourceTree = "<group>"; };
 		C124018629C7D8E900B32844 /* PlaceholderMessageBlock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderMessageBlock.swift; sourceTree = "<group>"; };
 		C124018729C7D8E900B32844 /* PodInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfo.swift; sourceTree = "<group>"; };
@@ -415,9 +416,10 @@
 		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>"; };
+		D85AEAC72B1403C000081044 /* PodDiagnostics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodDiagnostics.swift; sourceTree = "<group>"; };
+		D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodInfoView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -564,31 +566,31 @@
 		C124017D29C7D8E900B32844 /* MessageBlocks */ = {
 			isa = PBXGroup;
 			children = (
-				C124017E29C7D8E900B32844 /* PodInfoPulseLog.swift */,
-				C124017F29C7D8E900B32844 /* VersionResponse.swift */,
-				C124018029C7D8E900B32844 /* PodInfoActivationTime.swift */,
-				C124018129C7D8E900B32844 /* TempBasalExtraCommand.swift */,
-				C124018229C7D8E900B32844 /* DeactivatePodCommand.swift */,
 				C124018329C7D8E900B32844 /* AcknowledgeAlertCommand.swift */,
-				C124018429C7D8E900B32844 /* PodInfoConfiguredAlerts.swift */,
-				C124018529C7D8E900B32844 /* MessageBlock.swift */,
-				C124018629C7D8E900B32844 /* PlaceholderMessageBlock.swift */,
-				C124018729C7D8E900B32844 /* PodInfo.swift */,
-				C124018829C7D8E900B32844 /* BolusExtraCommand.swift */,
-				C124018929C7D8E900B32844 /* FaultConfigCommand.swift */,
-				C124018A29C7D8E900B32844 /* PodInfoPulseLogPlus.swift */,
-				C124018B29C7D8E900B32844 /* StatusResponse.swift */,
-				C124018C29C7D8E900B32844 /* GetStatusCommand.swift */,
-				C124018D29C7D8E900B32844 /* BasalScheduleExtraCommand.swift */,
-				C124018E29C7D8E900B32844 /* CancelDeliveryCommand.swift */,
 				C124018F29C7D8E900B32844 /* AssignAddressCommand.swift */,
+				C124018D29C7D8E900B32844 /* BasalScheduleExtraCommand.swift */,
 				C124019029C7D8E900B32844 /* BeepConfigCommand.swift */,
-				C124019129C7D8E900B32844 /* ErrorResponse.swift */,
-				C124019229C7D8E900B32844 /* SetupPodCommand.swift */,
+				C124018829C7D8E900B32844 /* BolusExtraCommand.swift */,
+				C124018E29C7D8E900B32844 /* CancelDeliveryCommand.swift */,
+				C124019629C7D8E900B32844 /* ConfigureAlertsCommand.swift */,
+				C124018229C7D8E900B32844 /* DeactivatePodCommand.swift */,
 				C124019329C7D8E900B32844 /* DetailedStatus.swift */,
+				C124019129C7D8E900B32844 /* ErrorResponse.swift */,
+				C124018929C7D8E900B32844 /* FaultConfigCommand.swift */,
+				C124018C29C7D8E900B32844 /* GetStatusCommand.swift */,
+				C124018629C7D8E900B32844 /* PlaceholderMessageBlock.swift */,
+				C124018529C7D8E900B32844 /* MessageBlock.swift */,
+				C124018729C7D8E900B32844 /* PodInfo.swift */,
+				C124018029C7D8E900B32844 /* PodInfoActivationTime.swift */,
+				C124017E29C7D8E900B32844 /* PodInfoPulseLog.swift */,
+				C124018A29C7D8E900B32844 /* PodInfoPulseLogPlus.swift */,
 				C124019429C7D8E900B32844 /* PodInfoResponse.swift */,
+				C124018429C7D8E900B32844 /* PodInfoTriggeredAlerts.swift */,
 				C124019529C7D8E900B32844 /* SetInsulinScheduleCommand.swift */,
-				C124019629C7D8E900B32844 /* ConfigureAlertsCommand.swift */,
+				C124019229C7D8E900B32844 /* SetupPodCommand.swift */,
+				C124018B29C7D8E900B32844 /* StatusResponse.swift */,
+				C124018129C7D8E900B32844 /* TempBasalExtraCommand.swift */,
+				C124017F29C7D8E900B32844 /* VersionResponse.swift */,
 			);
 			path = MessageBlocks;
 			sourceTree = "<group>";
@@ -722,11 +724,12 @@
 				C124024329C7DA9700B32844 /* PairPodView.swift */,
 				D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */,
 				C124024829C7DA9700B32844 /* PodDetailsView.swift */,
+				D85AEAC72B1403C000081044 /* PodDiagnostics.swift */,
 				C124022E29C7DA9700B32844 /* PodLifeHUDView.swift */,
 				C124024129C7DA9700B32844 /* PodSetupView.swift */,
 				D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */,
+				D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */,
 				D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */,
-				D845A14C2AF8A4FB00EA0853 /* ReadPulseLogView.swift */,
 				C124023729C7DA9700B32844 /* RileyLinkSetupView.swift */,
 				C124024729C7DA9700B32844 /* ScheduledExpirationReminderEditView.swift */,
 				C124023829C7DA9700B32844 /* SetupCompleteView.swift */,
@@ -1081,7 +1084,7 @@
 				C12401D429C7D8E900B32844 /* BeepPreference.swift in Sources */,
 				C12401B829C7D8E900B32844 /* PodInfoPulseLog.swift in Sources */,
 				D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */,
-				C12401BE29C7D8E900B32844 /* PodInfoConfiguredAlerts.swift in Sources */,
+				C12401BE29C7D8E900B32844 /* PodInfoTriggeredAlerts.swift in Sources */,
 				C12401E529C7D8E900B32844 /* PodCommsSession.swift in Sources */,
 				C12401DE29C7D8E900B32844 /* CRC16.swift in Sources */,
 				C12401C729C7D8E900B32844 /* BasalScheduleExtraCommand.swift in Sources */,
@@ -1127,7 +1130,6 @@
 				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 */,
@@ -1165,6 +1167,7 @@
 				D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */,
 				C124026F29C7DA9700B32844 /* PairPodViewModel.swift in Sources */,
 				C124026E29C7DA9700B32844 /* FrameworkLocalText.swift in Sources */,
+				D85AEAC82B1403C000081044 /* PodDiagnostics.swift in Sources */,
 				C124028F29C7DA9700B32844 /* PodDetailsView.swift in Sources */,
 				C124028629C7DA9700B32844 /* NotificationSettingsView.swift in Sources */,
 				C124027D29C7DA9700B32844 /* InsertCannulaView.swift in Sources */,
@@ -1177,6 +1180,7 @@
 				C124027B29C7DA9700B32844 /* ErrorView.swift in Sources */,
 				C124028829C7DA9700B32844 /* PodSetupView.swift in Sources */,
 				C124027F29C7DA9700B32844 /* SetupCompleteView.swift in Sources */,
+				D85AEACA2B1403CB00081044 /* ReadPodInfoView.swift in Sources */,
 				C124029429C7DA9700B32844 /* TimeView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

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

@@ -168,7 +168,7 @@ extension TimeInterval {
         if hours != 0 {
             str += String(format: "%uh", hours)
         }
-        if minutes != 0 || hours != 0 {
+        if minutes != 0 {
             str += String(format: "%um", minutes)
         }
         if seconds != 0 || str.isEmpty {

+ 5 - 5
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfo.swift

@@ -17,10 +17,10 @@ public protocol PodInfo {
 
 public enum PodInfoResponseSubType: UInt8, Equatable {
     case normal                      = 0x00
-    case configuredAlerts            = 0x01 // Returns information on configured alerts
-    case detailedStatus              = 0x02 // Returned on any pod fault
+    case triggeredAlerts             = 0x01 // Returns values for any unacknowledged triggered alerts
+    case detailedStatus              = 0x02 // Returns detailed pod status, returned for most calls after a pod fault
     case pulseLogPlus                = 0x03 // Returns up to the last 60 pulse log entries plus additional info
-    case activationTime              = 0x05 // Returns activation date, elapsed time, and fault code
+    case activationTime              = 0x05 // Returns pod activation time and possible fault code & fault time
     case pulseLogRecent              = 0x50 // Returns the last 50 pulse log entries
     case pulseLogPrevious            = 0x51 // Like 0x50, but returns up to the previous 50 entries before the last 50
     
@@ -28,8 +28,8 @@ public enum PodInfoResponseSubType: UInt8, Equatable {
         switch self {
         case .normal:
             return StatusResponse.self as! PodInfo.Type
-        case .configuredAlerts:
-            return PodInfoConfiguredAlerts.self
+        case .triggeredAlerts:
+            return PodInfoTriggeredAlerts.self
         case .detailedStatus:
             return DetailedStatus.self
         case .pulseLogPlus:

+ 28 - 17
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoActivationTime.swift

@@ -8,7 +8,7 @@
 
 import Foundation
 
-// Type 5 PodInfo returns the pod activation time, time pod alive, and the possible fault code
+// Type 5 PodInfo returns the pod activation time and possible fault code & fault time
 public struct PodInfoActivationTime : PodInfo {
     // OFF 1  2  3  4 5  6 7 8 9 10111213 1415161718
     // DATA   0  1  2 3  4 5 6 7 8 9 1011 1213141516
@@ -16,8 +16,12 @@ public struct PodInfoActivationTime : PodInfo {
 
     public let podInfoType: PodInfoResponseSubType = .activationTime
     public let faultEventCode: FaultEventCode
-    public let timeActivation: TimeInterval
-    public let dateTime: DateComponents
+    public let faultTime: TimeInterval
+    public let year: Int
+    public let month: Int
+    public let day: Int
+    public let hour: Int
+    public let minute: Int
     public let data: Data
     
     public init(encodedData: Data) throws {
@@ -25,22 +29,29 @@ public struct PodInfoActivationTime : PodInfo {
             throw MessageBlockError.notEnoughData
         }
         self.faultEventCode = FaultEventCode(rawValue: encodedData[1])
-        self.timeActivation = TimeInterval(minutes: Double((Int(encodedData[2] & 0b1) << 8) + Int(encodedData[3])))
-        self.dateTime = DateComponents(encodedDateTime: encodedData.subdata(in: 12..<17))
+        self.faultTime = TimeInterval(minutes: Double((Int(encodedData[2]) << 8) + Int(encodedData[3])))
+        self.year   = Int(encodedData[14])
+        self.month  = Int(encodedData[12])
+        self.day    = Int(encodedData[13])
+        self.hour   = Int(encodedData[15])
+        self.minute = Int(encodedData[16])
         self.data = Data(encodedData)
     }
 }
 
-extension DateComponents {
-    init(encodedDateTime: Data) {
-        self.init()
-        
-        year   = Int(encodedDateTime[2]) + 2000
-        month  = Int(encodedDateTime[0])
-        day    = Int(encodedDateTime[1])
-        hour   = Int(encodedDateTime[3])
-        minute = Int(encodedDateTime[4])
-        
-        calendar = Calendar(identifier: .gregorian)
-    }
+func activationTimeString(podInfoActivationTime: PodInfoActivationTime) -> String {
+    var result: [String] = []
+
+    // activation time info
+    result.append(String(format: "Year:   %u", podInfoActivationTime.year))
+    result.append(String(format: "Month:  %u", podInfoActivationTime.month))
+    result.append(String(format: "Day:    %u", podInfoActivationTime.day))
+    result.append(String(format: "Hour:   %u", podInfoActivationTime.hour))
+    result.append(String(format: "Minute: %u", podInfoActivationTime.minute))
+
+    // pod fault info
+    result.append(String(format: "\n%@", String(describing: podInfoActivationTime.faultEventCode)))
+    result.append(String(format: "Fault Time: %@", podInfoActivationTime.faultTime.timeIntervalStr))
+
+    return result.joined(separator: "\n")
 }

+ 0 - 55
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift

@@ -1,55 +0,0 @@
-//
-//  PodInfoConfiguredAlerts.swift
-//  OmniKit
-//
-//  Created by Eelke Jager on 16/09/2018.
-//  Copyright © 2018 Pete Schwamb. All rights reserved.
-//
-
-import Foundation
-
-// Type 1 Pod Info returns information about the currently configured alerts
-public struct PodInfoConfiguredAlerts : PodInfo {
-    // CMD 1  2  3 4  5 6  7 8  910 1112 1314 1516 1718 1920
-    // DATA   0  1 2  3 4  5 6  7 8  910 1112 1314 1516 1718
-    // 02 13 01 XXXX VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV
-
-    public let podInfoType : PodInfoResponseSubType = .configuredAlerts
-    public let word_278    : Data
-    public let alertsActivations : [AlertActivation]
-    public let data       : Data
-
-    public struct AlertActivation {
-        let beepType: BeepType
-        let unitsLeft: Double
-        let timeFromPodStart: UInt8
-        
-        public init(beepType: BeepType, timeFromPodStart: UInt8, unitsLeft: Double) {
-            self.beepType = beepType
-            self.timeFromPodStart = timeFromPodStart
-            self.unitsLeft = unitsLeft
-        }
-    }
-    
-    public init(encodedData: Data) throws {
-        guard encodedData.count >= 11 else {
-            throw MessageBlockError.notEnoughData
-        }
-
-        self.word_278 = encodedData[1...2]
-        
-        let numAlertTypes = 8
-        let beepType = BeepType.self
-        
-        var activations = [AlertActivation]()
-
-        for alarmType in (0..<numAlertTypes) {
-            let beepType = beepType.init(rawValue: UInt8(alarmType))
-            let timeFromPodStart = encodedData[(3 + alarmType * 2)] // Double(encodedData[(5 + alarmType)] & 0x3f)
-            let unitsLeft = Double(encodedData[(4 + alarmType * 2)]) / Pod.pulsesPerUnit
-            activations.append(AlertActivation(beepType: beepType!, timeFromPodStart: timeFromPodStart, unitsLeft: unitsLeft))
-        }
-        alertsActivations = activations
-        self.data         = encodedData
-    }
-}

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

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

+ 13 - 0
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift

@@ -49,3 +49,16 @@ public struct PodInfoPulseLogPlus : PodInfo {
         self.data = encodedData
     }
 }
+
+func pulseLogPlusString(podInfoPulseLogPlus: PodInfoPulseLogPlus) -> String {
+    var result: [String] = []
+
+    result.append(String(format: "Pod Active: %@", podInfoPulseLogPlus.timeActivation.timeIntervalStr))
+    result.append(String(format: "Fault Time: %@", podInfoPulseLogPlus.timeFaultEvent.timeIntervalStr))
+    result.append(String(format: "%@\n", String(describing: podInfoPulseLogPlus.faultEventCode)))
+
+    let lastPulseNumber = Int(podInfoPulseLogPlus.nEntries)
+    result.append(pulseLogString(pulseLogEntries: podInfoPulseLogPlus.pulseLog, lastPulseNumber: lastPulseNumber))
+
+    return result.joined(separator: "\n")
+}

+ 91 - 0
Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoTriggeredAlerts.swift

@@ -0,0 +1,91 @@
+//
+//  PodInfoTriggeredAlerts.swift
+//  OmniKit
+//
+//  Created by Eelke Jager on 16/09/2018.
+//  Copyright © 2018 Pete Schwamb. All rights reserved.
+//
+
+import Foundation
+
+// Type 1 Pod Info returns information about the currently unacknowledged triggered alert values
+public struct PodInfoTriggeredAlerts: PodInfo {
+    // CMD 1  2  3 4  5 6  7 8  910 1112 1314 1516 1718 1920
+    // DATA   0  1 2  3 4  5 6  7 8  910 1112 1314 1516 1718
+    // 02 13 01 XXXX VVVV VVVV VVVV VVVV VVVV VVVV VVVV VVVV
+
+    public let podInfoType: PodInfoResponseSubType = .triggeredAlerts
+    public let unknown_word: UInt16
+    public let alertsActivations: [AlertActivation]
+    public let data: Data
+
+    public struct AlertActivation {
+        let triggeredAlertValue: TriggeredAlertValue
+
+        public init(triggeredAlertValue: TriggeredAlertValue) {
+            self.triggeredAlertValue = triggeredAlertValue
+        }
+    }
+
+    public init(encodedData: Data) throws {
+        guard encodedData.count >= 11 else {
+            throw MessageBlockError.notEnoughData
+        }
+
+        let numAlerts = 8
+        var activations = [AlertActivation]()
+        var i = 3 // starting data index for first VVVV value
+        for alertNum in (0..<numAlerts) {
+            let val = Double(encodedData[i...].toBigEndian(UInt16.self))
+            if AlertSlot(rawValue: UInt8(alertNum)) == .slot4LowReservoir {
+                let triggeredAlertValue: TriggeredAlertValue = .unitsRemaining(val / Pod.pulsesPerUnit)
+                activations.append(AlertActivation(triggeredAlertValue: triggeredAlertValue))
+            } else {
+                let triggeredAlertValue: TriggeredAlertValue = .podTime(TimeInterval(minutes: val))
+                activations.append(AlertActivation(triggeredAlertValue: triggeredAlertValue))
+            }
+            i += 2
+        }
+        self.unknown_word = encodedData[1...].toBigEndian(UInt16.self)
+        self.alertsActivations = activations
+        self.data = encodedData
+    }
+}
+
+public enum TriggeredAlertValue {
+    case unitsRemaining(Double)
+    case podTime(TimeInterval)
+}
+
+extension TriggeredAlertValue: CustomDebugStringConvertible {
+    public var debugDescription: String {
+        switch self {
+        case .unitsRemaining(let units):
+            if units != 0 {
+                return "\(Int(units))U"
+            }
+        case .podTime(let triggerTime):
+            if triggerTime != 0 {
+                return "\(triggerTime.timeIntervalStr)"
+            }
+        }
+        return ""
+    }
+}
+
+func triggeredAlertsString(podInfoTriggeredAlerts: PodInfoTriggeredAlerts) -> String {
+    var result: [String] = []
+
+    for index in podInfoTriggeredAlerts.alertsActivations.indices {
+        // extract the alert slot debug description for a more helpful display
+        let description = AlertSlot(rawValue: UInt8(index)).debugDescription
+        let start = description.index(description.startIndex, offsetBy: 27)
+        let end = description.index(description.endIndex, offsetBy: -1)
+        let range = start..<end
+
+        let alert = podInfoTriggeredAlerts.alertsActivations[index]
+        result.append(String(format: "%@: %@", String(description[range]), String(describing: alert.triggeredAlertValue)))
+    }
+
+    return result.joined(separator: "\n")
+}

+ 90 - 6
Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -1173,7 +1173,7 @@ extension OmnipodPumpManager {
     }
 
     public func readPulseLog(completion: @escaping (Result<String, Error>) -> Void) {
-        // use hasSetupPod to be able to read the pulse log from a faulted Pod
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
         guard self.hasSetupPod else {
             completion(.failure(OmnipodPumpManagerError.noPodPaired))
             return
@@ -1210,6 +1210,90 @@ extension OmnipodPumpManager {
         }
     }
 
+    public func readPulseLogPlus(completion: @escaping (Result<String, Error>) -> Void) {
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion(.failure(OmnipodPumpManagerError.noPodPaired))
+            return
+        }
+        guard state.podState?.isFaulted == true || state.podState?.unfinalizedBolus?.scheduledCertainty == .uncertain || state.podState?.unfinalizedBolus?.isFinished() != false else
+        {
+            self.log.info("Skipping Read Pulse Log Plus due to bolus still in progress.")
+            completion(.failure(PodCommsError.unfinalizedBolus))
+            return
+        }
+
+        let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
+        podComms.runSession(withName: "Read Pulse Log Plus", using: rileyLinkSelector) { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .bipBeeeeep)
+                    let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .pulseLogPlus, beepBlock: beepBlock)
+                    let podInfoPulseLogPlus = podInfoResponse.podInfo as! PodInfoPulseLogPlus
+                    let str = pulseLogPlusString(podInfoPulseLogPlus: podInfoPulseLogPlus)
+                    completion(.success(str))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+
+    public func readActivationTime(completion: @escaping (Result<String, Error>) -> Void) {
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion(.failure(OmnipodPumpManagerError.noPodPaired))
+            return
+        }
+
+        let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
+        podComms.runSession(withName: "Read Activation Time", using: rileyLinkSelector) { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .beepBeep)
+                    let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .activationTime, beepBlock: beepBlock)
+                    let podInfoActivationTime = podInfoResponse.podInfo as! PodInfoActivationTime
+                    let str = activationTimeString(podInfoActivationTime: podInfoActivationTime)
+                    completion(.success(str))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+
+    public func readTriggeredAlerts(completion: @escaping (Result<String, Error>) -> Void) {
+        // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod
+        guard self.hasSetupPod else {
+            completion(.failure(OmnipodPumpManagerError.noPodPaired))
+            return
+        }
+
+        let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice
+        podComms.runSession(withName: "Read Triggered Alerts", using: rileyLinkSelector) { (result) in
+            do {
+                switch result {
+                case .success(let session):
+                    let beepBlock = self.beepMessageBlock(beepType: .beepBeep)
+                    let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .triggeredAlerts, beepBlock: beepBlock)
+                    let podInfoTriggeredAlerts = podInfoResponse.podInfo as! PodInfoTriggeredAlerts
+                    let str = triggeredAlertsString(podInfoTriggeredAlerts: podInfoTriggeredAlerts)
+                    completion(.success(str))
+                case .failure(let error):
+                    throw error
+                }
+            } catch let error {
+                completion(.failure(error))
+            }
+        }
+    }
+
     public func setConfirmationBeeps(newPreference: BeepPreference, completion: @escaping (OmnipodPumpManagerError?) -> Void) {
 
         // If there isn't an active pod or the pod is currently silenced,
@@ -1307,10 +1391,10 @@ extension OmnipodPumpManager {
             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)
+                // it's possible that a response from a previous successful pod alert configuration can be lost
+                // and thus the alert won't get reset here when reconfiguring pod alerts with a new silence pod state.
+                let acknowledgeAll = true   // protect against lost alert configuration response related issues
+                try session.configureAlerts(podAlerts, acknowledgeAll: acknowledgeAll, beepBlock: beepBlock)
                 self.setState { (state) in
                     state.silencePod = silencePod
                 }
@@ -2311,7 +2395,7 @@ extension OmnipodPumpManager {
         }
 
         for alert in state.activeAlerts {
-            if alert.alertIdentifier == alertIdentifier {
+            if alert.alertIdentifier == alertIdentifier || alert.repeatingAlertIdentifier == 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 {

+ 26 - 2
Dependencies/OmniKit/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift

@@ -348,6 +348,10 @@ class OmnipodSettingsViewModel: ObservableObject {
         }
     }
 
+    func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
+        pumpManager.playTestBeeps(completion: completion)
+    }
+
     func readPulseLog(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
         pumpManager.readPulseLog() { (result) in
             DispatchQueue.main.async {
@@ -356,8 +360,28 @@ class OmnipodSettingsViewModel: ObservableObject {
         }
     }
 
-    func playTestBeeps(_ completion: @escaping (Error?) -> Void) {
-        pumpManager.playTestBeeps(completion: completion)
+    func readPulseLogPlus(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readPulseLogPlus() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
+    func readActivationTime(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readActivationTime() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
+    }
+
+    func readTriggeredAlerts(_ completion: @escaping (_ result: Result<String, Error>) -> Void) {
+        pumpManager.readTriggeredAlerts() { (result) in
+            DispatchQueue.main.async {
+                completion(result)
+            }
+        }
     }
 
     func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) {

+ 15 - 19
Dependencies/OmniKit/OmniKitUI/Views/OmnipodSettingsView.swift

@@ -403,7 +403,8 @@ struct OmnipodSettingsView: View  {
 
                 if let podDetails = self.viewModel.podDetails {
                     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)
+                        FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row")
+                            .foregroundColor(Color.primary)
                     }
                 } else {
                     HStack {
@@ -416,7 +417,8 @@ struct OmnipodSettingsView: View  {
 
                 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)
+                        FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row")
+                            .foregroundColor(Color.primary)
                     }
                 } else {
                     HStack {
@@ -453,7 +455,8 @@ struct OmnipodSettingsView: View  {
                 }
                 NavigationLink(destination: BeepPreferenceSelectionView(initialValue: viewModel.beepPreference, onSave: viewModel.setConfirmationBeeps)) {
                     HStack {
-                        FrameworkLocalText("Confidence Reminders", comment: "Text for confidence reminders navigation link").foregroundColor(Color.primary)
+                        FrameworkLocalText("Confidence Reminders", comment: "Text for confidence reminders navigation link")
+                            .foregroundColor(Color.primary)
                         Spacer()
                         Text(viewModel.beepPreference.title)
                             .foregroundColor(.secondary)
@@ -461,7 +464,8 @@ struct OmnipodSettingsView: View  {
                 }
                 NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) {
                     HStack {
-                        FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link").foregroundColor(Color.primary)
+                        FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link")
+                            .foregroundColor(Color.primary)
                         Spacer()
                         Text(viewModel.silencePodPreference.title)
                             .foregroundColor(.secondary)
@@ -509,21 +513,13 @@ struct OmnipodSettingsView: View  {
                 }
             }
 
-            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)
+            Section() {
+                NavigationLink(destination: PodDiagnosticsView(
+                    title: LocalizedString("Pod Diagnostics", comment: "Title for the pod diagnostic view"),
+                    viewModel: viewModel))
+                {
+                    FrameworkLocalText("Pod Diagnostics", comment: "Text for pod diagnostics row")
+                        .foregroundColor(Color.primary)
                 }
             }
 

+ 10 - 10
Dependencies/OmniKit/OmniKitUI/Views/PlayTestBeepsView.swift

@@ -14,7 +14,11 @@ struct PlayTestBeepsView: View {
     @Environment(\.horizontalSizeClass) var horizontalSizeClass
     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
 
-    private var toRun: ((_ completion: @escaping (_ result: Error?) -> Void) -> Void)?
+    var toRun: ((_ completion: @escaping (_ result: Error?) -> Void) -> Void)?
+
+    private let title = LocalizedString("Play Test Beeps", comment: "navigation title for play test beeps")
+    private let actionString = LocalizedString("Playing Test Beeps...", comment: "button title when executing play test beeps")
+    private let failedString: String = LocalizedString("Failed to play test beeps.", comment: "Alert title for error when playing test beeps")
 
     @State private var alertIsPresented: Bool = false
     @State private var displayString: String = ""
@@ -23,10 +27,6 @@ struct PlayTestBeepsView: View {
     @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 {
@@ -48,7 +48,7 @@ struct PlayTestBeepsView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Play Test Beeps", comment: "navigation title for play test beeps"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .onFirstAppear {
@@ -61,6 +61,7 @@ struct PlayTestBeepsView: View {
             executing = true
             self.displayString = ""
             toRun?() { (error) in
+                executing = false
                 if let error = error {
                     self.displayString = ""
                     self.error = error
@@ -68,22 +69,21 @@ struct PlayTestBeepsView: View {
                 } 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")
+            return actionString
         } else {
-            return LocalizedString("Play Test Beeps", comment: "button title to play test beeps")
+            return title
         }
     }
 
     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")),
+            title: Text(failedString),
             message: Text(error?.localizedDescription ?? "No Error")
         )
     }

+ 90 - 0
Dependencies/OmniKit/OmniKitUI/Views/PodDiagnostics.swift

@@ -0,0 +1,90 @@
+//
+//  PodDiagnotics.swift
+//  OmniKit
+//
+//  Created by Joseph Moran on 11/25/23.
+//  Copyright © 2023 LoopKit Authors. All rights reserved.
+//
+
+import SwiftUI
+import LoopKit
+import LoopKitUI
+import HealthKit
+import OmniKit
+
+
+struct PodDiagnosticsView: View  {
+
+    var title: String
+    
+    @ObservedObject var viewModel: OmnipodSettingsViewModel
+
+    var body: some View {
+        List {
+            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: 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: ReadPodInfoView(
+                title: LocalizedString("Read Pulse Log", comment: "Text for read pulse log title"),
+                actionString: LocalizedString("Reading Pulse Log...", comment: "Text for read pulse log action"),
+                failedString: LocalizedString("Failed to read pulse log.", comment: "Alert title for error when reading pulse log"),
+                toRun: viewModel.readPulseLog))
+            {
+                FrameworkLocalText("Read Pulse Log", comment: "Text for read pulse log navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: ReadPodInfoView(
+                title: LocalizedString("Read Pulse Log Plus", comment: "Text for read pulse log plus title"),
+                actionString: LocalizedString("Reading Pulse Log Plus...", comment: "Text for read pulse log plus action"),
+                failedString: LocalizedString("Failed to read pulse log plus.", comment: "Alert title for error when reading pulse log plus"),
+                toRun: viewModel.readPulseLogPlus))
+            {
+                FrameworkLocalText("Read Pulse Log Plus", comment: "Text for read pulse log plus navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: ReadPodInfoView(
+                title: LocalizedString("Read Activation Time", comment: "Text for read activation time title"),
+                actionString: LocalizedString("Reading Activation Time...", comment: "Text for read activation time action"),
+                failedString: LocalizedString("Failed to read activation time.", comment: "Alert title for error when reading activation time"),
+                toRun: self.viewModel.readActivationTime))
+            {
+                FrameworkLocalText("Read Activation Time", comment: "Text for read activation time navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: ReadPodInfoView(
+                title: LocalizedString("Read Triggered Alerts", comment: "Text for read triggered alerts title"),
+                actionString: LocalizedString("Reading Triggered Alerts...", comment: "Text for read triggered alerts action"),
+                failedString: LocalizedString("Failed to read triggered alerts.", comment: "Alert title for error when reading triggered alerts"),
+                toRun: self.viewModel.readTriggeredAlerts))
+            {
+                FrameworkLocalText("Read Triggered Alerts", comment: "Text for read triggered alerts navigation link")
+                    .foregroundColor(Color.primary)
+            }
+            .disabled(self.viewModel.noPod)
+
+            NavigationLink(destination: PumpManagerDetailsView(
+                toRun: self.viewModel.pumpManagerDetails))
+            {
+                FrameworkLocalText("Pump Manager Details", comment: "Text for pump manager details navigation link")
+                    .foregroundColor(Color.primary)
+            }
+        }
+        .insetGroupedListStyle()
+        .navigationBarTitle(title)
+    }
+}

+ 8 - 4
Dependencies/OmniKit/OmniKitUI/Views/PumpManagerDetailsView.swift

@@ -14,7 +14,11 @@ struct PumpManagerDetailsView: View {
     @Environment(\.horizontalSizeClass) var horizontalSizeClass
     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
 
-    private var toRun: ((_ completion: @escaping (_ result: String) -> Void) -> Void)?
+    var toRun: ((_ completion: @escaping (_ result: String) -> Void) -> Void)?
+
+    private let title = LocalizedString("Pump Manager Details", comment: "navigation title for pump manager details")
+    private let actionString = LocalizedString("Retrieving Pump Manager Details...", comment: "button title when retrieving pump manager details")
+    private let buttonTitle = LocalizedString("Refresh Pump Manager Details", comment: "button title to refresh pump manager details")
 
     @State private var displayString: String = ""
     @State private var error: Error? = nil
@@ -61,7 +65,7 @@ struct PumpManagerDetailsView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Pump Manager Details", comment: "navigation title for pump manager details"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .onFirstAppear {
             asyncAction()
@@ -81,9 +85,9 @@ struct PumpManagerDetailsView: View {
 
     private var buttonText: String {
         if executing {
-            return LocalizedString("Retrieving Pump Manager Details...", comment: "button title when retrieving pump manager details")
+            return actionString
         } else {
-            return LocalizedString("Refresh Pump Manager Details", comment: "button title to refresh pump manager details")
+            return buttonTitle
         }
     }
 }

+ 40 - 33
Dependencies/OmniKit/OmniKitUI/Views/ReadPulseLogView.swift

@@ -1,8 +1,8 @@
 //
-//  ReadPulseLogView.swift
+//  ReadPodInfoView.swift
 //  OmniKit
 //
-//  Created by Joe Moran on 9/1/23.
+//  Created by Joe Moran on 11/25/23.
 //  Copyright © 2023 LoopKit Authors. All rights reserved.
 //
 
@@ -10,11 +10,16 @@ import SwiftUI
 import LoopKit
 import OmniKit
 
-struct ReadPulseLogView: View {
+
+struct ReadPodInfoView: View {
     @Environment(\.horizontalSizeClass) var horizontalSizeClass
     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
 
-    private var toRun: ((_ completion: @escaping (_ result: Result<String, Error>) -> Void) -> Void)?
+    var title: String           // e.g., "Read Pulse Log"
+    var actionString: String    // e.g., "Reading Pulse Log..."
+    var failedString: String    // e.g., "Failed to read pulse log."
+
+    var toRun: ((_ completion: @escaping (_ result: Result<String, Error>) -> Void) -> Void)?
 
     @State private var alertIsPresented: Bool = false
     @State private var displayString: String = ""
@@ -22,10 +27,6 @@ struct ReadPulseLogView: View {
     @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 {
@@ -62,7 +63,7 @@ struct ReadPulseLogView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Read Pulse Log", comment: "navigation title for read pulse log"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .onFirstAppear {
@@ -75,54 +76,60 @@ struct ReadPulseLogView: View {
             executing = true
             self.displayString = ""
             toRun?() { (result) in
+                executing = false
                 switch result {
-                case .success(let pulseLogString):
-                    self.displayString = pulseLogString
+                case .success(let resultString):
+                    self.displayString = resultString
                 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")
+            return actionString
         } else {
-            return LocalizedString("Read Pulse Log", comment: "button title to read pulse log")
+            return title
         }
     }
 
     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")),
+            title: Text(failedString),
             message: Text(error?.localizedDescription ?? "No Error")
         )
     }
 }
 
-struct ReadPulsePodLogView_Previews: PreviewProvider {
+struct ReadPodInfoView_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)))
+        NavigationView {
+            ReadPodInfoView(
+                title: "Read Pulse Log",
+                actionString: "Reading Pulse Log...",
+                failedString: "Failed to read pulse log"
+            ) { 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)))
+            }
         }
     }
 }

+ 68 - 67
Dependencies/OmniKit/OmniKitUI/Views/ReadPodStatusView.swift

@@ -10,68 +10,16 @@ import SwiftUI
 import LoopKit
 import OmniKit
 
-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)?
+    var toRun: ((_ completion: @escaping (_ result: PumpManagerResult<DetailedStatus>) -> Void) -> Void)?
+
+    private let title = LocalizedString("Read Pod Status", comment: "navigation title for read pod status")
+    private let actionString = LocalizedString("Reading Pod Status...", comment: "button title when executing read pod status")
+    private let failedString = LocalizedString("Failed to read pod status.", comment: "Alert title for error when reading pod status")
 
     @State private var alertIsPresented: Bool = false
     @State private var displayString: String = ""
@@ -79,10 +27,6 @@ struct ReadPodStatusView: View {
     @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 {
@@ -115,7 +59,7 @@ struct ReadPodStatusView: View {
             .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5))
         }
         .insetGroupedListStyle()
-        .navigationTitle(LocalizedString("Read Pod Status", comment: "navigation title for read pod status"))
+        .navigationTitle(title)
         .navigationBarTitleDisplayMode(.inline)
         .alert(isPresented: $alertIsPresented, content: { alert(error: error) })
         .onFirstAppear {
@@ -128,6 +72,7 @@ struct ReadPodStatusView: View {
             executing = true
             self.displayString = ""
             toRun?() { (result) in
+                executing = false
                 switch result {
                 case .success(let detailedStatus):
                     self.displayString = podStatusString(status: detailedStatus)
@@ -135,27 +80,83 @@ struct ReadPodStatusView: View {
                     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")
+            return actionString
         } else {
-            return LocalizedString("Read Pod Status", comment: "button title to read pod status")
+            return title
         }
     }
 
     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")),
+            title: Text(failedString),
             message: Text(error?.localizedDescription ?? "No Error")
         )
     }
 }
 
+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_Previews: PreviewProvider {
     static var previews: some View {
         NavigationView {
@@ -165,4 +166,4 @@ struct ReadPodStatusView_Previews: PreviewProvider {
             }
         }
     }
- }
+}