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

Merge branch 'UI' into UI-superBolus

polscm32 2 лет назад
Родитель
Сommit
f8379a55b5
64 измененных файлов с 1954 добавлено и 756 удалено
  1. 2 2
      .github/workflows/add_identifiers.yml
  2. 2 2
      .github/workflows/create_certs.yml
  3. 1 1
      Config.xcconfig
  4. 1 1
      Dependencies/G7SensorKit/G7SensorKitUI/nl.lproj/Localizable.strings
  5. 96 96
      Dependencies/OmniBLE/Localizations/nl.lproj/Localizable.strings
  6. 12 8
      Dependencies/OmniBLE/OmniBLE.xcodeproj/project.pbxproj
  7. 1 1
      Dependencies/OmniBLE/OmniBLE/Bluetooth/Packet/BLEPacket.swift
  8. 1 6
      Dependencies/OmniBLE/OmniBLE/Bluetooth/StringLengthPrefixEncoding.swift
  9. 1 1
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  10. 5 5
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfo.swift
  11. 28 17
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoActivationTime.swift
  12. 0 56
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift
  13. 3 3
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  14. 13 0
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift
  15. 92 0
      Dependencies/OmniBLE/OmniBLE/OmnipodCommon/MessageBlocks/PodInfoTriggeredAlerts.swift
  16. 87 6
      Dependencies/OmniBLE/OmniBLE/PumpManager/OmniBLEPumpManager.swift
  17. 24 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/ViewModels/OmniBLESettingsViewModel.swift
  18. 15 19
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/OmniBLESettingsView.swift
  19. 10 11
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PlayTestBeepsView.swift
  20. 89 0
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PodDiagnostics.swift
  21. 8 4
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/PumpManagerDetailsView.swift
  22. 39 33
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPulseLogView.swift
  23. 68 67
      Dependencies/OmniBLE/OmniBLE/PumpManagerUI/Views/ReadPodStatusView.swift
  24. 30 26
      Dependencies/OmniKit/OmniKit.xcodeproj/project.pbxproj
  25. 1 1
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/DetailedStatus.swift
  26. 5 5
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfo.swift
  27. 28 17
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoActivationTime.swift
  28. 0 55
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoConfiguredAlerts.swift
  29. 3 3
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLog.swift
  30. 13 0
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoPulseLogPlus.swift
  31. 91 0
      Dependencies/OmniKit/OmniKit/OmnipodCommon/MessageBlocks/PodInfoTriggeredAlerts.swift
  32. 90 6
      Dependencies/OmniKit/OmniKit/PumpManager/OmnipodPumpManager.swift
  33. 7 7
      Dependencies/OmniKit/OmniKitUI/Resources/nl.lproj/Localizable.strings
  34. 26 2
      Dependencies/OmniKit/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift
  35. 15 19
      Dependencies/OmniKit/OmniKitUI/Views/OmnipodSettingsView.swift
  36. 10 10
      Dependencies/OmniKit/OmniKitUI/Views/PlayTestBeepsView.swift
  37. 90 0
      Dependencies/OmniKit/OmniKitUI/Views/PodDiagnostics.swift
  38. 8 4
      Dependencies/OmniKit/OmniKitUI/Views/PumpManagerDetailsView.swift
  39. 40 33
      Dependencies/OmniKit/OmniKitUI/Views/ReadPulseLogView.swift
  40. 68 67
      Dependencies/OmniKit/OmniKitUI/Views/ReadPodStatusView.swift
  41. 193 1
      FreeAPS.xcodeproj/project.pbxproj
  42. 2 0
      FreeAPS/Resources/Info.plist
  43. 4 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  44. 6 0
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  45. 9 9
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  46. 4 4
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  47. 9 9
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  48. 5 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  49. 144 116
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  50. 4 2
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  51. 5 5
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  52. 2 0
      FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift
  53. 11 0
      FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift
  54. 13 0
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift
  55. 210 0
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  56. 26 15
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  57. 11 0
      LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json
  58. 13 0
      LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json
  59. 6 0
      LiveActivity/Assets.xcassets/Contents.json
  60. 11 0
      LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json
  61. 11 0
      LiveActivity/Info.plist
  62. 111 0
      LiveActivity/LiveActivity.swift
  63. 8 0
      LiveActivity/LiveActivityBundle.swift
  64. 13 1
      fastlane/Fastfile

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

@@ -14,8 +14,8 @@ jobs:
     runs-on: macos-13
     steps:
       # Uncomment to manually select Xcode version if needed
-      #- name: Select Xcode version
-      #  run: "sudo xcode-select --switch /Applications/Xcode_14.1.app/Contents/Developer"
+      - name: Select Xcode version
+        run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
       
       # Checks-out the repo
       - name: Checkout Repo

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

@@ -15,8 +15,8 @@ jobs:
     runs-on: macos-13
     steps:
       # Uncomment to manually select Xcode version if needed
-      #- name: Select Xcode version
-      #  run: "sudo xcode-select --switch /Applications/Xcode_14.1.app/Contents/Developer"
+      - name: Select Xcode version
+        run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
       
       # Checks-out the repo
       - name: Checkout Repo

+ 1 - 1
Config.xcconfig

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

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

@@ -33,7 +33,7 @@
 "Dexcom G7" = "Dexcom G7";
 
 /* No comment provided by engineer. */
-"Done" = "Gereed";
+"Done" = "OK";
 
 /* Field label */
 "Glucose" = "Glucosewaarde";

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


+ 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 {

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


+ 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 {
             }
         }
     }
- }
+}

+ 193 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -277,8 +277,17 @@
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorStateModel.swift */; };
+		6B1A8D192B14D91600E76752 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D182B14D91600E76752 /* WidgetKit.framework */; };
+		6B1A8D1B2B14D91600E76752 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */; };
+		6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */; };
+		6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */; };
+		6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B1A8D232B14D91700E76752 /* Assets.xcassets */; };
+		6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */; };
+		6BCF84DD2B16843A003AD46E /* LiveActitiyShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */; };
+		6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */; };
 		6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */; };
 		6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAE81192B118804DCD23034 /* SnoozeProvider.swift */; };
 		711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96653287EDB276A111288305 /* ManualTempBasalDataFlow.swift */; };
@@ -439,6 +448,13 @@
 			remoteGlobalIDString = 388E595725AD948C0019842D;
 			remoteInfo = FreeAPS;
 		};
+		6B1A8D262B14D91700E76752 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 388E595025AD948C0019842D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 6B1A8D162B14D91500E76752;
+			remoteInfo = LiveActivityExtension;
+		};
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXCopyFilesBuildPhase section */
@@ -496,6 +512,17 @@
 			name = "Embed App Extensions";
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 13;
+			files = (
+				6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */,
+			);
+			name = "Embed Foundation Extensions";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
@@ -809,6 +836,16 @@
 		66A5B83E7967C38F7CBD883C /* LibreConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigDataFlow.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
+		6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
+		6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LiveActivityExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+		6B1A8D182B14D91600E76752 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
+		6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+		6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBundle.swift; sourceTree = "<group>"; };
+		6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivity.swift; sourceTree = "<group>"; };
+		6B1A8D232B14D91700E76752 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
+		6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyShared.swift; sourceTree = "<group>"; };
 		6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorProvider.swift; sourceTree = "<group>"; };
 		72778B68C3004F71F6E79BDC /* PumpSettingsEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorStateModel.swift; sourceTree = "<group>"; };
 		79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorDataFlow.swift; sourceTree = "<group>"; };
@@ -987,6 +1024,15 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D142B14D91500E76752 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B1A8D1B2B14D91600E76752 /* SwiftUI.framework in Frameworks */,
+				6B1A8D192B14D91600E76752 /* WidgetKit.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
@@ -1338,6 +1384,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				6B1A8D2C2B156EC100E76752 /* LiveActivity */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
 				F90692A8274B7A980037068D /* HealthKit */,
 				38E8754D275556E100975559 /* WatchManager */,
@@ -1505,6 +1552,9 @@
 				3818AA56274C26A300843DB3 /* RileyLinkKit.framework */,
 				3818AA57274C26A300843DB3 /* RileyLinkKitUI.framework */,
 				3818AA49274C267000843DB3 /* CGMBLEKit.framework */,
+				6B1A8D012B14D88B00E76752 /* UniformTypeIdentifiers.framework */,
+				6B1A8D182B14D91600E76752 /* WidgetKit.framework */,
+				6B1A8D1A2B14D91600E76752 /* SwiftUI.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -1585,6 +1635,7 @@
 				3818AA44274C229000843DB3 /* Packages */,
 				38E8751D27554D5500975559 /* FreeAPSWatch */,
 				38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+				6B1A8D1C2B14D91600E76752 /* LiveActivity */,
 				388E595925AD948C0019842D /* Products */,
 				3818AA48274C267000843DB3 /* Frameworks */,
 				192F0FF5276AC36D0085BE4D /* Recovered References */,
@@ -1598,6 +1649,7 @@
 				38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */,
 				38E8751C27554D5500975559 /* FreeAPSWatch.app */,
 				38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */,
+				6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -2004,6 +2056,26 @@
 			path = AutotuneConfig;
 			sourceTree = "<group>";
 		};
+		6B1A8D1C2B14D91600E76752 /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
+				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
+				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
+				6B1A8D252B14D91700E76752 /* Info.plist */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
+		6B1A8D2C2B156EC100E76752 /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */,
+				6BCF84DC2B16843A003AD46E /* LiveActitiyShared.swift */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
 		6DC5D590658EF8B8DF94F9F5 /* AddCarbs */ = {
 			isa = PBXGroup;
 			children = (
@@ -2361,11 +2433,13 @@
 				388E595625AD948C0019842D /* Resources */,
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
 				38E8753D27554D5900975559 /* Embed Watch Content */,
+				6B1A8D122B14D88E00E76752 /* Embed Foundation Extensions */,
 			);
 			buildRules = (
 			);
 			dependencies = (
 				38E8753B27554D5900975559 /* PBXTargetDependency */,
+				6B1A8D272B14D91700E76752 /* PBXTargetDependency */,
 			);
 			name = FreeAPS;
 			packageProductDependencies = (
@@ -2435,13 +2509,29 @@
 			productReference = 38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */;
 			productType = "com.apple.product-type.bundle.unit-test";
 		};
+		6B1A8D162B14D91500E76752 /* LiveActivityExtension */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 6B1A8D292B14D91800E76752 /* Build configuration list for PBXNativeTarget "LiveActivityExtension" */;
+			buildPhases = (
+				6B1A8D132B14D91500E76752 /* Sources */,
+				6B1A8D142B14D91500E76752 /* Frameworks */,
+				6B1A8D152B14D91500E76752 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = LiveActivityExtension;
+			productName = LiveActivityExtension;
+			productReference = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */;
+			productType = "com.apple.product-type.app-extension";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
 		388E595025AD948C0019842D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastSwiftUpdateCheck = 1310;
 				LastUpgradeCheck = 1240;
 				TargetAttributes = {
 					388E595725AD948C0019842D = {
@@ -2503,6 +2593,7 @@
 				38FCF3EC25E9028E0078B0D1 /* FreeAPSTests */,
 				38E8751B27554D5500975559 /* FreeAPSWatch */,
 				38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */,
+				6B1A8D162B14D91500E76752 /* LiveActivityExtension */,
 			);
 		};
 /* End PBXProject section */
@@ -2549,6 +2640,14 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D152B14D91500E76752 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
@@ -2737,6 +2836,7 @@
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* DecimalTextField.swift in Sources */,
+				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
@@ -2891,6 +2991,7 @@
 				1D845DF2E3324130E1D95E67 /* DataTableProvider.swift in Sources */,
 				19F95FFA29F1102A00314DDC /* StatRootView.swift in Sources */,
 				0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */,
+				6BCF84DD2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
 				195D80B92AF697F700D25097 /* DynamicProvider.swift in Sources */,
 				D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */,
 				38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */,
@@ -2951,6 +3052,16 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		6B1A8D132B14D91500E76752 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6BCF84DE2B16843A003AD46E /* LiveActitiyShared.swift in Sources */,
+				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
+				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
@@ -2969,6 +3080,11 @@
 			target = 388E595725AD948C0019842D /* FreeAPS */;
 			targetProxy = 38FCF3F225E9028E0078B0D1 /* PBXContainerItemProxy */;
 		};
+		6B1A8D272B14D91700E76752 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 6B1A8D162B14D91500E76752 /* LiveActivityExtension */;
+			targetProxy = 6B1A8D262B14D91700E76752 /* PBXContainerItemProxy */;
+		};
 /* End PBXTargetDependency section */
 
 /* Begin PBXVariantGroup section */
@@ -3431,6 +3547,73 @@
 			};
 			name = Release;
 		};
+		6B1A8D2A2B14D91800E76752 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = LiveActivity/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).LiveActivity";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		6B1A8D2B2B14D91800E76752 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = "$(DEVELOPER_TEAM)";
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = LiveActivity/Info.plist;
+				INFOPLIST_KEY_CFBundleDisplayName = LiveActivity;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).LiveActivity";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
@@ -3479,6 +3662,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Debug;
 		};
+		6B1A8D292B14D91800E76752 /* Build configuration list for PBXNativeTarget "LiveActivityExtension" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				6B1A8D2A2B14D91800E76752 /* Debug */,
+				6B1A8D2B2B14D91800E76752 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Debug;
+		};
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */

+ 2 - 0
FreeAPS/Resources/Info.plist

@@ -84,6 +84,8 @@
 	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<key>NSHumanReadableCopyright</key>
 	<string>$(COPYRIGHT_NOTICE)</string>
+	<key>NSSupportsLiveActivities</key>
+	<true/>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>

+ 4 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -1,3 +1,4 @@
+import ActivityKit
 import CoreData
 import SwiftUI
 import Swinject
@@ -44,6 +45,9 @@ import Swinject
         _ = resolver.resolve(WatchManager.self)!
         _ = resolver.resolve(HealthKitManager.self)!
         _ = resolver.resolve(BluetoothStateManager.self)!
+        if #available(iOS 16.2, *) {
+            _ = resolver.resolve(LiveActivityBridge.self)!
+        }
     }
 
     init() {

+ 6 - 0
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -20,5 +20,11 @@ final class ServiceAssembly: Assembly {
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
+
+        if #available(iOS 16.2, *) {
+            container.register(LiveActivityBridge.self) { r in
+                LiveActivityBridge(resolver: r)
+            }
+        }
     }
 }

+ 9 - 9
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings

@@ -576,10 +576,10 @@ Enact a temp Basal or a temp target */
 "Temp Targets" = "Temporäre Ziele";
 
 /* Delete carbs from data table and Nightscout */
-"Delete Carbs?" = "Delete Carbs?";
+"Delete Carbs?" = "Kohlenhydrate löschen?";
 
 /* Delete insulin from pump history and Nightscout */
-"Delete Insulin?" = "Delete Insulin?";
+"Delete Insulin?" = "Insulin löschen?";
 
 /* Treatments list */
 "Treatments" = "Behandlungen";
@@ -1368,13 +1368,13 @@ Enact a temp Basal or a temp target */
 "Statistics and Home View" = "Statistiken und Home-Ansicht";
 
 /* Alert text */
-"Delete Carb Equivalents?" = "Delete Carb Equivalents?";
+"Delete Carb Equivalents?" = "Kohlenhydratäquivalente löschen?";
 
 /* */
-"All FPUs of the meal will be deleted." = "All FPUs of the meal will be deleted.";
+"All FPUs of the meal will be deleted." = "Alle FPUs der Mahlzeit werden gelöscht.";
 
 /* */
-"Delete Glucose?" = "Delete Glucose?";
+"Delete Glucose?" = "Glukose löschen?";
 
 /* */
 "Meal Presets" = "Mahlzeit Voreinstellungen";
@@ -1662,16 +1662,16 @@ Enact a temp Basal or a temp target */
 "2 hours" = "2 Stunden";
 
 /* */
-"4 hours" = "4 hours";
+"4 hours" = "4 Stunden";
 
 /* */
-"6 hours" = "6 hours";
+"6 hours" = "6 Stunden";
 
 /* */
-"12 hours" = "12 hours";
+"12 hours" = "12 Stunden";
 
 /* */
-"24 hours" = "24 hours";
+"24 hours" = "24 Stunden";
 
 /* Average BG = */
 "Average" = "Mittelwert";

+ 4 - 4
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings

@@ -38,7 +38,7 @@
 "Clear" = "Wissen";
 
 /* Button */
-"Done" = "Gereed";
+"Done" = "OK";
 
 /*  */
 "Wait please" = "Wachten";
@@ -1484,9 +1484,9 @@ Enact a temp Basal or a temp target */
 "Previous Pod Information" = "Vorige Pod informatie";
 
 /* Text for confidence reminders navigation link */
-"Confidence Reminders" = "Meldingen met piepjes vanuit de Pod";
+"Confidence Reminders" = "Bevestigingsmeldingen";
 
-"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Dit zijn meldingen die met piepjes uit de Pod komen en kunnen worden gebruikt ter bevestiging van geselecteerde opdrachten.";
+"Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced." = "Dit zijn bevestigingsmeldingen met piepjes die uit de Pod komen en kunnen worden gebruikt ter bevestiging van geselecteerde opdrachten als de Pod niet gedempt is.";
 
 /* button title for saving low reservoir reminder while saving */
 "Saving..." = "Opslaan...";
@@ -1510,7 +1510,7 @@ Enact a temp Basal or a temp target */
 "Enabled" = "Ingeschakeld";
 
 /* Title string for BeepPreference.extended */
-"Extended" = "Verlengd";
+"Extended" = "Uitgebreid";
 
 /* Description for BeepPreference.silent */
 "No confidence reminders are used." = "Er worden geen meldingen met piepjes gebruikt.";

+ 9 - 9
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings

@@ -576,10 +576,10 @@ Enact a temp Basal or a temp target */
 "Temp Targets" = "Временные цели";
 
 /* Delete carbs from data table and Nightscout */
-"Delete Carbs?" = "Delete Carbs?";
+"Delete Carbs?" = "Удалить углеводы?";
 
 /* Delete insulin from pump history and Nightscout */
-"Delete Insulin?" = "Delete Insulin?";
+"Delete Insulin?" = "Удалить инсулин?";
 
 /* Treatments list */
 "Treatments" = "События";
@@ -1368,13 +1368,13 @@ Enact a temp Basal or a temp target */
 "Statistics and Home View" = "Статистика и экран";
 
 /* Alert text */
-"Delete Carb Equivalents?" = "Delete Carb Equivalents?";
+"Delete Carb Equivalents?" = "Удалить эквиваленты углеводов?";
 
 /* */
-"All FPUs of the meal will be deleted." = "All FPUs of the meal will be deleted.";
+"All FPUs of the meal will be deleted." = "Все пищевые единицы будут удалены.";
 
 /* */
-"Delete Glucose?" = "Delete Glucose?";
+"Delete Glucose?" = "Удалить глюкозу?";
 
 /* */
 "Meal Presets" = "Шаблоны";
@@ -1662,16 +1662,16 @@ Enact a temp Basal or a temp target */
 "2 hours" = "2 часа";
 
 /* */
-"4 hours" = "4 hours";
+"4 hours" = "4 часа";
 
 /* */
-"6 hours" = "6 hours";
+"6 hours" = "6 часов";
 
 /* */
-"12 hours" = "12 hours";
+"12 hours" = "12 часов";
 
 /* */
-"24 hours" = "24 hours";
+"24 hours" = "24 часа";
 
 /* Average BG = */
 "Average" = "Средний";

+ 5 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -53,6 +53,7 @@ struct FreeAPSSettings: JSON, Equatable {
     var sweetMeals: Bool = false
     var sweetMealFactor: Decimal = 2
     var displayPredictions: Bool = true
+    var useLiveActivity: Bool = false
 }
 
 extension FreeAPSSettings: Decodable {
@@ -274,6 +275,10 @@ extension FreeAPSSettings: Decodable {
             settings.displayPredictions = displayPredictions
         }
 
+        if let useLiveActivity = try? container.decode(Bool.self, forKey: .useLiveActivity) {
+            settings.useLiveActivity = useLiveActivity
+        }
+
         self = settings
     }
 }

+ 144 - 116
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -86,20 +86,23 @@ struct MainChartView: View {
     @State private var unSmoothedGlucoseDots: [CGRect] = []
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
     @State private var bolusDots: [DotInfo] = []
-    @State private var bolusPath = Path()
     @State private var tempBasalPath = Path()
     @State private var regularBasalPath = Path()
     @State private var tempTargetsPath = Path()
     @State private var suspensionsPath = Path()
     @State private var carbsDots: [DotInfo] = []
-    @State private var carbsPath = Path()
     @State private var fpuDots: [DotInfo] = []
-    @State private var fpuPath = Path()
     @State private var glucoseYRange: GlucoseYRange = (0, 0, 0, 0)
-    @State private var offset: CGFloat = 0
     @State private var cachedMaxBasalRate: Decimal?
+    private var zoomScale: Double {
+        1.0 / Double(screenHours)
+    }
 
-    private let calculationQueue = DispatchQueue(label: "MainChartView.calculationQueue")
+    private let calculationQueue = DispatchQueue(
+        label: "MainChartView.calculationQueue",
+        qos: .userInteractive,
+        attributes: .concurrent
+    )
 
     private var dateFormatter: DateFormatter {
         let formatter = DateFormatter()
@@ -164,10 +167,6 @@ struct MainChartView: View {
             .onChange(of: vSizeClass) { _ in
                 update(fullSize: geo.size)
             }
-            .onChange(of: screenHours) { _ in
-                update(fullSize: geo.size)
-                // scroll.scrollTo(Config.endID, anchor: .trailing)
-            }
             .onReceive(
                 Foundation.NotificationCenter.default
                     .publisher(for: UIDevice.orientationDidChangeNotification)
@@ -178,34 +177,32 @@ struct MainChartView: View {
     }
 
     private func mainScrollView(fullSize: CGSize) -> some View {
-        ScrollView(.horizontal, showsIndicators: false) {
-            ScrollViewReader { scroll in
+        ScrollViewReader { scroll in
+            ScrollView(.horizontal, showsIndicators: false) {
                 ZStack(alignment: .top) {
                     tempTargetsView(fullSize: fullSize).drawingGroup()
                     basalView(fullSize: fullSize).drawingGroup()
-
                     mainView(fullSize: fullSize).id(Config.endID)
                         .drawingGroup()
-                        .onChange(of: glucose) { _ in
-                            scroll.scrollTo(Config.endID, anchor: .trailing)
-                        }
-                        .onChange(of: suggestion) { _ in
-                            scroll.scrollTo(Config.endID, anchor: .trailing)
-                        }
-                        .onChange(of: tempBasals) { _ in
-                            scroll.scrollTo(Config.endID, anchor: .trailing)
-                        }
-                        .onChange(of: screenHours) { _ in
-                            scroll.scrollTo(Config.endID, anchor: .trailing)
-                        }
-
-                        .onAppear {
-                            // add trigger to the end of main queue
-                            DispatchQueue.main.async {
-                                scroll.scrollTo(Config.endID, anchor: .trailing)
-                                didAppearTrigger = true
-                            }
-                        }
+                }
+            }
+            .onChange(of: glucose) { _ in
+                scroll.scrollTo(Config.endID, anchor: .trailing)
+            }
+            .onChange(of: suggestion) { _ in
+                scroll.scrollTo(Config.endID, anchor: .trailing)
+            }
+            .onChange(of: tempBasals) { _ in
+                scroll.scrollTo(Config.endID, anchor: .trailing)
+            }
+            .onChange(of: screenHours) { _ in
+                scroll.scrollTo(Config.endID, anchor: .trailing)
+            }
+            .onAppear {
+                // add trigger to the end of main queue
+                DispatchQueue.main.async {
+                    scroll.scrollTo(Config.endID, anchor: .trailing)
+                    didAppearTrigger = true
                 }
             }
         }
@@ -264,14 +261,16 @@ struct MainChartView: View {
 
     private func basalView(fullSize: CGSize) -> some View {
         ZStack {
-            tempBasalPath.fill(Color.basal.opacity(0.5))
-            tempBasalPath.stroke(Color.insulin, lineWidth: 1)
-            regularBasalPath.stroke(Color.insulin, style: StrokeStyle(lineWidth: 0.7, dash: [4]))
-            suspensionsPath.stroke(Color.loopGray.opacity(0.7), style: StrokeStyle(lineWidth: 0.7)).scaleEffect(x: 1, y: -1)
-            suspensionsPath.fill(Color.loopGray.opacity(0.2)).scaleEffect(x: 1, y: -1)
+            tempBasalPath.scale(x: zoomScale, anchor: .zero).fill(Color.basal.opacity(0.5))
+            tempBasalPath.scale(x: zoomScale, anchor: .zero).stroke(Color.insulin, lineWidth: 1)
+            regularBasalPath.scale(x: zoomScale, anchor: .zero)
+                .stroke(Color.insulin, style: StrokeStyle(lineWidth: 0.7, dash: [4]))
+            suspensionsPath.scale(x: zoomScale, anchor: .zero)
+                .stroke(Color.loopGray.opacity(0.7), style: StrokeStyle(lineWidth: 0.7)).scaleEffect(x: 1, y: -1)
+            suspensionsPath.scale(x: zoomScale, anchor: .zero).fill(Color.loopGray.opacity(0.2)).scaleEffect(x: 1, y: -1)
         }
         .scaleEffect(x: 1, y: -1)
-        .frame(width: fullGlucoseWidth(viewWidth: fullSize.width) + additionalWidth(viewWidth: fullSize.width))
+        .frame(width: glucoseAndAdditionalWidth(fullSize: fullSize))
         .frame(maxHeight: Config.basalHeight)
         .background(Color.clear)
         .onChange(of: tempBasals) { _ in
@@ -292,24 +291,51 @@ struct MainChartView: View {
     }
 
     private func mainView(fullSize: CGSize) -> some View {
-        Group {
-            VStack {
-                ZStack {
-                    xGridView(fullSize: fullSize)
-                    carbsView(fullSize: fullSize)
-                    fpuView(fullSize: fullSize)
-                    bolusView(fullSize: fullSize)
-                    if smooth { unSmoothedGlucoseView(fullSize: fullSize) }
-                    glucoseView(fullSize: fullSize)
-                    manualGlucoseView(fullSize: fullSize)
-                    manualGlucoseCenterView(fullSize: fullSize)
-                    announcementView(fullSize: fullSize)
-                    predictionsView(fullSize: fullSize)
-                }
-                timeLabelsView(fullSize: fullSize)
+        VStack {
+            ZStack {
+                xGridView(fullSize: fullSize)
+                carbsView(fullSize: fullSize)
+                fpuView(fullSize: fullSize)
+                bolusView(fullSize: fullSize)
+                if smooth { unSmoothedGlucoseView(fullSize: fullSize) }
+                glucoseView(fullSize: fullSize)
+                manualGlucoseView(fullSize: fullSize)
+                manualGlucoseCenterView(fullSize: fullSize)
+                announcementView(fullSize: fullSize)
+                predictionsView(fullSize: fullSize)
             }
+            timeLabelsView(fullSize: fullSize)
         }
-        .frame(width: fullGlucoseWidth(viewWidth: fullSize.width) + additionalWidth(viewWidth: fullSize.width))
+        .frame(width: glucoseAndAdditionalWidth(fullSize: fullSize))
+    }
+
+    /// returns the width of the full chart view including predictions for the current `screenHours`
+    private func glucoseAndAdditionalWidth(fullSize: CGSize) -> CGFloat {
+        // fullGlucoseWidth returns the width scaled to 1h screen hours. Scale it down to screenHours
+        fullGlucoseWidth(viewWidth: fullSize.width) / CGFloat(screenHours)
+            + additionalWidthScaled(viewWidth: fullSize.width)
+    }
+
+    /// returns the additional width for predictions, scaled to `screenHours`
+    private func additionalWidthScaled(viewWidth: CGFloat) -> CGFloat {
+        guard let predictions = suggestion?.predictions,
+              let deliveredAt = suggestion?.deliverAt,
+              let last = glucose.last
+        else {
+            return Config.minAdditionalWidth
+        }
+
+        let iob = predictions.iob?.count ?? 0
+        let zt = predictions.zt?.count ?? 0
+        let cob = predictions.cob?.count ?? 0
+        let uam = predictions.uam?.count ?? 0
+        let max = [iob, zt, cob, uam].max() ?? 0
+
+        let lastDeltaTime = last.dateString.timeIntervalSince(deliveredAt)
+        let additionalTime = CGFloat(TimeInterval(max) * 5.minutes.timeInterval - lastDeltaTime)
+        let oneSecondWidth = oneSecondStep(viewWidth: viewWidth) / CGFloat(screenHours)
+
+        return Swift.min(Swift.max(additionalTime * oneSecondWidth, Config.minAdditionalWidth), 275)
     }
 
     @Environment(\.colorScheme) var colorScheme
@@ -332,7 +358,7 @@ struct MainChartView: View {
             .stroke(useColour, lineWidth: 0.15)
 
             Path { path in // vertical timeline
-                let x = timeToXCoordinate(timerDate.timeIntervalSince1970, fullSize: fullSize)
+                let x = timeToXCoordinate(timerDate.timeIntervalSince1970, fullSize: fullSize) * zoomScale
                 path.move(to: CGPoint(x: x, y: 0))
                 path.addLine(to: CGPoint(x: x, y: fullSize.height - 20))
             }
@@ -389,7 +415,7 @@ struct MainChartView: View {
     private func glucoseView(fullSize: CGSize) -> some View {
         Path { path in
             for rect in glucoseDots {
-                path.addEllipse(in: rect)
+                path.addEllipse(in: scaleCenter(rect: rect))
             }
         }
         .fill(Color.loopGreen)
@@ -407,7 +433,7 @@ struct MainChartView: View {
     private func manualGlucoseView(fullSize: CGSize) -> some View {
         Path { path in
             for rect in manualGlucoseDots {
-                path.addEllipse(in: rect)
+                path.addEllipse(in: scaleCenter(rect: rect))
             }
         }
         .fill(Color.gray)
@@ -425,7 +451,9 @@ struct MainChartView: View {
     private func announcementView(fullSize: CGSize) -> some View {
         ZStack {
             ForEach(announcementDots, id: \.rect.minX) { info -> AnyView in
-                let position = CGPoint(x: info.rect.midX + 5, y: info.rect.maxY - Config.owlOffset)
+                let scaledRect = scaleCenter(rect: info.rect)
+
+                let position = CGPoint(x: scaledRect.midX + 5, y: scaledRect.maxY - Config.owlOffset)
                 let type: String =
                     info.note.contains("true") ?
                     Command.open :
@@ -454,7 +482,7 @@ struct MainChartView: View {
     private func manualGlucoseCenterView(fullSize: CGSize) -> some View {
         Path { path in
             for rect in manualGlucoseDotsCenter {
-                path.addEllipse(in: rect)
+                path.addEllipse(in: scaleCenter(rect: rect))
             }
         }
         .fill(Color.red)
@@ -477,8 +505,9 @@ struct MainChartView: View {
         Path { path in
             var lines: [CGPoint] = []
             for rect in unSmoothedGlucoseDots {
-                lines.append(CGPoint(x: rect.midX, y: rect.midY))
-                path.addEllipse(in: rect)
+                let scaled = scaleCenter(rect: rect)
+                lines.append(CGPoint(x: scaled.midX, y: scaled.midY))
+                path.addEllipse(in: scaled)
             }
             path.addLines(lines)
         }
@@ -496,13 +525,21 @@ struct MainChartView: View {
 
     private func bolusView(fullSize: CGSize) -> some View {
         ZStack {
+            let bolusPath = Path { path in
+                for dot in bolusDots {
+                    path.addEllipse(in: scaleCenter(rect: dot.rect))
+                }
+            }
+
             bolusPath
                 .fill(Color.insulin)
             bolusPath
                 .stroke(Color.primary, lineWidth: 0.5)
 
             ForEach(bolusDots, id: \.rect.minX) { info -> AnyView in
-                let position = CGPoint(x: info.rect.midX, y: info.rect.maxY + 8)
+                let rect = scaleCenter(rect: info.rect)
+
+                let position = CGPoint(x: rect.midX, y: rect.maxY + 8)
                 return Text(bolusFormatter.string(from: info.value as NSNumber)!).font(.caption2)
                     .position(position)
                     .asAny()
@@ -518,13 +555,21 @@ struct MainChartView: View {
 
     private func carbsView(fullSize: CGSize) -> some View {
         ZStack {
+            let carbsPath = Path { path in
+                for dot in carbsDots {
+                    path.addEllipse(in: scaleCenter(rect: dot.rect))
+                }
+            }
+
             carbsPath
                 .fill(Color.loopYellow)
             carbsPath
                 .stroke(Color.primary, lineWidth: 0.5)
 
             ForEach(carbsDots, id: \.rect.minX) { info -> AnyView in
-                let position = CGPoint(x: info.rect.midX, y: info.rect.minY - 8)
+                let rect = scaleCenter(rect: info.rect)
+
+                let position = CGPoint(x: rect.midX, y: rect.minY - 8)
                 return Text(carbsFormatter.string(from: info.value as NSNumber)!).font(.caption2)
                     .position(position)
                     .asAny()
@@ -540,6 +585,12 @@ struct MainChartView: View {
 
     private func fpuView(fullSize: CGSize) -> some View {
         ZStack {
+            let fpuPath = Path { path in
+                for dot in fpuDots {
+                    path.addEllipse(in: scaleCenter(rect: dot.rect))
+                }
+            }
+
             fpuPath
                 .fill(.orange.opacity(0.5))
             fpuPath
@@ -556,8 +607,10 @@ struct MainChartView: View {
     private func tempTargetsView(fullSize: CGSize) -> some View {
         ZStack {
             tempTargetsPath
+                .scale(x: zoomScale, anchor: .zero)
                 .fill(Color.tempBasal.opacity(0.5))
             tempTargetsPath
+                .scale(x: zoomScale, anchor: .zero)
                 .stroke(Color.basal.opacity(0.5), lineWidth: 1)
         }
         .onChange(of: glucose) { _ in
@@ -571,29 +624,37 @@ struct MainChartView: View {
         }
     }
 
+    private func scale(rect: CGRect) -> CGRect {
+        CGRect(origin: CGPoint(x: rect.origin.x * zoomScale, y: rect.origin.y), size: rect.size)
+    }
+
+    private func scaleCenter(rect: CGRect) -> CGRect {
+        CGRect(origin: CGPoint(x: rect.midX * zoomScale - rect.width / 2, y: rect.origin.y), size: rect.size)
+    }
+
     private func predictionsView(fullSize: CGSize) -> some View {
         Group {
             Path { path in
                 for rect in predictionDots[.iob] ?? [] {
-                    path.addEllipse(in: rect)
+                    path.addEllipse(in: scaleCenter(rect: rect))
                 }
             }.fill(Color.insulin)
 
             Path { path in
                 for rect in predictionDots[.cob] ?? [] {
-                    path.addEllipse(in: rect)
+                    path.addEllipse(in: scaleCenter(rect: rect))
                 }
             }.fill(Color.loopYellow)
 
             Path { path in
                 for rect in predictionDots[.zt] ?? [] {
-                    path.addEllipse(in: rect)
+                    path.addEllipse(in: scaleCenter(rect: rect))
                 }
             }.fill(Color.zt)
 
             Path { path in
                 for rect in predictionDots[.uam] ?? [] {
-                    path.addEllipse(in: rect)
+                    path.addEllipse(in: scaleCenter(rect: rect))
                 }
             }.fill(Color.uam)
         }
@@ -605,6 +666,8 @@ struct MainChartView: View {
 
 // MARK: - Calculations
 
+/// some of the calculations done here can take quite long (100ms+) and are not able to update data at a fast rate
+/// therefore we stick to the 1h screen window for these calculations and scale the results as needed to `screenHours` which has little extra overhead and enables changing the screen hours with no lag
 extension MainChartView {
     private func update(fullSize: CGSize) {
         calculatePredictionDots(fullSize: fullSize, type: .iob)
@@ -721,15 +784,8 @@ extension MainChartView {
                 return DotInfo(rect: rect, value: value.amount ?? 0)
             }
 
-            let path = Path { path in
-                for dot in dots {
-                    path.addEllipse(in: dot.rect)
-                }
-            }
-
             DispatchQueue.main.async {
                 bolusDots = dots
-                bolusPath = path
             }
         }
     }
@@ -748,15 +804,8 @@ extension MainChartView {
                 return DotInfo(rect: rect, value: value.carbs)
             }
 
-            let path = Path { path in
-                for dot in dots {
-                    path.addEllipse(in: dot.rect)
-                }
-            }
-
             DispatchQueue.main.async {
                 carbsDots = dots
-                carbsPath = path
             }
         }
     }
@@ -775,15 +824,8 @@ extension MainChartView {
                 return DotInfo(rect: rect, value: value.carbs)
             }
 
-            let path = Path { path in
-                for dot in dots {
-                    path.addEllipse(in: dot.rect)
-                }
-            }
-
             DispatchQueue.main.async {
                 fpuDots = dots
-                fpuPath = path
             }
         }
     }
@@ -858,9 +900,8 @@ extension MainChartView {
                 path.addLine(to: CGPoint(x: lastPoint.x, y: Config.basalHeight))
                 path.addLine(to: CGPoint(x: 0, y: Config.basalHeight))
             }
-            let adjustForOptionalExtraHours = screenHours > 12 ? screenHours - 12 : 0
-            let endDateTime = dayAgoTime + min(max(Int(screenHours - adjustForOptionalExtraHours), 12), 24).hours
-                .timeInterval + min(max(Int(screenHours - adjustForOptionalExtraHours), 12), 24).hours
+            let endDateTime = dayAgoTime + 12.hours
+                .timeInterval + 12.hours
                 .timeInterval
             let autotunedBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
@@ -920,8 +961,7 @@ extension MainChartView {
                     .map { self.timeToXCoordinate($0.timestamp.timeIntervalSince1970, fullSize: fullSize) }
                 let x0 = self.timeToXCoordinate(event.timestamp.timeIntervalSince1970, fullSize: fullSize)
 
-                let x1 = tbrTimeX ?? self.fullGlucoseWidth(viewWidth: fullSize.width) + self
-                    .additionalWidth(viewWidth: fullSize.width)
+                let x1 = tbrTimeX ?? self.fullGlucoseWidth(viewWidth: fullSize.width) + 275
 
                 return CGRect(x: x0, y: 0, width: x1 - x0, height: Config.basalHeight * 0.7)
             }
@@ -1067,32 +1107,11 @@ extension MainChartView {
     }
 
     private func fullGlucoseWidth(viewWidth: CGFloat) -> CGFloat {
-        viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
-    }
-
-    private func additionalWidth(viewWidth: CGFloat) -> CGFloat {
-        guard let predictions = suggestion?.predictions,
-              let deliveredAt = suggestion?.deliverAt,
-              let last = glucose.last
-        else {
-            return Config.minAdditionalWidth
-        }
-
-        let iob = predictions.iob?.count ?? 0
-        let zt = predictions.zt?.count ?? 0
-        let cob = predictions.cob?.count ?? 0
-        let uam = predictions.uam?.count ?? 0
-        let max = [iob, zt, cob, uam].max() ?? 0
-
-        let lastDeltaTime = last.dateString.timeIntervalSince(deliveredAt)
-        let additionalTime = CGFloat(TimeInterval(max) * 5.minutes.timeInterval - lastDeltaTime)
-        let oneSecondWidth = oneSecondStep(viewWidth: viewWidth)
-
-        return Swift.min(Swift.max(additionalTime * oneSecondWidth, Config.minAdditionalWidth), 275)
+        viewWidth * CGFloat(hours)
     }
 
     private func oneSecondStep(viewWidth: CGFloat) -> CGFloat {
-        viewWidth / (CGFloat(min(max(screenHours, 2), 24)) * CGFloat(1.hours.timeInterval))
+        viewWidth / CGFloat(1.hours.timeInterval)
     }
 
     private func maxPredValue() -> Int? {
@@ -1159,6 +1178,15 @@ extension MainChartView {
         return x
     }
 
+    /// inverse of `timeToXCoordinate`
+    private func xCoordinateToTime(x: CGFloat, fullSize: CGSize) -> TimeInterval {
+        let stepXFraction = fullGlucoseWidth(viewWidth: fullSize.width) / CGFloat(hours.hours.timeInterval)
+        let xx = x / stepXFraction
+        let xOffset = -Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
+        let time = xx - xOffset
+        return time
+    }
+
     private func glucoseToYCoordinate(_ glucoseValue: Int, fullSize: CGSize) -> CGFloat {
         let topYPaddint = Config.topYPadding + Config.basalHeight
         let bottomYPadding = Config.bottomYPadding

+ 4 - 2
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -367,11 +367,10 @@ extension Home {
         }
 
         var timeInterval: some View {
-            HStack {
+            HStack(alignment: .center) {
                 ForEach(timeButtons) { button in
                     Text(button.active ? NSLocalizedString(button.label, comment: "") : button.number).onTapGesture {
                         state.hours = button.hours
-                        highlightButtons()
                     }
                     .foregroundStyle(button.active ? (colorScheme == .dark ? Color.white : Color.black).opacity(0.9) : .secondary)
                     .frame(maxHeight: 30).padding(.horizontal, 8)
@@ -702,6 +701,9 @@ extension Home {
                 .background(colorBackground)
                 .edgesIgnoringSafeArea(.all)
             }
+            .onChange(of: state.hours) { _ in
+                highlightButtons()
+            }
             .onAppear {
                 configureView {
                     highlightButtons()

+ 5 - 5
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -174,7 +174,7 @@ extension NightscoutConfig {
                                 }
                                 return CarbRatioEntry(
                                     start: carbratio.time,
-                                    offset: (carbratio.timeAsSeconds ?? self.offset(carbratio.time)) / 60,
+                                    offset: self.offset(carbratio.time) / 60,
                                     ratio: carbratio.value
                                 ) }
                         let carbratiosProfile = CarbRatios(units: CarbUnit.grams, schedule: carbratios)
@@ -195,7 +195,7 @@ extension NightscoutConfig {
                                 }
                                 return BasalProfileEntry(
                                     start: basal.time,
-                                    minutes: (basal.timeAsSeconds ?? self.offset(basal.time)) / 60,
+                                    minutes: self.offset(basal.time) / 60,
                                     rate: basal.value
                                 ) }
                         // DASH pumps can have 0U/h basal rates but don't import if total basals (24 hours) amount to 0 U.
@@ -213,7 +213,7 @@ extension NightscoutConfig {
                         let sensitivities = fetchedProfile.sens.map { sensitivity -> InsulinSensitivityEntry in
                             InsulinSensitivityEntry(
                                 sensitivity: self.units == .mmolL ? sensitivity.value : sensitivity.value.asMgdL,
-                                offset: (sensitivity.timeAsSeconds ?? self.offset(sensitivity.time)) / 60,
+                                offset: self.offset(sensitivity.time) / 60,
                                 start: sensitivity.time
                             )
                         }
@@ -236,7 +236,7 @@ extension NightscoutConfig {
                                     low: self.units == .mmolL ? target.value : target.value.asMgdL,
                                     high: self.units == .mmolL ? target.value : target.value.asMgdL,
                                     start: target.time,
-                                    offset: (target.timeAsSeconds ?? self.offset(target.time)) / 60
+                                    offset: self.offset(target.time) / 60
                                 ) }
                         let targetsProfile = BGTargets(
                             units: self.units,
@@ -308,7 +308,7 @@ extension NightscoutConfig {
         func offset(_ string: String) -> Int {
             let hours = Int(string.prefix(2)) ?? 0
             let minutes = Int(string.suffix(2)) ?? 0
-            return hours * 60 + minutes * 60
+            return ((hours * 60) + minutes) * 60
         }
 
         func saveError(_ string: String) {

+ 2 - 0
FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift

@@ -9,6 +9,7 @@ extension NotificationsConfig {
         @Published var lowGlucose: Decimal = 0
         @Published var highGlucose: Decimal = 0
         @Published var carbsRequiredThreshold: Decimal = 0
+        @Published var useLiveActivity = false
         var units: GlucoseUnits = .mmolL
 
         override func subscribe() {
@@ -20,6 +21,7 @@ extension NotificationsConfig {
             subscribeSetting(\.useAlarmSound, on: $useAlarmSound) { useAlarmSound = $0 }
             subscribeSetting(\.addSourceInfoToGlucoseNotifications, on: $addSourceInfoToGlucoseNotifications) {
                 addSourceInfoToGlucoseNotifications = $0 }
+            subscribeSetting(\.useLiveActivity, on: $useLiveActivity) { useLiveActivity = $0 }
 
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
                 let value = max(min($0, 400), 40)

+ 11 - 0
FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift

@@ -55,6 +55,17 @@ extension NotificationsConfig {
                         Text("g").foregroundColor(.secondary)
                     }
                 }
+
+                if #available(iOS 16.2, *) {
+                    Section(
+                        header: Text("Live Activity"),
+                        footer: Text(
+                            "Live activity displays blood glucose live on the lock screen and on the dynamic island (if available)"
+                        )
+                    ) {
+                        Toggle("Show live activity", isOn: $state.useLiveActivity)
+                    }
+                }
             }
             .onAppear(perform: configureView)
             .navigationBarTitle("Notifications")

+ 13 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActitiyShared.swift

@@ -0,0 +1,13 @@
+import ActivityKit
+import Foundation
+
+struct LiveActivityAttributes: ActivityAttributes {
+    public struct ContentState: Codable, Hashable {
+        let bg: String
+        let trendSystemImage: String?
+        let change: String
+        let date: Date
+    }
+
+    let startDate: Date
+}

+ 210 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -0,0 +1,210 @@
+import ActivityKit
+import Foundation
+import Swinject
+import UIKit
+
+extension LiveActivityAttributes.ContentState {
+    static func formatGlucose(_ value: Int, mmol: Bool, forceSign: Bool) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if mmol {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        if forceSign {
+            formatter.positivePrefix = formatter.plusSign
+        }
+        formatter.roundingMode = .halfUp
+
+        return formatter
+            .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
+    }
+
+    init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool) {
+        guard let glucose = bg.glucose,
+              bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
+        else {
+            return nil
+        }
+
+        let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
+
+        let trendString: String?
+        switch bg.direction {
+        case .doubleUp,
+             .singleUp,
+             .tripleUp:
+            trendString = "arrow.up"
+
+        case .fortyFiveUp:
+            trendString = "arrow.up.right"
+
+        case .flat:
+            trendString = "arrow.right"
+
+        case .fortyFiveDown:
+            trendString = "arrow.down.right"
+
+        case .doubleDown,
+             .singleDown,
+             .tripleDown:
+            trendString = "arrow.down"
+
+        case .notComputable,
+             Optional.none,
+             .rateOutOfRange,
+             .some(.none):
+            trendString = nil
+        }
+
+        let change = prev?.glucose.map({
+            Self.formatGlucose(glucose - $0, mmol: mmol, forceSign: true)
+        }) ?? ""
+
+        self.init(bg: formattedBG, trendSystemImage: trendString, change: change, date: bg.dateString)
+    }
+}
+
+@available(iOS 16.2, *) private struct ActiveActivity {
+    let activity: Activity<LiveActivityAttributes>
+    let startDate: Date
+
+    func needsRecreation() -> Bool {
+        switch activity.activityState {
+        case .dismissed,
+             .ended:
+            return true
+        case .active,
+             .stale: break
+        @unknown default:
+            return true
+        }
+
+        return -startDate.timeIntervalSinceNow >
+            TimeInterval(60 * 60)
+    }
+}
+
+@available(iOS 16.2, *) final class LiveActivityBridge: Injectable {
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var broadcaster: Broadcaster!
+
+    private var settings: FreeAPSSettings {
+        settingsManager.settings
+    }
+
+    private var currentActivity: ActiveActivity?
+    private var latestGlucose: BloodGlucose?
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        broadcaster.register(GlucoseObserver.self, observer: self)
+
+        Foundation.NotificationCenter.default.addObserver(
+            forName: UIApplication.didEnterBackgroundNotification,
+            object: nil,
+            queue: nil
+        ) { _ in
+            self.forceActivityUpdate()
+        }
+
+        Foundation.NotificationCenter.default.addObserver(
+            forName: UIApplication.didBecomeActiveNotification,
+            object: nil,
+            queue: nil
+        ) { _ in
+            self.forceActivityUpdate()
+        }
+    }
+
+    /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
+    /// Ends existing live activities if live activities are not enabled in settings
+    private func forceActivityUpdate() {
+        // just before app resigns active, show a new activity
+        // only do this if there is no current activity or the current activity is older than 1h
+        if settings.useLiveActivity {
+            if currentActivity?.needsRecreation() ?? true
+            {
+                glucoseDidUpdate(glucoseStorage.recent())
+            }
+        } else {
+            Task {
+                await self.endActivity()
+            }
+        }
+    }
+
+    /// attempts to present this live activity state, creating a new activity if none exists yet
+    @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
+        // hide duplicate/unknown activities
+        for unknownActivity in Activity<LiveActivityAttributes>.activities
+            .filter({ self.currentActivity?.activity.id != $0.id })
+        {
+            await unknownActivity.end(nil, dismissalPolicy: .immediate)
+        }
+
+        let content = ActivityContent(state: state, staleDate: state.date.addingTimeInterval(TimeInterval(6 * 60)))
+
+        if let currentActivity {
+            if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
+                // activity is no longer visible or old. End it and try to push the update again
+                await endActivity()
+                await pushUpdate(state)
+            } else {
+                await currentActivity.activity.update(content)
+            }
+        } else {
+            do {
+                let activity = try Activity.request(
+                    attributes: LiveActivityAttributes(startDate: Date.now),
+                    content: content,
+                    pushType: nil
+                )
+                currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
+            } catch {
+                print("activity creation error: \(error)")
+            }
+        }
+    }
+
+    /// ends all live activities immediateny
+    private func endActivity() async {
+        if let currentActivity {
+            await currentActivity.activity.end(nil, dismissalPolicy: ActivityUIDismissalPolicy.immediate)
+            self.currentActivity = nil
+        }
+
+        // end any other activities
+        for unknownActivity in Activity<LiveActivityAttributes>.activities {
+            await unknownActivity.end(nil, dismissalPolicy: .immediate)
+        }
+    }
+}
+
+@available(iOS 16.2, *)
+extension LiveActivityBridge: GlucoseObserver {
+    func glucoseDidUpdate(_ glucose: [BloodGlucose]) {
+        // backfill latest glucose if contained in this update
+        if glucose.count > 1 {
+            latestGlucose = glucose[glucose.count - 2]
+        }
+        defer {
+            self.latestGlucose = glucose.last
+        }
+
+        guard let bg = glucose.last, let content = LiveActivityAttributes.ContentState(
+            new: bg,
+            prev: latestGlucose,
+            mmol: settings.units == .mmolL
+        ) else {
+            // no bg or value stale. Don't update the activity if there already is one, just let it turn stale so that it can still be used once current bg is available again
+            return
+        }
+
+        Task {
+            await self.pushUpdate(content)
+        }
+    }
+}

+ 26 - 15
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -482,15 +482,28 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
     }
 
     func uploadProfileAndSettings(_ force: Bool) {
-        // These should be modified anyways and not the defaults
-        guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
-              let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self),
-              let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self),
-              let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self),
-              let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self),
-              let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self)
-        else {
-            debug(.nightscout, "NightscoutManager uploadProfile Not all settings found to build profile!")
+        guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading insulinSensitivities")
+            return
+        }
+        guard let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading settings")
+            return
+        }
+        guard let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading preferences")
+            return
+        }
+        guard let targets = storage.retrieve(OpenAPS.Settings.bgTargets, as: BGTargets.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading bgTargets")
+            return
+        }
+        guard let carbRatios = storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading carbRatios")
+            return
+        }
+        guard let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self) else {
+            debug(.nightscout, "NightscoutManager uploadProfile: error loading basalProfile")
             return
         }
 
@@ -498,32 +511,30 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.sensitivity,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
-
         let target_low = targets.targets.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.low,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
         let target_high = targets.targets.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.high,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
         let cr = carbRatios.schedule.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),
                 value: item.ratio,
-                timeAsSeconds: item.offset
+                timeAsSeconds: item.offset * 60
             )
         }
-
         let basal = basalProfile.map { item -> NightscoutTimevalue in
             NightscoutTimevalue(
                 time: String(item.start.prefix(5)),

+ 11 - 0
LiveActivity/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 13 - 0
LiveActivity/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,13 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
LiveActivity/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 11 - 0
LiveActivity/Assets.xcassets/WidgetBackground.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 11 - 0
LiveActivity/Info.plist

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSExtension</key>
+	<dict>
+		<key>NSExtensionPointIdentifier</key>
+		<string>com.apple.widgetkit-extension</string>
+	</dict>
+</dict>
+</plist>

+ 111 - 0
LiveActivity/LiveActivity.swift

@@ -0,0 +1,111 @@
+import ActivityKit
+import SwiftUI
+import WidgetKit
+
+struct LiveActivity: Widget {
+    let dateFormatter: DateFormatter = {
+        var f = DateFormatter()
+        f.dateStyle = .none
+        f.timeStyle = .short
+        return f
+    }()
+
+    func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        if !context.isStale && !context.state.change.isEmpty {
+            Text(context.state.change)
+        } else {
+            Text("--")
+        }
+    }
+
+    func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        Text("Updated: \(dateFormatter.string(from: context.state.date))")
+    }
+
+    func bgLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
+        if context.isStale {
+            Text("--")
+        } else {
+            Text(context.state.bg)
+        }
+    }
+
+    @ViewBuilder func bgAndTrend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
+        if context.isStale {
+            Text("--")
+        } else {
+            Text(context.state.bg)
+            if let trendSystemImage = context.state.trendSystemImage {
+                Image(systemName: trendSystemImage)
+            }
+        }
+    }
+
+    var body: some WidgetConfiguration {
+        ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+            // Lock screen/banner UI goes here
+
+            HStack(spacing: 3) {
+                bgAndTrend(context: context).font(.title)
+                Spacer()
+                VStack(alignment: .trailing, spacing: 5) {
+                    changeLabel(context: context).font(.title3)
+                    updatedLabel(context: context).font(.caption).foregroundStyle(.black.opacity(0.7))
+                }
+            }
+            .privacySensitive()
+            .imageScale(.small)
+            .padding(.all, 15)
+            .background(Color.white.opacity(0.2))
+            .foregroundColor(Color.black)
+            .activityBackgroundTint(Color.cyan.opacity(0.2))
+            .activitySystemActionForegroundColor(Color.black)
+
+        } dynamicIsland: { context in
+            DynamicIsland {
+                // Expanded UI goes here.  Compose the expanded UI through
+                // various regions, like leading/trailing/center/bottom
+                DynamicIslandExpandedRegion(.leading) {
+                    HStack(spacing: 3) {
+                        bgAndTrend(context: context)
+                    }.imageScale(.small).font(.title).padding(.leading, 5)
+                }
+                DynamicIslandExpandedRegion(.trailing) {
+                    changeLabel(context: context).font(.title).padding(.trailing, 5)
+                }
+                DynamicIslandExpandedRegion(.bottom) {
+                    updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
+                        .padding(.bottom, 5)
+                }
+            } compactLeading: {
+                HStack(spacing: 1) {
+                    bgAndTrend(context: context)
+                }.bold().imageScale(.small).padding(.leading, 5)
+            } compactTrailing: {
+                changeLabel(context: context).padding(.trailing, 5)
+            } minimal: {
+                bgLabel(context: context).bold()
+            }
+            .widgetURL(URL(string: "freeaps-x://"))
+            .keylineTint(Color.cyan.opacity(0.5))
+        }
+    }
+}
+
+private extension LiveActivityAttributes {
+    static var preview: LiveActivityAttributes {
+        LiveActivityAttributes(startDate: Date())
+    }
+}
+
+private extension LiveActivityAttributes.ContentState {
+    static var test: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(bg: "100", trendSystemImage: "arrow.right", change: "+2", date: Date())
+    }
+}
+
+#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
+    LiveActivity()
+} contentStates: {
+    LiveActivityAttributes.ContentState.test
+}

+ 8 - 0
LiveActivity/LiveActivityBundle.swift

@@ -0,0 +1,8 @@
+import SwiftUI
+import WidgetKit
+
+@main struct LiveActivityBundle: WidgetBundle {
+    var body: some Widget {
+        LiveActivity()
+    }
+}

+ 13 - 1
fastlane/Fastfile

@@ -57,7 +57,8 @@ platform :ios do
       app_identifier: [
         "ru.artpancreas.#{TEAMID}.FreeAPS",
         "ru.artpancreas.#{TEAMID}.FreeAPS.watchkitapp",
-        "ru.artpancreas.#{TEAMID}.FreeAPS.watchkitapp.watchkitextension"
+        "ru.artpancreas.#{TEAMID}.FreeAPS.watchkitapp.watchkitextension",
+        "ru.artpancreas.#{TEAMID}.FreeAPS.LiveActivity"
       ]
     )
 
@@ -97,6 +98,12 @@ platform :ios do
       code_sign_identity: "iPhone Distribution",
       targets: ["FreeAPSWatch"]
     )
+    update_code_signing_settings(
+      path: "#{GITHUB_WORKSPACE}/FreeAPS.xcodeproj",
+      profile_name: mapping["ru.artpancreas.#{TEAMID}.FreeAPS.LiveActivity"],
+      code_sign_identity: "iPhone Distribution",
+      targets: ["LiveActivityExtension"]
+    )
 
     gym(
       export_method: "app-store",
@@ -162,6 +169,10 @@ platform :ios do
     configure_bundle_id("FreeAPSWatch", "ru.artpancreas.#{TEAMID}.FreeAPS.watchkitapp", [
       Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
     ])
+
+    configure_bundle_id("LiveActivityExtension", "ru.artpancreas.#{TEAMID}.FreeAPS.LiveActivity", [
+      Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
+    ])
     
   end
 
@@ -184,6 +195,7 @@ platform :ios do
         "ru.artpancreas.#{TEAMID}.FreeAPS",
         "ru.artpancreas.#{TEAMID}.FreeAPS.watchkitapp.watchkitextension",
         "ru.artpancreas.#{TEAMID}.FreeAPS.watchkitapp",
+        "ru.artpancreas.#{TEAMID}.FreeAPS.LiveActivity"
       ]
     )
   end