From 07585119241352147067a1667a5de2627b709c7d Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 21 Mar 2024 09:39:57 -0700 Subject: [PATCH 01/21] use setState instead of mutateState for OmniKit --- OmniKit/PumpManager/OmnipodPumpManager.swift | 38 ++++++++------------ 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 95a6097..7cfa710 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -143,14 +143,6 @@ public class OmnipodPumpManager: RileyLinkPumpManager { return setStateWithResult(changes) } - @discardableResult - private func mutateState(_ changes: (_ state: inout OmnipodPumpManagerState) -> Void) -> OmnipodPumpManagerState { - return setStateWithResult({ (state) -> OmnipodPumpManagerState in - changes(&state) - return state - }) - } - private func setStateWithResult(_ changes: (_ state: inout OmnipodPumpManagerState) -> ReturnType) -> ReturnType { var oldValue: OmnipodPumpManagerState! var returnType: ReturnType! @@ -1357,7 +1349,7 @@ extension OmnipodPumpManager: PumpManager { public var defaultExpirationReminderOffset: TimeInterval { set { - mutateState { (state) in + setState { (state) in state.defaultExpirationReminderOffset = newValue } } @@ -1368,7 +1360,7 @@ extension OmnipodPumpManager: PumpManager { public var lowReservoirReminderValue: Double { set { - mutateState { (state) in + setState { (state) in state.lowReservoirReminderValue = newValue } } @@ -1379,7 +1371,7 @@ extension OmnipodPumpManager: PumpManager { public var podAttachmentConfirmed: Bool { set { - mutateState { (state) in + setState { (state) in state.podAttachmentConfirmed = newValue } } @@ -1390,7 +1382,7 @@ extension OmnipodPumpManager: PumpManager { public var initialConfigurationCompleted: Bool { set { - mutateState { (state) in + setState { (state) in state.initialConfigurationCompleted = newValue } } @@ -1871,7 +1863,7 @@ extension OmnipodPumpManager: PumpManager { } public func syncDeliveryLimits(limits deliveryLimits: DeliveryLimits, completion: @escaping (Result) -> Void) { - mutateState { state in + setState { state in if let rate = deliveryLimits.maximumBasalRate?.doubleValue(for: .internationalUnitsPerHour) { state.maximumTempBasalRate = rate completion(.success(deliveryLimits)) @@ -1926,7 +1918,7 @@ extension OmnipodPumpManager: PumpManager { do { let beepBlock = self.beepMessageBlock(beepType: .beep) try session.configureAlerts([expirationReminder], beepBlock: beepBlock) - self.mutateState({ (state) in + self.setState({ (state) in state.scheduledExpirationReminderOffset = intervalBeforeExpiration }) completion(nil) @@ -1985,7 +1977,7 @@ extension OmnipodPumpManager: PumpManager { do { let beepBlock = self.beepMessageBlock(beepType: .beep) try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock) - self.mutateState({ (state) in + self.setState({ (state) in state.lowReservoirReminderValue = Double(value) }) completion(nil) @@ -2012,7 +2004,7 @@ extension OmnipodPumpManager: PumpManager { } } - self.mutateState { (state) in + self.setState { (state) in state.activeAlerts.insert(alert) } } @@ -2028,7 +2020,7 @@ extension OmnipodPumpManager: PumpManager { delegate?.retractAlert(identifier: repeatingIdentifier) } } - self.mutateState { (state) in + self.setState { (state) in state.activeAlerts.remove(alert) } } @@ -2102,7 +2094,7 @@ extension OmnipodPumpManager: PumpManager { } catch { return } - self.mutateState { state in + self.setState { state in state.activeAlerts.remove(alert) state.alertsWithPendingAcknowledgment.remove(alert) } @@ -2226,7 +2218,7 @@ extension OmnipodPumpManager: PodCommsDelegate { } } else { // Resetting podState - mutateState { state in + setState { state in state.updatePodStateFromPodComms(podState) } } @@ -2253,18 +2245,18 @@ extension OmnipodPumpManager { let beepBlock = self.beepMessageBlock(beepType: .beep) let _ = try session.acknowledgeAlerts(alerts: AlertSet(slots: [slot]), beepBlock: beepBlock) } catch { - self.mutateState { state in + self.setState { state in state.alertsWithPendingAcknowledgment.insert(alert) } completion(error) return } - self.mutateState { state in + self.setState { state in state.activeAlerts.remove(alert) } completion(nil) case .failure(let error): - self.mutateState { state in + self.setState { state in state.alertsWithPendingAcknowledgment.insert(alert) } completion(error) @@ -2273,7 +2265,7 @@ extension OmnipodPumpManager { } } else { // Non-pod alert - self.mutateState { state in + self.setState { state in state.activeAlerts.remove(alert) if alert == .timeOffsetChangeDetected { state.acknowledgedTimeOffsetAlert = true From 0207f27c0a55c6be78c1465f14bff3adb0785492 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 21 Mar 2024 10:00:04 -0700 Subject: [PATCH 02/21] rename alert slots to descriptive titles for OmniKit --- OmniKit/OmnipodCommon/AlertSlot.swift | 38 +++++++++++++-------------- OmniKit/PumpManager/PodState.swift | 14 +++++----- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/OmniKit/OmnipodCommon/AlertSlot.swift b/OmniKit/OmnipodCommon/AlertSlot.swift index 52cd8dd..d357519 100644 --- a/OmniKit/OmnipodCommon/AlertSlot.swift +++ b/OmniKit/OmnipodCommon/AlertSlot.swift @@ -118,23 +118,23 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { public var configuration: AlertConfiguration { switch self { case .waitingForPairingReminder: - return AlertConfiguration(alertType: .slot7, duration: .minutes(110), trigger: .timeUntilAlert(.minutes(10)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot7Expired, duration: .minutes(110), trigger: .timeUntilAlert(.minutes(10)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .finishSetupReminder: - return AlertConfiguration(alertType: .slot7, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot7Expired, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .expirationReminder(let alertTime): let active = alertTime != 0 // disable if alertTime is 0 - return AlertConfiguration(alertType: .slot3, active: active, duration: 0, trigger: .timeUntilAlert(alertTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot3ExpirationReminder, active: active, duration: 0, trigger: .timeUntilAlert(alertTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .expired(let alarmTime, let duration): let active = alarmTime != 0 // disable if alarmTime is 0 - return AlertConfiguration(alertType: .slot7, active: active, duration: duration, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot7Expired, active: active, duration: duration, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .shutdownImminent(let alarmTime): let active = alarmTime != 0 // disable if alarmTime is 0 - return AlertConfiguration(alertType: .slot2, active: active, duration: 0, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot2ShutdownImminent, active: active, duration: 0, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .lowReservoir(let units): let active = units != 0 // disable if units is 0 - return AlertConfiguration(alertType: .slot4, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot4LowReservoir, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .autoOff(let active, let countdownDuration): - return AlertConfiguration(alertType: .slot0, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + return AlertConfiguration(alertType: .slot0AutoOff, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .podSuspendedReminder(let active, let suspendTime): // A suspendTime of 0 is an untimed suspend let reminderInterval, duration: TimeInterval @@ -166,7 +166,7 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { beepRepeat = .once beepType = .noBeepCancel } - return AlertConfiguration(alertType: .slot5, active: active, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) + return AlertConfiguration(alertType: .slot5SuspendedReminder, active: active, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) case .suspendTimeExpired(let suspendTime): let active = suspendTime != 0 // disable if suspendTime is 0 let trigger: AlertTrigger @@ -181,7 +181,7 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { beepRepeat = .once beepType = .noBeepCancel } - return AlertConfiguration(alertType: .slot6, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) + return AlertConfiguration(alertType: .slot6SuspendTimeExpired, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) } } @@ -301,14 +301,14 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { } public enum AlertSlot: UInt8 { - case slot0 = 0x00 - case slot1 = 0x01 - case slot2 = 0x02 - case slot3 = 0x03 - case slot4 = 0x04 - case slot5 = 0x05 - case slot6 = 0x06 - case slot7 = 0x07 + case slot0AutoOff = 0x00 + case slot1NotUsed = 0x01 + case slot2ShutdownImminent = 0x02 + case slot3ExpirationReminder = 0x03 + case slot4LowReservoir = 0x04 + case slot5SuspendedReminder = 0x05 + case slot6SuspendTimeExpired = 0x06 + case slot7Expired = 0x07 public var bitMaskValue: UInt8 { return 1< Bool { - // slot5 is for podSuspendedReminder and slot6 is for suspendTimeExpired - if configuredAlerts.contains(where: { ($0.key == .slot5 || $0.key == .slot6) && $0.value.configuration.active }) { + // slot5SuspendedReminder is for podSuspendedReminder and slot6SuspendTimeExpired is for suspendTimeExpired + if configuredAlerts.contains(where: { ($0.key == .slot5SuspendedReminder || $0.key == .slot6SuspendTimeExpired) && $0.value.configuration.active }) { return true } return false diff --git a/OmniKit/PumpManager/PodState.swift b/OmniKit/PumpManager/PodState.swift index 4e4c027..391e078 100644 --- a/OmniKit/PumpManager/PodState.swift +++ b/OmniKit/PumpManager/PodState.swift @@ -136,7 +136,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl self.messageTransportState = MessageTransportState(packetNumber: packetNumber, messageNumber: messageNumber) self.primeFinishTime = nil self.setupProgress = .addressAssigned - self.configuredAlerts = [.slot7: .waitingForPairingReminder] + self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder] self.insulinType = insulinType self.lastDeliveryStatusReceived = initialDeliveryStatus // can be non-nil when testing } @@ -460,12 +460,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl } else { // Assume migration, and set up with alerts that are normally configured self.configuredAlerts = [ - .slot2: .shutdownImminent(0), - .slot3: .expirationReminder(0), - .slot4: .lowReservoir(0), - .slot5: .podSuspendedReminder(active: false, suspendTime: 0), - .slot6: .suspendTimeExpired(suspendTime: 0), - .slot7: .expired(alertTime: 0, duration: 0) + .slot2ShutdownImminent: .shutdownImminent(0), + .slot3ExpirationReminder: .expirationReminder(0), + .slot4LowReservoir: .lowReservoir(0), + .slot5SuspendedReminder: .podSuspendedReminder(active: false, suspendTime: 0), + .slot6SuspendTimeExpired: .suspendTimeExpired(suspendTime: 0), + .slot7Expired: .expired(alertTime: 0, duration: 0) ] } From 7439fc810288cea6ef3880e2225d347ed9088bc8 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 21 Mar 2024 14:32:29 -0700 Subject: [PATCH 03/21] Add SilentPod, diagnotics and update Pod menu --- OmniKit.xcodeproj/project.pbxproj | 37 ++ OmniKit/OmnipodCommon/AlertSlot.swift | 593 ++++++++++++++---- .../ConfigureAlertsCommand.swift | 8 +- OmniKit/OmnipodCommon/Pod.swift | 7 +- OmniKit/OmnipodCommon/PumpManagerAlert.swift | 40 +- .../OmnipodCommon/SilencePodPreference.swift | 32 + .../PumpManager/DetailedStatus+OmniKit.swift | 50 +- OmniKit/PumpManager/OmnipodPumpManager.swift | 401 +++++++++--- .../PumpManager/OmnipodPumpManagerState.swift | 23 +- OmniKit/PumpManager/PodComms.swift | 1 - OmniKit/PumpManager/PodCommsSession.swift | 53 +- OmniKit/PumpManager/PodState.swift | 72 ++- OmniKitPacketParser/main.swift | 15 +- OmniKitTests/AcknowledgeAlertsTests.swift | 2 +- OmniKitTests/MessageTests.swift | 43 +- .../ViewModels/OmnipodSettingsViewModel.swift | 68 +- OmniKitUI/Views/ActivityView.swift | 36 ++ .../Views/BeepPreferenceSelectionView.swift | 12 +- OmniKitUI/Views/FirstAppear.swift | 30 + OmniKitUI/Views/OmnipodSettingsView.swift | 79 ++- OmniKitUI/Views/PlayTestBeepsView.swift | 100 +++ OmniKitUI/Views/PodDetailsView.swift | 9 +- OmniKitUI/Views/PodDiagnostics.swift | 90 +++ OmniKitUI/Views/PodSetupView.swift | 2 +- OmniKitUI/Views/PumpManagerDetailsView.swift | 104 +++ OmniKitUI/Views/ReadPodInfoView.swift | 135 ++++ OmniKitUI/Views/ReadPodStatusView.swift | 169 +++++ OmniKitUI/Views/ReadPulseLogView.swift | 128 ++++ OmniKitUI/Views/SilencePodSelectionView.swift | 144 +++++ .../Views/UncertaintyRecoveredView.swift | 5 +- 30 files changed, 2097 insertions(+), 391 deletions(-) create mode 100644 OmniKit/OmnipodCommon/SilencePodPreference.swift create mode 100644 OmniKitUI/Views/ActivityView.swift create mode 100644 OmniKitUI/Views/FirstAppear.swift create mode 100644 OmniKitUI/Views/PlayTestBeepsView.swift create mode 100644 OmniKitUI/Views/PodDiagnostics.swift create mode 100644 OmniKitUI/Views/PumpManagerDetailsView.swift create mode 100644 OmniKitUI/Views/ReadPodInfoView.swift create mode 100644 OmniKitUI/Views/ReadPodStatusView.swift create mode 100644 OmniKitUI/Views/ReadPulseLogView.swift create mode 100644 OmniKitUI/Views/SilencePodSelectionView.swift diff --git a/OmniKit.xcodeproj/project.pbxproj b/OmniKit.xcodeproj/project.pbxproj index d8b78df..a3cfde9 100644 --- a/OmniKit.xcodeproj/project.pbxproj +++ b/OmniKit.xcodeproj/project.pbxproj @@ -191,6 +191,15 @@ D80339982A50085C004FF953 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12EDA0329C7DDC800435701 /* TimeInterval.swift */; }; D803399A2A500D3D004FF953 /* CRC8.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12401B229C7D8E900B32844 /* CRC8.swift */; }; D803399B2A50122F004FF953 /* Packet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12401B329C7D8E900B32844 /* Packet.swift */; }; + D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */; }; + D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1452AF8A4DA00EA0853 /* ActivityView.swift */; }; + D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1472AF8A4E400EA0853 /* FirstAppear.swift */; }; + D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */; }; + D845A14E2AF8A4FB00EA0853 /* ReadPodStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */; }; + 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 */; }; D803399C2A50128D004FF953 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12EDA0129C7DD4700435701 /* LocalizedString.swift */; }; /* End PBXBuildFile section */ @@ -435,6 +444,15 @@ C12EDA1529C7DFF100435701 /* HKUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; C12EDA1729C7E01800435701 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; C12EDA1A29C7E06900435701 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodPreference.swift; sourceTree = ""; }; + D845A1452AF8A4DA00EA0853 /* ActivityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; + D845A1472AF8A4E400EA0853 /* FirstAppear.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstAppear.swift; sourceTree = ""; }; + D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayTestBeepsView.swift; sourceTree = ""; }; + D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodStatusView.swift; sourceTree = ""; }; + D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpManagerDetailsView.swift; sourceTree = ""; }; + D845A1512AF8A51000EA0853 /* SilencePodSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodSelectionView.swift; sourceTree = ""; }; + D85AEAC72B1403C000081044 /* PodDiagnostics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodDiagnostics.swift; sourceTree = ""; }; + D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodInfoView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -568,6 +586,7 @@ C12401A429C7D8E900B32844 /* CRC16.swift */, C12401A529C7D8E900B32844 /* BasalSchedule.swift */, C12401A629C7D8E900B32844 /* BolusDeliveryTable.swift */, + D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */, C12401A729C7D8E900B32844 /* UnfinalizedDose.swift */, ); path = OmnipodCommon; @@ -712,6 +731,7 @@ C124022D29C7DA9700B32844 /* Views */ = { isa = PBXGroup; children = ( + D845A1452AF8A4DA00EA0853 /* ActivityView.swift */, C124024629C7DA9700B32844 /* AttachPodView.swift */, C124024C29C7DA9700B32844 /* BasalStateView.swift */, C124024429C7DA9700B32844 /* BeepPreferenceSelectionView.swift */, @@ -721,6 +741,7 @@ C124023129C7DA9700B32844 /* DesignElements */, C124023A29C7DA9700B32844 /* ExpirationReminderPickerView.swift */, C124024529C7DA9700B32844 /* ExpirationReminderSetupView.swift */, + D845A1472AF8A4E400EA0853 /* FirstAppear.swift */, C124023629C7DA9700B32844 /* InsertCannulaView.swift */, C124023C29C7DA9700B32844 /* InsulinTypeConfirmation.swift */, C124023529C7DA9700B32844 /* LowReservoirReminderEditView.swift */, @@ -730,11 +751,17 @@ C124024929C7DA9700B32844 /* OmnipodReservoirView.swift */, C124024029C7DA9700B32844 /* OmnipodSettingsView.swift */, C124024329C7DA9700B32844 /* PairPodView.swift */, + D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */, C124024829C7DA9700B32844 /* PodDetailsView.swift */, + D85AEAC72B1403C000081044 /* PodDiagnostics.swift */, C124024129C7DA9700B32844 /* PodSetupView.swift */, + D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */, + D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */, + D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */, C124023729C7DA9700B32844 /* RileyLinkSetupView.swift */, C124024729C7DA9700B32844 /* ScheduledExpirationReminderEditView.swift */, C124023829C7DA9700B32844 /* SetupCompleteView.swift */, + D845A1512AF8A51000EA0853 /* SilencePodSelectionView.swift */, C124024D29C7DA9700B32844 /* TimeView.swift */, C124023B29C7DA9700B32844 /* UncertaintyRecoveredView.swift */, ); @@ -1117,6 +1144,7 @@ C12401E429C7D8E900B32844 /* DetailedStatus+OmniKit.swift in Sources */, C12401C929C7D8E900B32844 /* AssignAddressCommand.swift in Sources */, C12401C429C7D8E900B32844 /* PodInfoPulseLogPlus.swift in Sources */, + D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */, C12401CD29C7D8E900B32844 /* DetailedStatus.swift in Sources */, C12401DF29C7D8E900B32844 /* BasalSchedule.swift in Sources */, C12401DC29C7D8E900B32844 /* InsulinTableEntry.swift in Sources */, @@ -1182,13 +1210,16 @@ C124028B29C7DA9700B32844 /* BeepPreferenceSelectionView.swift in Sources */, C12EDA1429C7DFBF00435701 /* TimeInterval.swift in Sources */, C124028D29C7DA9700B32844 /* AttachPodView.swift in Sources */, + D845A14F2AF8A4FB00EA0853 /* ReadPulseLogView.swift in Sources */, C124027229C7DA9700B32844 /* DeactivatePodViewModel.swift in Sources */, C124028C29C7DA9700B32844 /* ExpirationReminderSetupView.swift in Sources */, + D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */, C124029029C7DA9700B32844 /* OmnipodReservoirView.swift in Sources */, C124027C29C7DA9700B32844 /* LowReservoirReminderEditView.swift in Sources */, C124028129C7DA9700B32844 /* ExpirationReminderPickerView.swift in Sources */, C124028329C7DA9700B32844 /* InsulinTypeConfirmation.swift in Sources */, C124029129C7DA9700B32844 /* DeliveryUncertaintyRecoveryView.swift in Sources */, + D845A1502AF8A4FB00EA0853 /* PumpManagerDetailsView.swift in Sources */, C124028229C7DA9700B32844 /* UncertaintyRecoveredView.swift in Sources */, C124027E29C7DA9700B32844 /* RileyLinkSetupView.swift in Sources */, C124027429C7DA9700B32844 /* PodLifeState.swift in Sources */, @@ -1196,10 +1227,13 @@ C124027329C7DA9700B32844 /* RileyLinkListDataSource.swift in Sources */, C124029329C7DA9700B32844 /* BasalStateView.swift in Sources */, C124027A29C7DA9700B32844 /* LeadingImage.swift in Sources */, + D845A14E2AF8A4FB00EA0853 /* ReadPodStatusView.swift in Sources */, C124028729C7DA9700B32844 /* OmnipodSettingsView.swift in Sources */, C124028929C7DA9700B32844 /* LowReservoirReminderSetupView.swift in Sources */, C124027029C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */, C12EDA0E29C7DEFD00435701 /* NumberFormatter.swift in Sources */, + D845A1522AF8A51000EA0853 /* SilencePodSelectionView.swift in Sources */, + D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */, C12EDA1229C7DF4B00435701 /* IdentifiableClass.swift in Sources */, C124027529C7DA9700B32844 /* OmnipodSettingsViewModel.swift in Sources */, C124029729C7DA9700B32844 /* OmnipodUICoordinator.swift in Sources */, @@ -1210,8 +1244,10 @@ C124028A29C7DA9700B32844 /* PairPodView.swift in Sources */, C124029229C7DA9700B32844 /* DeactivatePodView.swift in Sources */, C124027929C7DA9700B32844 /* RoundedCard.swift in Sources */, + D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */, C124026F29C7DA9700B32844 /* PairPodViewModel.swift in Sources */, C124026E29C7DA9700B32844 /* FrameworkLocalText.swift in Sources */, + D85AEAC82B1403C000081044 /* PodDiagnostics.swift in Sources */, C124028F29C7DA9700B32844 /* PodDetailsView.swift in Sources */, C124028629C7DA9700B32844 /* NotificationSettingsView.swift in Sources */, C124027D29C7DA9700B32844 /* InsertCannulaView.swift in Sources */, @@ -1224,6 +1260,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; diff --git a/OmniKit/OmnipodCommon/AlertSlot.swift b/OmniKit/OmnipodCommon/AlertSlot.swift index d357519..bb7b2a6 100644 --- a/OmniKit/OmnipodCommon/AlertSlot.swift +++ b/OmniKit/OmnipodCommon/AlertSlot.swift @@ -8,11 +8,29 @@ import Foundation +fileprivate let defaultShutdownImminentTime = Pod.serviceDuration - Pod.endOfServiceImminentWindow +fileprivate let defaultExpirationReminderTime = Pod.nominalPodLife - Pod.defaultExpirationReminderOffset +fileprivate let defaultExpiredTime = Pod.nominalPodLife + +// PDM and pre-SwiftUI use every1MinuteFor3MinutesAndRepeatEvery15Minutes, but with SwiftUI use every15Minutes +fileprivate let suspendTimeExpiredBeepRepeat = BeepRepeat.every15Minutes + public enum AlertTrigger { case unitsRemaining(Double) case timeUntilAlert(TimeInterval) } +extension AlertTrigger: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .unitsRemaining(let units): + return "\(Int(units))U" + case .timeUntilAlert(let triggerTime): + return "triggerTime=\(triggerTime.timeIntervalStr)" + } + } +} + public enum BeepRepeat: UInt8 { case once = 0 case every1MinuteFor3MinutesAndRepeatEvery60Minutes = 1 @@ -29,29 +47,48 @@ public enum BeepRepeat: UInt8 { public struct AlertConfiguration { let slot: AlertSlot - let trigger: AlertTrigger let active: Bool let duration: TimeInterval + let trigger: AlertTrigger let beepRepeat: BeepRepeat let beepType: BeepType + let silent: Bool let autoOffModifier: Bool static let length = 6 - public init(alertType: AlertSlot, active: Bool = true, autoOffModifier: Bool = false, duration: TimeInterval, trigger: AlertTrigger, beepRepeat: BeepRepeat, beepType: BeepType) { + public init(alertType: AlertSlot, active: Bool = true, duration: TimeInterval = 0, trigger: AlertTrigger, beepRepeat: BeepRepeat, beepType: BeepType, silent: Bool, autoOffModifier: Bool = false) + { self.slot = alertType self.active = active - self.autoOffModifier = autoOffModifier self.duration = duration self.trigger = trigger self.beepRepeat = beepRepeat self.beepType = beepType + self.silent = silent + self.autoOffModifier = autoOffModifier } } extension AlertConfiguration: CustomDebugStringConvertible { public var debugDescription: String { - return "AlertConfiguration(slot:\(slot), active:\(active), autoOffModifier:\(autoOffModifier), duration:\(duration), trigger:\(trigger), beepRepeat:\(beepRepeat), beepType:\(beepType))" + var str = "slot:\(slot)" + if !active { + str += ", active:\(active)" + } + if duration != 0 { + str += ", duration:\(duration.timeIntervalStr)" + } + str += ", trigger:\(trigger), beepRepeat:\(beepRepeat)" + if beepType != .noBeepNonCancel { + str += ", beepType:\(beepType)" + } else { + str += ", silent:\(silent)" + } + if autoOffModifier { + str += ", autoOffModifier:\(autoOffModifier)" + } + return "\nAlertConfiguration(\(str))" } } @@ -60,54 +97,73 @@ extension AlertConfiguration: CustomDebugStringConvertible { public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { public typealias RawValue = [String: Any] - // 2 hours long, time for user to start pairing process - case waitingForPairingReminder + // slot0AutoOff: auto-off timer; requires user input every x minutes -- NOT IMPLEMENTED + case autoOff(active: Bool, offset: TimeInterval, countdownDuration: TimeInterval, silent: Bool = false) - // 1 hour long, time for user to finish priming, cannula insertion - case finishSetupReminder + // slot1NotUsed + case notUsed - // User configurable with PDM (1-24 hours before 72 hour expiration) "Change Pod Soon" - case expirationReminder(TimeInterval) + // slot2ShutdownImminent: 79 hour alarm (1 hour before shutdown) + // 2 sets of beeps every 15 minutes for 1 hour + case shutdownImminent(offset: TimeInterval, absAlertTime: TimeInterval, silent: Bool = false) - // 72 hour alarm - case expired(alertTime: TimeInterval, duration: TimeInterval) + // slot3ExpirationReminder: User configurable with PDM (1-24 hours before 72 hour expiration) + // 2 sets of beeps every minute for 3 minutes and repeat every 15 minutes + // The PDM doesn't use a duration for this alert (presumably because it is limited to 2^9-1 minutes or 8h31m) + case expirationReminder(offset: TimeInterval, absAlertTime: TimeInterval, duration: TimeInterval = 0, silent: Bool = false) - // 79 hour alarm (1 hour before shutdown) - case shutdownImminent(TimeInterval) + // slot4LowReservoir: reservoir below configured value alert + case lowReservoir(units: Double, silent: Bool = false) - // reservoir below configured value alert - case lowReservoir(Double) + // slot5SuspendedReminder: pod suspended reminder, before suspendTime; + // short beep every 15 minutes if > 30 min, else short beep every 5 minutes + case podSuspendedReminder(active: Bool, offset: TimeInterval, suspendTime: TimeInterval, timePassed: TimeInterval = 0, silent: Bool = false) - // auto-off timer; requires user input every x minutes - case autoOff(active: Bool, countdownDuration: TimeInterval) + // slot6SuspendTimeExpired: pod suspend time expired alarm, after suspendTime; + // 2 sets of beeps every minute for 3 minutes repeated every 15 minutes (PDM & pre-SwiftUI implementations) + // 2 sets of beeps every 15 minutes (for SwiftUI PumpManagerAlerts implementations) + case suspendTimeExpired(offset: TimeInterval, suspendTime: TimeInterval, silent: Bool = false) - // pod suspended reminder, before suspendTime; short beep every 15 minutes if > 30 min, else every 5 minutes - case podSuspendedReminder(active: Bool, suspendTime: TimeInterval) + // slot7Expired: 2 hours long, time for user to start pairing process + case waitingForPairingReminder + + // slot7Expired: 1 hour long, time for user to finish priming, cannula insertion + case finishSetupReminder - // pod suspend time expired alarm, after suspendTime; 2 sets of beeps every min for 3 minutes repeated every 15 minutes - case suspendTimeExpired(suspendTime: TimeInterval) + // slot7Expired: 72 hour alarm + case expired(offset: TimeInterval, absAlertTime: TimeInterval, duration: TimeInterval, silent: Bool = false) public var description: String { var alertName: String switch self { - case .waitingForPairingReminder: - return LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder") - case .finishSetupReminder: - return LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder") - case .expirationReminder: - alertName = LocalizedString("Expiration alert", comment: "Description for expiration alert") - case .expired: - alertName = LocalizedString("Expiration advisory", comment: "Description for expiration advisory") - case .shutdownImminent: - alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent") - case .lowReservoir(let units): - alertName = String(format: LocalizedString("Low reservoir advisory (%1$gU)", comment: "Format string for description for low reservoir advisory (1: reminder units)"), units) + // slot0AutoOff case .autoOff: - alertName = LocalizedString("Auto-off", comment: "Description for auto-off") + alertName = LocalizedString("Auto-off", comment: "Description for auto-off alert") + // slot1NotUsed + case .notUsed: + alertName = LocalizedString("Not used", comment: "Description for not used slot alert") + // slot2ShutdownImminent + case .shutdownImminent: + alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent alert") + // slot3ExpirationReminder + case .expirationReminder: + alertName = LocalizedString("Expiration reminder", comment: "Description for expiration reminder alert") + // slot4LowReservoir + case .lowReservoir: + alertName = LocalizedString("Low reservoir", comment: "Format string for description for low reservoir alert") + // slot5SuspendedReminder case .podSuspendedReminder: - alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder") + alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder alert") + // slot6SuspendTimeExpired case .suspendTimeExpired: - alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired") + alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired alert") + // slot7Expired + case .waitingForPairingReminder: + alertName = LocalizedString("Waiting for pairing reminder", comment: "Description waiting for pairing reminder alert") + case .finishSetupReminder: + alertName = LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder alert") + case .expired: + alertName = LocalizedString("Pod expired", comment: "Description for pod expired alert") } if self.configuration.active == false { alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier") @@ -117,71 +173,126 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { public var configuration: AlertConfiguration { switch self { - case .waitingForPairingReminder: - return AlertConfiguration(alertType: .slot7Expired, duration: .minutes(110), trigger: .timeUntilAlert(.minutes(10)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .finishSetupReminder: - return AlertConfiguration(alertType: .slot7Expired, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .expirationReminder(let alertTime): - let active = alertTime != 0 // disable if alertTime is 0 - return AlertConfiguration(alertType: .slot3ExpirationReminder, active: active, duration: 0, trigger: .timeUntilAlert(alertTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .expired(let alarmTime, let duration): - let active = alarmTime != 0 // disable if alarmTime is 0 - return AlertConfiguration(alertType: .slot7Expired, active: active, duration: duration, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .shutdownImminent(let alarmTime): - let active = alarmTime != 0 // disable if alarmTime is 0 - return AlertConfiguration(alertType: .slot2ShutdownImminent, active: active, duration: 0, trigger: .timeUntilAlert(alarmTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .lowReservoir(let units): + // slot0AutoOff + case .autoOff(let active, _, let countdownDuration, let silent): + return AlertConfiguration(alertType: .slot0AutoOff, active: active, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent, autoOffModifier: true) + + // slot1NotUsed + case .notUsed: + return AlertConfiguration(alertType: .slot1NotUsed, duration: .minutes(55), trigger: .timeUntilAlert(.minutes(5)), beepRepeat: .every5Minutes, beepType: .noBeepNonCancel, silent: false) + + // slot2ShutdownImminent + case .shutdownImminent(let offset, let absAlertTime, let silent): + let active = absAlertTime != 0 // disable if absAlertTime is 0 + let triggerTime: TimeInterval + if active { + triggerTime = absAlertTime - offset + } else { + triggerTime = 0 + } + return AlertConfiguration(alertType: .slot2ShutdownImminent, active: active, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent) + + // slot3ExpirationReminder + case .expirationReminder(let offset, let absAlertTime, let duration, let silent): + let active = absAlertTime != 0 // disable if absAlertTime is 0 + let triggerTime: TimeInterval + if active { + triggerTime = absAlertTime - offset + } else { + triggerTime = 0 + } + return AlertConfiguration(alertType: .slot3ExpirationReminder, active: active, duration: duration, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent) + + // slot4LowReservoir + case .lowReservoir(let units, let silent): let active = units != 0 // disable if units is 0 - return AlertConfiguration(alertType: .slot4LowReservoir, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .autoOff(let active, let countdownDuration): - return AlertConfiguration(alertType: .slot0AutoOff, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - case .podSuspendedReminder(let active, let suspendTime): - // A suspendTime of 0 is an untimed suspend + return AlertConfiguration(alertType: .slot4LowReservoir, active: active, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent) + + // slot5SuspendedReminder + // A suspendTime of 0 is an untimed suspend + // timePassed will be > 0 for an existing pod suspended reminder changing its silent state + case .podSuspendedReminder(let active, _, let suspendTime, let timePassed, let silent): let reminderInterval, duration: TimeInterval - let trigger: AlertTrigger - let beepRepeat: BeepRepeat + var beepRepeat: BeepRepeat let beepType: BeepType - if active { - if suspendTime >= TimeInterval(minutes :30) { - // Use 15-minute pod suspended reminder beeps for longer scheduled suspend times as per PDM. - reminderInterval = TimeInterval(minutes: 15) - beepRepeat = .every15Minutes - } else { - // Use 5-minute pod suspended reminder beeps for shorter scheduled suspend times. - reminderInterval = TimeInterval(minutes: 5) - beepRepeat = .every5Minutes - } + let trigger: AlertTrigger + var isActive: Bool = active + + if suspendTime == 0 || suspendTime >= TimeInterval(minutes: 30) { + // Use 15-minute pod suspended reminder beeps for untimed or longer scheduled suspend times. + reminderInterval = TimeInterval(minutes: 15) + beepRepeat = .every15Minutes + } else { + // Use 5-minute pod suspended reminder beeps for shorter scheduled suspend times. + reminderInterval = TimeInterval(minutes: 5) + beepRepeat = .every5Minutes + } + + // Make alert inactive if there isn't enough remaining in suspend time for a reminder beep. + let suspendTimeRemaining = suspendTime - timePassed + if suspendTime != 0 && suspendTimeRemaining <= reminderInterval { + isActive = false + } + + if isActive { + // Compute the alert trigger time as the interval until the next upcoming reminder interval + let triggerTime: TimeInterval = .seconds(reminderInterval - Double((Int(timePassed) % Int(reminderInterval)))) + if suspendTime == 0 { duration = 0 // Untimed suspend, no duration - } else if suspendTime > reminderInterval { - duration = suspendTime - reminderInterval // End after suspendTime total time } else { - duration = .minutes(1) // Degenerate case, end ASAP + // duration is from triggerTime to suspend time remaining + duration = suspendTimeRemaining - triggerTime } - trigger = .timeUntilAlert(reminderInterval) // Start after reminderInterval has passed + trigger = .timeUntilAlert(triggerTime) // time to next reminder interval with the suspend time beepType = .beep } else { + beepRepeat = .once duration = 0 trigger = .timeUntilAlert(.minutes(0)) - beepRepeat = .once beepType = .noBeepCancel } - return AlertConfiguration(alertType: .slot5SuspendedReminder, active: active, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) - case .suspendTimeExpired(let suspendTime): + return AlertConfiguration(alertType: .slot5SuspendedReminder, active: isActive, duration: duration, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType, silent: silent) + + // slot6SuspendTimeExpired + case .suspendTimeExpired(_, let suspendTime, let silent): let active = suspendTime != 0 // disable if suspendTime is 0 let trigger: AlertTrigger let beepRepeat: BeepRepeat let beepType: BeepType if active { trigger = .timeUntilAlert(suspendTime) - beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes + beepRepeat = suspendTimeExpiredBeepRepeat beepType = .bipBeepBipBeepBipBeepBipBeep } else { trigger = .timeUntilAlert(.minutes(0)) beepRepeat = .once beepType = .noBeepCancel } - return AlertConfiguration(alertType: .slot6SuspendTimeExpired, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) + return AlertConfiguration(alertType: .slot6SuspendTimeExpired, active: active, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType, silent: silent) + + // slot7Expired + case .waitingForPairingReminder: + // After pod is powered up, beep every 10 minutes for up to 2 hours before pairing before failing + let totalDuration: TimeInterval = .hours(2) + let startOffset: TimeInterval = .minutes(10) + return AlertConfiguration(alertType: .slot7Expired, duration: totalDuration - startOffset, trigger: .timeUntilAlert(startOffset), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: false) + case .finishSetupReminder: + // After pod is paired, beep every 5 minutes for up to 1 hour for pod setup to complete before failing + let totalDuration: TimeInterval = .hours(1) + let startOffset: TimeInterval = .minutes(5) + return AlertConfiguration(alertType: .slot7Expired, duration: totalDuration - startOffset, trigger: .timeUntilAlert(startOffset), beepRepeat: .every5Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: false) + case .expired(let offset, let absAlertTime, let duration, let silent): + // Normally used to alert at Pod.nominalPodLife (72 hours) for Pod.expirationAdvisoryWindow (7 hours) + // 2 sets of beeps repeating every 60 minutes + let active = absAlertTime != 0 // disable if absAlertTime is 0 + let triggerTime: TimeInterval + if active { + triggerTime = absAlertTime - offset + } else { + triggerTime = .minutes(0) + } + return AlertConfiguration(alertType: .slot7Expired, active: active, duration: duration, trigger: .timeUntilAlert(triggerTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: silent) } } @@ -194,51 +305,92 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { } switch name { - case "waitingForPairingReminder": - self = .waitingForPairingReminder - case "finishSetupReminder": - self = .finishSetupReminder - case "expirationReminder": - guard let alertTime = rawValue["alertTime"] as? Double else { - return nil - } - self = .expirationReminder(TimeInterval(alertTime)) - case "expired": - guard let alarmTime = rawValue["alarmTime"] as? Double, - let duration = rawValue["duration"] as? Double else + case "autoOff": + guard let active = rawValue["active"] as? Bool, + let countdownDuration = rawValue["countdownDuration"] as? TimeInterval else { return nil } - self = .expired(alertTime: TimeInterval(alarmTime), duration: TimeInterval(duration)) + let offset = rawValue["offset"] as? TimeInterval ?? 0 + let silent = rawValue["silent"] as? Bool ?? false + self = .autoOff(active: active, offset: offset, countdownDuration: countdownDuration, silent: silent) case "shutdownImminent": - guard let alarmTime = rawValue["alarmTime"] as? Double else { + guard let alarmTime = rawValue["alarmTime"] as? TimeInterval else { return nil } - self = .shutdownImminent(alarmTime) - case "lowReservoir": - guard let units = rawValue["units"] as? Double else { + let offset = rawValue["offset"] as? TimeInterval ?? 0 + let offsetToUse, absAlertTime: TimeInterval + if offset == 0 { + // use default values as no offset value was found + absAlertTime = defaultShutdownImminentTime + offsetToUse = absAlertTime - alarmTime + } else { + absAlertTime = offset + alarmTime + offsetToUse = offset + } + let silent = rawValue["silent"] as? Bool ?? false + self = .shutdownImminent(offset: offsetToUse, absAlertTime: absAlertTime, silent: silent) + case "expirationReminder": + guard let alertTime = rawValue["alertTime"] as? TimeInterval else { return nil } - self = .lowReservoir(units) - case "autoOff": - guard let active = rawValue["active"] as? Bool, - let countdownDuration = rawValue["countdownDuration"] as? Double else - { + let offset = rawValue["offset"] as? TimeInterval ?? 0 + let offsetToUse, absAlertTime: TimeInterval + if offset == 0 { + // use default values as no offset value was found + absAlertTime = defaultExpirationReminderTime + offsetToUse = absAlertTime - alertTime + } else { + absAlertTime = offset + alertTime + offsetToUse = offset + } + let duration = rawValue["duration"] as? TimeInterval ?? 0 + let silent = rawValue["silent"] as? Bool ?? false + self = .expirationReminder(offset: offsetToUse, absAlertTime: absAlertTime, duration: duration, silent: silent) + case "lowReservoir": + guard let units = rawValue["units"] as? Double else { return nil } - self = .autoOff(active: active, countdownDuration: TimeInterval(countdownDuration)) + let silent = rawValue["silent"] as? Bool ?? false + self = .lowReservoir(units: units, silent: silent) case "podSuspendedReminder": guard let active = rawValue["active"] as? Bool, - let suspendTime = rawValue["suspendTime"] as? Double else + let suspendTime = rawValue["suspendTime"] as? TimeInterval else { return nil } - self = .podSuspendedReminder(active: active, suspendTime: suspendTime) + let offset = rawValue["offset"] as? TimeInterval ?? 0 + let silent = rawValue["silent"] as? Bool ?? false + self = .podSuspendedReminder(active: active, offset: offset, suspendTime: suspendTime, silent: silent) case "suspendTimeExpired": guard let suspendTime = rawValue["suspendTime"] as? Double else { return nil } - self = .suspendTimeExpired(suspendTime: suspendTime) + let offset = rawValue["offset"] as? TimeInterval ?? 0 + let silent = rawValue["silent"] as? Bool ?? false + self = .suspendTimeExpired(offset: offset, suspendTime: suspendTime, silent: silent) + case "waitingForPairingReminder": + self = .waitingForPairingReminder + case "finishSetupReminder": + self = .finishSetupReminder + case "expired": + guard let alarmTime = rawValue["alarmTime"] as? TimeInterval, + let duration = rawValue["duration"] as? TimeInterval else + { + return nil + } + let offset = rawValue["offset"] as? TimeInterval ?? 0 + let offsetToUse, absAlertTime: TimeInterval + if offset == 0 { + // use default values as no offset value was found + absAlertTime = defaultExpiredTime + offsetToUse = absAlertTime - alarmTime + } else { + absAlertTime = offset + alarmTime + offsetToUse = offset + } + let silent = rawValue["silent"] as? Bool ?? false + self = .expired(offset: offsetToUse, absAlertTime: absAlertTime, duration: duration, silent: silent) default: return nil } @@ -248,50 +400,65 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { let name: String = { switch self { - case .waitingForPairingReminder: - return "waitingForPairingReminder" - case .finishSetupReminder: - return "finishSetupReminder" - case .expirationReminder: - return "expirationReminder" - case .expired: - return "expired" + case .autoOff: + return "autoOff" + case .notUsed: + return "notUsed" case .shutdownImminent: return "shutdownImminent" + case .expirationReminder: + return "expirationReminder" case .lowReservoir: return "lowReservoir" - case .autoOff: - return "autoOff" case .podSuspendedReminder: return "podSuspendedReminder" case .suspendTimeExpired: return "suspendTimeExpired" + case .waitingForPairingReminder: + return "waitingForPairingReminder" + case .finishSetupReminder: + return "finishSetupReminder" + case .expired: + return "expired" } }() - var rawValue: RawValue = [ "name": name, ] switch self { - case .expirationReminder(let alertTime): - rawValue["alertTime"] = alertTime - case .expired(let alarmTime, let duration): - rawValue["alarmTime"] = alarmTime - rawValue["duration"] = duration - case .shutdownImminent(let alarmTime): - rawValue["alarmTime"] = alarmTime - case .lowReservoir(let units): - rawValue["units"] = units - case .autoOff(let active, let countdownDuration): + case .autoOff(let active, let offset, let countdownDuration, let silent): rawValue["active"] = active + rawValue["offset"] = offset rawValue["countdownDuration"] = countdownDuration - case .podSuspendedReminder(let active, let suspendTime): + rawValue["silent"] = silent + case .shutdownImminent(let offset, let absAlertTime, let silent): + rawValue["offset"] = offset + rawValue["alarmTime"] = absAlertTime - offset + rawValue["silent"] = silent + case .expirationReminder(let offset, let absAlertTime, let duration, let silent): + rawValue["offset"] = offset + rawValue["alertTime"] = absAlertTime - offset + rawValue["duration"] = duration + rawValue["silent"] = silent + case .lowReservoir(let units, let silent): + rawValue["units"] = units + rawValue["silent"] = silent + case .podSuspendedReminder(let active, let offset, let suspendTime, _, let silent): rawValue["active"] = active + rawValue["offset"] = offset rawValue["suspendTime"] = suspendTime - case .suspendTimeExpired(let suspendTime): + rawValue["silent"] = silent + case .suspendTimeExpired(let offset, let suspendTime, let silent): + rawValue["offset"] = offset rawValue["suspendTime"] = suspendTime + rawValue["silent"] = silent + case .expired(let offset, let absAlertTime, let duration, let silent): + rawValue["offset"] = offset + rawValue["alarmTime"] = absAlertTime - offset + rawValue["duration"] = duration + rawValue["silent"] = silent default: break } @@ -373,9 +540,179 @@ public struct AlertSet: RawRepresentable, Collection, CustomStringConvertible, E // Returns true if there are any active suspend related alerts public func hasActiveSuspendAlert(configuredAlerts: [AlertSlot : PodAlert]) -> Bool { - // slot5SuspendedReminder is for podSuspendedReminder and slot6SuspendTimeExpired is for suspendTimeExpired - if configuredAlerts.contains(where: { ($0.key == .slot5SuspendedReminder || $0.key == .slot6SuspendTimeExpired) && $0.value.configuration.active }) { + if configuredAlerts.contains(where: { ($0.key == .slot5SuspendedReminder || $0.key == .slot6SuspendTimeExpired) && $0.value.configuration.active }) + { return true } return false } + +// Returns a descriptive string for all the alerts in alertSet +public func alertSetString(alertSet: AlertSet) -> String { + + if alertSet.isEmpty { + // Don't bother displaying any additional info for an inactive alert + return String(describing: alertSet) + } + + let alertDescription = alertSet.map { (slot) -> String in + switch slot { + case .slot0AutoOff: + return PodAlert.autoOff(active: true, offset: 0, countdownDuration: 0).description + case .slot1NotUsed: + return PodAlert.notUsed.description + case .slot2ShutdownImminent: + return PodAlert.shutdownImminent(offset: 0, absAlertTime: defaultShutdownImminentTime).description + case .slot3ExpirationReminder: + return PodAlert.expirationReminder(offset: 0, absAlertTime: defaultExpirationReminderTime).description + case .slot4LowReservoir: + return PodAlert.lowReservoir(units: Pod.maximumReservoirReading).description + case .slot5SuspendedReminder: + return PodAlert.podSuspendedReminder(active: true, offset: 0, suspendTime: .minutes(30)).description + case .slot6SuspendTimeExpired: + return PodAlert.suspendTimeExpired(offset: 0, suspendTime: .minutes(30)).description + case .slot7Expired: + return PodAlert.expired(offset: 0, absAlertTime: defaultExpiredTime, duration: Pod.expirationAdvisoryWindow).description + } + } + + return alertDescription.joined(separator: ", ") +} + +func configuredAlertsString(configuredAlerts: [AlertSlot : PodAlert]) -> String { + + if configuredAlerts.isEmpty { + return String(describing: configuredAlerts) + } + + let configuredAlertString = configuredAlerts.map { (configuredAlert) -> String in + + let podAlert = configuredAlert.value + let description = podAlert.description + guard podAlert.configuration.active else { + return description + } + + switch podAlert { + case .shutdownImminent(_, let absAlertTime, _): + return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr) + case .expirationReminder(_, let absAlertTime, _, _): + return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr) + case .lowReservoir(let unitTrigger, _): + return String(format: "%@ @ %dU", description, Int(unitTrigger)) + case .podSuspendedReminder(_, let offset, let suspendTime, _, _): + return String(format: "%@ ending @ %@ after %@", description, (offset + suspendTime).timeIntervalStr, suspendTime.timeIntervalStr) + case .suspendTimeExpired(let offset, let suspendTime, _): + return String(format: "%@ @ %@ after %@", description, (offset + suspendTime).timeIntervalStr, suspendTime.timeIntervalStr) + case .expired(_, let absAlertTime, _, _): + return String(format: "%@ @ %@", description, absAlertTime.timeIntervalStr) + default: + return "" + } + } + + return configuredAlertString.joined(separator: ", ") +} + +// Returns an array of appropriate PodAlerts with the specified silent value +// for all the configuredAlerts given all the current pod conditions. +func regeneratePodAlerts(silent: Bool, configuredAlerts: [AlertSlot: PodAlert], activeAlertSlots: AlertSet, currentPodTime: TimeInterval, currentReservoirLevel: Double) -> [PodAlert] { + var podAlerts: [PodAlert] = [] + + for alert in configuredAlerts { + // Just skip this alert if not previously active + guard alert.value.configuration.active else { + continue + } + + // Map alerts to corresponding appropriate new ones at the current pod time using the specified silent value. + switch alert.value { + + case .shutdownImminent(let offset, let alertTime, _): + // alertTime is absolute when offset is non-zero, otherwise use default value + var absAlertTime = offset != 0 ? alertTime : defaultShutdownImminentTime + if currentPodTime >= absAlertTime { + // alert trigger is not in the future, make inactive using a 0 value + absAlertTime = 0 + } + // create new shutdownImminent podAlert using the current timeActive and the original absolute alert time + podAlerts.append(PodAlert.shutdownImminent(offset: currentPodTime, absAlertTime: absAlertTime, silent: silent)) + + case .expirationReminder(let offset, let alertTime, let alertDuration, _): + let duration: TimeInterval + + // alertTime is absolute when offset is non-zero, otherwise use default value + var absAlertTime = offset != 0 ? alertTime : defaultExpirationReminderTime + if currentPodTime >= absAlertTime { + // alert trigger is not in the future, make inactive using a 0 value + absAlertTime = 0 + duration = 0 + } else { + duration = alertDuration + } + // create new expirationReminder podAlert using the current active time and the original absolute alert time and duration + podAlerts.append(PodAlert.expirationReminder(offset: currentPodTime, absAlertTime: absAlertTime, duration: duration, silent: silent)) + + case .lowReservoir(let unitTrigger, _): + let units: Double + if currentReservoirLevel > unitTrigger { + units = unitTrigger + } else { + // reservoir is no longer more than the unitTrigger, make inactive using a 0 value + units = 0 + } + podAlerts.append(PodAlert.lowReservoir(units: units, silent: silent)) + + case .podSuspendedReminder(let active, let offset, let suspendTime, _, _): + let timePassed: TimeInterval = min(currentPodTime - offset, .hours(2)) + // Pass along the computed time passed since alert was originally set so creation routine can + // do all the grunt work dealing with varying reminder intervals and time passing scenarios. + podAlerts.append(PodAlert.podSuspendedReminder(active: active, offset: offset, suspendTime: suspendTime, timePassed: timePassed, silent: silent)) + + case .suspendTimeExpired(let lastOffset, let lastSuspendTime, _): + let absAlertTime = lastOffset + lastSuspendTime + let suspendTime: TimeInterval + if currentPodTime >= absAlertTime { + // alert trigger is no longer in the future + if activeAlertSlots.contains(where: { $0 == .slot6SuspendTimeExpired } ) { + // The suspendTimeExpired alert has yet been acknowledged, + // set up a suspendTimeExpired alert for the next 15m interval. + // Compute a new suspendTime that is a multiple of 15 minutes + // from lastOffset which is at least one minute in the future. + let newOffsetSuspendTime = ceil((currentPodTime - lastOffset) / .minutes(15)) * .minutes(15) + let newAbsAlertTime = lastOffset + newOffsetSuspendTime + suspendTime = max(newAbsAlertTime - currentPodTime, .minutes(1)) + } else { + // The suspendTimeExpired alert was already been acknowledged, + // so now make this alert inactive by using a 0 suspendTime. + suspendTime = 0 + } + } else { + // recompute a new suspendTime based on the current pod time + suspendTime = absAlertTime - currentPodTime + print("setting new suspendTimeExpired suspendTime of \(suspendTime) with currentPodTime\(currentPodTime) and absAlertTime=\(absAlertTime)") + } + // create a new suspendTimeExpired PodAlert using the current active time and the computed suspendTime (if any) + podAlerts.append(PodAlert.suspendTimeExpired(offset: currentPodTime, suspendTime: suspendTime, silent: silent)) + + case .expired(let offset, let alertTime, let alertDuration, _): + let duration: TimeInterval + + // alertTime is absolute when offset is non-zero, otherwise use default value + var absAlertTime = offset != 0 ? alertTime : defaultExpiredTime + if currentPodTime >= absAlertTime { + // alert trigger is not in the future, make inactive using a 0 value + absAlertTime = 0 + duration = 0 + } else { + duration = alertDuration + } + // create new expired podAlert using the current active time and the original absolute alert time and duration + podAlerts.append(PodAlert.expired(offset: currentPodTime, absAlertTime: absAlertTime, duration: duration, silent: silent)) + + default: + break + } + } + return podAlerts +} diff --git a/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift b/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift index 360fb2e..4cf646d 100644 --- a/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift +++ b/OmniKit/OmnipodCommon/MessageBlocks/ConfigureAlertsCommand.swift @@ -21,7 +21,9 @@ public struct ConfigureAlertsCommand : NonceResyncableMessageBlock { UInt8(4 + configurations.count * AlertConfiguration.length), ]) data.appendBigEndian(nonce) - for config in configurations { + // Sorting the alerts not required, but it can be helpful for log analysis + let sorted = configurations.sorted { $0.slot.rawValue < $1.slot.rawValue } + for config in sorted { data.append(contentsOf: config.data) } return data @@ -92,6 +94,7 @@ extension AlertConfiguration { } self.beepType = beepType + self.silent = (beepType == .noBeepNonCancel) } public var data: Data { @@ -126,7 +129,8 @@ extension AlertConfiguration { data.appendBigEndian(minutes) } data.append(beepRepeat.rawValue) - data.append(beepType.rawValue) + let beepTypeToSet: BeepType = silent ? .noBeepNonCancel : beepType + data.append(beepTypeToSet.rawValue) return data } diff --git a/OmniKit/OmnipodCommon/Pod.swift b/OmniKit/OmnipodCommon/Pod.swift index 57595ea..4e6308c 100644 --- a/OmniKit/OmnipodCommon/Pod.swift +++ b/OmniKit/OmnipodCommon/Pod.swift @@ -30,9 +30,6 @@ public struct Pod { // Units per second for priming/cannula insertion public static let primeDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerPrimePulse - // User configured time before expiration advisory (PDM allows 1-24 hours) - public static let expirationAlertWindow = TimeInterval(hours: 2) - // Expiration advisory window: time after expiration alert, and end of service imminent alarm public static let expirationAdvisoryWindow = TimeInterval(hours: 7) @@ -111,7 +108,7 @@ public enum DeliveryStatus: UInt8, CustomStringConvertible { case bolusAndTempBasal = 6 case extendedBolusRunning = 9 case extendedBolusAndTempBasal = 10 - + public var suspended: Bool { return self == .suspended } @@ -119,7 +116,7 @@ public enum DeliveryStatus: UInt8, CustomStringConvertible { public var bolusing: Bool { return self == .bolusInProgress || self == .bolusAndTempBasal || self == .extendedBolusRunning || self == .extendedBolusAndTempBasal } - + public var tempBasalRunning: Bool { return self == .tempBasalRunning || self == .bolusAndTempBasal || self == .extendedBolusAndTempBasal } diff --git a/OmniKit/OmnipodCommon/PumpManagerAlert.swift b/OmniKit/OmnipodCommon/PumpManagerAlert.swift index b92fa2c..b1e4dac 100644 --- a/OmniKit/OmnipodCommon/PumpManagerAlert.swift +++ b/OmniKit/OmnipodCommon/PumpManagerAlert.swift @@ -1,5 +1,5 @@ // -// PodAlert.swift +// PumpManagerAlert.swift // OmniKit // // Created by Pete Schwamb on 7/9/20. @@ -11,7 +11,6 @@ import LoopKit import HealthKit public enum PumpManagerAlert: Hashable { - case multiCommand(triggeringSlot: AlertSlot?) case podExpireImminent(triggeringSlot: AlertSlot?) case userPodExpiration(triggeringSlot: AlertSlot?, scheduledExpirationReminderOffset: TimeInterval) case lowReservoir(triggeringSlot: AlertSlot?, lowReservoirReminderValue: Double) @@ -19,6 +18,7 @@ public enum PumpManagerAlert: Hashable { case suspendEnded(triggeringSlot: AlertSlot?) case podExpiring(triggeringSlot: AlertSlot?) case finishSetupReminder(triggeringSlot: AlertSlot?) + case unexpectedAlert(triggeringSlot: AlertSlot?) case timeOffsetChangeDetected var isRepeating: Bool { @@ -36,8 +36,6 @@ public enum PumpManagerAlert: Hashable { var contentTitle: String { switch self { - case .multiCommand: - return LocalizedString("Multiple Command Alert", comment: "Alert content title for multiCommand pod alert") case .userPodExpiration: return LocalizedString("Pod Expiration Reminder", comment: "Alert content title for userPodExpiration pod alert") case .podExpiring: @@ -52,6 +50,8 @@ public enum PumpManagerAlert: Hashable { return LocalizedString("Resume Insulin", comment: "Alert content title for suspendEnded pod alert") case .finishSetupReminder: return LocalizedString("Pod Pairing Incomplete", comment: "Alert content title for finishSetupReminder pod alert") + case .unexpectedAlert: + return LocalizedString("Unexpected Alert", comment: "Alert content title for unexpected pod alert") case .timeOffsetChangeDetected: return LocalizedString("Time Change Detected", comment: "Alert content title for timeOffsetChangeDetected pod alert") } @@ -59,8 +59,6 @@ public enum PumpManagerAlert: Hashable { var contentBody: String { switch self { - case .multiCommand: - return LocalizedString("Multiple Command Alert", comment: "Alert content body for multiCommand pod alert") case .userPodExpiration(_, let offset): let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour] @@ -81,6 +79,9 @@ public enum PumpManagerAlert: Hashable { return LocalizedString("The insulin suspension period has ended.\n\nYou can resume delivery from the banner on the home screen or from your pump settings screen. You will be reminded again in 15 minutes.", comment: "Alert content body for suspendEnded pod alert") case .finishSetupReminder: return LocalizedString("Please finish pairing your pod.", comment: "Alert content body for finishSetupReminder pod alert") + case .unexpectedAlert(let triggeringSlot): + let slotNumberString = triggeringSlot != nil ? String(describing: triggeringSlot!.rawValue) : "?" + return String(format: LocalizedString("Unexpected Pod Alert #%1@!", comment: "Alert content body for unexpected pod alert (1: slotNumberString)"), slotNumberString) case .timeOffsetChangeDetected: return LocalizedString("The time on your pump is different from the current time. You can review the pump time and and sync to current time in settings.", comment: "Alert content body for timeOffsetChangeDetected pod alert") } @@ -88,8 +89,6 @@ public enum PumpManagerAlert: Hashable { var triggeringSlot: AlertSlot? { switch self { - case .multiCommand(let slot): - return slot case .userPodExpiration(let slot, _): return slot case .podExpiring(let slot): @@ -104,6 +103,8 @@ public enum PumpManagerAlert: Hashable { return slot case .finishSetupReminder(let slot): return slot + case .unexpectedAlert(let slot): + return slot case .timeOffsetChangeDetected: return nil } @@ -139,8 +140,6 @@ public enum PumpManagerAlert: Hashable { var alertIdentifier: String { switch self { - case .multiCommand: - return "multiCommand" case .userPodExpiration: return "userPodExpiration" case .podExpiring: @@ -153,10 +152,12 @@ public enum PumpManagerAlert: Hashable { return "suspendInProgress" case .suspendEnded: return "suspendEnded" - case .timeOffsetChangeDetected: - return "timeOffsetChangeDetected" case .finishSetupReminder: return "finishSetupReminder" + case .unexpectedAlert: + return "unexpectedAlert" + case .timeOffsetChangeDetected: + return "timeOffsetChangeDetected" } } @@ -183,8 +184,6 @@ extension PumpManagerAlert: RawRepresentable { } switch identifier { - case "multiCommand": - self = .multiCommand(triggeringSlot: slot) case "userPodExpiration": guard let offset = rawValue["offset"] as? TimeInterval, offset > 0 else { return nil @@ -203,6 +202,8 @@ extension PumpManagerAlert: RawRepresentable { self = .suspendInProgress(triggeringSlot: slot) case "suspendEnded": self = .suspendEnded(triggeringSlot: slot) + case "unexpectedAlert": + self = .unexpectedAlert(triggeringSlot: slot) case "timeOffsetChangeDetected": self = .timeOffsetChangeDetected default: @@ -229,14 +230,3 @@ extension PumpManagerAlert: RawRepresentable { return rawValue } } - -extension PodAlert { - var isIgnored: Bool { - switch self { - case .podSuspendedReminder, .finishSetupReminder: - return true - default: - return false - } - } -} diff --git a/OmniKit/OmnipodCommon/SilencePodPreference.swift b/OmniKit/OmnipodCommon/SilencePodPreference.swift new file mode 100644 index 0000000..118bbf5 --- /dev/null +++ b/OmniKit/OmnipodCommon/SilencePodPreference.swift @@ -0,0 +1,32 @@ +// +// SilencePodPreference.swift +// OmniKit +// +// Created by Joe Moran on 8/30/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation + +public enum SilencePodPreference: Int, CaseIterable { + case disabled + case enabled + + public var title: String { + switch self { + case .disabled: + return LocalizedString("Disabled", comment: "Title string for SilencePodPreference.disabled") + case .enabled: + return LocalizedString("Silenced", comment: "Title string for SilencePodPreference.enabled") + } + } + + public var description: String { + switch self { + case .disabled: + return LocalizedString("Normal operation mode where audible Pod beeps are used for all Pod alerts and when confidence reminders are enabled.", comment: "Description for SilencePodPreference.disabled") + case .enabled: + return LocalizedString("All Pod alerts use no beeps and confirmation reminder beeps are suppressed. The Pod will only beep for fatal Pod faults and when playing test beeps.\n\n⚠️Warning - Whenever the Pod is silenced it must be kept within Bluetooth range of this device to receive notifications for Pod alerts.", comment: "Description for SilencePodPreference.enabled") + } + } +} diff --git a/OmniKit/PumpManager/DetailedStatus+OmniKit.swift b/OmniKit/PumpManager/DetailedStatus+OmniKit.swift index a1ae174..d5d92f3 100644 --- a/OmniKit/PumpManager/DetailedStatus+OmniKit.swift +++ b/OmniKit/PumpManager/DetailedStatus+OmniKit.swift @@ -8,47 +8,35 @@ import Foundation -// Returns an appropropriate Eros PDM style Ref string for the Detailed Status. For most Eros faults generating -// a standard style Ref code, TT-VVVHH-IIIRR-FFF is computed as {19|17}-{VV}{SSSS/60}-{NNNN/20}{RRRR/20}-PP. +// Returns an appropropriate PDM style Ref string for the Detailed Status. +// For most types, Ref: TT-VVVHH-IIIRR-FFF computed as {19|17}-{VV}{SSSS/60}-{NNNN/20}{RRRR/20}-PP extension DetailedStatus { public var pdmRef: String? { - let TT: UInt8 // 11 (0x31 fault), 17 (0x14 occlusion fault) or 19 (other faults) - let VVV: UInt8 // raw DetailedStatus VV byte (for non-occlusion faults) - let HH: UInt8 = UInt8(timeActive.hours) // # of pod hours - let III: UInt8 = UInt8(totalInsulinDelivered) // units of insulin - let RR: UInt8 = UInt8(self.reservoirLevel) // reservoir units, special 50+ U value becomes 51 as needed - let FFF: UInt8 // actual fault code value (for non-occlusion faults) + let TT, VVV, HH, III, RR, FFF: UInt8 switch faultEventCode.faultType { - - case .noFaults: - return nil // not a pod fault - - case .reservoirEmpty, .exceededMaximumPodLife80Hrs: - return nil // no Eros PDM Ref code is displayed for either of these faults - - // The Eros PDM does not display a Ref code an Auto Off 0 (the only # that the PDM uses) pod fault. - // While Loop doesn't use this feature, extend this to all Auto Off #'s in case they ever do get used. - case .autoOff0, .autoOff1, .autoOff2, .autoOff3, .autoOff4, .autoOff5, .autoOff6, .autoOff7: - return nil // no Eros PDM Ref code displayed for Auto-off pod faults - - // The Eros PDM treats the 0x31 (049) fault as a PDM error using a unique alternate TT=11 Ref code format. + case .noFaults, .reservoirEmpty, .exceededMaximumPodLife80Hrs: + return nil // no PDM Ref # generated for these cases case .insulinDeliveryCommandError: - return "11-144-0018-00049" // all fixed values for an Eros 0x31 fault - - // The Eros PDM uses VVV and FFF values of 000 in the Ref code for the 0x14 (020) occlusion fault. + // This fault is treated as a PDM fault which uses an alternate Ref format + return "11-144-0018-00049" // all fixed values for this fault case .occluded: - TT = 17 // Eros PDM Ref: 17-000HH-IIIRR-000 - VVV = 0 // no VVV value given for an Eros occlusion fault - FFF = 0 // no FFF value given for an Eros occlusion fault - - // The standard Ref code displayed for all other Eros pod faults + // Ref: 17-000HH-IIIRR-000 + TT = 17 // Occlusion detected Ref type + VVV = 0 // no VVV value for an occlusion fault + FFF = 0 // no FFF value for an occlusion fault default: - TT = 19 // Eros PDM Ref: 19-VVVHH-IIIRR-FFF - VVV = data[17] + // Ref: 19-VVVHH-IIIRR-FFF + TT = 19 // pod fault Ref type + VVV = data[17] // use the raw VV byte value FFF = faultEventCode.rawValue } + HH = UInt8(timeActive.hours) + III = UInt8(totalInsulinDelivered) + + RR = UInt8(self.reservoirLevel) // special 51.15 value used for > 50U will become 51 as needed + return String(format: "%02d-%03d%02d-%03d%02d-%03d", TT, VVV, HH, III, RR, FFF) } } diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 7cfa710..ef9ac5e 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -59,6 +59,8 @@ extension OmnipodPumpManagerError: LocalizedError { return LocalizedString("Insulin type not configured", comment: "Error description for insulin type not configured") case .notReadyForCannulaInsertion: return LocalizedString("Pod is not in a state ready for cannula insertion.", comment: "Error message when cannula insertion fails because the pod is in an unexpected state") + case .invalidSetting: + return LocalizedString("Invalid Setting", comment: "Error description for invalid setting") case .communication(let error): if let error = error as? LocalizedError { return error.errorDescription @@ -71,8 +73,6 @@ extension OmnipodPumpManagerError: LocalizedError { } else { return String(describing: error) } - case .invalidSetting: - return LocalizedString("Invalid Setting", comment: "Error description for invalid setting") } } @@ -171,14 +171,6 @@ public class OmnipodPumpManager: RileyLinkPumpManager { }) } } - - if oldValue.podState?.setupProgress != newValue.podState?.setupProgress, newValue.podState?.setupProgress == .completed { - self.pumpDelegate.notify() { (delegate) in - let date = Date() - let event = NewPumpEvent(date: date, dose: nil, raw: "Pod Change \(date)".data(using: .utf8)!, title: "Pod Change", type: .replaceComponent(componentType: .pump)) - delegate?.pumpManager(self, hasNewPumpEvents: [event], lastReconciliation: self.lastSync, replacePendingEvents: false) { _ in } - } - } } @@ -282,12 +274,14 @@ public class OmnipodPumpManager: RileyLinkPumpManager { override public var debugDescription: String { let lines = [ "## OmnipodPumpManager", + "", + super.debugDescription, "podComms: \(String(reflecting: podComms))", - "state: \(String(reflecting: state))", + "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)", "status: \(String(describing: status))", + "", "podStateObservers.count: \(podStateObservers.cleanupDeallocatedElements().count)", - "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)", - super.debugDescription, + "state: \(String(reflecting: state))", ] return lines.joined(separator: "\n") } @@ -548,10 +542,21 @@ extension OmnipodPumpManager { return false } + private var podTime: TimeInterval { + get { + guard let podState = state.podState else { + return 0 + } + let elapsed = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0) + let podActiveTime = podState.podTime + elapsed + return podActiveTime + } + } + // Returns a suitable beep command MessageBlock based the current beep preferences and // whether there is an unfinializedDose for a manual temp basal &/or a manual bolus. private func beepMessageBlock(beepType: BeepType) -> MessageBlock? { - guard self.beepPreference.shouldBeepForManualCommand else { + guard self.beepPreference.shouldBeepForManualCommand && !self.silencePod else { return nil } @@ -635,6 +640,13 @@ extension OmnipodPumpManager { } } + // Thread-safe + public var silencePod: Bool { + get { + return state.silencePod + } + } + // From last status response public var reservoirLevel: ReservoirLevel? { return state.reservoirLevel @@ -698,6 +710,7 @@ extension OmnipodPumpManager { if error != nil { state.unstoredDoses.append(contentsOf: dosesToStore) } + resetPodState(&state) }) completion() @@ -827,7 +840,7 @@ extension OmnipodPumpManager { state.pairingAttemptAddress = nil } } - + // Have new podState, reset all the per pod pump manager state self.resetPerPodPumpManagerState() @@ -914,15 +927,13 @@ extension OmnipodPumpManager { } } - let expiration = self.podExpiresAt ?? Date().addingTimeInterval(Pod.nominalPodLife) - let timeUntilExpirationReminder = expiration.addingTimeInterval(-self.state.defaultExpirationReminderOffset).timeIntervalSince(self.dateGenerator()) - + let expirationReminderTime = Pod.nominalPodLife - self.state.defaultExpirationReminderOffset let alerts: [PodAlert] = [ - .expirationReminder(self.state.defaultExpirationReminderOffset > 0 ? timeUntilExpirationReminder : 0), - .lowReservoir(self.state.lowReservoirReminderValue) + .expirationReminder(offset: self.podTime, absAlertTime: self.state.defaultExpirationReminderOffset > 0 ? expirationReminderTime : 0, silent: self.state.silencePod), + .lowReservoir(units: self.state.lowReservoirReminderValue, silent: self.state.silencePod) ] - let finishWait = try session.insertCannula(optionalAlerts: alerts) + let finishWait = try session.insertCannula(optionalAlerts: alerts, silent: self.state.silencePod) completion(.success(finishWait)) } catch let error { completion(.failure(.communication(error))) @@ -1007,6 +1018,62 @@ extension OmnipodPumpManager { } } + public func getDetailedStatus(completion: ((_ result: PumpManagerResult) -> Void)? = nil) { + + // use hasSetupPod here instead of hasActivePod as DetailedStatus can be read with a faulted Pod + guard self.hasSetupPod else { + completion?(.failure(PumpManagerError.configuration(OmnipodPumpManagerError.noPodPaired))) + return + } + + let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice + podComms.runSession(withName: "Get detailed status", using: rileyLinkSelector) { (result) in + do { + switch result { + case .success(let session): + let beepBlock = self.beepMessageBlock(beepType: .bipBip) + let detailedStatus = try session.getDetailedStatus(beepBlock: beepBlock) + session.dosesForStorage({ (doses) -> Bool in + self.store(doses: doses, in: session) + }) + completion?(.success(detailedStatus)) + case .failure(let error): + throw error + } + } catch let error { + completion?(.failure(.communication(error as? LocalizedError))) + self.log.error("Failed to fetch detailed status: %{public}@", String(describing: error)) + } + } + } + + public func acknowledgePodAlerts(_ alertsToAcknowledge: AlertSet, completion: @escaping (_ alerts: AlertSet?) -> Void) { + guard self.hasActivePod else { + completion(nil) + return + } + + let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice + self.podComms.runSession(withName: "Acknowledge Alerts", using: rileyLinkSelector) { (result) in + let session: PodCommsSession + switch result { + case .success(let s): + session = s + case .failure: + completion(nil) + return + } + + do { + let beepBlock = self.beepMessageBlock(beepType: .bipBip) + let alerts = try session.acknowledgeAlerts(alerts: alertsToAcknowledge, beepBlock: beepBlock) + completion(alerts) + } catch { + completion(nil) + } + } + } + public func setTime(completion: @escaping (OmnipodPumpManagerError?) -> Void) { let timeZone = TimeZone.currentFixed @@ -1037,7 +1104,7 @@ extension OmnipodPumpManager { switch result { case .success(let session): do { - let beep = self.beepPreference.shouldBeepForManualCommand + let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand let _ = try session.setTime(timeZone: timeZone, basalSchedule: self.state.basalSchedule, date: Date(), acknowledgementBeep: beep) self.setState { (state) in state.timeZone = timeZone @@ -1099,7 +1166,7 @@ extension OmnipodPumpManager { case .success: break } - let beep = self.beepPreference.shouldBeepForManualCommand + let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand let _ = try session.setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep) self.setState { (state) in @@ -1162,12 +1229,12 @@ extension OmnipodPumpManager { self.podComms.runSession(withName: "Play Test Beeps", using: rileyLinkSelector) { (result) in switch result { case .success(let session): - // preserve Pod completion beep state for any unfinalized manual insulin delivery - let beep = self.beepPreference.shouldBeepForManualCommand + // preserve the pod's completion beep state which gets reset playing beeps + let enabled: Bool = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand let result = session.beepConfig( beepType: .bipBeepBipBeepBipBeepBipBeep, - tempBasalCompletionBeep: beep && self.hasUnfinalizedManualTempBasal, - bolusCompletionBeep: beep && self.hasUnfinalizedManualBolus + tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal, + bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus ) switch result { @@ -1183,7 +1250,7 @@ extension OmnipodPumpManager { } public func readPulseLog(completion: @escaping (Result) -> 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 @@ -1220,18 +1287,106 @@ extension OmnipodPumpManager { } } + public func readPulseLogPlus(completion: @escaping (Result) -> 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) -> 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) -> 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) { - self.log.default("Set Confirmation Beeps to %s", String(describing: newPreference)) - guard self.hasActivePod else { + + // If there isn't an active pod or the pod is currently silenced, + // just need to update the internal state without any pod commands. + let name = String(format: "Set Beep Preference to %@", String(describing: newPreference)) + if !self.hasActivePod || self.silencePod { + self.log.default("%{public}@ for internal state only", name) self.setState { state in - state.confirmationBeeps = newPreference // set here to allow changes on a faulted Pod + state.confirmationBeeps = newPreference } completion(nil) return } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice - self.podComms.runSession(withName: "Set Confirmation Beeps Preference", using: rileyLinkSelector) { (result) in + self.podComms.runSession(withName: name, using: rileyLinkSelector) { (result) in switch result { case .success(let session): // enable/disable Pod completion beep state for any unfinalized manual insulin delivery @@ -1257,6 +1412,76 @@ extension OmnipodPumpManager { } } } + + // Reconfigures all active alerts in pod to be silent or not as well as sets/clears the + // self.silencePod state variable which silences all confirmation beeping when enabled. + public func setSilencePod(silencePod: Bool, completion: @escaping (OmnipodPumpManagerError?) -> Void) { + + let name = String(format: "%@ Pod", silencePod ? "Silence" : "Unsilence") + // allow Silence Pod changes without an active Pod + guard self.hasActivePod else { + self.log.default("%{public}@", name) + self.setState { state in + state.silencePod = silencePod + } + completion(nil) + return + } + + let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice + self.podComms.runSession(withName: name, using: rileyLinkSelector) { (result) in + + let session: PodCommsSession + switch result { + case .success(let s): + session = s + case .failure(let error): + completion(.communication(error)) + return + } + + guard let configuredAlerts = self.state.podState?.configuredAlerts, + let activeAlertSlots = self.state.podState?.activeAlertSlots, + let reservoirLevel = self.state.podState?.lastInsulinMeasurements?.reservoirLevel?.rawValue else + { + self.log.error("Missing podState") // should never happen + completion(OmnipodPumpManagerError.noPodPaired) + return + } + + let beepBlock: MessageBlock? + if !self.beepPreference.shouldBeepForManualCommand { + // No enabled completion beeps to worry about for any in-progress manual delivery + beepBlock = nil + } else if silencePod { + // Disable completion beeps for any in-progress manual delivery w/o beeping + beepBlock = BeepConfigCommand(beepType: .noBeepNonCancel) + } else { + // Emit a confirmation beep and enable completion beeps for any in-progress manual delivery + beepBlock = BeepConfigCommand( + beepType: .bipBip, + tempBasalCompletionBeep: self.hasUnfinalizedManualTempBasal, + bolusCompletionBeep: self.hasUnfinalizedManualBolus + ) + } + + let podAlerts = regeneratePodAlerts(silent: silencePod, configuredAlerts: configuredAlerts, activeAlertSlots: activeAlertSlots, currentPodTime: self.podTime, currentReservoirLevel: reservoirLevel) + do { + // Since non-responsive pod comms are currently only resolved for insulin related commands, + // it's possible that a 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 + } + completion(nil) + } catch { + self.log.error("Configure alerts %{public}@ failed: %{public}@", String(describing: podAlerts), String(describing: error)) + completion(.communication(error)) + } + } + } } // MARK: - PumpManager @@ -1467,7 +1692,7 @@ extension OmnipodPumpManager: PumpManager { // Use a beepBlock for the confirmation beep to avoid getting 3 beeps using cancel command beeps! let beepBlock = self.beepMessageBlock(beepType: .beeeeeep) - let result = session.suspendDelivery(suspendReminder: suspendReminder, beepBlock: beepBlock) + let result = session.suspendDelivery(suspendReminder: suspendReminder, silent: self.silencePod, beepBlock: beepBlock) switch result { case .certainFailure(let error): self.log.error("Failed to suspend: %{public}@", String(describing: error)) @@ -1513,8 +1738,8 @@ extension OmnipodPumpManager: PumpManager { do { let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date()) - let beep = self.beepPreference.shouldBeepForManualCommand - let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep, completionBeep: beep) + let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand + let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep) self.clearSuspendReminder() session.dosesForStorage() { (doses) -> Bool in return self.store(doses: doses, in: session) @@ -1590,8 +1815,14 @@ extension OmnipodPumpManager: PumpManager { // Round to nearest supported volume let enactUnits = roundToSupportedBolusVolume(units: units) - let acknowledgementBeep = self.beepPreference.shouldBeepForCommand(automatic: activationType.isAutomatic) - let completionBeep = beepPreference.shouldBeepForManualCommand && !activationType.isAutomatic + let acknowledgementBeep, completionBeep: Bool + if self.silencePod { + acknowledgementBeep = false + completionBeep = false + } else { + acknowledgementBeep = self.beepPreference.shouldBeepForCommand(automatic: activationType.isAutomatic) + completionBeep = beepPreference.shouldBeepForManualCommand && !activationType.isAutomatic + } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Bolus", using: rileyLinkSelector) { (result) in @@ -1683,7 +1914,7 @@ extension OmnipodPumpManager: PumpManager { } // when cancelling a bolus use the built-in type 6 beeeeeep to match PDM if confirmation beeps are enabled - let beepType: BeepType = self.beepPreference.shouldBeepForManualCommand ? .beeeeeep : .noBeepCancel + let beepType: BeepType = self.beepPreference.shouldBeepForManualCommand && !self.silencePod ? .beeeeeep : .noBeepCancel let result = session.cancelDelivery(deliveryType: .bolus, beepType: beepType) switch result { case .certainFailure(let error): @@ -1721,7 +1952,7 @@ extension OmnipodPumpManager: PumpManager { completion(.deviceState(PodCommsError.setupNotComplete)) return } - + // Legal duration values are [virtual] zero (to cancel current temp basal) or between 30 min and 12 hours guard duration < .ulpOfOne || (duration >= .minutes(30) && duration <= .hours(12)) else { completion(.deviceState(OmnipodPumpManagerError.invalidSetting)) @@ -1731,8 +1962,14 @@ extension OmnipodPumpManager: PumpManager { // Round to nearest supported rate let rate = roundToSupportedBasalRate(unitsPerHour: unitsPerHour) - let acknowledgementBeep = beepPreference.shouldBeepForCommand(automatic: automatic) - let completionBeep = beepPreference.shouldBeepForManualCommand && !automatic + let acknowledgementBeep, completionBeep: Bool + if self.silencePod { + acknowledgementBeep = false + completionBeep = false + } else { + acknowledgementBeep = beepPreference.shouldBeepForCommand(automatic: automatic) + completionBeep = beepPreference.shouldBeepForManualCommand && !automatic + } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Enact Temp Basal", using: rileyLinkSelector) { (result) in @@ -1909,12 +2146,21 @@ extension OmnipodPumpManager: PumpManager { return } - var timeUntilReminder : TimeInterval = 0 + let podTime = self.podTime + var expirationReminderPodTime: TimeInterval = 0 // default to expiration reminder alert inactive + + // If the interval before expiration is not a positive value (e.g., it's in the past), + // then the pod alert will get the default alert time of 0 making this alert inactive. if let intervalBeforeExpiration = intervalBeforeExpiration, intervalBeforeExpiration > 0 { - timeUntilReminder = expiresAt.addingTimeInterval(-intervalBeforeExpiration).timeIntervalSince(self.dateGenerator()) + let timeUntilReminder = expiresAt.addingTimeInterval(-intervalBeforeExpiration).timeIntervalSince(self.dateGenerator()) + // Only bother to set an expiration reminder pod alert if it is still at least a couple of minutes in the future + if timeUntilReminder > .minutes(2) { + expirationReminderPodTime = podTime + timeUntilReminder + self.log.debug("Update Expiration timeUntilReminder=%@, podTime=%@, expirationReminderPodTime=%@", timeUntilReminder.timeIntervalStr, podTime.timeIntervalStr, expirationReminderPodTime.timeIntervalStr) + } } - let expirationReminder = PodAlert.expirationReminder(timeUntilReminder) + let expirationReminder = PodAlert.expirationReminder(offset: podTime, absAlertTime: expirationReminderPodTime, silent: self.silencePod) do { let beepBlock = self.beepMessageBlock(beepType: .beep) try session.configureAlerts([expirationReminder], beepBlock: beepBlock) @@ -1942,7 +2188,8 @@ extension OmnipodPumpManager: PumpManager { expiration.addingTimeInterval(.hours(Double(i))) } let now = dateGenerator() - return allDates.filter { $0.timeIntervalSince(now) > 0 } + // Have a couple minutes of slop to avoid confusion trying to set an expiration reminder too close to now + return allDates.filter { $0.timeIntervalSince(now) > .minutes(2) } } public var scheduledExpirationReminder: Date? { @@ -1955,9 +2202,26 @@ extension OmnipodPumpManager: PumpManager { return expiration.addingTimeInterval(-.hours(round(offset.hours))) } + // Updates the low reservior reminder value both for the current pod (when applicable) and for future pods public func updateLowReservoirReminder(_ value: Int, completion: @escaping (OmnipodPumpManagerError?) -> Void) { + + let supportedValue = min(max(0, Double(value)), Pod.maximumReservoirReading) + let setLowReservoirReminderValue = { + self.log.default("Set Low Reservoir Reminder to %d U", value) + self.lowReservoirReminderValue = supportedValue + completion(nil) + } + guard self.hasActivePod else { - completion(OmnipodPumpManagerError.noPodPaired) + // no active pod, just set the internal state for the next pod + setLowReservoirReminderValue() + return + } + + guard let currentReservoirLevel = self.reservoirLevel?.rawValue, currentReservoirLevel > supportedValue else { + // Since the new low reservoir alert level is not below the current reservoir value, + // just set the internal state for the next pod to prevent an immediate low reservoir alert. + setLowReservoirReminderValue() return } @@ -1973,13 +2237,11 @@ extension OmnipodPumpManager: PumpManager { return } - let lowReservoirReminder = PodAlert.lowReservoir(Double(value)) + let lowReservoirReminder = PodAlert.lowReservoir(units: supportedValue, silent: self.silencePod) do { let beepBlock = self.beepMessageBlock(beepType: .beep) try session.configureAlerts([lowReservoirReminder], beepBlock: beepBlock) - self.setState({ (state) in - state.lowReservoirReminderValue = Double(value) - }) + self.lowReservoirReminderValue = supportedValue completion(nil) } catch { completion(.communication(error)) @@ -2041,6 +2303,8 @@ extension OmnipodPumpManager: PumpManager { } } else { log.error("Unconfigured alert slot triggered: %{public}@", String(describing: slot)) + let pumpManagerAlert = PumpManagerAlert.unexpectedAlert(triggeringSlot: slot) + issueAlert(alert: pumpManagerAlert) } } for alert in removed { @@ -2049,34 +2313,24 @@ extension OmnipodPumpManager: PumpManager { } private func getPumpManagerAlert(for podAlert: PodAlert, slot: AlertSlot) -> PumpManagerAlert? { - guard let podState = state.podState, let expiresAt = podState.expiresAt else { - preconditionFailure("trying to lookup alert info without podState") - } - - guard !podAlert.isIgnored else { - return nil - } switch podAlert { - case .podSuspendedReminder: - return PumpManagerAlert.suspendInProgress(triggeringSlot: slot) + case .shutdownImminent: + return PumpManagerAlert.podExpireImminent(triggeringSlot: slot) case .expirationReminder: - guard let offset = state.scheduledExpirationReminderOffset, offset > 0 else { - return nil + guard let podState = state.podState, let expiresAt = podState.expiresAt else { + preconditionFailure("trying to lookup expiresAt") } let timeToExpiry = TimeInterval(hours: expiresAt.timeIntervalSince(dateGenerator()).hours.rounded()) return PumpManagerAlert.userPodExpiration(triggeringSlot: slot, scheduledExpirationReminderOffset: timeToExpiry) - case .expired: - return PumpManagerAlert.podExpiring(triggeringSlot: slot) - case .shutdownImminent: - return PumpManagerAlert.podExpireImminent(triggeringSlot: slot) - case .lowReservoir(let units): + case .lowReservoir(let units, _): return PumpManagerAlert.lowReservoir(triggeringSlot: slot, lowReservoirReminderValue: units) - case .finishSetupReminder, .waitingForPairingReminder: - return PumpManagerAlert.finishSetupReminder(triggeringSlot: slot) case .suspendTimeExpired: return PumpManagerAlert.suspendEnded(triggeringSlot: slot) + case .expired: + return PumpManagerAlert.podExpiring(triggeringSlot: slot) default: + // No PumpManagerAlerts are used for any other pod alerts (including suspendInProgress). return nil } } @@ -2234,9 +2488,16 @@ 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 { + // Don't clear this pod alert here with the pod still suspended so that the suspend time expired + // pod alert beeping will continue until the pod is resumed which will then deactivate this alert. + log.default("Skipping acknowledgement of suspend time expired alert with a suspended pod") + completion(nil) + return + } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Acknowledge Alert", using: rileyLinkSelector) { (result) in switch result { @@ -2294,7 +2555,7 @@ extension FaultEventCode { case .exceededMaximumPodLife80Hrs: return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification") default: - return LocalizedString("Critical Pod Error", comment: "The title for AlarmCode.other notification") + return String(format: LocalizedString("Critical Pod Fault %1$03d", comment: "The title for AlarmCode.other notification: (1: fault code value)"), self.rawValue) } } diff --git a/OmniKit/PumpManager/OmnipodPumpManagerState.swift b/OmniKit/PumpManager/OmnipodPumpManagerState.swift index 3b2094a..3b4913d 100644 --- a/OmniKit/PumpManager/OmnipodPumpManagerState.swift +++ b/OmniKit/PumpManager/OmnipodPumpManagerState.swift @@ -35,6 +35,8 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable { public var unstoredDoses: [UnfinalizedDose] + public var silencePod: Bool + public var confirmationBeeps: BeepPreference public var scheduledExpirationReminderOffset: TimeInterval? @@ -100,6 +102,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable { self.basalSchedule = basalSchedule self.rileyLinkConnectionManagerState = rileyLinkConnectionManagerState self.unstoredDoses = [] + self.silencePod = false self.confirmationBeeps = .manualCommands self.insulinType = insulinType self.lowReservoirReminderValue = Pod.defaultLowReservoirReminder @@ -186,6 +189,8 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable { self.unstoredDoses = [] } + self.silencePod = rawValue["silencePod"] as? Bool ?? false + if let oldAutomaticBolusBeeps = rawValue["automaticBolusBeeps"] as? Bool, oldAutomaticBolusBeeps { self.confirmationBeeps = .extended } else if let oldConfirmationBeeps = rawValue["confirmationBeeps"] as? Bool, oldConfirmationBeeps { @@ -253,6 +258,7 @@ public struct OmnipodPumpManagerState: RawRepresentable, Equatable { "timeZone": timeZone.secondsFromGMT(), "basalSchedule": basalSchedule.rawValue, "unstoredDoses": unstoredDoses.map { $0.rawValue }, + "silencePod": silencePod, "confirmationBeeps": confirmationBeeps.rawValue, "activeAlerts": activeAlerts.map { $0.rawValue }, "podAttachmentConfirmed": podAttachmentConfirmed, @@ -303,8 +309,8 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible { "* timeZone: \(timeZone)", "* basalSchedule: \(String(describing: basalSchedule))", "* maximumTempBasalRate: \(maximumTempBasalRate)", - "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset))", - "* defaultExpirationReminderOffset: \(String(describing: defaultExpirationReminderOffset))", + "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))", + "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)", "* lowReservoirReminderValue: \(String(describing: lowReservoirReminderValue))", "* podAttachmentConfirmed: \(podAttachmentConfirmed)", "* activeAlerts: \(activeAlerts)", @@ -317,14 +323,21 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible { "* tempBasalEngageState: \(String(describing: tempBasalEngageState))", "* lastPumpDataReportDate: \(String(describing: lastPumpDataReportDate))", "* isPumpDataStale: \(String(describing: isPumpDataStale))", + "* silencePod: \(String(describing: silencePod))", "* confirmationBeeps: \(String(describing: confirmationBeeps))", "* pairingAttemptAddress: \(String(describing: pairingAttemptAddress))", "* insulinType: \(String(describing: insulinType))", + "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))", + "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)", "* rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))", "* lastRileyLinkBatteryAlertDate \(String(describing: lastRileyLinkBatteryAlertDate))", - String(reflecting: podState), - "* PreviousPodState: \(String(reflecting: previousPodState))", - String(reflecting: rileyLinkConnectionManagerState), + "", + "* RileyLinkConnectionManagerState: " + (rileyLinkConnectionManagerState == nil ? "nil" : String(describing: rileyLinkConnectionManagerState!)), + "", + "* PodState: " + (podState == nil ? "nil" : String(describing: podState!)), + "", + "* PreviousPodState: " + (previousPodState == nil ? "nil" : String(describing: previousPodState!)), + "", ].joined(separator: "\n") } } diff --git a/OmniKit/PumpManager/PodComms.swift b/OmniKit/PumpManager/PodComms.swift index e74e666..0389e3d 100644 --- a/OmniKit/PumpManager/PodComms.swift +++ b/OmniKit/PumpManager/PodComms.swift @@ -454,7 +454,6 @@ class PodComms: CustomDebugStringConvertible { var debugDescription: String { return [ "## PodComms", - "podState: \(String(reflecting: podState))", "configuredDevices: \(configuredDevices.value.map { $0.uuidString })", "delegate: \(String(describing: delegate != nil))", "" diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index 42e4908..086834f 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -242,7 +242,7 @@ public class PodCommsSession { /// /// - Parameters: /// - messageBlocks: The message blocks to send - /// - beepBlock: If specified, confirmation beep block message to append to the message blocks to send + /// - beepBlock: Optional confirmation beep block message to append to the message blocks to send /// - expectFollowOnMessage: If true, the pod will expect another message within 4 minutes, or will alarm with an 0x33 (51) fault. /// - Returns: The received message response /// - Throws: @@ -391,11 +391,22 @@ public class PodCommsSession { podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: currentDate, scheduledCertainty: .certain, insulinType: podState.insulinType)) } + // Configures the given pod alert(s) and registers the newly configured alert slot(s). + // When re-configuring all the pod alerts for a silence pod toggle, the optional acknowledgeAll can be + // specified to first acknowledge and clear all possible pending pod alerts and pod alert configurations. @discardableResult - func configureAlerts(_ alerts: [PodAlert], beepBlock: MessageBlock? = nil) throws -> StatusResponse { + func configureAlerts(_ alerts: [PodAlert], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse { let configurations = alerts.map { $0.configuration } let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations) - let status: StatusResponse = try send([configureAlerts], beepBlock: beepBlock) + let blocksToSend: [MessageBlock] + if acknowledgeAll { + // Do the acknowledgeAllAlerts command first to clear all previous pod alert configurations. + let acknowledgeAllAlerts = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: AlertSet(rawValue: ~0)) + blocksToSend = [acknowledgeAllAlerts, configureAlerts] + } else { + blocksToSend = [configureAlerts] + } + let status: StatusResponse = try send(blocksToSend, beepBlock: beepBlock) for alert in alerts { podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert) } @@ -428,10 +439,10 @@ public class PodCommsSession { } } - public func insertCannula(optionalAlerts: [PodAlert] = []) throws -> TimeInterval { + public func insertCannula(optionalAlerts: [PodAlert] = [], silent: Bool) throws -> TimeInterval { let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra - guard let activatedAt = podState.activatedAt else { + guard podState.activatedAt != nil else { throw PodCommsError.noPodPaired } @@ -451,12 +462,12 @@ public class PodCommsSession { } podState.updateFromStatusResponse(status, at: currentDate) } else { - // Configure all the non-optional Pod Alarms - let expirationTime = activatedAt + Pod.nominalPodLife - let timeUntilExpirationAdvisory = expirationTime.timeIntervalSinceNow - let expirationAdvisoryAlarm = PodAlert.expired(alertTime: timeUntilExpirationAdvisory, duration: Pod.expirationAdvisoryWindow) - let endOfServiceTime = activatedAt + Pod.serviceDuration - let shutdownImminentAlarm = PodAlert.shutdownImminent((endOfServiceTime - Pod.endOfServiceImminentWindow).timeIntervalSinceNow) + let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0) + let podTime = podState.podTime + elapsed + + // Configure the mandatory Pod Alerts for shutdown imminent alert (79 hours) and pod expiration alert (72 hours) along with any optional alerts + let shutdownImminentAlarm = PodAlert.shutdownImminent(offset: podTime, absAlertTime: Pod.serviceDuration - Pod.endOfServiceImminentWindow, silent: silent) + let expirationAdvisoryAlarm = PodAlert.expired(offset: podTime, absAlertTime: Pod.nominalPodLife, duration: Pod.expirationAdvisoryWindow, silent: silent) try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm] + optionalAlerts) } @@ -613,7 +624,8 @@ public class PodCommsSession { // A suspendReminder of 0 is an untimed suspend which only uses podSuspendedReminder alert beeps. // A suspendReminder of 1-5 minutes will only use suspendTimeExpired alert beeps. // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts. - public func suspendDelivery(suspendReminder: TimeInterval? = nil, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult { + // The configured alerts will set up as silent pod alerts if silent is true. + public func suspendDelivery(suspendReminder: TimeInterval? = nil, silent: Bool, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult { guard podState.unacknowledgedCommand == nil else { return .certainFailure(error: .unacknowledgedCommandPending) @@ -629,6 +641,9 @@ public class PodCommsSession { var podSuspendedReminderAlert: PodAlert? = nil var suspendTimeExpiredAlert: PodAlert? = nil let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0 + let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0) + let podTime = podState.podTime + elapsed + log.debug("suspendDelivery: podState.podTime=%@, elapsed=%.2fs, computed timeActive %@", podState.podTime.timeIntervalStr, elapsed, podTime.timeIntervalStr) let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeepCancel) var commandsToSend: [MessageBlock] = [cancelDeliveryCommand] @@ -636,14 +651,14 @@ public class PodCommsSession { // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time. if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) { // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders - podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, suspendTime: suspendTime) + podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, offset: podTime, suspendTime: suspendTime, silent: silent) alertConfigurations += [podSuspendedReminderAlert!.configuration] } // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed. if suspendTime > 0 { // a timed suspend using a suspend time expired alert - suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(suspendTime: suspendTime) + suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(offset: podTime, suspendTime: suspendTime, silent: silent) alertConfigurations += [suspendTimeExpiredAlert!.configuration] } @@ -683,8 +698,8 @@ public class PodCommsSession { private func cancelSuspendAlerts() throws -> StatusResponse { do { - let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, suspendTime: 0) - let suspendTimeExpired = PodAlert.suspendTimeExpired(suspendTime: 0) // A suspendTime of 0 deactivates this alert + let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, offset: 0, suspendTime: 0) + let suspendTimeExpired = PodAlert.suspendTimeExpired(offset: 0, suspendTime: 0) // A suspendTime of 0 deactivates this alert let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired]) return status @@ -783,7 +798,7 @@ public class PodCommsSession { } } - public func resumeBasal(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { + public func resumeBasal(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { guard podState.unacknowledgedCommand == nil else { throw PodCommsError.unacknowledgedCommandPending @@ -947,11 +962,11 @@ public class PodCommsSession { } } - public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> [AlertSlot: PodAlert] { + public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet { let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts) let status: StatusResponse = try send([cmd], beepBlock: beepBlock) podState.updateFromStatusResponse(status, at: currentDate) - return podState.activeAlerts + return podState.activeAlertSlots } func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) { diff --git a/OmniKit/PumpManager/PodState.swift b/OmniKit/PumpManager/PodState.swift index 391e078..b330fe1 100644 --- a/OmniKit/PumpManager/PodState.swift +++ b/OmniKit/PumpManager/PodState.swift @@ -59,16 +59,19 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl fileprivate var nonceState: NonceState public var activatedAt: Date? - public var expiresAt: Date? // set based on StatusResponse timeActive and can change with Pod clock drift and/or system time change + public var expiresAt: Date? // set based on timeActive and can change with Pod clock drift and/or system time change public var activeTime: TimeInterval? // Useful after pod deactivated or faulted. + public var podTime: TimeInterval // pod time from the last response, always whole minute values + public var podTimeUpdated: Date? // time that the podTime value was last updated + public var setupUnitsDelivered: Double? public let pmVersion: String public let piVersion: String public let lot: UInt32 public let tid: UInt32 - var activeAlertSlots: AlertSet + public var activeAlertSlots: AlertSet public var lastInsulinMeasurements: PodInsulinMeasurements? public var unacknowledgedCommand: PendingCommand? @@ -100,16 +103,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl public var configuredAlerts: [AlertSlot: PodAlert] public var insulinType: InsulinType - public var activeAlerts: [AlertSlot: PodAlert] { - var active = [AlertSlot: PodAlert]() - for slot in activeAlertSlots { - if let alert = configuredAlerts[slot] { - active[slot] = alert - } - } - return active - } - // Allow a grace period while the unacknowledged command is first being sent. public var needsCommsRecovery: Bool { if let unacknowledgedCommand = unacknowledgedCommand, !unacknowledgedCommand.isInFlight { @@ -138,6 +131,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl self.setupProgress = .addressAssigned self.configuredAlerts = [.slot7Expired: .waitingForPairingReminder] self.insulinType = insulinType + self.podTime = 0 self.lastDeliveryStatusReceived = initialDeliveryStatus // can be non-nil when testing } @@ -178,9 +172,21 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl let seed = UInt16(sum & 0xffff) ^ syncWord nonceState = NonceState(lot: lot, tid: tid, seed: seed) } - + + // Saves the current pod timeActive and will initialize the activatedAtComputed at + // pod startup and updates the expiresAt value to account for pod clock differences. private mutating func updatePodTimes(timeActive: TimeInterval) -> Date { let now = Date() + + guard timeActive >= self.podTime else { + // The pod active time went backwards and thus we have an apparent reset fault. + // Don't update any times or displayed expiresAt time will expectedly jump. + return now + } + + self.podTime = timeActive + self.podTimeUpdated = now + let activatedAtComputed = now - timeActive if activatedAt == nil { self.activatedAt = activatedAtComputed @@ -284,7 +290,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl // save the current pod delivery state for verification before any insulin delivery command self.lastDeliveryStatusReceived = deliveryStatus - // See if the pod's deliveryStatus indicates some insulin delivery that podState isn't tracking + // See if the pod's deliveryStatus indicates some insulin delivery that podState isn't tracking if deliveryStatus.bolusing && unfinalizedBolus == nil { // active bolus that we aren't tracking if podProgressStatus.readyForDelivery { // Create an unfinalizedBolus with the remaining bolus amount to capture what we can. @@ -432,6 +438,16 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl self.activeAlertSlots = .none } + if let podTime = rawValue["podTime"] as? TimeInterval, + let podTimeUpdated = rawValue["podTimeUpdated"] as? Date + { + self.podTime = podTime + self.podTimeUpdated = podTimeUpdated + } else { + self.podTime = 0 + self.podTimeUpdated = nil + } + if let setupProgressRaw = rawValue["setupProgress"] as? Int, let setupProgress = SetupProgress(rawValue: setupProgressRaw) { @@ -460,12 +476,12 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl } else { // Assume migration, and set up with alerts that are normally configured self.configuredAlerts = [ - .slot2ShutdownImminent: .shutdownImminent(0), - .slot3ExpirationReminder: .expirationReminder(0), - .slot4LowReservoir: .lowReservoir(0), - .slot5SuspendedReminder: .podSuspendedReminder(active: false, suspendTime: 0), - .slot6SuspendTimeExpired: .suspendTimeExpired(suspendTime: 0), - .slot7Expired: .expired(alertTime: 0, duration: 0) + .slot2ShutdownImminent: .shutdownImminent(offset: 0, absAlertTime: 0), + .slot3ExpirationReminder: .expirationReminder(offset: 0, absAlertTime: 0), + .slot4LowReservoir: .lowReservoir(units: 0), + .slot5SuspendedReminder: .podSuspendedReminder(active: false, offset: 0, suspendTime: 0), + .slot6SuspendTimeExpired: .suspendTimeExpired(offset: 0, suspendTime: 0), + .slot7Expired: .expired(offset: 0, absAlertTime: 0, duration: 0) ] } @@ -474,7 +490,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) { self.insulinType = insulinType } else { - insulinType = .novolog + self.insulinType = .novolog } self.lastDeliveryStatusReceived = nil @@ -515,6 +531,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl rawValue["activeTime"] = activeTime rawValue["activatedAt"] = activatedAt rawValue["expiresAt"] = expiresAt + rawValue["podTime"] = podTime + rawValue["podTimeUpdated"] = podTimeUpdated rawValue["setupUnitsDelivered"] = setupUnitsDelivered @@ -535,6 +553,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl "* address: \(String(format: "%04X", address))", "* activatedAt: \(String(reflecting: activatedAt))", "* expiresAt: \(String(reflecting: expiresAt))", + "* podTime: \(podTime.timeIntervalStr)", + "* podTimeUpdated: \(String(reflecting: podTimeUpdated))", "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))", "* piVersion: \(piVersion)", "* pmVersion: \(pmVersion)", @@ -547,16 +567,14 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl "* unfinalizedSuspend: \(String(describing: unfinalizedSuspend))", "* unfinalizedResume: \(String(describing: unfinalizedResume))", "* finalizedDoses: \(String(describing: finalizedDoses))", - "* activeAlerts: \(String(describing: activeAlerts))", + "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))", "* messageTransportState: \(String(describing: messageTransportState))", "* setupProgress: \(setupProgress)", "* primeFinishTime: \(String(describing: primeFinishTime))", - "* configuredAlerts: \(String(describing: configuredAlerts))", + "* configuredAlerts: \(configuredAlertsString(configuredAlerts: configuredAlerts))", "* insulinType: \(String(describing: insulinType))", - "* pdmRef: \(String(describing: fault?.pdmRef))", - "", - fault != nil ? String(reflecting: fault!) : "fault: nil", - "", + "* pdmRef: " + (fault?.pdmRef == nil ? "nil" : String(describing: fault!.pdmRef!)), + "* Fault: " + (fault == nil ? "nil" : String(describing: fault!)), ].joined(separator: "\n") } } diff --git a/OmniKitPacketParser/main.swift b/OmniKitPacketParser/main.swift index c9485c7..40a394d 100644 --- a/OmniKitPacketParser/main.swift +++ b/OmniKitPacketParser/main.swift @@ -277,12 +277,6 @@ class XcodeLogParser { } } -if CommandLine.argc <= 1 { - print("No file names specified in command arguments to parse!") - print("Set the Xcode Arguments Passed on Launch using Product->Scheme->Edit Scheme...") - print("to specify the full path to rtl, Loop Report, or Xcode log file(s) to parse.\n") - exit(1) -} for filename in CommandLine.arguments[1...] { let rtlOmniParser = RTLOmniLineParser() @@ -293,7 +287,7 @@ for filename in CommandLine.arguments[1...] { do { let data = try String(contentsOfFile: filename, encoding: .utf8) let lines = data.components(separatedBy: .newlines) - + for line in lines { switch line { case Regex("ID1:[0-9a-fA-F]+ PTYPE:"): @@ -304,14 +298,17 @@ for filename in CommandLine.arguments[1...] { // 2018-12-27 01:46:56 +0000 send 1f0e41a6101f1a0e81ed50b102010a0101a000340034170d000208000186a00000000000000111 loopIssueReportParser.parseLine(line) case Regex("RL (Send|Recv) ?\\(single\\): [0-9a-fA-F]+"): - // 2019-02-09 08:23:27.605518-0800 Loop[2978:2294033] [PeripheralManager+RileyLink] RL Send (single): 17050005000000000002580000281f0c27a4591f0c27a447 - // 2019-02-09 08:23:28.262888-0800 Loop[2978:2294816] [PeripheralManager+RileyLink] RL Recv(single): dd0c2f1f079e674b1f079e6769 +// 2019-02-09 08:23:27.605518-0800 Loop[2978:2294033] [PeripheralManager+RileyLink] RL Send (single): 17050005000000000002580000281f0c27a4591f0c27a447 +// 2019-02-09 08:23:28.262888-0800 Loop[2978:2294816] [PeripheralManager+RileyLink] RL Recv(single): dd0c2f1f079e674b1f079e6769 xcodeLogParser.parseLine(line) default: break } + + } } catch let error { print("Error: \(error)") } } + diff --git a/OmniKitTests/AcknowledgeAlertsTests.swift b/OmniKitTests/AcknowledgeAlertsTests.swift index 6c1ba85..e49743e 100644 --- a/OmniKitTests/AcknowledgeAlertsTests.swift +++ b/OmniKitTests/AcknowledgeAlertsTests.swift @@ -22,7 +22,7 @@ class AcknowledgeAlertsTests: XCTestCase { let cmd = try AcknowledgeAlertCommand(encodedData: Data(hexadecimalString: "11052f9b5b2f10")!) XCTAssertEqual(.acknowledgeAlert,cmd.blockType) XCTAssertEqual(0x2f9b5b2f, cmd.nonce) - XCTAssert(cmd.alerts.contains(.slot4)) + XCTAssert(cmd.alerts.contains(.slot4LowReservoir)) } catch (let error) { XCTFail("message decoding threw error: \(error)") } diff --git a/OmniKitTests/MessageTests.swift b/OmniKitTests/MessageTests.swift index 1a2aefc..5543ef4 100644 --- a/OmniKitTests/MessageTests.swift +++ b/OmniKitTests/MessageTests.swift @@ -288,49 +288,49 @@ class MessageTests: XCTestCase { do { // Decode let status = try StatusResponse(encodedData: Data(hexadecimalString: "1d28008200004446ebff")!) - XCTAssert(status.alerts.contains(.slot3)) - XCTAssert(status.alerts.contains(.slot7)) + XCTAssert(status.alerts.contains(.slot3ExpirationReminder)) + XCTAssert(status.alerts.contains(.slot7Expired)) } catch (let error) { XCTFail("message decoding threw error: \(error)") } } func testConfigureAlertsCommand() { - // 79a4 10df 0502 - // Pod expires 1 minute short of 3 days - let podSoftExpirationTime = TimeInterval(hours:72) - TimeInterval(minutes:1) - let alertConfig1 = AlertConfiguration(alertType: .slot7, active: true, autoOffModifier: false, duration: .hours(7), trigger: .timeUntilAlert(podSoftExpirationTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - XCTAssertEqual("79a410df0502", alertConfig1.data.hexadecimalString) + // 020f 0000 0202 + let alertConfig0 = AlertConfiguration(alertType: .slot0AutoOff, active: false, duration: .minutes(15), trigger: .timeUntilAlert(0), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: false, autoOffModifier: true) + XCTAssertEqual("020f00000202", alertConfig0.data.hexadecimalString) // 2800 1283 0602 let podHardExpirationTime = TimeInterval(hours:79) - TimeInterval(minutes:1) - let alertConfig2 = AlertConfiguration(alertType: .slot2, active: true, autoOffModifier: false, duration: .minutes(0), trigger: .timeUntilAlert(podHardExpirationTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + let alertConfig2 = AlertConfiguration(alertType: .slot2ShutdownImminent, active: true, duration: .minutes(0), trigger: .timeUntilAlert(podHardExpirationTime), beepRepeat: .every15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: false) XCTAssertEqual("280012830602", alertConfig2.data.hexadecimalString) - // 020f 0000 0202 - let alertConfig3 = AlertConfiguration(alertType: .slot0, active: false, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(0), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) - XCTAssertEqual("020f00000202", alertConfig3.data.hexadecimalString) - - let configureAlerts = ConfigureAlertsCommand(nonce: 0xfeb6268b, configurations:[alertConfig1, alertConfig2, alertConfig3]) - XCTAssertEqual("1916feb6268b79a410df0502280012830602020f00000202", configureAlerts.data.hexadecimalString) - + // 79a4 10df 0502 + // Pod expires 1 minute short of 3 days + let podSoftExpirationTime = TimeInterval(hours:72) - TimeInterval(minutes:1) + let alertConfig7 = AlertConfiguration(alertType: .slot7Expired, active: true, duration: .hours(7), trigger: .timeUntilAlert(podSoftExpirationTime), beepRepeat: .every60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep, silent: false) + XCTAssertEqual("79a410df0502", alertConfig7.data.hexadecimalString) + + let configureAlerts = ConfigureAlertsCommand(nonce: 0xfeb6268b, configurations:[alertConfig0, alertConfig2, alertConfig7]) + XCTAssertEqual("1916feb6268b020f0000020228001283060279a410df0502", configureAlerts.data.hexadecimalString) + do { let decoded = try ConfigureAlertsCommand(encodedData: Data(hexadecimalString: "1916feb6268b79a410df0502280012830602020f00000202")!) XCTAssertEqual(3, decoded.configurations.count) - + let config1 = decoded.configurations[0] - XCTAssertEqual(.slot7, config1.slot) + XCTAssertEqual(.slot7Expired, config1.slot) XCTAssertEqual(true, config1.active) XCTAssertEqual(false, config1.autoOffModifier) XCTAssertEqual(.hours(7), config1.duration) - if case AlertTrigger.timeUntilAlert(let duration) = config1.trigger { - XCTAssertEqual(podSoftExpirationTime, duration) + if case AlertTrigger.timeUntilAlert(let triggerTime) = config1.trigger { + XCTAssertEqual(podSoftExpirationTime, triggerTime) } XCTAssertEqual(.every60Minutes, config1.beepRepeat) XCTAssertEqual(.bipBeepBipBeepBipBeepBipBeep, config1.beepType) - + let cfg = try AlertConfiguration(encodedData: Data(hexadecimalString: "4c0000640102")!) - XCTAssertEqual(.slot4, cfg.slot) + XCTAssertEqual(.slot4LowReservoir, cfg.slot) XCTAssertEqual(true, cfg.active) XCTAssertEqual(false, cfg.autoOffModifier) XCTAssertEqual(0, cfg.duration) @@ -340,7 +340,6 @@ class MessageTests: XCTestCase { XCTAssertEqual(.every1MinuteFor3MinutesAndRepeatEvery60Minutes, cfg.beepRepeat) XCTAssertEqual(.bipBeepBipBeepBipBeepBipBeep, cfg.beepType) - } catch (let error) { XCTFail("message decoding threw error: \(error)") } diff --git a/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift b/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift index 081fff7..a8e00da 100644 --- a/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift +++ b/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift @@ -35,6 +35,8 @@ class OmnipodSettingsViewModel: ObservableObject { @Published var beepPreference: BeepPreference + @Published var silencePodPreference: SilencePodPreference + @Published var rileylinkConnected: Bool var activatedAtString: String { @@ -131,7 +133,7 @@ class OmnipodSettingsViewModel: ObservableObject { var recoveryText: String? { if case .fault = podCommState { - return LocalizedString("Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted") + return LocalizedString("⚠️ Insulin delivery stopped. Change Pod now.", comment: "The action string on pod status page when pod faulted") } else if podOk && isPodDataStale { return LocalizedString("Make sure your phone and pod are close to each other. If communication issues persist, move to a new area.", comment: "The action string on pod status page when pod data is stale") } else if let serviceTimeRemaining = pumpManager.podServiceTimeRemaining, serviceTimeRemaining <= Pod.serviceDuration - Pod.nominalPodLife { @@ -230,6 +232,7 @@ class OmnipodSettingsViewModel: ObservableObject { lowReservoirAlertValue = Int(self.pumpManager.state.lowReservoirReminderValue) podCommState = self.pumpManager.podCommState beepPreference = self.pumpManager.beepPreference + silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled insulinType = self.pumpManager.insulinType podDetails = self.pumpManager.podDetails previousPodDetails = self.pumpManager.previousPodDetails @@ -278,7 +281,7 @@ class OmnipodSettingsViewModel: ObservableObject { } func stopUsingOmnipodTapped() { - self.pumpManager.notifyDelegateOfDeactivation { + pumpManager.notifyDelegateOfDeactivation { DispatchQueue.main.async { self.didFinish?() } @@ -337,10 +340,54 @@ class OmnipodSettingsViewModel: ObservableObject { } } + func readPodStatus(_ completion: @escaping (_ result: PumpManagerResult) -> Void) { + pumpManager.getDetailedStatus() { (result) in + DispatchQueue.main.async { + completion(result) + } + } + } + func playTestBeeps(_ completion: @escaping (Error?) -> Void) { pumpManager.playTestBeeps(completion: completion) } + func readPulseLog(_ completion: @escaping (_ result: Result) -> Void) { + pumpManager.readPulseLog() { (result) in + DispatchQueue.main.async { + completion(result) + } + } + } + + func readPulseLogPlus(_ completion: @escaping (_ result: Result) -> Void) { + pumpManager.readPulseLogPlus() { (result) in + DispatchQueue.main.async { + completion(result) + } + } + } + + func readActivationTime(_ completion: @escaping (_ result: Result) -> Void) { + pumpManager.readActivationTime() { (result) in + DispatchQueue.main.async { + completion(result) + } + } + } + + func readTriggeredAlerts(_ completion: @escaping (_ result: Result) -> Void) { + pumpManager.readTriggeredAlerts() { (result) in + DispatchQueue.main.async { + completion(result) + } + } + } + + func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) { + completion(pumpManager.debugDescription) + } + func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) { pumpManager.setConfirmationBeeps(newPreference: preference) { error in DispatchQueue.main.async { @@ -352,6 +399,17 @@ class OmnipodSettingsViewModel: ObservableObject { } } + func setSilencePod(_ silencePodPreference: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) { + pumpManager.setSilencePod(silencePod: silencePodPreference == .enabled) { error in + DispatchQueue.main.async { + if error == nil { + self.silencePodPreference = silencePodPreference + } + completion(error) + } + } + } + func didChangeInsulinType(_ newType: InsulinType?) { self.pumpManager.insulinType = newType } @@ -367,6 +425,10 @@ class OmnipodSettingsViewModel: ObservableObject { } } + var noPod: Bool { + return podCommState == .noPod + } + var podError: String? { switch podCommState { case .fault(let status): @@ -378,7 +440,7 @@ class OmnipodSettingsViewModel: ObservableObject { case .occluded, .occlusionCheckStartup1, .occlusionCheckStartup2, .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold, .occlusionCheckValueTooHigh: return LocalizedString("Pod Occlusion", comment: "Error message for reservoir view when pod occlusion checks failed") default: - return LocalizedString("Pod Error", comment: "Error message for reservoir view during general pod fault") + return String(format: LocalizedString("Pod Fault %1$03d", comment: "Error message for reservoir view during general pod fault: (1: fault code value)"), status.faultEventCode.rawValue) } case .active: if isPodDataStale { diff --git a/OmniKitUI/Views/ActivityView.swift b/OmniKitUI/Views/ActivityView.swift new file mode 100644 index 0000000..221d31c --- /dev/null +++ b/OmniKitUI/Views/ActivityView.swift @@ -0,0 +1,36 @@ +// +// ActivityView.swift +// OmniKit +// +// Created by Joe Moran on 9/17/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +struct ActivityView: UIViewControllerRepresentable { + @Binding var isPresented: Bool + let activityItems: [Any] + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + controller.completionWithItemsHandler = { (_, _, _, _) in + self.isPresented = false + } + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { + } +} + +fileprivate struct ActivityViewController: UIViewControllerRepresentable { + var activityItems: [Any] + var applicationActivities: [UIActivity]? = nil + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + return UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {} +} diff --git a/OmniKitUI/Views/BeepPreferenceSelectionView.swift b/OmniKitUI/Views/BeepPreferenceSelectionView.swift index 7d757ee..e9a2e65 100644 --- a/OmniKitUI/Views/BeepPreferenceSelectionView.swift +++ b/OmniKitUI/Views/BeepPreferenceSelectionView.swift @@ -39,7 +39,7 @@ struct BeepPreferenceSelectionView: View { VStack { List { Section { - Text(LocalizedString("Confidence reminders are beeps from the pod which can be used to acknowledge selected commands.", comment: "Help text for BeepPreferenceSelectionView")).fixedSize(horizontal: false, vertical: true) + Text(LocalizedString("Confidence reminders are beeps from the Pod which can be used to acknowledge selected commands when the Pod is not silenced.", comment: "Help text for BeepPreferenceSelectionView")).fixedSize(horizontal: false, vertical: true) .padding(.vertical, 10) } @@ -88,7 +88,7 @@ struct BeepPreferenceSelectionView: View { } .insetGroupedListStyle() - .navigationTitle("Confidence Reminders") + .navigationTitle(LocalizedString("Confidence Reminders", comment: "navigation title for confidence reminders")) .navigationBarTitleDisplayMode(.inline) .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) } @@ -110,15 +110,15 @@ struct BeepPreferenceSelectionView: View { private var cancelButton: some View { Button(action: { self.presentationMode.wrappedValue.dismiss() } ) { - Text(LocalizedString("Cancel", comment: "Button title for cancelling low reservoir reminder edit")) + Text(LocalizedString("Cancel", comment: "Button title for cancelling confidence reminders edit")) } } var saveButtonText: String { if saving { - return LocalizedString("Saving...", comment: "button title for saving low reservoir reminder while saving") + return LocalizedString("Saving...", comment: "button title for saving confidence reminder while saving") } else { - return LocalizedString("Save", comment: "button title for saving low reservoir reminder") + return LocalizedString("Save", comment: "button title for saving confidence reminder") } } @@ -135,7 +135,7 @@ struct BeepPreferenceSelectionView: View { } -struct ContentView_Previews: PreviewProvider { +struct BeepPreferenceSelectionView_Previews: PreviewProvider { static var previews: some View { NavigationView { BeepPreferenceSelectionView(initialValue: .extended) { selectedValue, completion in diff --git a/OmniKitUI/Views/FirstAppear.swift b/OmniKitUI/Views/FirstAppear.swift new file mode 100644 index 0000000..664bde6 --- /dev/null +++ b/OmniKitUI/Views/FirstAppear.swift @@ -0,0 +1,30 @@ +// +// FirstAppear.swift +// Omnipod +// +// Created by Joe Moran on 9/24/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI + +extension View { + func onFirstAppear(_ action: @escaping () -> ()) -> some View { + modifier(FirstAppear(action: action)) + } +} + +private struct FirstAppear: ViewModifier { + let action: () -> () + + // State used to insure action is invoked here only once + @State private var hasAppeared = false + + func body(content: Content) -> some View { + content.onAppear { + guard !hasAppeared else { return } + hasAppeared = true + action() + } + } +} diff --git a/OmniKitUI/Views/OmnipodSettingsView.swift b/OmniKitUI/Views/OmnipodSettingsView.swift index d110670..804c037 100644 --- a/OmniKitUI/Views/OmnipodSettingsView.swift +++ b/OmniKitUI/Views/OmnipodSettingsView.swift @@ -14,28 +14,27 @@ import OmniKit import RileyLinkBLEKit struct OmnipodSettingsView: View { - + @ObservedObject var viewModel: OmnipodSettingsViewModel @ObservedObject var rileyLinkListDataSource: RileyLinkListDataSource var handleRileyLinkSelection: (RileyLinkDevice) -> Void - + @State private var showingDeleteConfirmation = false - - @State private var showSuspendOptions = false; - @State private var showManualTempBasalOptions = false; + @State private var showSuspendOptions = false + + @State private var showManualTempBasalOptions = false - @State private var showSyncTimeOptions = false; + @State private var showSyncTimeOptions = false - @State private var sendingTestBeepsCommand = false; + @State private var sendingTestBeepsCommand = false @State private var cancelingTempBasal = false var supportedInsulinTypes: [InsulinType] - @Environment(\.guidanceColors) var guidanceColors @Environment(\.insulinTintColor) var insulinTintColor @@ -286,7 +285,7 @@ struct OmnipodSettingsView: View { headerImage lifecycleProgress - + HStack(alignment: .top) { deliveryStatus Spacer() @@ -309,7 +308,7 @@ struct OmnipodSettingsView: View { }.padding(.vertical, 8) } } - + Section(header: SectionHeader(label: LocalizedString("Activity", comment: "Section header for activity section"))) { suspendResumeRow() .disabled(!self.viewModel.podOk) @@ -350,7 +349,7 @@ struct OmnipodSettingsView: View { manualTempBasalRow } } - .disabled(cancelingTempBasal) + .disabled(cancelingTempBasal || !self.viewModel.podOk) Section(header: HStack { FrameworkLocalText("Devices", comment: "Header for devices section of RileyLinkSetupView") @@ -390,7 +389,7 @@ struct OmnipodSettingsView: View { Text(self.viewModel.activatedAtString) .foregroundColor(Color.secondary) } - + HStack { if let expiresAt = viewModel.expiresAt, expiresAt < Date() { FrameworkLocalText("Pod Expired", comment: "Label for pod expiration row, past tense") @@ -401,21 +400,36 @@ struct OmnipodSettingsView: View { Text(self.viewModel.expiresAtString) .foregroundColor(Color.secondary) } - + if let podDetails = self.viewModel.podDetails { - NavigationLink(destination: PodDetailsView(podDetails: podDetails, title: LocalizedString("Device Details", comment: "title for device details page"))) { - FrameworkLocalText("Device Details", comment: "Text for device details disclosure row").foregroundColor(Color.primary) + NavigationLink(destination: PodDetailsView(podDetails: podDetails, title: LocalizedString("Pod Details", comment: "title for pod details page"))) { + FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row") + .foregroundColor(Color.primary) + } + } else { + HStack { + FrameworkLocalText("Pod Details", comment: "Text for pod details disclosure row") + Spacer() + Text("—") + .foregroundColor(Color.secondary) + } + } + + if let previousPodDetails = viewModel.previousPodDetails { + NavigationLink(destination: PodDetailsView(podDetails: previousPodDetails, title: LocalizedString("Previous Pod", comment: "title for previous pod page"))) { + FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row") + .foregroundColor(Color.primary) } } else { HStack { - FrameworkLocalText("Device Details", comment: "Text for device details disclosure row") + FrameworkLocalText("Previous Pod Details", comment: "Text for previous pod details row") Spacer() Text("—") .foregroundColor(Color.secondary) } } } - + Section() { Button(action: { self.viewModel.navigateTo?(self.viewModel.lifeState.nextPodLifecycleAction) @@ -424,7 +438,7 @@ struct OmnipodSettingsView: View { .foregroundColor(self.viewModel.lifeState.nextPodLifecycleActionColor) } } - + Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "Section header for configuration section"))) { NavigationLink(destination: @@ -441,15 +455,25 @@ 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) } } + NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) { + HStack { + FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link") + .foregroundColor(Color.primary) + Spacer() + Text(viewModel.silencePodPreference.title) + .foregroundColor(.secondary) + } + } NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) { HStack { - FrameworkLocalText("Insulin Type", comment: "Text for confidence reminders navigation link").foregroundColor(Color.primary) + FrameworkLocalText("Insulin Type", comment: "Text for insulin type navigation link").foregroundColor(Color.primary) if let currentTitle = viewModel.insulinType?.brandName { Spacer() Text(currentTitle) @@ -458,7 +482,7 @@ struct OmnipodSettingsView: View { } } } - + Section() { HStack { FrameworkLocalText("Pump Time", comment: "The title of the command to change pump time zone") @@ -489,14 +513,15 @@ struct OmnipodSettingsView: View { } } - if let previousPodDetails = viewModel.previousPodDetails { - Section() { - NavigationLink(destination: PodDetailsView(podDetails: previousPodDetails, title: LocalizedString("Previous Pod", comment: "title for previous pod page"))) { - FrameworkLocalText("Previous Pod Information", comment: "Text for previous pod information row").foregroundColor(Color.primary) - } + Section() { + 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) } } - if self.viewModel.lifeState.allowsPumpManagerRemoval { Section() { diff --git a/OmniKitUI/Views/PlayTestBeepsView.swift b/OmniKitUI/Views/PlayTestBeepsView.swift new file mode 100644 index 0000000..f544654 --- /dev/null +++ b/OmniKitUI/Views/PlayTestBeepsView.swift @@ -0,0 +1,100 @@ +// +// PlayTestBeepsView.swift +// OmniKit +// +// Created by Joe Moran on 9/1/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit + + +struct PlayTestBeepsView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode: Binding + + 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 = "" + @State private var successMessage = LocalizedString("Play test beeps command sent successfully.\n\nIf you did not hear any beeps from your Pod, the piezo speaker in your Pod may be broken or disabled.", comment: "Success message for play test beeps") + @State private var error: Error? = nil + @State private var executing: Bool = false + @State private var showActivityView = false + + var body: some View { + VStack { + List { + Section { + Text(self.displayString).fixedSize(horizontal: false, vertical: true) + } + } + VStack { + Button(action: { + asyncAction() + }) { + Text(buttonText) + .actionButtonStyle(.primary) + } + .padding() + .disabled(executing) + } + .padding(self.horizontalSizeClass == .regular ? .bottom : []) + .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) + } + .insetGroupedListStyle() + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) + .onFirstAppear { + asyncAction() + } + } + + private func asyncAction () { + DispatchQueue.global(qos: .utility).async { + executing = true + self.displayString = "" + toRun?() { (error) in + executing = false + if let error = error { + self.displayString = "" + self.error = error + self.alertIsPresented = true + } else { + self.displayString = successMessage + } + } + } + } + + private var buttonText: String { + if executing { + return actionString + } else { + return title + } + } + + private func alert(error: Error?) -> SwiftUI.Alert { + return SwiftUI.Alert( + title: Text(failedString), + message: Text(error?.localizedDescription ?? "No Error") + ) + } +} + +struct PlayTestBeepsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + PlayTestBeepsView() { completion in + completion(nil) + } + } + } +} diff --git a/OmniKitUI/Views/PodDetailsView.swift b/OmniKitUI/Views/PodDetailsView.swift index 2eeb950..5a5ed77 100644 --- a/OmniKitUI/Views/PodDetailsView.swift +++ b/OmniKitUI/Views/PodDetailsView.swift @@ -89,7 +89,7 @@ struct PodDetailsView: View { var body: some View { List { row(LocalizedString("Lot Number", comment: "description label for lot number pod details row"), value: String(describing: podDetails.lotNumber)) - row(LocalizedString("Sequence Number", comment: "description label for sequence number pod details row"), value: String(describing: podDetails.sequenceNumber)) + row(LocalizedString("Sequence Number", comment: "description label for sequence number pod details row"), value: String(format: "%07d", podDetails.sequenceNumber)) row(LocalizedString("PI Version", comment: "description label for pi version pod details row"), value: podDetails.piVersion) row(LocalizedString("PM Version", comment: "description label for ble firmware version pod details row"), value: podDetails.pmVersion) row(LocalizedString("Total Delivery", comment: "description label for total delivery pod details row"), value: totalDeliveryText) @@ -108,10 +108,7 @@ struct PodDetailsView: View { Text(LocalizedString("Pod Fault Details", comment: "description label for pod fault details")) .fontWeight(.semibold) }.padding(.vertical, 4) - Text(String(describing: fault)) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(.secondary) - Text("Ref: " + pdmRef) + Text(String(format: LocalizedString("Internal Pod fault code %1$03d\n%2$@\nRef: %3$@\n", comment: "The format string for the pod fault info: (1: fault code) (2: fault description) (3: pdm ref string)"), fault.rawValue, fault.faultDescription, pdmRef)) .fixedSize(horizontal: false, vertical: true) .foregroundColor(.secondary) } @@ -124,6 +121,6 @@ struct PodDetailsView: View { struct PodDetailsView_Previews: PreviewProvider { static var previews: some View { - PodDetailsView(podDetails: PodDetails(lotNumber: 0x1234, sequenceNumber: 0x1234, piVersion: "1.1.1", pmVersion: "2.2.2", totalDelivery: 10, lastStatus: Date(), fault: FaultEventCode(rawValue: 0x67), activatedAt: Date().addingTimeInterval(.days(1))), title: "Device Details") + PodDetailsView(podDetails: PodDetails(lotNumber: 123456789, sequenceNumber: 1234567, piVersion: "2.1.0", pmVersion: "2.1.0", totalDelivery: 99, lastStatus: Date(), fault: FaultEventCode(rawValue: 064), activatedAt: Date().addingTimeInterval(.days(2)), pdmRef: "19-02448-09951-064"), title: "Device Details") } } diff --git a/OmniKitUI/Views/PodDiagnostics.swift b/OmniKitUI/Views/PodDiagnostics.swift new file mode 100644 index 0000000..7331a0e --- /dev/null +++ b/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) + } +} diff --git a/OmniKitUI/Views/PodSetupView.swift b/OmniKitUI/Views/PodSetupView.swift index fbf6220..88c0308 100644 --- a/OmniKitUI/Views/PodSetupView.swift +++ b/OmniKitUI/Views/PodSetupView.swift @@ -107,7 +107,7 @@ struct PodSetupView: View { } -struct RileyLinkSetupView_Previews: PreviewProvider { +struct PodSetupView_Previews: PreviewProvider { static var previews: some View { PodSetupView(nextAction: {}, allowDebugFeatures: true, skipOnboarding: {}) } diff --git a/OmniKitUI/Views/PumpManagerDetailsView.swift b/OmniKitUI/Views/PumpManagerDetailsView.swift new file mode 100644 index 0000000..206de32 --- /dev/null +++ b/OmniKitUI/Views/PumpManagerDetailsView.swift @@ -0,0 +1,104 @@ +// +// PumpManagerDetailsView.swift +// OmniKit +// +// Created by Joe Moran on 9/26/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit + + +struct PumpManagerDetailsView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode: Binding + + 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 + @State private var executing: Bool = false + @State private var showActivityView: Bool = false + + init(toRun: @escaping (_ completion: @escaping (_ result: String) -> Void) -> Void) { + self.toRun = toRun + } + + var body: some View { + VStack { + List { + Section { + let myFont = Font + .system(size: 12) + .monospaced() + Text(self.displayString) + .font(myFont) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + self.showActivityView = true + }) { + Image(systemName: "square.and.arrow.up") + } + } + }.sheet(isPresented: $showActivityView) { + ActivityView(isPresented: $showActivityView, activityItems: [self.displayString]) + } + VStack { + Button(action: { + asyncAction() + }) { + Text(buttonText) + .actionButtonStyle(.primary) + } + .padding() + .disabled(executing) + } + .padding(self.horizontalSizeClass == .regular ? .bottom : []) + .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) + } + .insetGroupedListStyle() + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .onFirstAppear { + asyncAction() + } + } + + private func asyncAction () { + DispatchQueue.global(qos: .utility).async { + executing = true + self.displayString = "" + toRun?() { (result) in + self.displayString = result + executing = false + } + } + } + + private var buttonText: String { + if executing { + return actionString + } else { + return buttonTitle + } + } +} + +struct PumpManagerDetailsView_Previews: PreviewProvider { + static var previews: some View { + let examplePumpManagerDetails: String = "## OmnipodPumpManager\n\n## RileyLinkPumpManager\nlastTimerTick: 2023-10-07 22:35:39 +0000\n\n## RileyLinkDeviceManager\n\ncentral: \n\nautoConnectIDs: [\"F0178BCA-967D-504A-8C3A-99E84964B459\"]\n\ntimerTickEnabled: true\n\nidleListeningState: disabled\n\n## RileyLinkDevice\n* name: JPM OrangePro\n* lastIdle: 0001-01-01 00:00:00 +0000\n* isIdleListeningPending: false\n* isTimerTickEnabled: true\n* isTimerTickNotifying: true\n* radioFirmware: Optional(subg_rfspy 2.2)\n* bleFirmware: Optional(ble_rfspy 2.0)\n* peripheralManager: \n* sessionQueue.operationCount: 2\n\npodComms: ## PodComms\nconfiguredDevices: [\"F0178BCA-967D-504A-8C3A-99E84964B459\"]\ndelegate: true\n\nstatusObservers.count: 2\nstatus: ## PumpManagerStatus\n* timeZone: GMT-0700 (fixed)\n* device: <, name:Omnipod, manufacturer:Insulet, model:Eros, firmware:2.10.0, software:1.0, localIdentifier:1F05DD9A>\n* pumpBatteryChargeRemaining: nil\n* basalDeliveryState: Optional(LoopKit.PumpManagerStatus.BasalDeliveryState.active(2023-10-07 22:33:48 +0000))\n* bolusState: noBolus\n* insulinType: Optional(LoopKit.InsulinType.humalog)\n* deliveryIsUncertain: false\n\npodStateObservers.count: 1\nstate: ## OmnipodPumpManagerState\n* isOnboarded: true\n* timeZone: GMT-0700 (fixed)\n* basalSchedule: BasalSchedule(entries: [OmniKit.BasalScheduleEntry(rate: 0.9, startTime: 0.0)])\n* maximumTempBasalRate: 2.0\n* scheduledExpirationReminderOffset: Optional(\"24h0m\")\n* defaultExpirationReminderOffset: 24h0m\n* lowReservoirReminderValue: 50.0\n* podAttachmentConfirmed: true\n* activeAlerts: []\n* alertsWithPendingAcknowledgment: []\n* acknowledgedTimeOffsetAlert: false\n* initialConfigurationCompleted: true\n* unstoredDoses: []\n* suspendEngageState: stable\n* bolusEngageState: stable\n* tempBasalEngageState: stable\n* lastPumpDataReportDate: Optional(2023-10-07 22:35:24 +0000)\n* isPumpDataStale: false\n* silencePod: false\n* confirmationBeeps: manualCommands\n* pairingAttemptAddress: nil\n* insulinType: Optional(LoopKit.InsulinType.humalog)\n* scheduledExpirationReminderOffset: Optional(\"24h0m\")\n* defaultExpirationReminderOffset: 24h0m\n* rileyLinkBatteryAlertLevel: nil\n* lastRileyLinkBatteryAlertDate 0001-01-01 00:00:00 +0000\n* RileyLinkConnectionManagerState: RileyLinkConnectionState(autoConnectIDs: Set([\"F0178BCA-967D-504A-8C3A-99E84964B459\"]))\n* PodState: ### PodState\n* address: 1F05DD9A\n* activatedAt: Optional(2023-10-07 22:31:21 +0000)\n* expiresAt: Optional(2023-10-10 22:30:51 +0000)\n* timeActive: 4m\n* timeActiveUpdated: Optional(2023-10-07 22:35:38 +0000)\n* setupUnitsDelivered: Optional(2.65)\n* piVersion: 2.10.0\n* pmVersion: 2.10.0\n* lot: 72353\n* tid: 3280440\n* suspendState: resumed(2023-10-07 22:33:48 +0000)\n* unacknowledgedCommand: nil\n* unfinalizedBolus: nil\n* unfinalizedTempBasal: nil\n* unfinalizedSuspend: nil\n* unfinalizedResume: Optional(Resume: 10/7/23, 3:33:48 PM Certain)\n* finalizedDoses: []\n* activeAlertsSlots: No alerts\n* messageTransportState: MessageTransportState(packetNumber: 2, messageNumber: 8)\n* setupProgress: completed\n* primeFinishTime: Optional(2023-10-07 22:33:16 +0000)\n* configuredAlerts: [OmniKit.AlertSlot.slot4LowReservoir: Low reservoir, OmniKit.AlertSlot.slot3ExpirationReminder: Expiration reminder, OmniKit.AlertSlot.slot2ShutdownImminent: Shutdown imminent, OmniKit.AlertSlot.slot7Expired: Pod expired]\n* insulinType: humalog\n* PdmRef: nil\n* Fault: nil\n\n* PreviousPodState: nil\n" + NavigationView { + PumpManagerDetailsView() { completion in + completion(examplePumpManagerDetails) + } + } + } +} diff --git a/OmniKitUI/Views/ReadPodInfoView.swift b/OmniKitUI/Views/ReadPodInfoView.swift new file mode 100644 index 0000000..62db6f2 --- /dev/null +++ b/OmniKitUI/Views/ReadPodInfoView.swift @@ -0,0 +1,135 @@ +// +// ReadPodInfoView.swift +// OmniKit +// +// Created by Joe Moran on 11/25/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import OmniKit + + +struct ReadPodInfoView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode: Binding + + 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) -> Void) -> Void)? + + @State private var alertIsPresented: Bool = false + @State private var displayString: String = "" + @State private var error: Error? = nil + @State private var executing: Bool = false + @State private var showActivityView: Bool = false + + var body: some View { + VStack { + List { + Section { + let myFont = Font + .system(size: 12) + .monospaced() + Text(self.displayString) + .font(myFont) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + self.showActivityView = true + }) { + Image(systemName: "square.and.arrow.up") + } + } + }.sheet(isPresented: $showActivityView) { + ActivityView(isPresented: $showActivityView, activityItems: [self.displayString]) + } + VStack { + Button(action: { + asyncAction() + }) { + Text(buttonText) + .actionButtonStyle(.primary) + } + .padding() + .disabled(executing) + } + .padding(self.horizontalSizeClass == .regular ? .bottom : []) + .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) + } + .insetGroupedListStyle() + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) + .onFirstAppear { + asyncAction() + } + } + + private func asyncAction () { + DispatchQueue.global(qos: .utility).async { + executing = true + self.displayString = "" + toRun?() { (result) in + executing = false + switch result { + case .success(let resultString): + self.displayString = resultString + case .failure(let error): + self.displayString = "" + self.error = error + self.alertIsPresented = true + } + } + } + } + + private var buttonText: String { + if executing { + return actionString + } else { + return title + } + } + + private func alert(error: Error?) -> SwiftUI.Alert { + return SwiftUI.Alert( + title: Text(failedString), + message: Text(error?.localizedDescription ?? "No Error") + ) + } +} + +struct ReadPodInfoView_Previews: PreviewProvider { + static var previews: some View { + 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))) + } + } + } +} diff --git a/OmniKitUI/Views/ReadPodStatusView.swift b/OmniKitUI/Views/ReadPodStatusView.swift new file mode 100644 index 0000000..f79f78f --- /dev/null +++ b/OmniKitUI/Views/ReadPodStatusView.swift @@ -0,0 +1,169 @@ +// +// ReadPodStatusView.swift +// OmniKit +// +// Created by Joe Moran on 8/15/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import OmniKit + + +struct ReadPodStatusView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode: Binding + + var toRun: ((_ completion: @escaping (_ result: PumpManagerResult) -> 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 = "" + @State private var error: LocalizedError? = nil + @State private var executing: Bool = false + @State private var showActivityView: Bool = false + + var body: some View { + VStack { + List { + Section { + Text(self.displayString).fixedSize(horizontal: false, vertical: true) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + self.showActivityView = true + }) { + Image(systemName: "square.and.arrow.up") + } + } + }.sheet(isPresented: $showActivityView) { + ActivityView(isPresented: $showActivityView, activityItems: [self.displayString]) + } + VStack { + Button(action: { + asyncAction() + }) { + Text(buttonText) + .actionButtonStyle(.primary) + } + .padding() + .disabled(executing) + } + .padding(self.horizontalSizeClass == .regular ? .bottom : []) + .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) + } + .insetGroupedListStyle() + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) + .onFirstAppear { + asyncAction() + } + } + + private func asyncAction () { + DispatchQueue.global(qos: .utility).async { + executing = true + self.displayString = "" + toRun?() { (result) in + executing = false + switch result { + case .success(let detailedStatus): + self.displayString = podStatusString(status: detailedStatus) + case .failure(let error): + self.error = error + self.alertIsPresented = true + } + } + } + } + + private var buttonText: String { + if executing { + return actionString + } else { + return title + } + } + + private func alert(error: Error?) -> SwiftUI.Alert { + return SwiftUI.Alert( + 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 { + let detailedStatus = try! DetailedStatus(encodedData: Data([0x02, 0x0d, 0x00, 0x00, 0x00, 0x0e, 0x00, 0xc3, 0x6a, 0x02, 0x07, 0x03, 0xff, 0x02, 0x09, 0x20, 0x00, 0x28, 0x99, 0x08, 0x00, 0x82])) + ReadPodStatusView() { completion in + completion(.success(detailedStatus)) + } + } + } +} diff --git a/OmniKitUI/Views/ReadPulseLogView.swift b/OmniKitUI/Views/ReadPulseLogView.swift new file mode 100644 index 0000000..acaa00d --- /dev/null +++ b/OmniKitUI/Views/ReadPulseLogView.swift @@ -0,0 +1,128 @@ +// +// ReadPulseLogView.swift +// OmniKit +// +// Created by Joe Moran on 9/1/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import OmniKit + +struct ReadPulseLogView: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode: Binding + + private var toRun: ((_ completion: @escaping (_ result: Result) -> Void) -> Void)? + + @State private var alertIsPresented: Bool = false + @State private var displayString: String = "" + @State private var error: Error? = nil + @State private var executing: Bool = false + @State private var showActivityView: Bool = false + + init(toRun: @escaping (_ completion: @escaping (_ result: Result) -> Void) -> Void) { + self.toRun = toRun + } + + var body: some View { + VStack { + List { + Section { + let myFont = Font + .system(size: 12) + .monospaced() + Text(self.displayString) + .font(myFont) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + self.showActivityView = true + }) { + Image(systemName: "square.and.arrow.up") + } + } + }.sheet(isPresented: $showActivityView) { + ActivityView(isPresented: $showActivityView, activityItems: [self.displayString]) + } + VStack { + Button(action: { + asyncAction() + }) { + Text(buttonText) + .actionButtonStyle(.primary) + } + .padding() + .disabled(executing) + } + .padding(self.horizontalSizeClass == .regular ? .bottom : []) + .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) + } + .insetGroupedListStyle() + .navigationTitle(LocalizedString("Read Pulse Log", comment: "navigation title for read pulse log")) + .navigationBarTitleDisplayMode(.inline) + .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) + .onFirstAppear { + asyncAction() + } + } + + private func asyncAction () { + DispatchQueue.global(qos: .utility).async { + executing = true + self.displayString = "" + toRun?() { (result) in + switch result { + case .success(let pulseLogString): + self.displayString = pulseLogString + case .failure(let error): + self.displayString = "" + self.error = error + self.alertIsPresented = true + } + executing = false + } + } + } + + private var buttonText: String { + if executing { + return LocalizedString("Reading Pulse Log...", comment: "button title when executing read pulse log") + } else { + return LocalizedString("Read Pulse Log", comment: "button title to read pulse log") + } + } + + private func alert(error: Error?) -> SwiftUI.Alert { + return SwiftUI.Alert( + title: Text(LocalizedString("Failed to read pulse log.", comment: "Alert title for error when reading pulse log")), + message: Text(error?.localizedDescription ?? "No Error") + ) + } +} + +struct ReadPulsePodLogView_Previews: PreviewProvider { + static var previews: some View { + ReadPulseLogView() { completion in + let podInfoPulseLogRecent = try! PodInfoPulseLogRecent(encodedData: Data([0x50, 0x03, 0x17, + 0x39, 0x72, 0x58, 0x01, 0x3c, 0x72, 0x43, 0x01, 0x41, 0x72, 0x5a, 0x01, 0x44, 0x71, 0x47, 0x01, + 0x49, 0x51, 0x59, 0x01, 0x4c, 0x51, 0x44, 0x01, 0x51, 0x73, 0x59, 0x01, 0x54, 0x50, 0x43, 0x01, + 0x59, 0x50, 0x5a, 0x81, 0x5c, 0x51, 0x42, 0x81, 0x61, 0x73, 0x59, 0x81, 0x00, 0x75, 0x43, 0x80, + 0x05, 0x70, 0x5a, 0x80, 0x08, 0x50, 0x44, 0x80, 0x0d, 0x50, 0x5b, 0x80, 0x10, 0x75, 0x43, 0x80, + 0x15, 0x72, 0x5e, 0x80, 0x18, 0x73, 0x45, 0x80, 0x1d, 0x72, 0x5b, 0x00, 0x20, 0x70, 0x43, 0x00, + 0x25, 0x50, 0x5c, 0x00, 0x28, 0x50, 0x46, 0x00, 0x2d, 0x50, 0x5a, 0x00, 0x30, 0x75, 0x47, 0x00, + 0x35, 0x72, 0x59, 0x00, 0x38, 0x70, 0x46, 0x00, 0x3d, 0x75, 0x57, 0x00, 0x40, 0x72, 0x43, 0x00, + 0x45, 0x73, 0x55, 0x00, 0x48, 0x73, 0x41, 0x00, 0x4d, 0x70, 0x52, 0x00, 0x50, 0x73, 0x3f, 0x00, + 0x55, 0x74, 0x4d, 0x00, 0x58, 0x72, 0x3d, 0x80, 0x5d, 0x73, 0x4d, 0x80, 0x60, 0x71, 0x3d, 0x80, + 0x01, 0x51, 0x50, 0x80, 0x04, 0x72, 0x3d, 0x80, 0x09, 0x50, 0x4e, 0x80, 0x0c, 0x51, 0x40, 0x80, + 0x11, 0x74, 0x50, 0x80, 0x14, 0x71, 0x40, 0x80, 0x19, 0x50, 0x4d, 0x80, 0x1c, 0x75, 0x3f, 0x00, + 0x21, 0x72, 0x52, 0x00, 0x24, 0x72, 0x40, 0x00, 0x29, 0x71, 0x53, 0x00, 0x2c, 0x50, 0x42, 0x00, + 0x31, 0x51, 0x55, 0x00, 0x34, 0x50, 0x42, 0x00 ])) + let lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry) + completion(.success(pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber))) + } + } +} diff --git a/OmniKitUI/Views/SilencePodSelectionView.swift b/OmniKitUI/Views/SilencePodSelectionView.swift new file mode 100644 index 0000000..107316e --- /dev/null +++ b/OmniKitUI/Views/SilencePodSelectionView.swift @@ -0,0 +1,144 @@ +// +// SilencePodSelectionView.swift +// OmniKit +// +// Created by Joe Moran 8/30/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKit +import LoopKitUI +import OmniKit + +struct SilencePodSelectionView: View { + + @Environment(\.horizontalSizeClass) var horizontalSizeClass + @Environment(\.presentationMode) var presentationMode: Binding + + private var initialValue: SilencePodPreference + @State private var preference: SilencePodPreference + private var onSave: ((_ selectedValue: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) -> Void)? + + @State private var alertIsPresented: Bool = false + @State private var error: LocalizedError? + @State private var saving: Bool = false + + + init(initialValue: SilencePodPreference, onSave: @escaping (_ selectedValue: SilencePodPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) -> Void) { + self.initialValue = initialValue + self._preference = State(initialValue: initialValue) + self.onSave = onSave + } + + var body: some View { + contentWithCancel + } + + var content: some View { + VStack { + List { + Section { + Text(LocalizedString("Silence Pod mode suppresses all Pod alert and confirmation reminder beeping.", comment: "Help text for Silence Pod view")).fixedSize(horizontal: false, vertical: true) + .padding(.vertical, 10) + } + Section { + ForEach(SilencePodPreference.allCases, id: \.self) { preference in + HStack { + CheckmarkListItem( + title: Text(preference.title), + description: Text(preference.description), + isSelected: Binding( + get: { self.preference == preference }, + set: { isSelected in + if isSelected { + self.preference = preference + } + } + ) + ) + } + .padding(.vertical, 10) + } + } + .buttonStyle(PlainButtonStyle()) // Disable row highlighting on selection + } + VStack { + Button(action: { + saving = true + onSave?(preference) { (error) in + saving = false + if let error = error { + self.error = error + self.alertIsPresented = true + } else { + self.presentationMode.wrappedValue.dismiss() + } + } + }) { + Text(saveButtonText) + .actionButtonStyle(.primary) + } + .padding() + .disabled(saving || !valueChanged) + } + .padding(self.horizontalSizeClass == .regular ? .bottom : []) + .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) + } + .insetGroupedListStyle() + .navigationTitle(LocalizedString("Silence Pod", comment: "navigation title for Silnce Pod")) + .navigationBarTitleDisplayMode(.inline) + .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) + } + + private var contentWithCancel: some View { + if saving { + return AnyView(content + .navigationBarBackButtonHidden(true) + ) + } else if valueChanged { + return AnyView(content + .navigationBarBackButtonHidden(true) + .navigationBarItems(leading: cancelButton) + ) + } else { + return AnyView(content) + } + } + + private var cancelButton: some View { + Button(action: { self.presentationMode.wrappedValue.dismiss() } ) { + Text(LocalizedString("Cancel", comment: "Button title for cancelling silence pod edit")) + } + } + + var saveButtonText: String { + if saving { + return LocalizedString("Saving...", comment: "button title for saving silence pod preference while saving") + } else { + return LocalizedString("Save", comment: "button title for saving silence pod preference") + } + } + + private var valueChanged: Bool { + return preference != initialValue + } + + private func alert(error: Error?) -> SwiftUI.Alert { + return SwiftUI.Alert( + title: Text(LocalizedString("Failed to update silence pod preference.", comment: "Alert title for error when updating silence pod preference")), + message: Text(error?.localizedDescription ?? "No Error") + ) + } +} + +struct SilencePodSelectionView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + SilencePodSelectionView(initialValue: .disabled) { selectedValue, completion in + print("Selected: \(selectedValue)") + completion(nil) + } + } + } +} diff --git a/OmniKitUI/Views/UncertaintyRecoveredView.swift b/OmniKitUI/Views/UncertaintyRecoveredView.swift index cefb407..7a9e984 100644 --- a/OmniKitUI/Views/UncertaintyRecoveredView.swift +++ b/OmniKitUI/Views/UncertaintyRecoveredView.swift @@ -16,9 +16,8 @@ struct UncertaintyRecoveredView: View { var body: some View { GuidePage(content: { - Text(LocalizedString("Loop has recovered communication with the pod on your body.\n\nInsulin delivery records have been updated and should match what has actually been delivered.\n\nYou may continue to use Loop normally now.", comment: "Text body for page showing insulin uncertainty has been recovered.")) - .fixedSize(horizontal: false, vertical: true) - .padding([.top, .bottom]) + Text(String(format: LocalizedString("%1$@ has recovered communication with the pod on your body.\n\nInsulin delivery records have been updated and should match what has actually been delivered.\n\nYou may continue to use %2$@ normally now.", comment: "Text body for page showing insulin uncertainty has been recovered (1: appName) (2: appName)"), self.appName, self.appName)) + .padding([.top, .bottom]) }) { VStack { Button(action: { From 50bc368205a3210df293bb5ccdc7584aeaf47ad9 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 22 Mar 2024 08:43:45 -0700 Subject: [PATCH 04/21] remove file that is not used --- OmniKit.xcodeproj/project.pbxproj | 1 - OmniKitUI/Views/ReadPulseLogView.swift | 128 ------------------------- 2 files changed, 129 deletions(-) delete mode 100644 OmniKitUI/Views/ReadPulseLogView.swift diff --git a/OmniKit.xcodeproj/project.pbxproj b/OmniKit.xcodeproj/project.pbxproj index a3cfde9..5550835 100644 --- a/OmniKit.xcodeproj/project.pbxproj +++ b/OmniKit.xcodeproj/project.pbxproj @@ -1210,7 +1210,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 */, diff --git a/OmniKitUI/Views/ReadPulseLogView.swift b/OmniKitUI/Views/ReadPulseLogView.swift deleted file mode 100644 index acaa00d..0000000 --- a/OmniKitUI/Views/ReadPulseLogView.swift +++ /dev/null @@ -1,128 +0,0 @@ -// -// ReadPulseLogView.swift -// OmniKit -// -// Created by Joe Moran on 9/1/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import SwiftUI -import LoopKit -import OmniKit - -struct ReadPulseLogView: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.presentationMode) var presentationMode: Binding - - private var toRun: ((_ completion: @escaping (_ result: Result) -> Void) -> Void)? - - @State private var alertIsPresented: Bool = false - @State private var displayString: String = "" - @State private var error: Error? = nil - @State private var executing: Bool = false - @State private var showActivityView: Bool = false - - init(toRun: @escaping (_ completion: @escaping (_ result: Result) -> Void) -> Void) { - self.toRun = toRun - } - - var body: some View { - VStack { - List { - Section { - let myFont = Font - .system(size: 12) - .monospaced() - Text(self.displayString) - .font(myFont) - } - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - self.showActivityView = true - }) { - Image(systemName: "square.and.arrow.up") - } - } - }.sheet(isPresented: $showActivityView) { - ActivityView(isPresented: $showActivityView, activityItems: [self.displayString]) - } - VStack { - Button(action: { - asyncAction() - }) { - Text(buttonText) - .actionButtonStyle(.primary) - } - .padding() - .disabled(executing) - } - .padding(self.horizontalSizeClass == .regular ? .bottom : []) - .background(Color(UIColor.secondarySystemGroupedBackground).shadow(radius: 5)) - } - .insetGroupedListStyle() - .navigationTitle(LocalizedString("Read Pulse Log", comment: "navigation title for read pulse log")) - .navigationBarTitleDisplayMode(.inline) - .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) - .onFirstAppear { - asyncAction() - } - } - - private func asyncAction () { - DispatchQueue.global(qos: .utility).async { - executing = true - self.displayString = "" - toRun?() { (result) in - switch result { - case .success(let pulseLogString): - self.displayString = pulseLogString - case .failure(let error): - self.displayString = "" - self.error = error - self.alertIsPresented = true - } - executing = false - } - } - } - - private var buttonText: String { - if executing { - return LocalizedString("Reading Pulse Log...", comment: "button title when executing read pulse log") - } else { - return LocalizedString("Read Pulse Log", comment: "button title to read pulse log") - } - } - - private func alert(error: Error?) -> SwiftUI.Alert { - return SwiftUI.Alert( - title: Text(LocalizedString("Failed to read pulse log.", comment: "Alert title for error when reading pulse log")), - message: Text(error?.localizedDescription ?? "No Error") - ) - } -} - -struct ReadPulsePodLogView_Previews: PreviewProvider { - static var previews: some View { - ReadPulseLogView() { completion in - let podInfoPulseLogRecent = try! PodInfoPulseLogRecent(encodedData: Data([0x50, 0x03, 0x17, - 0x39, 0x72, 0x58, 0x01, 0x3c, 0x72, 0x43, 0x01, 0x41, 0x72, 0x5a, 0x01, 0x44, 0x71, 0x47, 0x01, - 0x49, 0x51, 0x59, 0x01, 0x4c, 0x51, 0x44, 0x01, 0x51, 0x73, 0x59, 0x01, 0x54, 0x50, 0x43, 0x01, - 0x59, 0x50, 0x5a, 0x81, 0x5c, 0x51, 0x42, 0x81, 0x61, 0x73, 0x59, 0x81, 0x00, 0x75, 0x43, 0x80, - 0x05, 0x70, 0x5a, 0x80, 0x08, 0x50, 0x44, 0x80, 0x0d, 0x50, 0x5b, 0x80, 0x10, 0x75, 0x43, 0x80, - 0x15, 0x72, 0x5e, 0x80, 0x18, 0x73, 0x45, 0x80, 0x1d, 0x72, 0x5b, 0x00, 0x20, 0x70, 0x43, 0x00, - 0x25, 0x50, 0x5c, 0x00, 0x28, 0x50, 0x46, 0x00, 0x2d, 0x50, 0x5a, 0x00, 0x30, 0x75, 0x47, 0x00, - 0x35, 0x72, 0x59, 0x00, 0x38, 0x70, 0x46, 0x00, 0x3d, 0x75, 0x57, 0x00, 0x40, 0x72, 0x43, 0x00, - 0x45, 0x73, 0x55, 0x00, 0x48, 0x73, 0x41, 0x00, 0x4d, 0x70, 0x52, 0x00, 0x50, 0x73, 0x3f, 0x00, - 0x55, 0x74, 0x4d, 0x00, 0x58, 0x72, 0x3d, 0x80, 0x5d, 0x73, 0x4d, 0x80, 0x60, 0x71, 0x3d, 0x80, - 0x01, 0x51, 0x50, 0x80, 0x04, 0x72, 0x3d, 0x80, 0x09, 0x50, 0x4e, 0x80, 0x0c, 0x51, 0x40, 0x80, - 0x11, 0x74, 0x50, 0x80, 0x14, 0x71, 0x40, 0x80, 0x19, 0x50, 0x4d, 0x80, 0x1c, 0x75, 0x3f, 0x00, - 0x21, 0x72, 0x52, 0x00, 0x24, 0x72, 0x40, 0x00, 0x29, 0x71, 0x53, 0x00, 0x2c, 0x50, 0x42, 0x00, - 0x31, 0x51, 0x55, 0x00, 0x34, 0x50, 0x42, 0x00 ])) - let lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry) - completion(.success(pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber))) - } - } -} From 1568d54b13961965a91f54f1710606f7b642d791 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 22 Mar 2024 08:45:28 -0700 Subject: [PATCH 05/21] fix typo in comment --- OmniKitUI/Views/FirstAppear.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OmniKitUI/Views/FirstAppear.swift b/OmniKitUI/Views/FirstAppear.swift index 664bde6..25f49bb 100644 --- a/OmniKitUI/Views/FirstAppear.swift +++ b/OmniKitUI/Views/FirstAppear.swift @@ -1,6 +1,6 @@ // // FirstAppear.swift -// Omnipod +// OmniKit // // Created by Joe Moran on 9/24/23. // Copyright © 2023 LoopKit Authors. All rights reserved. From 20fd39dac9a03d209bf0c18736ac3e2c486f86ca Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 22 Mar 2024 08:57:24 -0700 Subject: [PATCH 06/21] revert inadvertent change to packet parser --- OmniKitPacketParser/main.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/OmniKitPacketParser/main.swift b/OmniKitPacketParser/main.swift index 40a394d..c9485c7 100644 --- a/OmniKitPacketParser/main.swift +++ b/OmniKitPacketParser/main.swift @@ -277,6 +277,12 @@ class XcodeLogParser { } } +if CommandLine.argc <= 1 { + print("No file names specified in command arguments to parse!") + print("Set the Xcode Arguments Passed on Launch using Product->Scheme->Edit Scheme...") + print("to specify the full path to rtl, Loop Report, or Xcode log file(s) to parse.\n") + exit(1) +} for filename in CommandLine.arguments[1...] { let rtlOmniParser = RTLOmniLineParser() @@ -287,7 +293,7 @@ for filename in CommandLine.arguments[1...] { do { let data = try String(contentsOfFile: filename, encoding: .utf8) let lines = data.components(separatedBy: .newlines) - + for line in lines { switch line { case Regex("ID1:[0-9a-fA-F]+ PTYPE:"): @@ -298,17 +304,14 @@ for filename in CommandLine.arguments[1...] { // 2018-12-27 01:46:56 +0000 send 1f0e41a6101f1a0e81ed50b102010a0101a000340034170d000208000186a00000000000000111 loopIssueReportParser.parseLine(line) case Regex("RL (Send|Recv) ?\\(single\\): [0-9a-fA-F]+"): -// 2019-02-09 08:23:27.605518-0800 Loop[2978:2294033] [PeripheralManager+RileyLink] RL Send (single): 17050005000000000002580000281f0c27a4591f0c27a447 -// 2019-02-09 08:23:28.262888-0800 Loop[2978:2294816] [PeripheralManager+RileyLink] RL Recv(single): dd0c2f1f079e674b1f079e6769 + // 2019-02-09 08:23:27.605518-0800 Loop[2978:2294033] [PeripheralManager+RileyLink] RL Send (single): 17050005000000000002580000281f0c27a4591f0c27a447 + // 2019-02-09 08:23:28.262888-0800 Loop[2978:2294816] [PeripheralManager+RileyLink] RL Recv(single): dd0c2f1f079e674b1f079e6769 xcodeLogParser.parseLine(line) default: break } - - } } catch let error { print("Error: \(error)") } } - From a9435f4e30dac8be270df6fe22814740d12833f4 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sat, 23 Mar 2024 16:40:48 -0700 Subject: [PATCH 07/21] put Silence Pod and Pod Diagnostics behind build-time flag * supports Loop by using DEBUG_FEATURES_ENABLED flag * make suspend message more generic --- .../OmnipodUICoordinator.swift | 2 +- OmniKitUI/Views/OmnipodSettingsView.swift | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift b/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift index 838a463..c394e32 100644 --- a/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift +++ b/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift @@ -197,7 +197,7 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl } } - let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection, supportedInsulinTypes: allowedInsulinTypes) + let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection, supportedInsulinTypes: allowedInsulinTypes, allowDebugFeatures: allowDebugFeatures) return hostingController(rootView: view) case .pairAndPrime: pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManager) diff --git a/OmniKitUI/Views/OmnipodSettingsView.swift b/OmniKitUI/Views/OmnipodSettingsView.swift index 804c037..2e29021 100644 --- a/OmniKitUI/Views/OmnipodSettingsView.swift +++ b/OmniKitUI/Views/OmnipodSettingsView.swift @@ -38,6 +38,8 @@ struct OmnipodSettingsView: View { @Environment(\.guidanceColors) var guidanceColors @Environment(\.insulinTintColor) var insulinTintColor + let allowDebugFeatures : Bool + private var daysRemaining: Int? { if case .timeRemaining(let remaining, _) = viewModel.lifeState, remaining > .days(1) { return Int(remaining.days) @@ -462,13 +464,15 @@ struct OmnipodSettingsView: View { .foregroundColor(.secondary) } } - NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) { - HStack { - FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link") - .foregroundColor(Color.primary) - Spacer() - Text(viewModel.silencePodPreference.title) - .foregroundColor(.secondary) + if (allowDebugFeatures) { + NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) { + HStack { + FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link") + .foregroundColor(Color.primary) + Spacer() + Text(viewModel.silencePodPreference.title) + .foregroundColor(.secondary) + } } } NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) { @@ -513,13 +517,15 @@ struct OmnipodSettingsView: View { } } - 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) + if (allowDebugFeatures) { + 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) + } } } @@ -564,7 +570,7 @@ struct OmnipodSettingsView: View { var suspendOptionsActionSheet: ActionSheet { ActionSheet( title: FrameworkLocalText("Suspend Delivery", comment: "Title for suspend duration selection action sheet"), - message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. When would you like Loop to remind you to resume delivery?", comment: "Message for suspend duration selection action sheet"), + message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. When would you like this app to remind you to resume delivery?", comment: "Message for suspend duration selection action sheet"), buttons: [ .default(FrameworkLocalText("30 minutes", comment: "Button text for 30 minute suspend duration"), action: { self.viewModel.suspendDelivery(duration: .minutes(30)) }), .default(FrameworkLocalText("1 hour", comment: "Button text for 1 hour suspend duration"), action: { self.viewModel.suspendDelivery(duration: .hours(1)) }), From d2a84c5cf3cc38435cb643bdd0555e7a42d734c9 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 25 Mar 2024 17:02:02 -0700 Subject: [PATCH 08/21] enable user to set another expiration reminder for current pod --- .../Views/NotificationSettingsView.swift | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/OmniKitUI/Views/NotificationSettingsView.swift b/OmniKitUI/Views/NotificationSettingsView.swift index d806b67..be3554b 100644 --- a/OmniKitUI/Views/NotificationSettingsView.swift +++ b/OmniKitUI/Views/NotificationSettingsView.swift @@ -66,20 +66,16 @@ struct NotificationSettingsView: View { private func scheduledReminderRow(scheduledDate: Date?, allowedDates: [Date]) -> some View { Group { - if let scheduledDate = scheduledDate, scheduledDate <= Date() { - scheduledReminderRowContents(disclosure: false) - } else { - NavigationLink( - destination: ScheduledExpirationReminderEditView( - scheduledExpirationReminderDate: scheduledDate, - allowedDates: allowedDates, - dateFormatter: dateFormatter, - onSave: onSaveScheduledExpirationReminder, - onFinish: { scheduleReminderDateEditViewIsShown = false }), - isActive: $scheduleReminderDateEditViewIsShown) - { - scheduledReminderRowContents(disclosure: true) - } + NavigationLink( + destination: ScheduledExpirationReminderEditView( + scheduledExpirationReminderDate: scheduledDate, + allowedDates: allowedDates, + dateFormatter: dateFormatter, + onSave: onSaveScheduledExpirationReminder, + onFinish: { scheduleReminderDateEditViewIsShown = false }), + isActive: $scheduleReminderDateEditViewIsShown) + { + scheduledReminderRowContents(disclosure: true) } } } From ef8d6ce579bd58b611483822869b684fa64511e3 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 26 Mar 2024 05:51:28 -0700 Subject: [PATCH 09/21] update message about pod reminder --- OmniKitUI/Views/NotificationSettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OmniKitUI/Views/NotificationSettingsView.swift b/OmniKitUI/Views/NotificationSettingsView.swift index be3554b..86fa2bd 100644 --- a/OmniKitUI/Views/NotificationSettingsView.swift +++ b/OmniKitUI/Views/NotificationSettingsView.swift @@ -42,7 +42,7 @@ struct NotificationSettingsView: View { if let allowedDates = allowedScheduledReminderDates { RoundedCard( - footer: LocalizedString("This is a reminder that you scheduled when you paired your current Pod.", comment: "Footer text for scheduled reminder area")) + footer: LocalizedString("This is a reminder that you scheduled for your current Pod.", comment: "Footer text for scheduled reminder area")) { Text(LocalizedString("Scheduled Reminder", comment: "Scheduled reminder card title on NotificationSettingsView")) Divider() From c68e260b3c978a53fb3456c257945fbe1b7fa777 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 3 Apr 2024 06:42:49 -0700 Subject: [PATCH 10/21] Revert "put Silence Pod and Pod Diagnostics behind build-time flag" This reverts commit a9435f4e30dac8be270df6fe22814740d12833f4. --- .../OmnipodUICoordinator.swift | 2 +- OmniKitUI/Views/OmnipodSettingsView.swift | 36 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift b/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift index c394e32..838a463 100644 --- a/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift +++ b/OmniKitUI/ViewControllers/OmnipodUICoordinator.swift @@ -197,7 +197,7 @@ class OmnipodUICoordinator: UINavigationController, PumpManagerOnboarding, Compl } } - let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection, supportedInsulinTypes: allowedInsulinTypes, allowDebugFeatures: allowDebugFeatures) + let view = OmnipodSettingsView(viewModel: viewModel, rileyLinkListDataSource: rileyLinkListDataSource, handleRileyLinkSelection: handleRileyLinkSelection, supportedInsulinTypes: allowedInsulinTypes) return hostingController(rootView: view) case .pairAndPrime: pumpManagerOnboardingDelegate?.pumpManagerOnboarding(didCreatePumpManager: pumpManager) diff --git a/OmniKitUI/Views/OmnipodSettingsView.swift b/OmniKitUI/Views/OmnipodSettingsView.swift index 2e29021..804c037 100644 --- a/OmniKitUI/Views/OmnipodSettingsView.swift +++ b/OmniKitUI/Views/OmnipodSettingsView.swift @@ -38,8 +38,6 @@ struct OmnipodSettingsView: View { @Environment(\.guidanceColors) var guidanceColors @Environment(\.insulinTintColor) var insulinTintColor - let allowDebugFeatures : Bool - private var daysRemaining: Int? { if case .timeRemaining(let remaining, _) = viewModel.lifeState, remaining > .days(1) { return Int(remaining.days) @@ -464,15 +462,13 @@ struct OmnipodSettingsView: View { .foregroundColor(.secondary) } } - if (allowDebugFeatures) { - NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) { - HStack { - FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link") - .foregroundColor(Color.primary) - Spacer() - Text(viewModel.silencePodPreference.title) - .foregroundColor(.secondary) - } + NavigationLink(destination: SilencePodSelectionView(initialValue: viewModel.silencePodPreference, onSave: viewModel.setSilencePod)) { + HStack { + FrameworkLocalText("Silence Pod", comment: "Text for silence pod navigation link") + .foregroundColor(Color.primary) + Spacer() + Text(viewModel.silencePodPreference.title) + .foregroundColor(.secondary) } } NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) { @@ -517,15 +513,13 @@ struct OmnipodSettingsView: View { } } - if (allowDebugFeatures) { - 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) - } + 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) } } @@ -570,7 +564,7 @@ struct OmnipodSettingsView: View { var suspendOptionsActionSheet: ActionSheet { ActionSheet( title: FrameworkLocalText("Suspend Delivery", comment: "Title for suspend duration selection action sheet"), - message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. When would you like this app to remind you to resume delivery?", comment: "Message for suspend duration selection action sheet"), + message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. When would you like Loop to remind you to resume delivery?", comment: "Message for suspend duration selection action sheet"), buttons: [ .default(FrameworkLocalText("30 minutes", comment: "Button text for 30 minute suspend duration"), action: { self.viewModel.suspendDelivery(duration: .minutes(30)) }), .default(FrameworkLocalText("1 hour", comment: "Button text for 1 hour suspend duration"), action: { self.viewModel.suspendDelivery(duration: .hours(1)) }), From c6f216ced095ddd3c187f8237962fb39e0482e29 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sun, 7 Apr 2024 13:42:22 -0700 Subject: [PATCH 11/21] Update the message in SilencePodPreference --- OmniKit/OmnipodCommon/SilencePodPreference.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OmniKit/OmnipodCommon/SilencePodPreference.swift b/OmniKit/OmnipodCommon/SilencePodPreference.swift index 118bbf5..a4d5b64 100644 --- a/OmniKit/OmnipodCommon/SilencePodPreference.swift +++ b/OmniKit/OmnipodCommon/SilencePodPreference.swift @@ -26,7 +26,7 @@ public enum SilencePodPreference: Int, CaseIterable { case .disabled: return LocalizedString("Normal operation mode where audible Pod beeps are used for all Pod alerts and when confidence reminders are enabled.", comment: "Description for SilencePodPreference.disabled") case .enabled: - return LocalizedString("All Pod alerts use no beeps and confirmation reminder beeps are suppressed. The Pod will only beep for fatal Pod faults and when playing test beeps.\n\n⚠️Warning - Whenever the Pod is silenced it must be kept within Bluetooth range of this device to receive notifications for Pod alerts.", comment: "Description for SilencePodPreference.enabled") + return LocalizedString("All Pod alerts use no beeps and confirmation reminder beeps are suppressed. The Pod will only beep for fatal Pod faults and when playing test beeps.\n\n⚠️Warning - If your phone is out of range of the pod while this feature is enabled, you will not receive any in-app notifications; and the pod will not beep to alert you.", comment: "Description for SilencePodPreference.enabled") } } } From 2b4b7f8f3dc7a1fd3e2efd25ae532650a484ff69 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 8 Apr 2024 14:05:16 -0700 Subject: [PATCH 12/21] fix pairing issue introduced in pr 28 --- OmniKitUI/ViewModels/PairPodViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OmniKitUI/ViewModels/PairPodViewModel.swift b/OmniKitUI/ViewModels/PairPodViewModel.swift index 45f5dc1..8edf440 100644 --- a/OmniKitUI/ViewModels/PairPodViewModel.swift +++ b/OmniKitUI/ViewModels/PairPodViewModel.swift @@ -193,7 +193,10 @@ class PairPodViewModel: ObservableObject, Identifiable { DispatchQueue.main.async { switch status { case .failure(let error): - if self.autoRetryAttempted { + if self.podPairer.podCommState == .noPod { + let pairAndPrimeError = OmnipodPairingError.pumpManagerError(error) + self.state = .error(pairAndPrimeError) + } else if self.autoRetryAttempted { self.autoRetryAttempted = false // allow for an auto retry on the next user attempt let pairAndPrimeError = OmnipodPairingError.pumpManagerError(error) self.state = .error(pairAndPrimeError) From 82b76cf329f427413857e619d575bbf7055359ee Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 9 Apr 2024 16:44:46 -0700 Subject: [PATCH 13/21] Cleanup around diagnostic commands --- OmniKit.xcodeproj/project.pbxproj | 12 +- OmniKit/PumpManager/OmnipodPumpManager.swift | 238 +++++++++--------- .../ViewModels/OmnipodSettingsViewModel.swift | 57 +---- OmniKitUI/Views/FirstAppear.swift | 30 --- OmniKitUI/Views/OmnipodSettingsView.swift | 9 +- OmniKitUI/Views/PlayTestBeepsView.swift | 34 ++- ...nostics.swift => PodDiagnosticsView.swift} | 44 ++-- OmniKitUI/Views/PumpManagerDetailsView.swift | 27 +- OmniKitUI/Views/ReadPodInfoView.swift | 37 ++- OmniKitUI/Views/ReadPodStatusView.swift | 36 ++- 10 files changed, 227 insertions(+), 297 deletions(-) delete mode 100644 OmniKitUI/Views/FirstAppear.swift rename OmniKitUI/Views/{PodDiagnostics.swift => PodDiagnosticsView.swift} (71%) diff --git a/OmniKit.xcodeproj/project.pbxproj b/OmniKit.xcodeproj/project.pbxproj index 5550835..ffb0d10 100644 --- a/OmniKit.xcodeproj/project.pbxproj +++ b/OmniKit.xcodeproj/project.pbxproj @@ -193,12 +193,11 @@ D803399B2A50122F004FF953 /* Packet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12401B329C7D8E900B32844 /* Packet.swift */; }; D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */; }; D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1452AF8A4DA00EA0853 /* ActivityView.swift */; }; - D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1472AF8A4E400EA0853 /* FirstAppear.swift */; }; D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */; }; D845A14E2AF8A4FB00EA0853 /* ReadPodStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */; }; 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 */; }; + D85AEAC82B1403C000081044 /* PodDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85AEAC72B1403C000081044 /* PodDiagnosticsView.swift */; }; D85AEACA2B1403CB00081044 /* ReadPodInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */; }; D803399C2A50128D004FF953 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12EDA0129C7DD4700435701 /* LocalizedString.swift */; }; /* End PBXBuildFile section */ @@ -446,12 +445,11 @@ C12EDA1A29C7E06900435701 /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodPreference.swift; sourceTree = ""; }; D845A1452AF8A4DA00EA0853 /* ActivityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; - D845A1472AF8A4E400EA0853 /* FirstAppear.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirstAppear.swift; sourceTree = ""; }; D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayTestBeepsView.swift; sourceTree = ""; }; D845A14B2AF8A4FB00EA0853 /* ReadPodStatusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodStatusView.swift; sourceTree = ""; }; D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpManagerDetailsView.swift; sourceTree = ""; }; D845A1512AF8A51000EA0853 /* SilencePodSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilencePodSelectionView.swift; sourceTree = ""; }; - D85AEAC72B1403C000081044 /* PodDiagnostics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodDiagnostics.swift; sourceTree = ""; }; + D85AEAC72B1403C000081044 /* PodDiagnosticsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodDiagnosticsView.swift; sourceTree = ""; }; D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadPodInfoView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -741,7 +739,6 @@ C124023129C7DA9700B32844 /* DesignElements */, C124023A29C7DA9700B32844 /* ExpirationReminderPickerView.swift */, C124024529C7DA9700B32844 /* ExpirationReminderSetupView.swift */, - D845A1472AF8A4E400EA0853 /* FirstAppear.swift */, C124023629C7DA9700B32844 /* InsertCannulaView.swift */, C124023C29C7DA9700B32844 /* InsulinTypeConfirmation.swift */, C124023529C7DA9700B32844 /* LowReservoirReminderEditView.swift */, @@ -753,7 +750,7 @@ C124024329C7DA9700B32844 /* PairPodView.swift */, D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */, C124024829C7DA9700B32844 /* PodDetailsView.swift */, - D85AEAC72B1403C000081044 /* PodDiagnostics.swift */, + D85AEAC72B1403C000081044 /* PodDiagnosticsView.swift */, C124024129C7DA9700B32844 /* PodSetupView.swift */, D845A14D2AF8A4FB00EA0853 /* PumpManagerDetailsView.swift */, D85AEAC92B1403CB00081044 /* ReadPodInfoView.swift */, @@ -1232,7 +1229,6 @@ C124027029C7DA9700B32844 /* DeliveryUncertaintyRecoveryViewModel.swift in Sources */, C12EDA0E29C7DEFD00435701 /* NumberFormatter.swift in Sources */, D845A1522AF8A51000EA0853 /* SilencePodSelectionView.swift in Sources */, - D845A1482AF8A4E400EA0853 /* FirstAppear.swift in Sources */, C12EDA1229C7DF4B00435701 /* IdentifiableClass.swift in Sources */, C124027529C7DA9700B32844 /* OmnipodSettingsViewModel.swift in Sources */, C124029729C7DA9700B32844 /* OmnipodUICoordinator.swift in Sources */, @@ -1246,7 +1242,7 @@ D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */, C124026F29C7DA9700B32844 /* PairPodViewModel.swift in Sources */, C124026E29C7DA9700B32844 /* FrameworkLocalText.swift in Sources */, - D85AEAC82B1403C000081044 /* PodDiagnostics.swift in Sources */, + D85AEAC82B1403C000081044 /* PodDiagnosticsView.swift in Sources */, C124028F29C7DA9700B32844 /* PodDetailsView.swift in Sources */, C124028629C7DA9700B32844 /* NotificationSettingsView.swift in Sources */, C124027D29C7DA9700B32844 /* InsertCannulaView.swift in Sources */, diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index ef9ac5e..7a0fcd0 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -1018,35 +1018,36 @@ extension OmnipodPumpManager { } } - public func getDetailedStatus(completion: ((_ result: PumpManagerResult) -> Void)? = nil) { + public func getDetailedStatus() async throws -> DetailedStatus { // use hasSetupPod here instead of hasActivePod as DetailedStatus can be read with a faulted Pod guard self.hasSetupPod else { - completion?(.failure(PumpManagerError.configuration(OmnipodPumpManagerError.noPodPaired))) - return + throw PumpManagerError.configuration(OmnipodPumpManagerError.noPodPaired) } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice - podComms.runSession(withName: "Get detailed status", using: rileyLinkSelector) { (result) in - do { - switch result { - case .success(let session): - let beepBlock = self.beepMessageBlock(beepType: .bipBip) - let detailedStatus = try session.getDetailedStatus(beepBlock: beepBlock) - session.dosesForStorage({ (doses) -> Bool in - self.store(doses: doses, in: session) - }) - completion?(.success(detailedStatus)) - case .failure(let error): - throw error + return try await withCheckedThrowingContinuation { continuation in + podComms.runSession(withName: "Get detailed status", using: rileyLinkSelector) { (result) in + do { + switch result { + case .success(let session): + let beepBlock = self.beepMessageBlock(beepType: .bipBip) + let detailedStatus = try session.getDetailedStatus(beepBlock: beepBlock) + session.dosesForStorage({ (doses) -> Bool in + self.store(doses: doses, in: session) + }) + continuation.resume(returning: detailedStatus) + case .failure(let error): + continuation.resume(throwing: error) + } + } catch let error { + self.log.error("Failed to fetch detailed status: %{public}@", String(describing: error)) + continuation.resume(throwing: PumpManagerError.communication(error as? LocalizedError)) } - } catch let error { - completion?(.failure(.communication(error as? LocalizedError))) - self.log.error("Failed to fetch detailed status: %{public}@", String(describing: error)) } } } - + public func acknowledgePodAlerts(_ alertsToAcknowledge: AlertSet, completion: @escaping (_ alerts: AlertSet?) -> Void) { guard self.hasActivePod else { completion(nil) @@ -1214,159 +1215,170 @@ extension OmnipodPumpManager { #endif } - public func playTestBeeps(completion: @escaping (Error?) -> Void) { + public func playTestBeeps() async throws { guard self.hasActivePod else { - completion(OmnipodPumpManagerError.noPodPaired) - return + throw OmnipodPumpManagerError.noPodPaired } guard state.podState?.unfinalizedBolus?.scheduledCertainty == .uncertain || state.podState?.unfinalizedBolus?.isFinished() != false else { self.log.info("Skipping Play Test Beeps due to bolus still in progress.") - completion(PodCommsError.unfinalizedBolus) - return + throw PodCommsError.unfinalizedBolus } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice - self.podComms.runSession(withName: "Play Test Beeps", using: rileyLinkSelector) { (result) in - switch result { - case .success(let session): - // preserve the pod's completion beep state which gets reset playing beeps - let enabled: Bool = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand - let result = session.beepConfig( - beepType: .bipBeepBipBeepBipBeepBipBeep, - tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal, - bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus - ) - + try await withCheckedThrowingContinuation { continuation in + self.podComms.runSession(withName: "Play Test Beeps", using: rileyLinkSelector) { (result) in switch result { - case .success: - completion(nil) + case .success(let session): + // preserve the pod's completion beep state which gets reset playing beeps + let enabled: Bool = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand + let result = session.beepConfig( + beepType: .bipBeepBipBeepBipBeepBipBeep, + tempBasalCompletionBeep: enabled && self.hasUnfinalizedManualTempBasal, + bolusCompletionBeep: enabled && self.hasUnfinalizedManualBolus + ) + + switch result { + case .success: + continuation.resume() + case .failure(let error): + continuation.resume(throwing: error) + } case .failure(let error): - completion(error) + continuation.resume(throwing: error) } - case .failure(let error): - completion(error) } } } - public func readPulseLog(completion: @escaping (Result) -> Void) { + public func readPulseLog() async throws -> String { // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod guard self.hasSetupPod else { - completion(.failure(OmnipodPumpManagerError.noPodPaired)) - return + throw OmnipodPumpManagerError.noPodPaired } + guard state.podState?.isFaulted == true || state.podState?.unfinalizedBolus?.scheduledCertainty == .uncertain || state.podState?.unfinalizedBolus?.isFinished() != false else { self.log.info("Skipping Read Pulse Log due to bolus still in progress.") - completion(.failure(PodCommsError.unfinalizedBolus)) - return + throw PodCommsError.unfinalizedBolus } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice - self.podComms.runSession(withName: "Read Pulse Log", using: rileyLinkSelector) { (result) in - switch result { - case .success(let session): - do { - // read the most recent 50 entries from the pulse log - let beepBlock = self.beepMessageBlock(beepType: .bipBeeeeep) - let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .pulseLogRecent, beepBlock: beepBlock) - guard let podInfoPulseLogRecent = podInfoResponse.podInfo as? PodInfoPulseLogRecent else { - self.log.error("Unable to decode PulseLogRecent: %s", String(describing: podInfoResponse)) - completion(.failure(PodCommsError.unexpectedResponse(response: .podInfoResponse))) - return + return try await withCheckedThrowingContinuation { continuation in + self.podComms.runSession(withName: "Read Pulse Log", using: rileyLinkSelector) { (result) in + switch result { + case .success(let session): + do { + // read the most recent 50 entries from the pulse log + let beepBlock = self.beepMessageBlock(beepType: .bipBeeeeep) + let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .pulseLogRecent, beepBlock: beepBlock) + guard let podInfoPulseLogRecent = podInfoResponse.podInfo as? PodInfoPulseLogRecent else { + self.log.error("Unable to decode PulseLogRecent: %s", String(describing: podInfoResponse)) + throw PodCommsError.unexpectedResponse(response: .podInfoResponse) + } + let lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry) + let str = pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber) + continuation.resume(returning: str) + } catch { + continuation.resume(throwing: error) } - let lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry) - let str = pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber) - completion(.success(str)) - } catch let error { - completion(.failure(error)) + case .failure(let error): + continuation.resume(throwing: error) } - case .failure(let error): - completion(.failure(error)) } } } - public func readPulseLogPlus(completion: @escaping (Result) -> Void) { + public func readPulseLogPlus() async throws -> String { // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod guard self.hasSetupPod else { - completion(.failure(OmnipodPumpManagerError.noPodPaired)) - return + throw OmnipodPumpManagerError.noPodPaired } 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 + throw PodCommsError.unfinalizedBolus } 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 + return try await withCheckedThrowingContinuation { continuation in + 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) + guard let podInfoPulseLogPlus = podInfoResponse.podInfo as? PodInfoPulseLogPlus else { + self.log.error("Unable to decode Pulse Log Plus: %s", String(describing: podInfoResponse)) + throw PodCommsError.unexpectedResponse(response: .podInfoResponse) + } + let str = pulseLogPlusString(podInfoPulseLogPlus: podInfoPulseLogPlus) + continuation.resume(returning: str) + case .failure(let error): + continuation.resume(throwing: error) + } + } catch { + continuation.resume(throwing: error) } - } catch let error { - completion(.failure(error)) } } } - public func readActivationTime(completion: @escaping (Result) -> Void) { + public func readActivationTime() async throws -> String { // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod guard self.hasSetupPod else { - completion(.failure(OmnipodPumpManagerError.noPodPaired)) - return + throw OmnipodPumpManagerError.noPodPaired } 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 + return try await withCheckedThrowingContinuation { continuation in + 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) + guard let podInfoActivationTime = podInfoResponse.podInfo as? PodInfoActivationTime else { + self.log.error("Unable to decode Activation Time: %s", String(describing: podInfoResponse)) + throw PodCommsError.unexpectedResponse(response: .podInfoResponse) + } + let str = activationTimeString(podInfoActivationTime: podInfoActivationTime) + continuation.resume(returning: str) + case .failure(let error): + continuation.resume(throwing: error) + } + } catch { + continuation.resume(throwing: error) } - } catch let error { - completion(.failure(error)) } } } - public func readTriggeredAlerts(completion: @escaping (Result) -> Void) { + public func readTriggeredAlerts() async throws -> String { // use hasSetupPod here instead of hasActivePod as PodInfo can be read with a faulted Pod guard self.hasSetupPod else { - completion(.failure(OmnipodPumpManagerError.noPodPaired)) - return + throw OmnipodPumpManagerError.noPodPaired } 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 + return try await withCheckedThrowingContinuation { continuation in + 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) + guard let podInfoTriggeredAlerts = podInfoResponse.podInfo as? PodInfoTriggeredAlerts else { + self.log.error("Unable to decode Read Triggered Alerts: %s", String(describing: podInfoResponse)) + throw PodCommsError.unexpectedResponse(response: .podInfoResponse) + } + let str = triggeredAlertsString(podInfoTriggeredAlerts: podInfoTriggeredAlerts) + continuation.resume(returning: str) + case .failure(let error): + continuation.resume(throwing: error) + } + } catch let error { + continuation.resume(throwing: error) } - } catch let error { - completion(.failure(error)) } } } diff --git a/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift b/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift index a8e00da..3dae32c 100644 --- a/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift +++ b/OmniKitUI/ViewModels/OmnipodSettingsViewModel.swift @@ -340,52 +340,8 @@ class OmnipodSettingsViewModel: ObservableObject { } } - func readPodStatus(_ completion: @escaping (_ result: PumpManagerResult) -> Void) { - pumpManager.getDetailedStatus() { (result) in - DispatchQueue.main.async { - completion(result) - } - } - } - - func playTestBeeps(_ completion: @escaping (Error?) -> Void) { - pumpManager.playTestBeeps(completion: completion) - } - - func readPulseLog(_ completion: @escaping (_ result: Result) -> Void) { - pumpManager.readPulseLog() { (result) in - DispatchQueue.main.async { - completion(result) - } - } - } - - func readPulseLogPlus(_ completion: @escaping (_ result: Result) -> Void) { - pumpManager.readPulseLogPlus() { (result) in - DispatchQueue.main.async { - completion(result) - } - } - } - - func readActivationTime(_ completion: @escaping (_ result: Result) -> Void) { - pumpManager.readActivationTime() { (result) in - DispatchQueue.main.async { - completion(result) - } - } - } - - func readTriggeredAlerts(_ completion: @escaping (_ result: Result) -> Void) { - pumpManager.readTriggeredAlerts() { (result) in - DispatchQueue.main.async { - completion(result) - } - } - } - - func pumpManagerDetails(_ completion: @escaping (_ result: String) -> Void) { - completion(pumpManager.debugDescription) + func playTestBeeps() async throws { + try await pumpManager.playTestBeeps() } func setConfirmationBeeps(_ preference: BeepPreference, _ completion: @escaping (_ error: LocalizedError?) -> Void) { @@ -429,6 +385,10 @@ class OmnipodSettingsViewModel: ObservableObject { return podCommState == .noPod } + var diagnosticCommands: DiagnosticCommands { + return pumpManager + } + var podError: String? { switch podCommState { case .fault(let status): @@ -656,3 +616,8 @@ extension OmnipodPumpManager { } +extension OmnipodPumpManager: DiagnosticCommands { + func pumpManagerDetails() -> String { + return debugDescription + } +} diff --git a/OmniKitUI/Views/FirstAppear.swift b/OmniKitUI/Views/FirstAppear.swift deleted file mode 100644 index 25f49bb..0000000 --- a/OmniKitUI/Views/FirstAppear.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// FirstAppear.swift -// OmniKit -// -// Created by Joe Moran on 9/24/23. -// Copyright © 2023 LoopKit Authors. All rights reserved. -// - -import SwiftUI - -extension View { - func onFirstAppear(_ action: @escaping () -> ()) -> some View { - modifier(FirstAppear(action: action)) - } -} - -private struct FirstAppear: ViewModifier { - let action: () -> () - - // State used to insure action is invoked here only once - @State private var hasAppeared = false - - func body(content: Content) -> some View { - content.onAppear { - guard !hasAppeared else { return } - hasAppeared = true - action() - } - } -} diff --git a/OmniKitUI/Views/OmnipodSettingsView.swift b/OmniKitUI/Views/OmnipodSettingsView.swift index 804c037..ba4bddb 100644 --- a/OmniKitUI/Views/OmnipodSettingsView.swift +++ b/OmniKitUI/Views/OmnipodSettingsView.swift @@ -270,7 +270,10 @@ struct OmnipodSettingsView: View { VStack(alignment: .trailing) { Button(action: { sendingTestBeepsCommand = true - viewModel.playTestBeeps { _ in + Task { @MainActor in + do { + try await viewModel.playTestBeeps() + } sendingTestBeepsCommand = false } }) { @@ -516,7 +519,9 @@ struct OmnipodSettingsView: View { Section() { NavigationLink(destination: PodDiagnosticsView( title: LocalizedString("Pod Diagnostics", comment: "Title for the pod diagnostic view"), - viewModel: viewModel)) + diagnosticCommands: viewModel.diagnosticCommands, + podOk: viewModel.podOk, + noPod: viewModel.noPod)) { FrameworkLocalText("Pod Diagnostics", comment: "Text for pod diagnostics row") .foregroundColor(Color.primary) diff --git a/OmniKitUI/Views/PlayTestBeepsView.swift b/OmniKitUI/Views/PlayTestBeepsView.swift index f544654..965b9f6 100644 --- a/OmniKitUI/Views/PlayTestBeepsView.swift +++ b/OmniKitUI/Views/PlayTestBeepsView.swift @@ -14,15 +14,15 @@ struct PlayTestBeepsView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.presentationMode) var presentationMode: Binding - var toRun: ((_ completion: @escaping (_ result: Error?) -> Void) -> Void)? + var playTestBeeps: () async throws -> 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") + private let successMessage = LocalizedString("Play test beeps command sent successfully.\n\nIf you did not hear any beeps from your Pod, the piezo speaker in your Pod may be broken or disabled.", comment: "Success message for play test beeps") @State private var alertIsPresented: Bool = false @State private var displayString: String = "" - @State private var successMessage = LocalizedString("Play test beeps command sent successfully.\n\nIf you did not hear any beeps from your Pod, the piezo speaker in your Pod may be broken or disabled.", comment: "Success message for play test beeps") @State private var error: Error? = nil @State private var executing: Bool = false @State private var showActivityView = false @@ -36,7 +36,7 @@ struct PlayTestBeepsView: View { } VStack { Button(action: { - asyncAction() + Task { await playTestBeepsAndHandleError() } }) { Text(buttonText) .actionButtonStyle(.primary) @@ -51,25 +51,19 @@ struct PlayTestBeepsView: View { .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) - .onFirstAppear { - asyncAction() + .task { + await playTestBeepsAndHandleError() } } - private func asyncAction () { - DispatchQueue.global(qos: .utility).async { - executing = true + private func playTestBeepsAndHandleError() async { + do { + try await playTestBeeps() + self.displayString = successMessage + } catch { self.displayString = "" - toRun?() { (error) in - executing = false - if let error = error { - self.displayString = "" - self.error = error - self.alertIsPresented = true - } else { - self.displayString = successMessage - } - } + self.error = error + self.alertIsPresented = true } } @@ -92,8 +86,8 @@ struct PlayTestBeepsView: View { struct PlayTestBeepsView_Previews: PreviewProvider { static var previews: some View { NavigationView { - PlayTestBeepsView() { completion in - completion(nil) + PlayTestBeepsView { + print("Beep!") } } } diff --git a/OmniKitUI/Views/PodDiagnostics.swift b/OmniKitUI/Views/PodDiagnosticsView.swift similarity index 71% rename from OmniKitUI/Views/PodDiagnostics.swift rename to OmniKitUI/Views/PodDiagnosticsView.swift index 7331a0e..859a145 100644 --- a/OmniKitUI/Views/PodDiagnostics.swift +++ b/OmniKitUI/Views/PodDiagnosticsView.swift @@ -1,5 +1,5 @@ // -// PodDiagnotics.swift +// PodDiagnosticsView.swift // OmniKit // // Created by Joseph Moran on 11/25/23. @@ -12,73 +12,85 @@ import LoopKitUI import HealthKit import OmniKit +protocol DiagnosticCommands { + func playTestBeeps() async throws + func readPulseLog() async throws -> String + func readPulseLogPlus() async throws -> String + func readActivationTime() async throws -> String + func readTriggeredAlerts() async throws -> String + func getDetailedStatus() async throws -> DetailedStatus + func pumpManagerDetails() -> String +} struct PodDiagnosticsView: View { var title: String - @ObservedObject var viewModel: OmnipodSettingsViewModel + var diagnosticCommands: DiagnosticCommands + var podOk: Bool + var noPod: Bool var body: some View { List { - NavigationLink(destination: ReadPodStatusView(toRun: viewModel.readPodStatus)) { + NavigationLink(destination: ReadPodStatusView(getDetailedStatus: diagnosticCommands.getDetailedStatus)) { FrameworkLocalText("Read Pod Status", comment: "Text for read pod status navigation link") .foregroundColor(Color.primary) } - .disabled(self.viewModel.noPod) + .disabled(noPod) - NavigationLink(destination: PlayTestBeepsView(toRun: viewModel.playTestBeeps)) { + NavigationLink(destination: PlayTestBeepsView(playTestBeeps: { + try await diagnosticCommands.playTestBeeps() + })) { FrameworkLocalText("Play Test Beeps", comment: "Text for play test beeps navigation link") .foregroundColor(Color.primary) } - .disabled(!self.viewModel.podOk) + .disabled(!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)) + action: { try await diagnosticCommands.readPulseLog() })) { FrameworkLocalText("Read Pulse Log", comment: "Text for read pulse log navigation link") .foregroundColor(Color.primary) } - .disabled(self.viewModel.noPod) + .disabled(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)) + action: { try await diagnosticCommands.readPulseLogPlus() })) { FrameworkLocalText("Read Pulse Log Plus", comment: "Text for read pulse log plus navigation link") .foregroundColor(Color.primary) } - .disabled(self.viewModel.noPod) + .disabled(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)) + action: { try await diagnosticCommands.readActivationTime() })) { FrameworkLocalText("Read Activation Time", comment: "Text for read activation time navigation link") .foregroundColor(Color.primary) } - .disabled(self.viewModel.noPod) + .disabled(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)) + action: { try await diagnosticCommands.readTriggeredAlerts() })) { FrameworkLocalText("Read Triggered Alerts", comment: "Text for read triggered alerts navigation link") .foregroundColor(Color.primary) } - .disabled(self.viewModel.noPod) + .disabled(noPod) - NavigationLink(destination: PumpManagerDetailsView( - toRun: self.viewModel.pumpManagerDetails)) + NavigationLink(destination: PumpManagerDetailsView() { diagnosticCommands.pumpManagerDetails() }) { FrameworkLocalText("Pump Manager Details", comment: "Text for pump manager details navigation link") .foregroundColor(Color.primary) diff --git a/OmniKitUI/Views/PumpManagerDetailsView.swift b/OmniKitUI/Views/PumpManagerDetailsView.swift index 206de32..ba081d3 100644 --- a/OmniKitUI/Views/PumpManagerDetailsView.swift +++ b/OmniKitUI/Views/PumpManagerDetailsView.swift @@ -14,7 +14,7 @@ struct PumpManagerDetailsView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.presentationMode) var presentationMode: Binding - var toRun: ((_ completion: @escaping (_ result: String) -> Void) -> Void)? + var getPumpManagerDetails: () -> String 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") @@ -25,10 +25,6 @@ struct PumpManagerDetailsView: View { @State private var executing: Bool = false @State private var showActivityView: Bool = false - init(toRun: @escaping (_ completion: @escaping (_ result: String) -> Void) -> Void) { - self.toRun = toRun - } - var body: some View { VStack { List { @@ -53,7 +49,7 @@ struct PumpManagerDetailsView: View { } VStack { Button(action: { - asyncAction() + self.displayString = getPumpManagerDetails() }) { Text(buttonText) .actionButtonStyle(.primary) @@ -67,19 +63,8 @@ struct PumpManagerDetailsView: View { .insetGroupedListStyle() .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) - .onFirstAppear { - asyncAction() - } - } - - private func asyncAction () { - DispatchQueue.global(qos: .utility).async { - executing = true - self.displayString = "" - toRun?() { (result) in - self.displayString = result - executing = false - } + .task { + self.displayString = getPumpManagerDetails() } } @@ -96,9 +81,7 @@ struct PumpManagerDetailsView_Previews: PreviewProvider { static var previews: some View { let examplePumpManagerDetails: String = "## OmnipodPumpManager\n\n## RileyLinkPumpManager\nlastTimerTick: 2023-10-07 22:35:39 +0000\n\n## RileyLinkDeviceManager\n\ncentral: \n\nautoConnectIDs: [\"F0178BCA-967D-504A-8C3A-99E84964B459\"]\n\ntimerTickEnabled: true\n\nidleListeningState: disabled\n\n## RileyLinkDevice\n* name: JPM OrangePro\n* lastIdle: 0001-01-01 00:00:00 +0000\n* isIdleListeningPending: false\n* isTimerTickEnabled: true\n* isTimerTickNotifying: true\n* radioFirmware: Optional(subg_rfspy 2.2)\n* bleFirmware: Optional(ble_rfspy 2.0)\n* peripheralManager: \n* sessionQueue.operationCount: 2\n\npodComms: ## PodComms\nconfiguredDevices: [\"F0178BCA-967D-504A-8C3A-99E84964B459\"]\ndelegate: true\n\nstatusObservers.count: 2\nstatus: ## PumpManagerStatus\n* timeZone: GMT-0700 (fixed)\n* device: <, name:Omnipod, manufacturer:Insulet, model:Eros, firmware:2.10.0, software:1.0, localIdentifier:1F05DD9A>\n* pumpBatteryChargeRemaining: nil\n* basalDeliveryState: Optional(LoopKit.PumpManagerStatus.BasalDeliveryState.active(2023-10-07 22:33:48 +0000))\n* bolusState: noBolus\n* insulinType: Optional(LoopKit.InsulinType.humalog)\n* deliveryIsUncertain: false\n\npodStateObservers.count: 1\nstate: ## OmnipodPumpManagerState\n* isOnboarded: true\n* timeZone: GMT-0700 (fixed)\n* basalSchedule: BasalSchedule(entries: [OmniKit.BasalScheduleEntry(rate: 0.9, startTime: 0.0)])\n* maximumTempBasalRate: 2.0\n* scheduledExpirationReminderOffset: Optional(\"24h0m\")\n* defaultExpirationReminderOffset: 24h0m\n* lowReservoirReminderValue: 50.0\n* podAttachmentConfirmed: true\n* activeAlerts: []\n* alertsWithPendingAcknowledgment: []\n* acknowledgedTimeOffsetAlert: false\n* initialConfigurationCompleted: true\n* unstoredDoses: []\n* suspendEngageState: stable\n* bolusEngageState: stable\n* tempBasalEngageState: stable\n* lastPumpDataReportDate: Optional(2023-10-07 22:35:24 +0000)\n* isPumpDataStale: false\n* silencePod: false\n* confirmationBeeps: manualCommands\n* pairingAttemptAddress: nil\n* insulinType: Optional(LoopKit.InsulinType.humalog)\n* scheduledExpirationReminderOffset: Optional(\"24h0m\")\n* defaultExpirationReminderOffset: 24h0m\n* rileyLinkBatteryAlertLevel: nil\n* lastRileyLinkBatteryAlertDate 0001-01-01 00:00:00 +0000\n* RileyLinkConnectionManagerState: RileyLinkConnectionState(autoConnectIDs: Set([\"F0178BCA-967D-504A-8C3A-99E84964B459\"]))\n* PodState: ### PodState\n* address: 1F05DD9A\n* activatedAt: Optional(2023-10-07 22:31:21 +0000)\n* expiresAt: Optional(2023-10-10 22:30:51 +0000)\n* timeActive: 4m\n* timeActiveUpdated: Optional(2023-10-07 22:35:38 +0000)\n* setupUnitsDelivered: Optional(2.65)\n* piVersion: 2.10.0\n* pmVersion: 2.10.0\n* lot: 72353\n* tid: 3280440\n* suspendState: resumed(2023-10-07 22:33:48 +0000)\n* unacknowledgedCommand: nil\n* unfinalizedBolus: nil\n* unfinalizedTempBasal: nil\n* unfinalizedSuspend: nil\n* unfinalizedResume: Optional(Resume: 10/7/23, 3:33:48 PM Certain)\n* finalizedDoses: []\n* activeAlertsSlots: No alerts\n* messageTransportState: MessageTransportState(packetNumber: 2, messageNumber: 8)\n* setupProgress: completed\n* primeFinishTime: Optional(2023-10-07 22:33:16 +0000)\n* configuredAlerts: [OmniKit.AlertSlot.slot4LowReservoir: Low reservoir, OmniKit.AlertSlot.slot3ExpirationReminder: Expiration reminder, OmniKit.AlertSlot.slot2ShutdownImminent: Shutdown imminent, OmniKit.AlertSlot.slot7Expired: Pod expired]\n* insulinType: humalog\n* PdmRef: nil\n* Fault: nil\n\n* PreviousPodState: nil\n" NavigationView { - PumpManagerDetailsView() { completion in - completion(examplePumpManagerDetails) - } + PumpManagerDetailsView() { examplePumpManagerDetails } } } } diff --git a/OmniKitUI/Views/ReadPodInfoView.swift b/OmniKitUI/Views/ReadPodInfoView.swift index 62db6f2..b77d4a4 100644 --- a/OmniKitUI/Views/ReadPodInfoView.swift +++ b/OmniKitUI/Views/ReadPodInfoView.swift @@ -19,7 +19,7 @@ struct ReadPodInfoView: View { var actionString: String // e.g., "Reading Pulse Log..." var failedString: String // e.g., "Failed to read pulse log." - var toRun: ((_ completion: @escaping (_ result: Result) -> Void) -> Void)? + var action: () async throws -> String @State private var alertIsPresented: Bool = false @State private var displayString: String = "" @@ -51,7 +51,9 @@ struct ReadPodInfoView: View { } VStack { Button(action: { - asyncAction() + Task { @MainActor in + await attemptAction() + } }) { Text(buttonText) .actionButtonStyle(.primary) @@ -66,27 +68,22 @@ struct ReadPodInfoView: View { .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) - .onFirstAppear { - asyncAction() + .task { + await attemptAction() } } - private func asyncAction () { - DispatchQueue.global(qos: .utility).async { - executing = true + private func attemptAction() async { + executing = true + self.displayString = "" + do { + self.displayString = try await action() + } catch { self.displayString = "" - toRun?() { (result) in - executing = false - switch result { - case .success(let resultString): - self.displayString = resultString - case .failure(let error): - self.displayString = "" - self.error = error - self.alertIsPresented = true - } - } + self.error = error + self.alertIsPresented = true } + executing = false } private var buttonText: String { @@ -112,7 +109,7 @@ struct ReadPodInfoView_Previews: PreviewProvider { 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, @@ -128,7 +125,7 @@ struct ReadPodInfoView_Previews: PreviewProvider { 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))) + return pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber) } } } diff --git a/OmniKitUI/Views/ReadPodStatusView.swift b/OmniKitUI/Views/ReadPodStatusView.swift index f79f78f..97594f1 100644 --- a/OmniKitUI/Views/ReadPodStatusView.swift +++ b/OmniKitUI/Views/ReadPodStatusView.swift @@ -15,7 +15,7 @@ struct ReadPodStatusView: View { @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.presentationMode) var presentationMode: Binding - var toRun: ((_ completion: @escaping (_ result: PumpManagerResult) -> Void) -> Void)? + var getDetailedStatus: () async throws -> DetailedStatus 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") @@ -23,7 +23,7 @@ struct ReadPodStatusView: View { @State private var alertIsPresented: Bool = false @State private var displayString: String = "" - @State private var error: LocalizedError? = nil + @State private var error: Error? = nil @State private var executing: Bool = false @State private var showActivityView: Bool = false @@ -47,7 +47,9 @@ struct ReadPodStatusView: View { } VStack { Button(action: { - asyncAction() + Task { @MainActor in + await fetchDetailedStatus() + } }) { Text(buttonText) .actionButtonStyle(.primary) @@ -62,26 +64,22 @@ struct ReadPodStatusView: View { .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .alert(isPresented: $alertIsPresented, content: { alert(error: error) }) - .onFirstAppear { - asyncAction() + .task { + await fetchDetailedStatus() } } - private func asyncAction () { - DispatchQueue.global(qos: .utility).async { + private func fetchDetailedStatus() async { + do { executing = true self.displayString = "" - toRun?() { (result) in - executing = false - switch result { - case .success(let detailedStatus): - self.displayString = podStatusString(status: detailedStatus) - case .failure(let error): - self.error = error - self.alertIsPresented = true - } - } + let detailedStatus = try await getDetailedStatus() + self.displayString = podStatusString(status: detailedStatus) + } catch { + self.error = error + self.alertIsPresented = true } + executing = false } private var buttonText: String { @@ -161,9 +159,7 @@ struct ReadPodStatusView_Previews: PreviewProvider { static var previews: some View { NavigationView { let detailedStatus = try! DetailedStatus(encodedData: Data([0x02, 0x0d, 0x00, 0x00, 0x00, 0x0e, 0x00, 0xc3, 0x6a, 0x02, 0x07, 0x03, 0xff, 0x02, 0x09, 0x20, 0x00, 0x28, 0x99, 0x08, 0x00, 0x82])) - ReadPodStatusView() { completion in - completion(.success(detailedStatus)) - } + ReadPodStatusView() { detailedStatus } } } } From 4dd86e71a22a27bec1eb7af4ce726138b4e00aa1 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 10 Apr 2024 07:00:30 -0700 Subject: [PATCH 14/21] modify order of OmnipodPumpManagerState.swift: debugDescription to match OmniBLE --- .../PumpManager/OmnipodPumpManagerState.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManagerState.swift b/OmniKit/PumpManager/OmnipodPumpManagerState.swift index 3b4913d..4d45a7b 100644 --- a/OmniKit/PumpManager/OmnipodPumpManagerState.swift +++ b/OmniKit/PumpManager/OmnipodPumpManagerState.swift @@ -309,14 +309,6 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible { "* timeZone: \(timeZone)", "* basalSchedule: \(String(describing: basalSchedule))", "* maximumTempBasalRate: \(maximumTempBasalRate)", - "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))", - "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)", - "* lowReservoirReminderValue: \(String(describing: lowReservoirReminderValue))", - "* podAttachmentConfirmed: \(podAttachmentConfirmed)", - "* activeAlerts: \(activeAlerts)", - "* alertsWithPendingAcknowledgment: \(alertsWithPendingAcknowledgment)", - "* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)", - "* initialConfigurationCompleted: \(initialConfigurationCompleted)", "* unstoredDoses: \(String(describing: unstoredDoses))", "* suspendEngageState: \(String(describing: suspendEngageState))", "* bolusEngageState: \(String(describing: bolusEngageState))", @@ -325,10 +317,16 @@ extension OmnipodPumpManagerState: CustomDebugStringConvertible { "* isPumpDataStale: \(String(describing: isPumpDataStale))", "* silencePod: \(String(describing: silencePod))", "* confirmationBeeps: \(String(describing: confirmationBeeps))", - "* pairingAttemptAddress: \(String(describing: pairingAttemptAddress))", "* insulinType: \(String(describing: insulinType))", "* scheduledExpirationReminderOffset: \(String(describing: scheduledExpirationReminderOffset?.timeIntervalStr))", "* defaultExpirationReminderOffset: \(defaultExpirationReminderOffset.timeIntervalStr)", + "* lowReservoirReminderValue: \(String(describing: lowReservoirReminderValue))", + "* podAttachmentConfirmed: \(podAttachmentConfirmed)", + "* activeAlerts: \(activeAlerts)", + "* alertsWithPendingAcknowledgment: \(alertsWithPendingAcknowledgment)", + "* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)", + "* initialConfigurationCompleted: \(initialConfigurationCompleted)", + "* pairingAttemptAddress: \(String(describing: pairingAttemptAddress))", "* rileyLinkBatteryAlertLevel: \(String(describing: rileyLinkBatteryAlertLevel))", "* lastRileyLinkBatteryAlertDate \(String(describing: lastRileyLinkBatteryAlertDate))", "", From 9dd4ee979848972314aa7bc1f0fa9e15bc79bfef Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 10 Apr 2024 07:01:07 -0700 Subject: [PATCH 15/21] add missing localization --- OmniKitUI/Views/UncertaintyRecoveredView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OmniKitUI/Views/UncertaintyRecoveredView.swift b/OmniKitUI/Views/UncertaintyRecoveredView.swift index 7a9e984..129018f 100644 --- a/OmniKitUI/Views/UncertaintyRecoveredView.swift +++ b/OmniKitUI/Views/UncertaintyRecoveredView.swift @@ -29,7 +29,7 @@ struct UncertaintyRecoveredView: View { } } } - .navigationBarTitle(Text("Comms Recovered"), displayMode: .large) + .navigationBarTitle(LocalizedString("Comms Recovered", comment: "navigation bar title for comms recovered"), displayMode: .large) .navigationBarBackButtonHidden(true) } } From 4c02d1dc0d42b00c537c80e071ea420a3dd74f40 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Wed, 10 Apr 2024 21:25:41 -0700 Subject: [PATCH 16/21] restore code inadvertently removed --- OmniKit/PumpManager/OmnipodPumpManager.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 7a0fcd0..c1aee52 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -171,6 +171,14 @@ public class OmnipodPumpManager: RileyLinkPumpManager { }) } } + + if oldValue.podState?.setupProgress != newValue.podState?.setupProgress, newValue.podState?.setupProgress == .completed { + self.pumpDelegate.notify() { (delegate) in + let date = Date() + let event = NewPumpEvent(date: date, dose: nil, raw: "Pod Change \(date)".data(using: .utf8)!, title: "Pod Change", type: .replaceComponent(componentType: .pump)) + delegate?.pumpManager(self, hasNewPumpEvents: [event], lastReconciliation: self.lastSync, replacePendingEvents: false) { _ in } + } + } } From 460e057706d5c02e4bb73e2d2f0e379cd16cf66c Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 11 Apr 2024 15:44:24 -0700 Subject: [PATCH 17/21] Skip alert acknowledgements with faulted pod or no active pod --- OmniKit/PumpManager/OmnipodPumpManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index c1aee52..43f20eb 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -2503,7 +2503,8 @@ extension OmnipodPumpManager: PodCommsDelegate { extension OmnipodPumpManager { public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) { guard self.hasActivePod else { - completion(OmnipodPumpManagerError.noPodPaired) + log.default("Skipping alert acknowledgements with no active pod") + completion(nil) return } From ad0f2d903c4c67ea0f66d75ccab96c1eac7d86f8 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 11 Apr 2024 20:44:01 -0700 Subject: [PATCH 18/21] Revert "enable user to set another expiration reminder for current pod" This reverts commit 5178b006717849f4dc37857b2d008ab0f84eed6d. --- .../Views/NotificationSettingsView.swift | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/OmniKitUI/Views/NotificationSettingsView.swift b/OmniKitUI/Views/NotificationSettingsView.swift index 86fa2bd..d451d6c 100644 --- a/OmniKitUI/Views/NotificationSettingsView.swift +++ b/OmniKitUI/Views/NotificationSettingsView.swift @@ -66,16 +66,20 @@ struct NotificationSettingsView: View { private func scheduledReminderRow(scheduledDate: Date?, allowedDates: [Date]) -> some View { Group { - NavigationLink( - destination: ScheduledExpirationReminderEditView( - scheduledExpirationReminderDate: scheduledDate, - allowedDates: allowedDates, - dateFormatter: dateFormatter, - onSave: onSaveScheduledExpirationReminder, - onFinish: { scheduleReminderDateEditViewIsShown = false }), - isActive: $scheduleReminderDateEditViewIsShown) - { - scheduledReminderRowContents(disclosure: true) + if let scheduledDate = scheduledDate, scheduledDate <= Date() { + scheduledReminderRowContents(disclosure: false) + } else { + NavigationLink( + destination: ScheduledExpirationReminderEditView( + scheduledExpirationReminderDate: scheduledDate, + allowedDates: allowedDates, + dateFormatter: dateFormatter, + onSave: onSaveScheduledExpirationReminder, + onFinish: { scheduleReminderDateEditViewIsShown = false }), + isActive: $scheduleReminderDateEditViewIsShown) + { + scheduledReminderRowContents(disclosure: true) + } } } } From b69f6359345093892b72315a2b01c4bcae0a6af0 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 11 Apr 2024 20:49:35 -0700 Subject: [PATCH 19/21] enable user to set another expiration reminder of current pod unless allowDates.isEmpty --- OmniKitUI/Views/NotificationSettingsView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OmniKitUI/Views/NotificationSettingsView.swift b/OmniKitUI/Views/NotificationSettingsView.swift index d451d6c..d0ab82f 100644 --- a/OmniKitUI/Views/NotificationSettingsView.swift +++ b/OmniKitUI/Views/NotificationSettingsView.swift @@ -66,7 +66,8 @@ struct NotificationSettingsView: View { private func scheduledReminderRow(scheduledDate: Date?, allowedDates: [Date]) -> some View { Group { - if let scheduledDate = scheduledDate, scheduledDate <= Date() { + // Make the expiration reminder time read-only if there aren't any more available times. + if allowedDates.isEmpty { scheduledReminderRowContents(disclosure: false) } else { NavigationLink( From 5e885c5174c50986aa0dd18180f9885f97934b89 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 11 Apr 2024 21:06:09 -0700 Subject: [PATCH 20/21] Update language in messages, add localizations, match OmniBLE --- OmniKitUI/Views/AttachPodView.swift | 4 ++-- OmniKitUI/Views/CheckInsertedCannulaView.swift | 2 +- OmniKitUI/Views/DeactivatePodView.swift | 2 +- OmniKitUI/Views/ExpirationReminderSetupView.swift | 2 +- OmniKitUI/Views/LowReservoirReminderSetupView.swift | 2 +- OmniKitUI/Views/ManualTempBasalEntryView.swift | 6 ++---- OmniKitUI/Views/NotificationSettingsView.swift | 8 ++++---- OmniKitUI/Views/OmnipodSettingsView.swift | 2 +- OmniKitUI/Views/PairPodView.swift | 2 +- OmniKitUI/Views/SetupCompleteView.swift | 4 ++-- 10 files changed, 16 insertions(+), 18 deletions(-) diff --git a/OmniKitUI/Views/AttachPodView.swift b/OmniKitUI/Views/AttachPodView.swift index 1ae9a7a..555f165 100644 --- a/OmniKitUI/Views/AttachPodView.swift +++ b/OmniKitUI/Views/AttachPodView.swift @@ -33,7 +33,7 @@ struct AttachPodView: View { HStack { InstructionList(instructions: [ LocalizedString("Prepare site.", comment: "Label text for step one of attach pod instructions"), - LocalizedString("Remove the pod's needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"), + LocalizedString("Remove the Pod's clear needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"), LocalizedString("Check Pod, apply to site, then confirm pod attachment.", comment: "Label text for step three of attach pod instructions") ]) } @@ -53,7 +53,7 @@ struct AttachPodView: View { .zIndex(1) } .alert(item: $activeModal, content: self.alert(for:)) - .navigationBarTitle("Attach Pod", displayMode: .automatic) + .navigationBarTitle(LocalizedString("Attach Pod", comment: "navigation bar title attach pod"), displayMode: .automatic) .navigationBarItems(trailing: cancelButton) .navigationBarBackButtonHidden(true) } diff --git a/OmniKitUI/Views/CheckInsertedCannulaView.swift b/OmniKitUI/Views/CheckInsertedCannulaView.swift index 64ffce9..9ce401b 100644 --- a/OmniKitUI/Views/CheckInsertedCannulaView.swift +++ b/OmniKitUI/Views/CheckInsertedCannulaView.swift @@ -54,7 +54,7 @@ struct CheckInsertedCannulaView: View { }.padding() } .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal } - .navigationBarTitle("Check Cannula", displayMode: .automatic) + .navigationBarTitle(LocalizedString("Check Cannula", comment: "navigation bar title for check cannula"), displayMode: .automatic) .navigationBarItems(trailing: cancelButton) .navigationBarBackButtonHidden(true) } diff --git a/OmniKitUI/Views/DeactivatePodView.swift b/OmniKitUI/Views/DeactivatePodView.swift index 25ae874..5947e77 100644 --- a/OmniKitUI/Views/DeactivatePodView.swift +++ b/OmniKitUI/Views/DeactivatePodView.swift @@ -72,7 +72,7 @@ struct DeactivatePodView: View { .padding() } .alert(isPresented: $removePodModalIsPresented) { removePodModal } - .navigationBarTitle("Deactivate Pod", displayMode: .automatic) + .navigationBarTitle(LocalizedString("Deactivate Pod", comment: "navigation bar title for deactivate pod"), displayMode: .automatic) .navigationBarItems(trailing: Button("Cancel") { viewModel.didCancel?() diff --git a/OmniKitUI/Views/ExpirationReminderSetupView.swift b/OmniKitUI/Views/ExpirationReminderSetupView.swift index a764e14..9e5fa58 100644 --- a/OmniKitUI/Views/ExpirationReminderSetupView.swift +++ b/OmniKitUI/Views/ExpirationReminderSetupView.swift @@ -19,7 +19,7 @@ struct ExpirationReminderSetupView: View { var body: some View { GuidePage(content: { VStack(alignment: .leading, spacing: 15) { - Text(LocalizedString("The App notifies you in advance of Pod expiration.\n\nScroll to set the number of hours advance notice you would like to have.", comment: "Description text on ExpirationReminderSetupView")).fixedSize(horizontal: false, vertical: true) + Text(LocalizedString("You will be notified in advance of Pod expiration.\n\nScroll to set the number of hours advance notice you would like to have.", comment: "Description text on ExpirationReminderSetupView")).fixedSize(horizontal: false, vertical: true) Divider() ExpirationReminderPickerView(expirationReminderDefault: $expirationReminderDefault, collapsible: false, showingHourPicker: true) .onChange(of: expirationReminderDefault) { value in diff --git a/OmniKitUI/Views/LowReservoirReminderSetupView.swift b/OmniKitUI/Views/LowReservoirReminderSetupView.swift index 4a64990..d133ff7 100644 --- a/OmniKitUI/Views/LowReservoirReminderSetupView.swift +++ b/OmniKitUI/Views/LowReservoirReminderSetupView.swift @@ -29,7 +29,7 @@ struct LowReservoirReminderSetupView: View { var body: some View { GuidePage(content: { VStack(alignment: .leading, spacing: 15) { - Text(LocalizedString("The App notifies you when the amount of insulin in the Pod reaches this level (50-10 U).\n\nScroll to set the number of units at which you would like to be reminded.", comment: "Description text on LowReservoirReminderSetupView")) + Text(LocalizedString("You will be notified when the amount of insulin in the Pod reaches a selected level.\n\nScroll to set the number of units (1 to 50) at which you would like to be reminded.", comment: "Description text on LowReservoirReminderSetupView")) Divider() HStack { Text(LocalizedString("Low Reservoir", comment: "Label text for low reservoir value row")) diff --git a/OmniKitUI/Views/ManualTempBasalEntryView.swift b/OmniKitUI/Views/ManualTempBasalEntryView.swift index a676aaf..0a77234 100644 --- a/OmniKitUI/Views/ManualTempBasalEntryView.swift +++ b/OmniKitUI/Views/ManualTempBasalEntryView.swift @@ -91,7 +91,7 @@ struct ManualTempBasalEntryView: View { .frame(maxHeight: 162.0) .alert(isPresented: $showingMissingConfigAlert, content: { missingConfigAlert }) Section { - Text(LocalizedString("Loop will not automatically adjust your insulin delivery until the temporary basal rate finishes or is canceled.", comment: "Description text on manual temp basal action sheet")) + Text(LocalizedString("Your insulin delivery will not be automatically adjusted until the temporary basal rate finishes or is canceled.", comment: "Description text on manual temp basal action sheet")) .font(.footnote) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -143,18 +143,16 @@ struct ManualTempBasalEntryView: View { var missingConfigAlert: SwiftUI.Alert { return SwiftUI.Alert( title: Text(LocalizedString("Missing Config", comment: "Alert title for missing temp basal configuration")), - message: Text(LocalizedString("This Pump has not been configured with a maximum basal rate because it was added before manual temp basal was a feature. Please go to Therapy Settings -> Delivery Limits and set a new Maximum Basal Rate.", comment: "Alert format string for missing temp basal configuration.")) + message: Text(LocalizedString("This Pump has not been configured with a maximum basal rate because it was added before manual temp basal was a feature. Please set a new Maximum Basal Rate.", comment: "Alert format string for missing temp basal configuration.")) ) } - var cancelButton: some View { Button(LocalizedString("Cancel", comment: "Cancel button text in navigation bar on insert cannula screen")) { didCancel?() } .accessibility(identifier: "button_cancel") } - } diff --git a/OmniKitUI/Views/NotificationSettingsView.swift b/OmniKitUI/Views/NotificationSettingsView.swift index d0ab82f..6a20b73 100644 --- a/OmniKitUI/Views/NotificationSettingsView.swift +++ b/OmniKitUI/Views/NotificationSettingsView.swift @@ -35,14 +35,14 @@ struct NotificationSettingsView: View { RoundedCardScrollView { RoundedCard( title: LocalizedString("Omnipod Reminders", comment: "Title for omnipod reminders section"), - footer: LocalizedString("The app configures a reminder on the pod to notify you in advance of Pod expiration. Set the number of hours advance notice you would like to configure when pairing a new Pod.", comment: "Footer text for omnipod reminders section") + footer: LocalizedString("A reminder is configured during Pod setup to notify you in advance of its expiration. Set the number of hours advance notice you would like to configure by default when pairing a new Pod.", comment: "Footer text for pod reminders section") ) { ExpirationReminderPickerView(expirationReminderDefault: $expirationReminderDefault) } if let allowedDates = allowedScheduledReminderDates { RoundedCard( - footer: LocalizedString("This is a reminder that you scheduled for your current Pod.", comment: "Footer text for scheduled reminder area")) + footer: LocalizedString("Expiration reminder time for the current Pod.", comment: "Footer text for scheduled reminder area")) { Text(LocalizedString("Scheduled Reminder", comment: "Scheduled reminder card title on NotificationSettingsView")) Divider() @@ -50,13 +50,13 @@ struct NotificationSettingsView: View { } } - RoundedCard(footer: LocalizedString("The App notifies you when the amount of insulin in the Pod reaches this level.", comment: "Footer text for low reservoir value row")) { + RoundedCard(footer: LocalizedString("You will be notified when the amount of insulin in the Pod reaches this level.", comment: "Footer text for low reservoir value row")) { lowReservoirValueRow } RoundedCard( title: LocalizedString("Critical Alerts", comment: "Title for critical alerts description"), - footer: LocalizedString("The reminders above will not sound if your device is in Silent or Do Not Disturb mode.\n\nThere are other critical Pod alerts and alarms that will sound even if your device is set to Silent or Do Not Disturb mode.", comment: "Description text for critical alerts") + footer: LocalizedString("The reminders above will not sound on your device when it is in Silent or Do Not Disturb mode. There are other critical Pod alerts that will sound on your device even when set to Silent or Do Not Disturb mode.\n\nThe Pod will also use audible beeps for all Pod reminders and alerts except when the Pod is Silenced.", comment: "Description text for critical alerts") ) } .navigationBarTitle(LocalizedString("Notification Settings", comment: "navigation title for notification settings")) diff --git a/OmniKitUI/Views/OmnipodSettingsView.swift b/OmniKitUI/Views/OmnipodSettingsView.swift index ba4bddb..e629f5a 100644 --- a/OmniKitUI/Views/OmnipodSettingsView.swift +++ b/OmniKitUI/Views/OmnipodSettingsView.swift @@ -569,7 +569,7 @@ struct OmnipodSettingsView: View { var suspendOptionsActionSheet: ActionSheet { ActionSheet( title: FrameworkLocalText("Suspend Delivery", comment: "Title for suspend duration selection action sheet"), - message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. When would you like Loop to remind you to resume delivery?", comment: "Message for suspend duration selection action sheet"), + message: FrameworkLocalText("Insulin delivery will be stopped until you resume manually. Select when you want to be reminded to resume delivery?", comment: "Message for suspend duration selection action sheet"), buttons: [ .default(FrameworkLocalText("30 minutes", comment: "Button text for 30 minute suspend duration"), action: { self.viewModel.suspendDelivery(duration: .minutes(30)) }), .default(FrameworkLocalText("1 hour", comment: "Button text for 1 hour suspend duration"), action: { self.viewModel.suspendDelivery(duration: .hours(1)) }), diff --git a/OmniKitUI/Views/PairPodView.swift b/OmniKitUI/Views/PairPodView.swift index ec88f68..5e97d71 100644 --- a/OmniKitUI/Views/PairPodView.swift +++ b/OmniKitUI/Views/PairPodView.swift @@ -22,7 +22,7 @@ struct PairPodView: View { HStack { InstructionList(instructions: [ - LocalizedString("Fill a new pod with U-100 Insulin (leave Pod needle cap on). Listen for 2 beeps.", comment: "Label text for step 1 of pair pod instructions"), + LocalizedString("Fill a new pod with U-100 Insulin (leave clear Pod needle cap on). Listen for 2 beeps.", comment: "Label text for step 1 of pair pod instructions"), LocalizedString("Keep the RileyLink about 6 inches from the pod during pairing.", comment: "Label text for step 2 of pair pod instructions") ]) .disabled(viewModel.state.instructionsDisabled) diff --git a/OmniKitUI/Views/SetupCompleteView.swift b/OmniKitUI/Views/SetupCompleteView.swift index a372fab..a22badf 100644 --- a/OmniKitUI/Views/SetupCompleteView.swift +++ b/OmniKitUI/Views/SetupCompleteView.swift @@ -40,7 +40,7 @@ struct SetupCompleteView: View { GuidePage(content: { VStack { LeadingImage("Pod") - Text(String(format: LocalizedString("Your Pod is ready for use.\n\n%1$@ will remind you to change your pod before it expires. You can change this to a time convenient for you.", comment: "Format string for instructions for setup complete view. (1: app name)"), appName)) + Text(LocalizedString("Your Pod is ready for use.\n\nThe default reminder was scheduled for this Pod at setup. You can change the reminder to a more convenient time in Pod Notification Settings.", comment: "Format string for instructions for setup complete view")) .fixedSize(horizontal: false, vertical: true) Divider() VStack(alignment: .leading) { @@ -83,7 +83,7 @@ struct SetupCompleteView: View { .background(Color(UIColor.systemBackground)) .zIndex(1) } - .navigationBarTitle("Setup Complete", displayMode: .automatic) + .navigationBarTitle(LocalizedString("Setup Complete", comment: "Title of SetupCompleteView"), displayMode: .automatic) } private func scheduledReminderDateString(_ scheduledDate: Date?) -> String { From 8fecaae20f0ce0131d84e47f1130384c85935db5 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 12 Apr 2024 21:26:30 -0700 Subject: [PATCH 21/21] modify preview provider --- OmniKitUI/Views/NotificationSettingsView.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OmniKitUI/Views/NotificationSettingsView.swift b/OmniKitUI/Views/NotificationSettingsView.swift index 6a20b73..3f3af8e 100644 --- a/OmniKitUI/Views/NotificationSettingsView.swift +++ b/OmniKitUI/Views/NotificationSettingsView.swift @@ -35,7 +35,7 @@ struct NotificationSettingsView: View { RoundedCardScrollView { RoundedCard( title: LocalizedString("Omnipod Reminders", comment: "Title for omnipod reminders section"), - footer: LocalizedString("A reminder is configured during Pod setup to notify you in advance of its expiration. Set the number of hours advance notice you would like to configure by default when pairing a new Pod.", comment: "Footer text for pod reminders section") + footer: LocalizedString("A reminder is configured during Pod setup to notify you in advance of its expiration. Set the number of hours advance notice you would like to configure by default when pairing a new Pod.", comment: "Footer text for omnipod reminders section") ) { ExpirationReminderPickerView(expirationReminderDefault: $expirationReminderDefault) } @@ -125,14 +125,15 @@ struct NotificationSettingsView: View { struct NotificationSettingsView_Previews: PreviewProvider { static var previews: some View { return Group { + let now = Date() NavigationView { - NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: Date(), allowedScheduledReminderDates: [Date()], lowReservoirReminderValue: 20) + NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: now + TimeInterval(hours: 1), allowedScheduledReminderDates: [now, now - TimeInterval(hours: 2), now - TimeInterval(hours: 3)], lowReservoirReminderValue: 20) .previewDevice(PreviewDevice(rawValue:"iPod touch (7th generation)")) .previewDisplayName("iPod touch (7th generation)") } NavigationView { - NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: Date(), allowedScheduledReminderDates: [Date()], lowReservoirReminderValue: 20) + NotificationSettingsView(dateFormatter: DateFormatter(), expirationReminderDefault: .constant(2), scheduledReminderDate: now + TimeInterval(hours: 1), allowedScheduledReminderDates: [now, now - TimeInterval(hours: 2), now - TimeInterval(hours: 3)], lowReservoirReminderValue: 20) .colorScheme(.dark) .previewDevice(PreviewDevice(rawValue: "iPhone XS Max")) .previewDisplayName("iPhone XS Max - Dark")