From 976d477da227c64d7bcb76bcac113636822c0ff7 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Sun, 17 Nov 2024 15:42:10 -0800 Subject: [PATCH 01/14] Miscellaneous Omnipod code improvements & cleanup + Remove unneeded & redundant didSend() & didReceive() message logging + Improved and additional PodState debugDescription display for insulin values + Improved pod suspend testing when updating delivery status + Update suspended, bolusing, tempBasalRunning, extendedBolusRunning definitions for better efficency and clarity using @dnzxy suggestion on OmniBLE PR #123 --- OmniKit/OmnipodCommon/Pod.swift | 37 +++++++++++++++++--- OmniKit/PumpManager/OmnipodPumpManager.swift | 2 -- OmniKit/PumpManager/PodState.swift | 6 ++-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/OmniKit/OmnipodCommon/Pod.swift b/OmniKit/OmnipodCommon/Pod.swift index 2acaf2c..dc19a5f 100644 --- a/OmniKit/OmnipodCommon/Pod.swift +++ b/OmniKit/OmnipodCommon/Pod.swift @@ -54,7 +54,7 @@ public struct Pod { // Supported basal rates // Eros minimum scheduled basal rate is 0.05 U/H while for Dash supports 0 U/H. - // Would need to have this value based on productID to be able to share this with Eros. + // Would need to have this value based on productID to be able to share this file with DASH. public static let supportedBasalRates: [Double] = (1...600).map { Double($0) / Double(pulsesPerUnit) } // Supported temp basal rates @@ -113,19 +113,46 @@ public enum DeliveryStatus: UInt8, CustomStringConvertible { case extendedBolusAndTempBasal = 10 public var suspended: Bool { - return self == .suspended || self == .priming || self == .extendedBolusWhileSuspended + // returns true if both the tempBasal and basal bits are clear + let suspendedStates: Set = [ + .suspended, + .priming, + .extendedBolusWhileSuspended, + ] + return suspendedStates.contains(self) } public var bolusing: Bool { - return self == .bolusInProgress || self == .bolusAndTempBasal || self == .extendedBolusRunning || self == .extendedBolusAndTempBasal || self == .priming || self == .extendedBolusWhileSuspended + // returns true if either the immediateBolus or extendedBolus bits are set + let bolusingStates: Set = [ + .priming, + .bolusInProgress, + .bolusAndTempBasal, + .extendedBolusWhileSuspended, + .extendedBolusRunning, + .extendedBolusAndTempBasal, + ] + return bolusingStates.contains(self) } public var tempBasalRunning: Bool { - return self == .tempBasalRunning || self == .bolusAndTempBasal || self == .extendedBolusAndTempBasal + // returns true if the tempBasal bit is set + let tempBasalRunningStates: Set = [ + .tempBasalRunning, + .bolusAndTempBasal, + .extendedBolusAndTempBasal, + ] + return tempBasalRunningStates.contains(self) } public var extendedBolusRunning: Bool { - return self == .extendedBolusRunning || self == .extendedBolusAndTempBasal || self == .extendedBolusWhileSuspended + // returns true if the extendedBolus bit is set + let extendedBolusRunningStates: Set = [ + .extendedBolusWhileSuspended, + .extendedBolusRunning, + .extendedBolusAndTempBasal, + ] + return extendedBolusRunningStates.contains(self) } public var description: String { diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 630074c..a422d15 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -2448,12 +2448,10 @@ extension OmnipodPumpManager: PumpManager { extension OmnipodPumpManager: MessageLogger { func didSend(_ message: Data) { - log.default("didSend: %{public}@", message.hexadecimalString) self.logDeviceCommunication(message.hexadecimalString, type: .send) } func didReceive(_ message: Data) { - log.default("didReceive: %{public}@", message.hexadecimalString) self.logDeviceCommunication(message.hexadecimalString, type: .receive) } diff --git a/OmniKit/PumpManager/PodState.swift b/OmniKit/PumpManager/PodState.swift index b330fe1..ad396fe 100644 --- a/OmniKit/PumpManager/PodState.swift +++ b/OmniKit/PumpManager/PodState.swift @@ -300,7 +300,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl if deliveryStatus.tempBasalRunning && unfinalizedTempBasal == nil { // active temp basal that we aren't tracking // unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: 0, startTime: Date(), duration: .minutes(30), isHighTemp: false, scheduledCertainty: .certain, insulinType: insulinType) } - if deliveryStatus != .suspended && isSuspended { // active basal that we aren't tracking + if !deliveryStatus.suspended && isSuspended { // active basal that we aren't tracking let resumeStartTime = Date() suspendState = .resumed(resumeStartTime) unfinalizedResume = UnfinalizedDose(resumeStartTime: resumeStartTime, scheduledCertainty: .certain, insulinType: insulinType) @@ -555,7 +555,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl "* expiresAt: \(String(reflecting: expiresAt))", "* podTime: \(podTime.timeIntervalStr)", "* podTimeUpdated: \(String(reflecting: podTimeUpdated))", - "* setupUnitsDelivered: \(String(reflecting: setupUnitsDelivered))", + "* setupUnitsDelivered: \(setupUnitsDelivered == nil ? "?" : setupUnitsDelivered!.twoDecimals) U", "* piVersion: \(piVersion)", "* pmVersion: \(pmVersion)", "* lot: \(lot)", @@ -568,6 +568,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl "* unfinalizedResume: \(String(describing: unfinalizedResume))", "* finalizedDoses: \(String(describing: finalizedDoses))", "* activeAlertsSlots: \(alertSetString(alertSet: activeAlertSlots))", + "* delivered: \(lastInsulinMeasurements == nil ? "?" : lastInsulinMeasurements!.delivered.twoDecimals) U", + "* reservoirLevel: \(lastInsulinMeasurements == nil || lastInsulinMeasurements!.reservoirLevel == nil || lastInsulinMeasurements!.reservoirLevel == Pod.reservoirLevelAboveThresholdMagicNumber ? "50+" : lastInsulinMeasurements!.reservoirLevel!.twoDecimals) U", "* messageTransportState: \(String(describing: messageTransportState))", "* setupProgress: \(setupProgress)", "* primeFinishTime: \(String(describing: primeFinishTime))", From 5162a05cbbf1f3d2c21b9b7818376b639476755c Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Sun, 17 Nov 2024 22:40:24 -0800 Subject: [PATCH 02/14] Unacknowledged command handling fixes and PodCommsSession improvements + Don't append an optional beep block to a getStatus with a pending unacknowledged command + Handle unacknowledged commands cases in acknowledgeAlerts(), beepConfig() & configureAlert() + Use checkCommandAgainstStatus() for seq # mismatch to determine insulin delivery status + Remove unneeded @discardResult attribute on various PodCommsSessions functions + Comment improvements and better source code synchronization between OmniKit and OmniBLE --- OmniKit/PumpManager/PodCommsSession.swift | 92 +++++++++++++++++++++-- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index bbb74f7..cfe5b31 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -251,6 +251,9 @@ public class PodCommsSession { /// - PodCommsError.unexpectedResponse /// - PodCommsError.rejectedMessage /// - PodCommsError.nonceResyncFailed + /// - PodCommsError.unexpectedPacketType + /// - PodCommsError.emptyResponse + /// - PodCommsError.unacknowledgedMessage /// - MessageError /// - RileyLinkDeviceError func send(_ messageBlocks: [MessageBlock], beepBlock: MessageBlock? = nil, expectFollowOnMessage: Bool = false) throws -> T { @@ -258,8 +261,13 @@ public class PodCommsSession { var triesRemaining = 2 // Retries only happen for nonce resync var blocksToSend = messageBlocks - // If a beep block was specified & pod isn't faulted, append the beep block to emit the confirmation beep - if let beepBlock = beepBlock, podState.isFaulted == false { + // If a beep block was specified & the pod isn't faulted AND there isn't an unacknowledged + // command for a getStatus command, append the beep block to emit the confirmation beep. + // Since a beep command changes lastProgrammingMessageSeqNum, we need skip appending a beep + // block while still trying to resolve an unacknowldged delivery command with getStatus calls. + if let beepBlock = beepBlock, podState.isFaulted == false && + !(podState.unacknowledgedCommand != nil && blocksToSend[0].blockType == .getStatus) + { blocksToSend += [beepBlock] } @@ -396,6 +404,12 @@ public class PodCommsSession { // specified to first acknowledge and clear all possible pending pod alerts and pod alert configurations. @discardableResult func configureAlerts(_ alerts: [PodAlert], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse { + + guard podState.unacknowledgedCommand == nil || podState.setupProgress != .completed else { + log.info("Fail configure alerts with unacknowledged command and incomplete pod setup") + throw PodCommsError.unacknowledgedCommandPending + } + let configurations = alerts.map { $0.configuration } let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations) let blocksToSend: [MessageBlock] @@ -421,6 +435,11 @@ public class PodCommsSession { return .failure(PodCommsError.podFault(fault: fault)) } + guard podState.unacknowledgedCommand == nil || podState.setupProgress != .completed else { + log.info("Fail beep config with unacknowledged command and incomplete pod setup") + return .failure(PodCommsError.unacknowledgedCommandPending) + } + let beepConfigCommand = BeepConfigCommand(beepType: beepType, tempBasalCompletionBeep: tempBasalCompletionBeep, bolusCompletionBeep: bolusCompletionBeep) do { let statusResponse: StatusResponse = try send([beepConfigCommand]) @@ -699,7 +718,6 @@ public class PodCommsSession { } // Cancels any suspend related alerts, called when setting a basal schedule with active suspend alerts - @discardableResult private func cancelSuspendAlerts() throws -> StatusResponse { do { @@ -821,7 +839,6 @@ public class PodCommsSession { // use cancelDelivery with .none to get status as well as to validate & advance the nonce // Throws PodCommsError - @discardableResult public func cancelNone(beepBlock: MessageBlock? = nil) throws -> StatusResponse { var statusResponse: StatusResponse @@ -839,7 +856,6 @@ public class PodCommsSession { } // Throws PodCommsError - @discardableResult public func getStatus(beepBlock: MessageBlock? = nil) throws -> StatusResponse { let statusResponse: StatusResponse = try send([GetStatusCommand()], beepBlock: beepBlock) @@ -850,7 +866,6 @@ public class PodCommsSession { return statusResponse } - @discardableResult public func getDetailedStatus(beepBlock: MessageBlock? = nil) throws -> DetailedStatus { let infoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: .detailedStatus)], beepBlock: beepBlock) @@ -870,14 +885,13 @@ public class PodCommsSession { return detailedStatus } - @discardableResult public func readPodInfo(podInfoResponseSubType: PodInfoResponseSubType, beepBlock: MessageBlock? = nil) throws -> PodInfoResponse { let podInfoCommand = GetStatusCommand(podInfoType: podInfoResponseSubType) let podInfoResponse: PodInfoResponse = try send([podInfoCommand], beepBlock: beepBlock) return podInfoResponse } - // Reconnected to the pod, and we know program was successful + // Reconnected to the pod, and we know program was successful based on lastProgrammingMessageSeqNum private func unacknowledgedCommandWasReceived(pendingCommand: PendingCommand, podStatus: StatusResponse) { switch pendingCommand { case .program(let program, _, let commandDate, _): @@ -908,6 +922,60 @@ public class PodCommsSession { } } + // Reconnected to the pod and we didn't match lastProgrammingMessageSeqNum which indicates + // that the command was not received. Now verify the pendingCommand against the current pod + // delivery status to decide whether the delivery related command might have been received or not. + // Returns true if the command was received based on the pod delivery status and podState was updated. + private func checkCommandAgainstStatus(pendingCommand: PendingCommand, podStatus: StatusResponse) -> Bool { + let deliveryStatus = podStatus.deliveryStatus + var podStatusMatched = false + switch pendingCommand { + case .program(let program, _, let commandDate, _): + if let dose = program.unfinalizedDose(at: commandDate, withCertainty: .certain, insulinType: podState.insulinType) { + switch dose.doseType { + case .bolus: + if deliveryStatus.bolusing { + podState.unfinalizedBolus = dose + podStatusMatched = true + } + case .tempBasal: + if deliveryStatus.tempBasalRunning { + podState.unfinalizedTempBasal = dose + podStatusMatched = true + } + case .resume: + if !deliveryStatus.suspended { + podState.suspendState = .resumed(commandDate) + podStatusMatched = true + } + default: + break + } + } + case .stopProgram(let stopProgram, _, let commandDate, _): + if stopProgram.contains(.bolus), let bolus = podState.unfinalizedBolus, !bolus.isFinished(at: commandDate) { + if !deliveryStatus.bolusing { + podState.unfinalizedBolus?.cancel(at: commandDate, withRemaining: podStatus.bolusNotDelivered) + podStatusMatched = true + } + } + if stopProgram.contains(.tempBasal), let tempBasal = podState.unfinalizedTempBasal, !tempBasal.isFinished(at: commandDate) { + if !deliveryStatus.tempBasalRunning { + podState.unfinalizedTempBasal?.cancel(at: commandDate) + podStatusMatched = true + } + } + if stopProgram.contains(.basal) { + if !deliveryStatus.suspended { + podState.finalizedDoses.append(UnfinalizedDose(suspendStartTime: commandDate, scheduledCertainty: .certain)) + podState.suspendState = .suspended(commandDate) + podStatusMatched = true + } + } + } + return podStatusMatched + } + public func recoverUnacknowledgedCommand(using status: StatusResponse) { if let pendingCommand = podState.unacknowledgedCommand { self.log.default("Recovering from unacknowledged command %{public}@, status = %{public}@", String(describing: pendingCommand), String(describing: status)) @@ -915,6 +983,8 @@ public class PodCommsSession { if status.lastProgrammingMessageSeqNum == pendingCommand.sequence { self.log.default("Unacknowledged command was received by pump") unacknowledgedCommandWasReceived(pendingCommand: pendingCommand, podStatus: status) + } else if checkCommandAgainstStatus(pendingCommand: pendingCommand, podStatus: status) { + self.log.default("Accepted unacknowledged command was received based on pod delivery status of ${public}@", String(describing: status.deliveryStatus)) } else { self.log.default("Unacknowledged command was not received by pump") } @@ -970,6 +1040,12 @@ public class PodCommsSession { } public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet { + + guard podState.unacknowledgedCommand == nil || podState.setupProgress != .completed else { + log.info("Fail acknowledge alerts with unacknowledged command and pod setup complete") + throw PodCommsError.unacknowledgedCommandPending + } + let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts) let status: StatusResponse = try send([cmd], beepBlock: beepBlock) podState.updateFromStatusResponse(status, at: currentDate) From ef98bb10fce8602ea9526a88fd8d26bf28a4f176 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Tue, 19 Nov 2024 23:53:47 -0800 Subject: [PATCH 03/14] Add missing pod suspend test improvement and log type change to PumpManager --- OmniKit/PumpManager/OmnipodPumpManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index a422d15..734bc52 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -1871,7 +1871,7 @@ extension OmnipodPumpManager: PumpManager { }) if let podState = self.state.podState, podState.isSuspended || podState.lastDeliveryStatusReceived?.suspended == true { - self.log.error("Not enacting bolus because podState or last status received indicates pod is suspended") + self.log.info("Not enacting bolus because podState or last status received indicates pod is suspended") completion(.deviceState(PodCommsError.podSuspended)) return } @@ -2053,7 +2053,7 @@ extension OmnipodPumpManager: PumpManager { return } - guard status.deliveryStatus != .suspended else { + guard !status.deliveryStatus.suspended else { self.log.info("Canceling temp basal because status return indicates pod is suspended!") completion(.communication(PodCommsError.podSuspended)) return From 0d131a3abcc0988e3e1521667a01c8cd000ad09c Mon Sep 17 00:00:00 2001 From: marionbarker Date: Thu, 28 Nov 2024 10:20:32 -0500 Subject: [PATCH 04/14] return clock icon when isClockOffset is true --- .../PumpManager/OmniPodPumpManager+UI.swift | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/OmniKitUI/PumpManager/OmniPodPumpManager+UI.swift b/OmniKitUI/PumpManager/OmniPodPumpManager+UI.swift index 69fdebe..681e5a0 100644 --- a/OmniKitUI/PumpManager/OmniPodPumpManager+UI.swift +++ b/OmniKitUI/PumpManager/OmniPodPumpManager+UI.swift @@ -47,6 +47,24 @@ extension OmnipodPumpManager: PumpManagerUI { } +public enum OmniKitStatusBadge: DeviceStatusBadge { + case timeSyncNeeded + + public var image: UIImage? { + switch self { + case .timeSyncNeeded: + return UIImage(systemName: "clock.fill") + } + } + + public var state: DeviceStatusBadgeState { + switch self { + case .timeSyncNeeded: + return .warning + } + } +} + // MARK: - PumpStatusIndicator extension OmnipodPumpManager { public var pumpStatusHighlight: DeviceStatusHighlight? { @@ -58,7 +76,10 @@ extension OmnipodPumpManager { } public var pumpStatusBadge: DeviceStatusBadge? { - return nil + if isClockOffset { + return OmniKitStatusBadge.timeSyncNeeded + } else { + return nil + } } - } From c0bdf81e62be1613ed756d0c131c86f3cd2d5df0 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Tue, 3 Dec 2024 11:03:19 -0800 Subject: [PATCH 05/14] Improved commenting on constants that vary from Eros and Dash --- OmniKit/OmnipodCommon/Pod.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OmniKit/OmnipodCommon/Pod.swift b/OmniKit/OmnipodCommon/Pod.swift index dc19a5f..286303e 100644 --- a/OmniKit/OmnipodCommon/Pod.swift +++ b/OmniKit/OmnipodCommon/Pod.swift @@ -53,15 +53,15 @@ public struct Pod { public static let reservoirCapacity: Double = 200 // Supported basal rates - // Eros minimum scheduled basal rate is 0.05 U/H while for Dash supports 0 U/H. - // Would need to have this value based on productID to be able to share this file with DASH. + // Eros minimum scheduled basal rate is 0.05 U/H while Dash supports 0 U/H. public static let supportedBasalRates: [Double] = (1...600).map { Double($0) / Double(pulsesPerUnit) } // Supported temp basal rates + // Both Eros and Dash support a minimum temp basal rate of 0 U/H. public static let supportedTempBasalRates: [Double] = (0...600).map { Double($0) / Double(pulsesPerUnit) } - // The internal basal rate used for non-Eros pods - // Would need to have this value based on productID to be able to share this file with Eros. + // The internal basal rate used for zero basal rates + // Eros uses 0.0 while Dash uses a near zero rate public static let zeroBasalRate: Double = 0.0 // Maximum number of basal schedule entries supported From 22dd0e970e09bf7d34454e63d0ca9d58fb6362d0 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Tue, 3 Dec 2024 16:31:13 -0800 Subject: [PATCH 06/14] Update comments to use U/hr instead of U/H for better consistency --- OmniKit/OmnipodCommon/Pod.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OmniKit/OmnipodCommon/Pod.swift b/OmniKit/OmnipodCommon/Pod.swift index 286303e..c5b32e7 100644 --- a/OmniKit/OmnipodCommon/Pod.swift +++ b/OmniKit/OmnipodCommon/Pod.swift @@ -53,11 +53,11 @@ public struct Pod { public static let reservoirCapacity: Double = 200 // Supported basal rates - // Eros minimum scheduled basal rate is 0.05 U/H while Dash supports 0 U/H. + // Eros minimum scheduled basal rate is 0.05 U/hr while Dash supports 0 U/hr. public static let supportedBasalRates: [Double] = (1...600).map { Double($0) / Double(pulsesPerUnit) } // Supported temp basal rates - // Both Eros and Dash support a minimum temp basal rate of 0 U/H. + // Both Eros and Dash support a minimum temp basal rate of 0 U/hr. public static let supportedTempBasalRates: [Double] = (0...600).map { Double($0) / Double(pulsesPerUnit) } // The internal basal rate used for zero basal rates From b79d85b1e9009eb4516a0cb28779de7b97b84ae4 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Sun, 22 Dec 2024 21:44:35 -0800 Subject: [PATCH 07/14] Improved & automatic unacknowledged command recovery New resolveUnacknowledgedCommand() attempts a GetStatusCommand to resolve any unacknowledged command or throws .unacknowledgedCommandPending on failure. Have all PodCommSession funcs call resolveUnacknowledgedCommand() instead of failing if called with a pending unacknowledged command. --- OmniKit/PumpManager/PodCommsSession.swift | 98 +++++++++++++++++------ 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index cfe5b31..0f6f942 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -399,15 +399,47 @@ public class PodCommsSession { podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: currentDate, scheduledCertainty: .certain, insulinType: podState.insulinType)) } + // + // Attempts to resolve any unacknowledged command by using a GetStatusCommand. + // podState.unacknowledgeCommand is guaranteed to be nil upon successful return. + // Throws PodCommsError.unacknowledgedCommandPending if unsuccessful for any reason. + // + private func resolveUnacknowledgedCommand(function: String = #function) throws { + + guard podState.unacknowledgedCommand != nil else { + return // no unacknowledged command to resolve + } + + let statusResponse: StatusResponse + do { + // Send a GetStatusCommand to try to resolve the unacknowleged command. + statusResponse = try send([GetStatusCommand()]) + } catch let error { + log.info("GetStatus failed with %{public}@ trying to resolve unacknowledged command in %{public}@", String(describing: error), function) + throw PodCommsError.unacknowledgedCommandPending + } + + // Success -- now use the statusResponse to resolve the unacknowledged command & update the podState + recoverUnacknowledgedCommand(using: statusResponse) + podState.updateFromStatusResponse(statusResponse, at: currentDate) + + // recoverUnacknowledgedCommand() should have resolved the unacknowledged command, but check to be sure. + guard podState.unacknowledgedCommand == nil else { + log.error("failed to resolve the unacknowledged command with GetStatus in %{public}@!", function) + throw PodCommsError.unacknowledgedCommandPending + } + + log.info("resolved the unacknowledged command in %{public}@", function) + } + // 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], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil || podState.setupProgress != .completed else { - log.info("Fail configure alerts with unacknowledged command and incomplete pod setup") - throw PodCommsError.unacknowledgedCommandPending + if podState.unacknowledgedCommand != nil { + try resolveUnacknowledgedCommand() } let configurations = alerts.map { $0.configuration } @@ -435,9 +467,12 @@ public class PodCommsSession { return .failure(PodCommsError.podFault(fault: fault)) } - guard podState.unacknowledgedCommand == nil || podState.setupProgress != .completed else { - log.info("Fail beep config with unacknowledged command and incomplete pod setup") - return .failure(PodCommsError.unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try resolveUnacknowledgedCommand() + } catch let error { + return .failure(error) + } } let beepConfigCommand = BeepConfigCommand(beepType: beepType, tempBasalCompletionBeep: tempBasalCompletionBeep, bolusCompletionBeep: bolusCompletionBeep) @@ -530,8 +565,12 @@ public class PodCommsSession { public func bolus(units: Double, automatic: Bool = false, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0, extendedUnits: Double = 0.0, extendedDuration: TimeInterval = 0) -> DeliveryCommandResult { - guard podState.unacknowledgedCommand == nil else { - return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try resolveUnacknowledgedCommand() + } catch { + return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + } } let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse) @@ -577,8 +616,12 @@ public class PodCommsSession { public func setTempBasal(rate: Double, duration: TimeInterval, isHighTemp: Bool, automatic: Bool, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) -> DeliveryCommandResult { - guard podState.unacknowledgedCommand == nil else { - return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try resolveUnacknowledgedCommand() + } catch { + return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) + } } let tempBasalCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, tempBasalRate: rate, duration: duration) @@ -651,8 +694,12 @@ public class PodCommsSession { // 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) + if podState.unacknowledgedCommand != nil { + do { + try resolveUnacknowledgedCommand() + } catch { + return .certainFailure(error: .unacknowledgedCommandPending) + } } guard podState.setupProgress == .completed else { @@ -735,8 +782,12 @@ public class PodCommsSession { // N.B., Using the built-in cancel delivery command beepType method when cancelling all insulin delivery will emit 3 different sets of cancel beeps!!! public func cancelDelivery(deliveryType: CancelDeliveryCommand.DeliveryType, beepType: BeepType = .noBeepCancel, beepBlock: MessageBlock? = nil) -> CancelDeliveryResult { - guard podState.unacknowledgedCommand == nil else { - return .certainFailure(error: .unacknowledgedCommandPending) + if podState.unacknowledgedCommand != nil { + do { + try resolveUnacknowledgedCommand() + } catch { + return .certainFailure(error: .unacknowledgedCommandPending) + } } guard podState.setupProgress == .completed else { @@ -765,8 +816,9 @@ public class PodCommsSession { } public func setTime(timeZone: TimeZone, basalSchedule: BasalSchedule, date: Date, acknowledgementBeep: Bool = false) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil else { - throw PodCommsError.unacknowledgedCommandPending + + if podState.unacknowledgedCommand != nil { + try resolveUnacknowledgedCommand() } let result = cancelDelivery(deliveryType: .all) @@ -784,8 +836,8 @@ public class PodCommsSession { public func setBasalSchedule(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil else { - throw PodCommsError.unacknowledgedCommandPending + if podState.unacknowledgedCommand != nil { + try resolveUnacknowledgedCommand() } let basalScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, basalSchedule: schedule, scheduleOffset: scheduleOffset) @@ -825,11 +877,10 @@ public class PodCommsSession { public func resumeBasal(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { - guard podState.unacknowledgedCommand == nil else { - throw PodCommsError.unacknowledgedCommandPending + if podState.unacknowledgedCommand != nil { + try resolveUnacknowledgedCommand() } - let status = try setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval) podState.suspendState = .resumed(currentDate) @@ -1041,9 +1092,8 @@ public class PodCommsSession { public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet { - guard podState.unacknowledgedCommand == nil || podState.setupProgress != .completed else { - log.info("Fail acknowledge alerts with unacknowledged command and pod setup complete") - throw PodCommsError.unacknowledgedCommandPending + if podState.unacknowledgedCommand != nil { + try resolveUnacknowledgedCommand() } let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts) From 2d9959c107d41d5c8c64b1fa69049c103874cc02 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Mon, 23 Dec 2024 23:40:52 -0800 Subject: [PATCH 08/14] Use getStatus() more for unacknowledged command handling & simplified code Fix deactivatePod() logic error to correctly handle an unacknowledged command Simplify logging by not including the calling function's name --- OmniKit/PumpManager/PodCommsSession.swift | 43 +++++++++-------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index 0f6f942..ee47a83 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -357,8 +357,7 @@ public class PodCommsSession { try configureAlerts([finishSetupReminder]) } else { // Not the first time through, check to see if prime bolus was successfully started - let status: StatusResponse = try send([GetStatusCommand()]) - podState.updateFromStatusResponse(status, at: currentDate) + let status = try getStatus() if status.podProgressStatus == .priming || status.podProgressStatus == .primingCompleted { podState.setupProgress = .priming return podState.primeFinishTime?.timeIntervalSinceNow ?? primeDuration @@ -383,8 +382,7 @@ public class PodCommsSession { public func programInitialBasalSchedule(_ basalSchedule: BasalSchedule, scheduleOffset: TimeInterval) throws { if podState.setupProgress == .settingInitialBasalSchedule { // We started basal schedule programming, but didn't get confirmation somehow, so check status - let status: StatusResponse = try send([GetStatusCommand()]) - podState.updateFromStatusResponse(status, at: currentDate) + let status = try getStatus() if status.podProgressStatus == .basalInitialized { podState.setupProgress = .initialBasalScheduleSet podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: currentDate, scheduledCertainty: .certain, insulinType: podState.insulinType)) @@ -400,36 +398,30 @@ public class PodCommsSession { } // - // Attempts to resolve any unacknowledged command by using a GetStatusCommand. + // Attempts to resolve any unacknowledged command by calling getStatus(). // podState.unacknowledgeCommand is guaranteed to be nil upon successful return. // Throws PodCommsError.unacknowledgedCommandPending if unsuccessful for any reason. // - private func resolveUnacknowledgedCommand(function: String = #function) throws { + private func resolveUnacknowledgedCommand() throws { guard podState.unacknowledgedCommand != nil else { return // no unacknowledged command to resolve } - let statusResponse: StatusResponse do { - // Send a GetStatusCommand to try to resolve the unacknowleged command. - statusResponse = try send([GetStatusCommand()]) + _ = try getStatus() // should resolve the unacknowledged command if successful } catch let error { - log.info("GetStatus failed with %{public}@ trying to resolve unacknowledged command in %{public}@", String(describing: error), function) + log.error("GetStatus failed trying to resolve unacknowledged command: %{public}@", String(describing: error)) throw PodCommsError.unacknowledgedCommandPending } - // Success -- now use the statusResponse to resolve the unacknowledged command & update the podState - recoverUnacknowledgedCommand(using: statusResponse) - podState.updateFromStatusResponse(statusResponse, at: currentDate) - - // recoverUnacknowledgedCommand() should have resolved the unacknowledged command, but check to be sure. + // Verify that getStatus successfully resolved the unacknowledged command. guard podState.unacknowledgedCommand == nil else { - log.error("failed to resolve the unacknowledged command with GetStatus in %{public}@!", function) + log.error("Successful getStatus didn't resolve the unacknowledged command!") throw PodCommsError.unacknowledgedCommandPending } - log.info("resolved the unacknowledged command in %{public}@", function) + log.info("Successfully resolved pending unacknowledged command") } // Configures the given pod alert(s) and registers the newly configured alert slot(s). @@ -502,19 +494,16 @@ public class PodCommsSession { if podState.setupProgress == .startingInsertCannula || podState.setupProgress == .cannulaInserting { // We started cannula insertion, but didn't get confirmation somehow, so check status - let status: StatusResponse = try send([GetStatusCommand()]) + let status = try getStatus() if status.podProgressStatus == .insertingCannula { podState.setupProgress = .cannulaInserting - podState.updateFromStatusResponse(status, at: currentDate) // return a non-zero wait time based on the bolus not yet delivered return (status.bolusNotDelivered / Pod.primeDeliveryRate) + 1 } if status.podProgressStatus.readyForDelivery { markSetupProgressCompleted(statusResponse: status) - podState.updateFromStatusResponse(status, at: currentDate) return TimeInterval(0) // Already done; no need to wait } - podState.updateFromStatusResponse(status, at: currentDate) } else { let elapsed: TimeInterval = -(podState.podTimeUpdated?.timeIntervalSinceNow ?? 0) let podTime = podState.podTime + elapsed @@ -541,11 +530,10 @@ public class PodCommsSession { public func checkInsertionCompleted() throws { if podState.setupProgress == .cannulaInserting { - let response: StatusResponse = try send([GetStatusCommand()]) + let response = try getStatus() if response.podProgressStatus.readyForDelivery { markSetupProgressCompleted(statusResponse: response) } - podState.updateFromStatusResponse(response, at: currentDate) } } @@ -1068,13 +1056,14 @@ public class PodCommsSession { } do { - let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) - let status: StatusResponse = try send([deactivatePod]) - if podState.unacknowledgedCommand != nil { - recoverUnacknowledgedCommand(using: status) + // Try to resolve the unacknowledged command now as DeactivatePodCommand + // destroys any chance of correctly handling the unacknowledged command. + try? resolveUnacknowledgedCommand() } + let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) + let status: StatusResponse = try send([deactivatePod]) podState.updateFromStatusResponse(status, at: currentDate) if podState.activeTime == nil, let activatedAt = podState.activatedAt { From 2d41740c31859919d1aa1f8ce84d2b175a389526 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Tue, 24 Dec 2024 22:58:30 -0800 Subject: [PATCH 09/14] Rename resolveUnacknowledgedCommand() to tryToResolvePendingCommand() --- OmniKit/PumpManager/PodCommsSession.swift | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index ee47a83..7fb0df1 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -398,26 +398,26 @@ public class PodCommsSession { } // - // Attempts to resolve any unacknowledged command by calling getStatus(). + // Attempts to resolve any pending unacknowledged command by calling getStatus(). // podState.unacknowledgeCommand is guaranteed to be nil upon successful return. // Throws PodCommsError.unacknowledgedCommandPending if unsuccessful for any reason. // - private func resolveUnacknowledgedCommand() throws { + private func tryToResolvePendingCommand() throws { guard podState.unacknowledgedCommand != nil else { - return // no unacknowledged command to resolve + return // no pending unacknowledged command to resolve } do { - _ = try getStatus() // should resolve the unacknowledged command if successful + _ = try getStatus() // should resolve the pending unacknowledged command if successful } catch let error { - log.error("GetStatus failed trying to resolve unacknowledged command: %{public}@", String(describing: error)) + log.error("GetStatus failed trying to resolve pending unacknowledged command: %{public}@", String(describing: error)) throw PodCommsError.unacknowledgedCommandPending } - // Verify that getStatus successfully resolved the unacknowledged command. + // Verify that getStatus successfully resolved the pending unacknowledged command. guard podState.unacknowledgedCommand == nil else { - log.error("Successful getStatus didn't resolve the unacknowledged command!") + log.error("Successful getStatus didn't resolve the pending unacknowledged command!") throw PodCommsError.unacknowledgedCommandPending } @@ -431,7 +431,7 @@ public class PodCommsSession { func configureAlerts(_ alerts: [PodAlert], acknowledgeAll: Bool = false, beepBlock: MessageBlock? = nil) throws -> StatusResponse { if podState.unacknowledgedCommand != nil { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } let configurations = alerts.map { $0.configuration } @@ -461,7 +461,7 @@ public class PodCommsSession { if podState.unacknowledgedCommand != nil { do { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } catch let error { return .failure(error) } @@ -555,7 +555,7 @@ public class PodCommsSession { if podState.unacknowledgedCommand != nil { do { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } catch { return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) } @@ -606,7 +606,7 @@ public class PodCommsSession { if podState.unacknowledgedCommand != nil { do { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } catch { return DeliveryCommandResult.certainFailure(error: .unacknowledgedCommandPending) } @@ -684,7 +684,7 @@ public class PodCommsSession { if podState.unacknowledgedCommand != nil { do { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } catch { return .certainFailure(error: .unacknowledgedCommandPending) } @@ -772,7 +772,7 @@ public class PodCommsSession { if podState.unacknowledgedCommand != nil { do { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } catch { return .certainFailure(error: .unacknowledgedCommandPending) } @@ -806,7 +806,7 @@ public class PodCommsSession { public func setTime(timeZone: TimeZone, basalSchedule: BasalSchedule, date: Date, acknowledgementBeep: Bool = false) throws -> StatusResponse { if podState.unacknowledgedCommand != nil { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } let result = cancelDelivery(deliveryType: .all) @@ -825,7 +825,7 @@ public class PodCommsSession { public func setBasalSchedule(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { if podState.unacknowledgedCommand != nil { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } let basalScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, basalSchedule: schedule, scheduleOffset: scheduleOffset) @@ -866,7 +866,7 @@ public class PodCommsSession { public func resumeBasal(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse { if podState.unacknowledgedCommand != nil { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } let status = try setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, programReminderInterval: programReminderInterval) @@ -1059,7 +1059,7 @@ public class PodCommsSession { if podState.unacknowledgedCommand != nil { // Try to resolve the unacknowledged command now as DeactivatePodCommand // destroys any chance of correctly handling the unacknowledged command. - try? resolveUnacknowledgedCommand() + try? tryToResolvePendingCommand() } let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) @@ -1082,7 +1082,7 @@ public class PodCommsSession { public func acknowledgeAlerts(alerts: AlertSet, beepBlock: MessageBlock? = nil) throws -> AlertSet { if podState.unacknowledgedCommand != nil { - try resolveUnacknowledgedCommand() + try tryToResolvePendingCommand() } let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts) From f82393cb9b4925ee206b2d6caf022ee4eee0f4f9 Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Sat, 18 Jan 2025 00:53:30 -0800 Subject: [PATCH 10/14] Logic fix for 049 pod fault with concurrent temp basal commands + Fixes Trio issue #455, see for more details + Tweaks to runTemporaryBasalProgram() to avoid using stale podState + Change enactBolus() to use same updated pod suspended testing logic --- OmniKit/PumpManager/OmnipodPumpManager.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 734bc52..00aebeb 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -1870,7 +1870,7 @@ extension OmnipodPumpManager: PumpManager { state.bolusEngageState = .engaging }) - if let podState = self.state.podState, podState.isSuspended || podState.lastDeliveryStatusReceived?.suspended == true { + guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended == false else { self.log.info("Not enacting bolus because podState or last status received indicates pod is suspended") completion(.deviceState(PodCommsError.podSuspended)) return @@ -1968,7 +1968,7 @@ extension OmnipodPumpManager: PumpManager { public func runTemporaryBasalProgram(unitsPerHour: Double, for duration: TimeInterval, automatic: Bool, completion: @escaping (PumpManagerError?) -> Void) { - guard self.hasActivePod, let podState = self.state.podState else { + guard self.hasActivePod else { completion(.configuration(OmnipodPumpManagerError.noPodPaired)) return } @@ -2009,7 +2009,7 @@ extension OmnipodPumpManager: PumpManager { return } - if let podState = self.state.podState, podState.isSuspended || podState.lastDeliveryStatusReceived?.suspended == true { + guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended == false else { self.log.info("Not enacting temp basal because podState or last status received indicates pod is suspended") completion(.deviceState(PodCommsError.podSuspended)) return From cce7badf9663d29176c3c809e06a19fd68e918ce Mon Sep 17 00:00:00 2001 From: marionbarker Date: Fri, 7 Feb 2025 17:12:13 -0800 Subject: [PATCH 11/14] fix for pump manager returns bogus podSuspended; * add self.tryToValidateComms to handle uncertain comms; * ensure podSuspended is only returned when actually suspended --- OmniKit/PumpManager/OmnipodPumpManager.swift | 75 +++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 00aebeb..d36a60e 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -1003,6 +1003,33 @@ extension OmnipodPumpManager { } } + // Will execute a getStatus command if the pod is not connected, the last delivery statusreceived is invalid, + // or there is an unacknowledged command. If the getStatus fails, returns its error that can be passed on the + // higher level instead of having to return a potentially inappropriate error when using uncertain pod status. + // Returns nil if comms looks OK or a getStatus was successful executed or the getStatus error on failure. + private func tryToValidateComms(session: PodCommsSession) -> LocalizedError? { + + // If we're connected, have a valid delivery status, and there is not an unacknowledged command, we're all good to go + if state.rileyLinkConnectionManagerState != nil && self.state.podState?.lastDeliveryStatusReceived != nil && self.state.podState?.unacknowledgedCommand == nil { + return nil + } + + // Attempt to do a getStatus to try to resolve any outstanding comms issues + do { + let _ = try session.getStatus() + // Paranoid debug testing as the successful getStatus should have set the last delivery status and handled any unacknowledged command. + if self.state.podState?.lastDeliveryStatusReceived == nil || self.state.podState?.unacknowledgedCommand != nil { + self.log.error("### tryToValidateComms getStatus successful, but still have no last delivery status or an unacknowledged command!") + } else { + self.log.debug("### tryToValidateComms getStatus resolved all pending comms issues") + } + return nil + } catch let error { + self.log.debug("### tryToValidateComms getStatus failed, returning: %@", error.localizedDescription) + return error as? LocalizedError + } + } + // MARK: - Pump Commands public func getPodStatus(completion: ((_ result: PumpManagerResult) -> Void)? = nil) { @@ -1119,6 +1146,11 @@ extension OmnipodPumpManager { switch result { case .success(let session): do { + if let error = self.tryToValidateComms(session: session) { + completion(.state(error)) + return + } + 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 @@ -1171,6 +1203,11 @@ extension OmnipodPumpManager { do { switch result { case .success(let session): + if let error = self.tryToValidateComms(session: session) { + completion(error) + return + } + let scheduleOffset = timeZone.scheduleOffset(forDate: Date()) let result = session.cancelDelivery(deliveryType: .all) switch result { @@ -1415,6 +1452,11 @@ extension OmnipodPumpManager { self.podComms.runSession(withName: name, using: rileyLinkSelector) { (result) in switch result { case .success(let session): + if let error = self.tryToValidateComms(session: session) { + completion(.communication(error)) + return + } + // enable/disable Pod completion beep state for any unfinalized manual insulin delivery let enabled = newPreference.shouldBeepForManualCommand let beepType: BeepType = enabled ? .bipBip : .noBeepNonCancel @@ -1466,6 +1508,11 @@ extension OmnipodPumpManager { return } + if let error = self.tryToValidateComms(session: session) { + completion(.state(error)) + return + } + guard let configuredAlerts = self.state.podState?.configuredAlerts, let activeAlertSlots = self.state.podState?.activeAlertSlots, let reservoirLevel = self.state.podState?.lastInsulinMeasurements?.reservoirLevel?.rawValue else @@ -1716,6 +1763,11 @@ extension OmnipodPumpManager: PumpManager { state.suspendEngageState = .engaging }) + if let error = self.tryToValidateComms(session: session) { + completion(error) + return + } + // 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, silent: self.silencePod, beepBlock: beepBlock) @@ -1762,6 +1814,11 @@ extension OmnipodPumpManager: PumpManager { state.suspendEngageState = .disengaging }) + if let error = self.tryToValidateComms(session: session) { + completion(error) + return + } + do { let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date()) let beep = self.silencePod ? false : self.beepPreference.shouldBeepForManualCommand @@ -1870,7 +1927,14 @@ extension OmnipodPumpManager: PumpManager { state.bolusEngageState = .engaging }) - guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended == false else { + if let error = self.tryToValidateComms(session: session) { + completion(.deviceState(error)) + return + } + + // Use a lastDeliveryStatusReceived?.suspended != true test here to not return a pod suspended failure if + // there is not a valid last delivery status (which shouldn't even happen now with tryToValidateComms()). + guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended != true else { self.log.info("Not enacting bolus because podState or last status received indicates pod is suspended") completion(.deviceState(PodCommsError.podSuspended)) return @@ -2009,7 +2073,14 @@ extension OmnipodPumpManager: PumpManager { return } - guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended == false else { + if let error = self.tryToValidateComms(session: session) { + completion(.deviceState(PumpManagerError.deviceState(error))) + return + } + + // Use a lastDeliveryStatusReceived?.suspended != true test here to not return a pod suspended failure if + // there is not a valid last delivery status (which shouldn't even happen now with tryToValidateComms()). + guard let podState = self.state.podState, !podState.isSuspended && podState.lastDeliveryStatusReceived?.suspended != true else { self.log.info("Not enacting temp basal because podState or last status received indicates pod is suspended") completion(.deviceState(PodCommsError.podSuspended)) return From 9d9d499cddf27c1fa8a95541f23f0e549faf53a1 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Sun, 9 Feb 2025 21:06:36 -0800 Subject: [PATCH 12/14] remove an unneeded connection check, revise some comments and remove extra debug logging --- OmniKit/PumpManager/OmnipodPumpManager.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index d36a60e..d850a7b 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -1003,26 +1003,20 @@ extension OmnipodPumpManager { } } - // Will execute a getStatus command if the pod is not connected, the last delivery statusreceived is invalid, - // or there is an unacknowledged command. If the getStatus fails, returns its error that can be passed on the - // higher level instead of having to return a potentially inappropriate error when using uncertain pod status. - // Returns nil if comms looks OK or a getStatus was successful executed or the getStatus error on failure. + // If the last delivery status received is invalid or there is an unacknowledged command, execute a getStatus command + // for the current PodCommsSession. If the getStatus fails, return its error to be passed on to the higher level. + // Return nil if comms looks OK or the getStatus was successful. private func tryToValidateComms(session: PodCommsSession) -> LocalizedError? { - // If we're connected, have a valid delivery status, and there is not an unacknowledged command, we're all good to go - if state.rileyLinkConnectionManagerState != nil && self.state.podState?.lastDeliveryStatusReceived != nil && self.state.podState?.unacknowledgedCommand == nil { + // Since we're already connected for this session, if we have a delivery status and no unacknowledged command, return nil + if self.state.podState?.lastDeliveryStatusReceived != nil && self.state.podState?.unacknowledgedCommand == nil { return nil } // Attempt to do a getStatus to try to resolve any outstanding comms issues do { let _ = try session.getStatus() - // Paranoid debug testing as the successful getStatus should have set the last delivery status and handled any unacknowledged command. - if self.state.podState?.lastDeliveryStatusReceived == nil || self.state.podState?.unacknowledgedCommand != nil { - self.log.error("### tryToValidateComms getStatus successful, but still have no last delivery status or an unacknowledged command!") - } else { - self.log.debug("### tryToValidateComms getStatus resolved all pending comms issues") - } + self.log.debug("### tryToValidateComms getStatus resolved all pending comms issues") return nil } catch let error { self.log.debug("### tryToValidateComms getStatus failed, returning: %@", error.localizedDescription) From 447caa4d94e8b75aa5d8b49e797eaa60da068f69 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 10 Feb 2025 17:36:24 -0800 Subject: [PATCH 13/14] use correct .communication errors instead of .state or .deviceState --- OmniKit/PumpManager/OmnipodPumpManager.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index d850a7b..aba8b5e 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -1141,7 +1141,7 @@ extension OmnipodPumpManager { case .success(let session): do { if let error = self.tryToValidateComms(session: session) { - completion(.state(error)) + completion(.communication(error)) return } @@ -1503,7 +1503,7 @@ extension OmnipodPumpManager { } if let error = self.tryToValidateComms(session: session) { - completion(.state(error)) + completion(.communication(error)) return } @@ -1922,7 +1922,7 @@ extension OmnipodPumpManager: PumpManager { }) if let error = self.tryToValidateComms(session: session) { - completion(.deviceState(error)) + completion(.communication(error)) return } @@ -2068,7 +2068,7 @@ extension OmnipodPumpManager: PumpManager { } if let error = self.tryToValidateComms(session: session) { - completion(.deviceState(PumpManagerError.deviceState(error))) + completion(.communication(error)) return } From 05683a2689a2ef414e7e5b65f8763a9d31c6d1a0 Mon Sep 17 00:00:00 2001 From: Sam King Date: Tue, 11 Mar 2025 02:41:51 -0700 Subject: [PATCH 14/14] Add link dependencies --- OmniKit.xcodeproj/project.pbxproj | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/OmniKit.xcodeproj/project.pbxproj b/OmniKit.xcodeproj/project.pbxproj index 3432185..3bc7eaa 100644 --- a/OmniKit.xcodeproj/project.pbxproj +++ b/OmniKit.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ 275A98C02AE456EA0097F476 /* SlideButton in Frameworks */ = {isa = PBXBuildFile; productRef = 275A98BF2AE456EA0097F476 /* SlideButton */; }; + 3B0FD2A12D803BB000E5E921 /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0FD2A02D803BB000E5E921 /* RileyLinkBLEKit.framework */; }; + 3B0FD2A32D803BB800E5E921 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0FD2A22D803BB800E5E921 /* RileyLinkKit.framework */; }; + 3B0FD2A62D803C2000E5E921 /* OmniKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C124016C29C7D87A00B32844 /* OmniKit.framework */; }; + 3B0FD2A82D803C2C00E5E921 /* RileyLinkKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B0FD2A72D803C2C00E5E921 /* RileyLinkKitUI.framework */; }; B60BB2EC2BC757A700D2BB39 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B60BB2EA2BC7579E00D2BB39 /* Bundle.swift */; }; C1229C1A29C7E5BC0066A89C /* RileyLinkBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1229C1729C7E5BC0066A89C /* RileyLinkBLEKit.framework */; }; C1229C1B29C7E5BC0066A89C /* RileyLinkBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C1229C1729C7E5BC0066A89C /* RileyLinkBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -192,6 +196,7 @@ 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 */; }; + D803399C2A50128D004FF953 /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12EDA0129C7DD4700435701 /* LocalizedString.swift */; }; D845A1352AF89DEC00EA0853 /* SilencePodPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1342AF89DEC00EA0853 /* SilencePodPreference.swift */; }; D845A1462AF8A4DA00EA0853 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1452AF8A4DA00EA0853 /* ActivityView.swift */; }; D845A14A2AF8A4EF00EA0853 /* PlayTestBeepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1492AF8A4EF00EA0853 /* PlayTestBeepsView.swift */; }; @@ -200,7 +205,6 @@ D845A1522AF8A51000EA0853 /* SilencePodSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D845A1512AF8A51000EA0853 /* SilencePodSelectionView.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 */ /* Begin PBXContainerItemProxy section */ @@ -255,6 +259,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3B0FD2A02D803BB000E5E921 /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B0FD2A22D803BB800E5E921 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B0FD2A72D803C2C00E5E921 /* RileyLinkKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B60BB2EA2BC7579E00D2BB39 /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; C10E1EE62AE59EDC00B4E993 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; C1229C1729C7E5BC0066A89C /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -467,6 +474,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3B0FD2A32D803BB800E5E921 /* RileyLinkKit.framework in Frameworks */, + 3B0FD2A12D803BB000E5E921 /* RileyLinkBLEKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -474,6 +483,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 3B0FD2A82D803C2C00E5E921 /* RileyLinkKitUI.framework in Frameworks */, + 3B0FD2A62D803C2000E5E921 /* OmniKit.framework in Frameworks */, 275A98C02AE456EA0097F476 /* SlideButton in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -822,6 +833,9 @@ C12ED9F329C7DCE100435701 /* Frameworks */ = { isa = PBXGroup; children = ( + 3B0FD2A72D803C2C00E5E921 /* RileyLinkKitUI.framework */, + 3B0FD2A22D803BB800E5E921 /* RileyLinkKit.framework */, + 3B0FD2A02D803BB000E5E921 /* RileyLinkBLEKit.framework */, C1229C1729C7E5BC0066A89C /* RileyLinkBLEKit.framework */, C1229C1829C7E5BC0066A89C /* RileyLinkKit.framework */, C1229C1929C7E5BC0066A89C /* RileyLinkKitUI.framework */,