diff --git a/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift b/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift index a5180b8b2..64d79f260 100644 --- a/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift +++ b/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift @@ -19,7 +19,7 @@ public struct DetailedStatus : PodInfo, Equatable { public let podProgressStatus: PodProgressStatus public let deliveryStatus: DeliveryStatus public let bolusNotDelivered: Double - public let podMessageCounter: UInt8 + public let lastProgrammingMessageSeqNum: UInt8 // updated by pod for 03, 08, $11, $19, $1A, $1C, $1E & $1F command messages public let totalInsulinDelivered: Double public let faultEventCode: FaultEventCode public let faultEventTimeSinceActivation: TimeInterval? @@ -28,10 +28,10 @@ public struct DetailedStatus : PodInfo, Equatable { public let unacknowledgedAlerts: AlertSet public let faultAccessingTables: Bool public let errorEventInfo: ErrorEventInfo? - public let receiverLowGain: Int8 - public let radioRSSI: Int8 + public let receiverLowGain: UInt8 + public let radioRSSI: UInt8 public let previousPodProgressStatus: PodProgressStatus? - public let unknownValue: Data + // YYYY is uninitialized data for Eros public let data: Data public init(encodedData: Data) throws { @@ -48,9 +48,9 @@ public struct DetailedStatus : PodInfo, Equatable { self.bolusNotDelivered = Pod.pulseSize * Double((Int(encodedData[3] & 0x3) << 8) | Int(encodedData[4])) - self.podMessageCounter = encodedData[5] + self.lastProgrammingMessageSeqNum = encodedData[5] - self.totalInsulinDelivered = Pod.pulseSize * Double((Int(encodedData[6]) << 8) | Int(encodedData[7])) + self.totalInsulinDelivered = Pod.pulseSize * Double(encodedData[6...7].toBigEndian(UInt16.self)) self.faultEventCode = FaultEventCode(rawValue: encodedData[8]) @@ -73,7 +73,7 @@ public struct DetailedStatus : PodInfo, Equatable { self.unacknowledgedAlerts = AlertSet(rawValue: encodedData[15]) - self.faultAccessingTables = encodedData[16] == 2 + self.faultAccessingTables = (encodedData[16] & 2) != 0 if encodedData[17] == 0x00 { self.errorEventInfo = nil // this byte is not valid (no fault has occurred) @@ -81,8 +81,8 @@ public struct DetailedStatus : PodInfo, Equatable { self.errorEventInfo = ErrorEventInfo(rawValue: encodedData[17]) } - self.receiverLowGain = Int8(encodedData[18] >> 6) - self.radioRSSI = Int8(encodedData[18] & 0x3F) + self.receiverLowGain = UInt8(encodedData[18] >> 6) + self.radioRSSI = UInt8(encodedData[18] & 0x3F) if encodedData[19] == 0xFF { self.previousPodProgressStatus = nil // this byte is not valid (no fault has occurred) @@ -90,14 +90,48 @@ public struct DetailedStatus : PodInfo, Equatable { self.previousPodProgressStatus = PodProgressStatus(rawValue: encodedData[19] & 0xF)! } - self.unknownValue = encodedData[20...21] - self.data = Data(encodedData) } public var isFaulted: Bool { return faultEventCode.faultType != .noFaults || podProgressStatus == .activationTimeExceeded } + + // 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 + public var pdmRef: String? { + let TT, VVV, HH, III, RR, FFF: UInt8 + let refStr = LocalizedString("Ref", comment: "PDM style 'Ref' string") + + switch faultEventCode.faultType { + case .noFaults, .reservoirEmpty, .exceededMaximumPodLife80Hrs: + return nil // no PDM Ref # generated for these cases + case .insulinDeliveryCommandError: + // This fault is treated as a PDM fault which uses an alternate Ref format + return String(format: "%@:\u{00a0}11-144-0018-00049", refStr) // all fixed values for this fault + case .occluded: + // 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: + // 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) + + if let reservoirLevel = self.reservoirLevel { + RR = UInt8(reservoirLevel) + } else { + RR = 51 // value used for 50+ U + } + + return String(format: "%@:\u{00a0}%02d-%03d%02d-%03d%02d-%03d", refStr, TT, VVV, HH, III, RR, FFF) + } } extension DetailedStatus: CustomDebugStringConvertible { @@ -109,7 +143,7 @@ extension DetailedStatus: CustomDebugStringConvertible { "* podProgressStatus: \(podProgressStatus)", "* deliveryStatus: \(deliveryStatus.description)", "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U", - "* podMessageCounter: \(podMessageCounter)", + "* lastProgrammingMessageSeqNum: \(lastProgrammingMessageSeqNum)", "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U", "* faultEventCode: \(faultEventCode.description)", "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "none")", @@ -121,7 +155,6 @@ extension DetailedStatus: CustomDebugStringConvertible { "* receiverLowGain: \(receiverLowGain)", "* radioRSSI: \(radioRSSI)", "* previousPodProgressStatus: \(previousPodProgressStatus?.description ?? "NA")", - "* unknownValue: 0x\(unknownValue.hexadecimalString)", "", ].joined(separator: "\n") } @@ -169,14 +202,14 @@ extension Double { // Type for the ErrorEventInfo VV byte if valid // a: insulin state table corruption found during error logging -// bb: internal 2-bit variable set and manipulated in main loop routines +// bb: internal 2-bit occlusion type // c: immediate bolus in progress during error // dddd: Pod Progress at time of first logged fault event // public struct ErrorEventInfo: CustomStringConvertible, Equatable { let rawValue: UInt8 let insulinStateTableCorruption: Bool // 'a' bit - let internalVariable: Int // 'bb' 2-bit internal variable + let occlusionType: Int // 'bb' 2-bit occlusion type let immediateBolusInProgress: Bool // 'c' bit let podProgressStatus: PodProgressStatus // 'dddd' bits @@ -189,7 +222,7 @@ public struct ErrorEventInfo: CustomStringConvertible, Equatable { return [ "rawValue: 0x\(hexString)", "insulinStateTableCorruption: \(insulinStateTableCorruption)", - "internalVariable: \(internalVariable)", + "occlusionType: \(occlusionType)", "immediateBolusInProgress: \(immediateBolusInProgress)", "podProgressStatus: \(podProgressStatus)", ].joined(separator: ", ") @@ -198,7 +231,7 @@ public struct ErrorEventInfo: CustomStringConvertible, Equatable { init(rawValue: UInt8) { self.rawValue = rawValue self.insulinStateTableCorruption = (rawValue & 0x80) != 0 - self.internalVariable = Int((rawValue & 0x60) >> 5) + self.occlusionType = Int((rawValue & 0x60) >> 5) self.immediateBolusInProgress = (rawValue & 0x10) != 0 self.podProgressStatus = PodProgressStatus(rawValue: rawValue & 0xF)! } diff --git a/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift b/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift index e232e713d..04ec640d9 100644 --- a/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift +++ b/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift @@ -17,7 +17,7 @@ public struct StatusResponse : MessageBlock { public let reservoirLevel: Double? public let insulin: Double public let bolusNotDelivered: Double - public let podMessageCounter: UInt8 + public let lastProgrammingMessageSeqNum: UInt8 // updated by pod for 03, 08, $11, $19, $1A, $1C, $1E & $1F command messages public let alerts: AlertSet @@ -48,7 +48,7 @@ public struct StatusResponse : MessageBlock { let lowInsulinBits = Int(encodedData[4] >> 7) self.insulin = Double(highInsulinBits | midInsulinBits | lowInsulinBits) / Pod.pulsesPerUnit - self.podMessageCounter = (encodedData[4] >> 3) & 0xf + self.lastProgrammingMessageSeqNum = (encodedData[4] >> 3) & 0xf self.bolusNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit @@ -65,7 +65,7 @@ public struct StatusResponse : MessageBlock { extension StatusResponse: CustomDebugStringConvertible { public var debugDescription: String { - return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulin), bolusNotDelivered:\(bolusNotDelivered), seq:\(podMessageCounter), alerts:\(alerts))" + return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulin), bolusNotDelivered:\(bolusNotDelivered), lastProgrammingMessageSeqNum:\(lastProgrammingMessageSeqNum), alerts:\(alerts))" } } diff --git a/OmniKit/MessageTransport/MessageBlocks/VersionResponse.swift b/OmniKit/MessageTransport/MessageBlocks/VersionResponse.swift index f97548a12..6b3f0bdd4 100644 --- a/OmniKit/MessageTransport/MessageBlocks/VersionResponse.swift +++ b/OmniKit/MessageTransport/MessageBlocks/VersionResponse.swift @@ -33,13 +33,24 @@ public struct VersionResponse : MessageBlock { public let pmVersion: FirmwareVersion public let piVersion: FirmwareVersion - public let podProgressStatus: PodProgressStatus public let lot: UInt32 public let tid: UInt32 - public let gain: UInt8? // Only in the shorter assignAddress response - public let rssi: UInt8? // Only in the shorter assignAddress response public let address: UInt32 - + public let productID: UInt8 // always 2 (for PM = PI = 2.7.0), 2nd gen Omnipod? + public let podProgressStatus: PodProgressStatus + + // These values only included in the shorter 0x15 VersionResponse for the AssignAddress command. + public let gain: UInt8? // 2-bit value, max gain is at 0, min gain is at 2 + public let rssi: UInt8? // 6-bit value, max rssi seen 61 + + // These values only included in the longer 0x1B VersionResponse for the SetupPod command. + public let pulseSize: Double? // VVVV / 100,000, must be 0x1388 / 100,000 = 0.05U + public let secondsPerBolusPulse: Double? // BR / 8, nominally 0x10 / 8 = 2 seconds per pulse + public let secondsPerPrimePulse: Double? // PR / 8, nominally 0x08 / 8 = 1 seconds per priming pulse + public let primeUnits: Double? // PP * pulseSize, nominally 0x34 * 0.05U = 2.6U + public let cannulaInsertionUnits: Double? // CP * pulseSize, nominally 0x0A * 0.05U = 0.5U + public let serviceDuration: TimeInterval? // PL hours, nominally 0x50 = 80 hours + public let data: Data public init(encodedData: Data) throws { @@ -48,58 +59,86 @@ public struct VersionResponse : MessageBlock { switch responseLength { case assignAddressVersionLength: - // This is the shorter 0x15 response to the 07 AssignAddress command - // 01 15 020700 020700 02 02 0000a377 0003ab37 9f 1f00ee87 + // This is the shorter 0x15 response for the 07 AssignAddress command. // 0 1 2 5 8 9 10 14 18 19 - // 01 LL MXMYMZ IXIYIZ 02 0J LLLLLLLL TTTTTTTT GS IIIIIIII + // 01 LL MXMYMZ IXIYIZ ID 0J LLLLLLLL TTTTTTTT GS IIIIIIII + // 01 15 020700 020700 02 02 0000a377 0003ab37 9f 1f00ee87 + // // LL = 0x15 (assignAddressVersionLength) - // PM = MX.MY.MZ - // PI = IX.IY.IZ - // 0J = Pod progress state (typically 02, could be 01) + // PM MX.MY.MZ = 02.07.02 (for PM 2.7.0) + // PI IX.IY.IZ = 02.07.02 (for PI 2.7.0) + // ID = Product ID (always 02 for PM = PI = 2.7.0) + // 0J = Pod progress state (typically 02, but could be 01, for this particular response) // LLLLLLLL = Lot // TTTTTTTT = Tid // GS = ggssssss (Gain/RSSI) - // IIIIIIII = address + // IIIIIIII = connection ID address pmVersion = FirmwareVersion(encodedData: encodedData.subdata(in: 2..<5)) piVersion = FirmwareVersion(encodedData: encodedData.subdata(in: 5..<8)) - if let podProgress = PodProgressStatus(rawValue: encodedData[9]) { - self.podProgressStatus = podProgress - } else { + productID = encodedData[8] + guard let progressStatus = PodProgressStatus(rawValue: encodedData[9]) else { throw MessageBlockError.parseError } + podProgressStatus = progressStatus lot = encodedData[10...].toBigEndian(UInt32.self) tid = encodedData[14...].toBigEndian(UInt32.self) gain = (encodedData[18] & 0xc0) >> 6 rssi = encodedData[18] & 0x3f address = encodedData[19...].toBigEndian(UInt32.self) + // These values only included in the longer 0x1B VersionResponse for the 03 SetupPod command. + pulseSize = nil + secondsPerBolusPulse = nil + secondsPerPrimePulse = nil + primeUnits = nil + cannulaInsertionUnits = nil + serviceDuration = nil + case setupPodVersionLength: - // This is the longer 0x1B response to the 03 SetupPod command - // 01 1b 13881008340a50 020700 020700 02 03 0000a62b 00044794 1f00ee87 - // 0 1 2 9 12 16 17 21 25 - // 01 LL 13881008340A50 MXMYMZ IXIYIZ 02 0J LLLLLLLL TTTTTTTT IIIIIIII - // LL = 0x1B (setupPodVersionMessageLength) - // PM = MX.MY.MZ - // PI = IX.IY.IZ - // 0J = Pod progress state (should always be 03) + // This is the longer 0x1B response for the 03 SetupPod command. + // 0 1 2 4 5 6 7 8 9 12 15 16 17 21 25 + // 01 LL VVVV BR PR PP CP PL MXMYMZ IXIYIZ ID 0J LLLLLLLL TTTTTTTT IIIIIIII + // 01 1b 1388 10 08 34 0a 50 020700 020700 02 03 0000a62b 00044794 1f00ee87 + // + // LL = 0x1b (setupPodVersionMessageLength) + // VVVV = 0x1388, pulse Volume in micro-units of U100 insulin per tenth of pulse (5000/100000 = 0.05U per pulse) + // BR = 0x10, Basic pulse Rate in # of eighth secs per pulse (16/8 = 2 seconds per pulse) + // PR = 0x08, Prime pulse Rate in # of eighth secs per pulse for priming boluses (8/8 = 1 second per priming pulse) + // PP = 0x34 = 52, # of Prime Pulses (52 pulses x 0.05U/pulse = 2.6U) + // CP = 0x0A = 10, # of Cannula insertion Pulses (10 pulses x 0.05U/pulse = 0.5U) + // PL = 0x50 = 80, # of hours maximum Pod Life + // PM = MX.MY.MZ = 02.07.02 (for PM 2.7.0) + // PI = IX.IY.IZ = 02.07.02 (for PI 2.7.0) + // ID = Product ID (always 02 for PM = PI = 2.7.0) + // 0J = Pod progress state (should be 03 for this particular response) // LLLLLLLL = Lot // TTTTTTTT = Tid - // IIIIIIII = address + // IIIIIIII = connection ID address pmVersion = FirmwareVersion(encodedData: encodedData.subdata(in: 9..<12)) piVersion = FirmwareVersion(encodedData: encodedData.subdata(in: 12..<15)) - if let podProgress = PodProgressStatus(rawValue: encodedData[16]) { - self.podProgressStatus = podProgress - } else { + productID = encodedData[15] + guard let progressStatus = PodProgressStatus(rawValue: encodedData[16]) else { throw MessageBlockError.parseError } + podProgressStatus = progressStatus lot = encodedData[17...].toBigEndian(UInt32.self) tid = encodedData[21...].toBigEndian(UInt32.self) - gain = nil // No GS byte in the longer SetupPod response - rssi = nil // No GS byte in the longer SetupPod response address = encodedData[25...].toBigEndian(UInt32.self) + // These values should be verified elsewhere and appropriately handled. + pulseSize = Double(encodedData[2...].toBigEndian(UInt16.self)) / 100000 + secondsPerBolusPulse = Double(encodedData[4]) / 8 + secondsPerPrimePulse = Double(encodedData[5]) / 8 + primeUnits = Double(encodedData[6]) * Pod.pulseSize + cannulaInsertionUnits = Double(encodedData[7]) * Pod.pulseSize + serviceDuration = TimeInterval.hours(Double(encodedData[8])) + + // These values only included in the shorter 0x15 VersionResponse for the AssignAddress command. + gain = nil + rssi = nil + default: throw MessageBlockError.parseError } @@ -116,7 +155,7 @@ public struct VersionResponse : MessageBlock { extension VersionResponse: CustomDebugStringConvertible { public var debugDescription: String { - return "VersionResponse(lot:\(lot), tid:\(tid), gain:\(gain?.description ?? "NA"), rssi:\(rssi?.description ?? "NA") address:\(Data(bigEndian: address).hexadecimalString), podProgressStatus:\(podProgressStatus), pmVersion:\(pmVersion), piVersion:\(piVersion))" + return "VersionResponse(lot:\(lot), tid:\(tid), address:\(Data(bigEndian: address).hexadecimalString), pmVersion:\(pmVersion), piVersion:\(piVersion), productID:\(productID), podProgressStatus:\(podProgressStatus), gain:\(gain?.description ?? "NA"), rssi:\(rssi?.description ?? "NA"), pulseSize:\(pulseSize?.description ?? "NA"), secondsPerBolusPulse:\(secondsPerBolusPulse?.description ?? "NA"), secondsPerPrimePulse:\(secondsPerPrimePulse?.description ?? "NA"), primeUnits:\(primeUnits?.description ?? "NA"), cannulaInsertionUnits:\(cannulaInsertionUnits?.description ?? "NA"), serviceDuration:\(serviceDuration?.description ?? "NA"), )" } } diff --git a/OmniKit/Model/AlertSlot.swift b/OmniKit/Model/AlertSlot.swift index 8137b853b..f0b6cca7a 100644 --- a/OmniKit/Model/AlertSlot.swift +++ b/OmniKit/Model/AlertSlot.swift @@ -81,23 +81,33 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { // auto-off timer; requires user input every x minutes case autoOffAlarm(active: Bool, countdownDuration: TimeInterval) + // pod suspended reminder, before suspendTime; short beep every 15 minutes if > 30 min, else every 5 minutes + case podSuspendedReminder(active: Bool, suspendTime: TimeInterval) + + // pod suspend time expired alarm, after suspendTime; 2 sets of beeps every min for 3 minutes repeated every 15 minutes + case suspendTimeExpired(suspendTime: TimeInterval) + 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 ", comment: "Description for finish setup") + return LocalizedString("Finish setup reminder", comment: "Description for finish setup reminder") case .expirationAlert: alertName = LocalizedString("Expiration alert", comment: "Description for expiration alert") case .expirationAdvisoryAlarm: - alertName = LocalizedString("Pod expiration advisory alarm", comment: "Description for expiration advisory alarm") + alertName = LocalizedString("Expiration advisory", comment: "Description for expiration advisory") case .shutdownImminentAlarm: - alertName = LocalizedString("Shutdown imminent alarm", comment: "Description for shutdown imminent alarm") + alertName = LocalizedString("Shutdown imminent", comment: "Description for shutdown imminent") case .lowReservoirAlarm: - alertName = LocalizedString("Low reservoir advisory alarm", comment: "Description for low reservoir alarm") + alertName = LocalizedString("Low reservoir advisory", comment: "Description for low reservoir advisory") case .autoOffAlarm: - alertName = LocalizedString("Auto-off alarm", comment: "Description for auto-off alarm") + alertName = LocalizedString("Auto-off", comment: "Description for auto-off") + case .podSuspendedReminder: + alertName = LocalizedString("Pod suspended reminder", comment: "Description for pod suspended reminder") + case .suspendTimeExpired: + alertName = LocalizedString("Suspend time expired", comment: "Description for suspend time expired") } if self.configuration.active == false { alertName += LocalizedString(" (inactive)", comment: "Description for an inactive alert modifier") @@ -125,6 +135,53 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { return AlertConfiguration(alertType: .slot4, active: active, duration: 0, trigger: .unitsRemaining(units), beepRepeat: .every1MinuteFor3MinutesAndRepeatEvery60Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) case .autoOffAlarm(let active, let countdownDuration): return AlertConfiguration(alertType: .slot0, active: active, autoOffModifier: true, duration: .minutes(15), trigger: .timeUntilAlert(countdownDuration), beepRepeat: .every1MinuteFor15Minutes, beepType: .bipBeepBipBeepBipBeepBipBeep) + case .podSuspendedReminder(let active, let suspendTime): + // A suspendTime of 0 is an untimed suspend + let reminderInterval, duration: TimeInterval + let trigger: AlertTrigger + let 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 + } + 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 + } + trigger = .timeUntilAlert(reminderInterval) // Start after reminderInterval has passed + beepType = .beep + } else { + duration = 0 + trigger = .timeUntilAlert(.minutes(0)) + beepRepeat = .once + beepType = .noBeep + } + return AlertConfiguration(alertType: .slot5, 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 + let beepRepeat: BeepRepeat + let beepType: BeepType + if active { + trigger = .timeUntilAlert(suspendTime) + beepRepeat = .every1MinuteFor3MinutesAndRepeatEvery15Minutes + beepType = .bipBeepBipBeepBipBeepBipBeep + } else { + trigger = .timeUntilAlert(.minutes(0)) + beepRepeat = .once + beepType = .noBeep + } + return AlertConfiguration(alertType: .slot6, active: active, duration: 0, trigger: trigger, beepRepeat: beepRepeat, beepType: beepType) } } @@ -170,6 +227,18 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { return nil } self = .autoOffAlarm(active: active, countdownDuration: TimeInterval(countdownDuration)) + case "podSuspendedReminder": + guard let active = rawValue["active"] as? Bool, + let suspendTime = rawValue["suspendTime"] as? Double else + { + return nil + } + self = .podSuspendedReminder(active: active, suspendTime: suspendTime) + case "suspendTimeExpired": + guard let suspendTime = rawValue["suspendTime"] as? Double else { + return nil + } + self = .suspendTimeExpired(suspendTime: suspendTime) default: return nil } @@ -193,6 +262,10 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { return "lowReservoirAlarm" case .autoOffAlarm: return "autoOffAlarm" + case .podSuspendedReminder: + return "podSuspendedReminder" + case .suspendTimeExpired: + return "suspendTimeExpired" } }() @@ -214,6 +287,11 @@ public enum PodAlert: CustomStringConvertible, RawRepresentable, Equatable { case .autoOffAlarm(let active, let countdownDuration): rawValue["active"] = active rawValue["countdownDuration"] = countdownDuration + case .podSuspendedReminder(let active, let suspendTime): + rawValue["active"] = active + rawValue["suspendTime"] = suspendTime + case .suspendTimeExpired(let suspendTime): + rawValue["suspendTime"] = suspendTime default: break } diff --git a/OmniKit/Model/BasalDeliveryTable.swift b/OmniKit/Model/BasalDeliveryTable.swift index b1dacc348..5255a68e6 100644 --- a/OmniKit/Model/BasalDeliveryTable.swift +++ b/OmniKit/Model/BasalDeliveryTable.swift @@ -212,6 +212,9 @@ public struct RateEntry { remainingSegments -= 1 } else { let numSegments = min(maxSegmentsPerEntry, Int(round(remainingPulses / pulsesPerSegment))) + if numSegments == 0 { + break // prevent infinite loop and subsequent malloc crash with certain bad rate values + } remainingSegments -= numSegments let pulseCount = pulsesPerSegment * Double(numSegments) let entry = RateEntry(totalPulses: pulseCount, delayBetweenPulses: delayBetweenPulses) diff --git a/OmniKit/Model/FaultEventCode.swift b/OmniKit/Model/FaultEventCode.swift index 69823485b..8ce4921a1 100644 --- a/OmniKit/Model/FaultEventCode.swift +++ b/OmniKit/Model/FaultEventCode.swift @@ -400,10 +400,7 @@ public struct FaultEventCode: CustomStringConvertible, Equatable { return LocalizedString("Empty reservoir", comment: "Description for Empty reservoir pod fault") case .exceededMaximumPodLife80Hrs: return LocalizedString("Pod expired", comment: "Description for Pod expired pod fault") - case .occluded, - .occlusionCheckValueTooHigh, .occlusionCheckStartup1, .occlusionCheckStartup2, - .occlusionCheckTimeouts1, .occlusionCheckTimeouts2, .occlusionCheckTimeouts3, - .occlusionCheckPulseIssue, .occlusionCheckBolusProblem, .occlusionCheckAboveThreshold: + case .occluded: return LocalizedString("Occlusion detected", comment: "Description for Occlusion detected pod fault") default: return String(format: LocalizedString("Internal pod fault %1$03d", comment: "The format string for Internal pod fault (1: The fault code value)"), rawValue) diff --git a/OmniKit/Model/Pod.swift b/OmniKit/Model/Pod.swift index 636a9627a..32588420c 100644 --- a/OmniKit/Model/Pod.swift +++ b/OmniKit/Model/Pod.swift @@ -9,19 +9,22 @@ import Foundation public struct Pod { - // Volume of insulin in one motor pulse + // Volume of U100 insulin in one motor pulse + // Must agree with value returned by pod during the pairing process. public static let pulseSize: Double = 0.05 - // Number of pulses required to deliver one unit of insulin + // Number of pulses required to deliver one unit of U100 insulin public static let pulsesPerUnit: Double = 1 / Pod.pulseSize // Seconds per pulse for boluses + // Checked to verify it agrees with value returned by pod during the pairing process. public static let secondsPerBolusPulse: Double = 2 // Units per second for boluses public static let bolusDeliveryRate: Double = Pod.pulseSize / Pod.secondsPerBolusPulse // Seconds per pulse for priming/cannula insertion + // Checked to verify it agrees with value returned by pod during the pairing process. public static let secondsPerPrimePulse: Double = 1 // Units per second for priming/cannula insertion @@ -37,6 +40,7 @@ public struct Pod { public static let endOfServiceImminentWindow = TimeInterval(hours: 1) // Total pod service time. A fault is triggered if this time is reached before pod deactivation. + // Checked to verify it agrees with value returned by pod during the pairing process. public static let serviceDuration = TimeInterval(hours: 80) // Nomimal pod life (72 hours) @@ -57,13 +61,15 @@ public struct Pod { // Minimum duration of a single basal schedule entry public static let minimumBasalScheduleEntryDuration = TimeInterval.minutes(30) - // Amount of insulin delivered with 1 second between pulses for priming + // Default amount for priming bolus using secondsPerPrimePulse timing. + // Checked to verify it agrees with value returned by pod during the pairing process. public static let primeUnits = 2.6 - // Amount of insulin delivered with 1 second between pulses for cannula insertion - public static let cannulaInsertionUnitsBase = 0.5 + // Default amount for cannula insertion bolus using secondsPerPrimePulse timing. + // Checked to verify it agrees with value returned by pod during the pairing process. + public static let cannulaInsertionUnits = 0.5 + public static let cannulaInsertionUnitsExtra = 0.0 // edit to add a fixed additional amount of insulin during cannula insertion - public static let cannulaInsertionUnits = cannulaInsertionUnitsBase + cannulaInsertionUnitsExtra // Default and limits for expiration reminder alerts public static let expirationReminderAlertDefaultTimeBeforeExpiration = TimeInterval.hours(2) diff --git a/OmniKit/Model/PodProgressStatus.swift b/OmniKit/Model/PodProgressStatus.swift index c7be50615..cf21fd607 100644 --- a/OmniKit/Model/PodProgressStatus.swift +++ b/OmniKit/Model/PodProgressStatus.swift @@ -30,10 +30,6 @@ public enum PodProgressStatus: UInt8, CustomStringConvertible, Equatable { return self == .fiftyOrLessUnits || self == .aboveFiftyUnits } - public var unfinishedPairing: Bool { - return self.rawValue < PodProgressStatus.aboveFiftyUnits.rawValue - } - public var description: String { switch self { case .initialized: @@ -43,7 +39,7 @@ public enum PodProgressStatus: UInt8, CustomStringConvertible, Equatable { case .reminderInitialized: return LocalizedString("Reminder initialized", comment: "Pod pairing reminder initialized") case .pairingCompleted: - return LocalizedString("Paired completed", comment: "Pod status when pairing completed") + return LocalizedString("Pairing completed", comment: "Pod status when pairing completed") case .priming: return LocalizedString("Priming", comment: "Pod status when priming") case .primingCompleted: diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 0fdc69fdf..bf1b6b193 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -29,8 +29,6 @@ public protocol PodStateObserver: class { public enum OmnipodPumpManagerError: Error { case noPodPaired case podAlreadyPaired - case podAlreadyPrimed - case notReadyForPrime case notReadyForCannulaInsertion } @@ -39,12 +37,8 @@ extension OmnipodPumpManagerError: LocalizedError { switch self { case .noPodPaired: return LocalizedString("No pod paired", comment: "Error message shown when no pod is paired") - case .podAlreadyPrimed: - return LocalizedString("Pod already primed", comment: "Error message shown when prime is attempted, but pod is already primed") case .podAlreadyPaired: return LocalizedString("Pod already paired", comment: "Error message shown when user cannot pair because pod is already paired") - case .notReadyForPrime: - return LocalizedString("Pod is not in a state ready for priming.", comment: "Error message when prime fails because the pod is in an unexpected state") 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") } @@ -54,12 +48,8 @@ extension OmnipodPumpManagerError: LocalizedError { switch self { case .noPodPaired: return nil - case .podAlreadyPrimed: - return nil case .podAlreadyPaired: return nil - case .notReadyForPrime: - return nil case .notReadyForCannulaInsertion: return nil } @@ -69,12 +59,8 @@ extension OmnipodPumpManagerError: LocalizedError { switch self { case .noPodPaired: return LocalizedString("Please pair a new pod", comment: "Recover suggestion shown when no pod is paired") - case .podAlreadyPrimed: - return nil case .podAlreadyPaired: return nil - case .notReadyForPrime: - return nil case .notReadyForCannulaInsertion: return nil } @@ -476,7 +462,7 @@ extension OmnipodPumpManager { private func jumpStartPod(address: UInt32, lot: UInt32, tid: UInt32, fault: DetailedStatus? = nil, startDate: Date? = nil, mockFault: Bool) { let start = startDate ?? Date() var podState = PodState(address: address, piVersion: "jumpstarted", pmVersion: "jumpstarted", lot: lot, tid: tid) - podState.setupProgress = .podConfigured + podState.setupProgress = .podPaired podState.activatedAt = start podState.expiresAt = start + .hours(72) @@ -499,7 +485,7 @@ extension OmnipodPumpManager { #if targetEnvironment(simulator) // If we're in the simulator, create a mock PodState let mockFaultDuringPairing = false - let mockCommsErrorDuringPairing = true + let mockCommsErrorDuringPairing = false DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + .seconds(2)) { self.jumpStartPod(address: 0x1f0b3557, lot: 40505, tid: 6439, mockFault: mockFaultDuringPairing) let fault: DetailedStatus? = self.setStateWithResult({ (state) in @@ -517,13 +503,13 @@ extension OmnipodPumpManager { } #else let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice - let configureAndPrimeSession = { (result: PodComms.SessionRunResult) in + let primeSession = { (result: PodComms.SessionRunResult) in switch result { case .success(let session): // We're on the session queue session.assertOnSessionQueue() - self.log.default("Beginning pod configuration and prime") + self.log.default("Beginning pod prime") // Clean up any previously un-stored doses if needed let unstoredDoses = self.state.unstoredDoses @@ -544,21 +530,16 @@ extension OmnipodPumpManager { } } - let needsPairing = setStateWithResult({ (state) -> PumpManagerResult in + let needsPairing = setStateWithResult({ (state) -> Bool in guard let podState = state.podState else { - return .success(true) // Needs pairing + return true // Needs pairing } - guard podState.setupProgress.primingNeeded else { - return .failure(OmnipodPumpManagerError.podAlreadyPrimed) - } - - // If still need configuring, run pair() - return .success(podState.setupProgress == .addressAssigned) + // Return true if not yet paired + return podState.setupProgress.isPaired == false }) - switch needsPairing { - case .success(true): + if needsPairing { self.log.default("Pairing pod before priming") // Create random address with 20 bits to match PDM, could easily use 24 bits instead @@ -577,17 +558,15 @@ extension OmnipodPumpManager { } // Calls completion - configureAndPrimeSession(result) + primeSession(result) } - case .success(false): + } else { self.log.default("Pod already paired. Continuing.") - self.podComms.runSession(withName: "Configure and prime pod", using: deviceSelector) { (result) in + self.podComms.runSession(withName: "Prime pod", using: deviceSelector) { (result) in // Calls completion - configureAndPrimeSession(result) + primeSession(result) } - case .failure(let error): - completion(.failure(error)) } #endif } @@ -648,11 +627,6 @@ extension OmnipodPumpManager { } let finishWait = try session.insertCannula() - - DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + finishWait) { - // Runs a new session - self.checkCannulaInsertionFinished() - } completion(.success(finishWait)) } catch let error { completion(.failure(error)) @@ -664,40 +638,35 @@ extension OmnipodPumpManager { #endif } - private func emitConfirmationBeep(session: PodCommsSession, beepConfigType: BeepConfigType) { - if self.confirmationBeeps { - let _ = session.beepConfig(beepConfigType: beepConfigType, basalCompletionBeep: true, tempBasalCompletionBeep: tempBasalConfirmationBeeps, bolusCompletionBeep: true) - } - } - - public func checkCannulaInsertionFinished() { + public func checkCannulaInsertionFinished(completion: @escaping (Error?) -> Void) { let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Check cannula insertion finished", using: deviceSelector) { (result) in switch result { case .success(let session): do { try session.checkInsertionCompleted() + completion(nil) } catch let error { self.log.error("Failed to fetch pod status: %{public}@", String(describing: error)) + completion(error) } case .failure(let error): self.log.error("Failed to fetch pod status: %{public}@", String(describing: error)) + completion(error) } } } - public func refreshStatus(completion: ((_ result: PumpManagerResult) -> Void)? = nil) { + public func refreshStatus(emitConfirmationBeep: Bool = false, completion: ((_ result: PumpManagerResult) -> Void)? = nil) { guard self.hasActivePod else { completion?(.failure(OmnipodPumpManagerError.noPodPaired)) return } - self.getPodStatus(storeDosesOnSuccess: false, completion: completion) + self.getPodStatus(storeDosesOnSuccess: false, emitConfirmationBeep: emitConfirmationBeep, completion: completion) } - // MARK: - Pump Commands - - private func getPodStatus(storeDosesOnSuccess: Bool, completion: ((_ result: PumpManagerResult) -> Void)? = nil) { + private func getPodStatus(storeDosesOnSuccess: Bool, emitConfirmationBeep: Bool, completion: ((_ result: PumpManagerResult) -> Void)? = nil) { guard state.podState?.unfinalizedBolus?.scheduledCertainty == .uncertain || state.podState?.unfinalizedBolus?.isFinished != false else { self.log.info("Skipping status request due to unfinalized bolus in progress.") completion?(.failure(PodCommsError.unfinalizedBolus)) @@ -709,7 +678,9 @@ extension OmnipodPumpManager { do { switch result { case .success(let session): - let status = try session.getStatus() + let beepType: BeepConfigType? = self.confirmationBeeps && emitConfirmationBeep ? .bipBip : nil + let status = try session.getStatus(confirmationBeepType: beepType) + if storeDosesOnSuccess { session.dosesForStorage({ (doses) -> Bool in self.store(doses: doses, in: session) @@ -726,6 +697,8 @@ extension OmnipodPumpManager { } } + // MARK: - Pump Commands + public func acknowledgeAlerts(_ alertsToAcknowledge: AlertSet, completion: @escaping (_ alerts: [AlertSlot: PodAlert]?) -> Void) { guard self.hasActivePod else { completion(nil) @@ -744,8 +717,8 @@ extension OmnipodPumpManager { } do { - let alerts = try session.acknowledgeAlerts(alerts: alertsToAcknowledge) - self.emitConfirmationBeep(session: session, beepConfigType: .bipBip) + let beepType: BeepConfigType? = self.confirmationBeeps ? .bipBip : nil + let alerts = try session.acknowledgeAlerts(alerts: alertsToAcknowledge, confirmationBeepType: beepType) completion(alerts) } catch { completion(nil) @@ -755,25 +728,17 @@ extension OmnipodPumpManager { public func setTime(completion: @escaping (Error?) -> Void) { - let timeZone = TimeZone.currentFixed - - let preError = setStateWithResult { (state) -> Error? in - guard state.hasActivePod else { - return OmnipodPumpManagerError.noPodPaired - } - - guard state.podState?.unfinalizedBolus?.isFinished != false else { - return PodCommsError.unfinalizedBolus - } - - return nil + guard state.hasActivePod else { + completion(OmnipodPumpManagerError.noPodPaired) + return } - if let error = preError { - completion(error) + guard state.podState?.unfinalizedBolus?.isFinished != false else { + completion(PodCommsError.unfinalizedBolus) return } + let timeZone = TimeZone.currentFixed let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Set time zone", using: rileyLinkSelector) { (result) in switch result { @@ -827,7 +792,7 @@ extension OmnipodPumpManager { switch result { case .success(let session): let scheduleOffset = timeZone.scheduleOffset(forDate: Date()) - let result = session.cancelDelivery(deliveryType: .all, beepType: .noBeep) + let result = session.cancelDelivery(deliveryType: .all) switch result { case .certainFailure(let error): throw error @@ -911,7 +876,7 @@ extension OmnipodPumpManager { public func readPodStatus(completion: @escaping (Result) -> Void) { // use hasSetupPod to be able to read pod info from a faulted Pod guard self.hasSetupPod else { - completion(.failure(PodCommsError.noPodPaired)) + completion(.failure(OmnipodPumpManagerError.noPodPaired)) return } @@ -920,8 +885,8 @@ extension OmnipodPumpManager { do { switch result { case .success(let session): - let detailedStatus = try session.getDetailedStatus() - self.emitConfirmationBeep(session: session, beepConfigType: .bipBip) + let beepType: BeepConfigType? = self.confirmationBeeps ? .bipBip : nil + let detailedStatus = try session.getDetailedStatus(confirmationBeepType: beepType) session.dosesForStorage({ (doses) -> Bool in self.store(doses: doses, in: session) }) @@ -947,8 +912,8 @@ extension OmnipodPumpManager { switch result { case .success(let session): do { - try session.testingCommands() - self.emitConfirmationBeep(session: session, beepConfigType: .beepBeepBeep) + let beepType: BeepConfigType? = self.confirmationBeeps ? .beepBeepBeep : nil + try session.testingCommands(confirmationBeepType: beepType) completion(nil) } catch let error { completion(error) @@ -964,7 +929,7 @@ extension OmnipodPumpManager { completion(OmnipodPumpManagerError.noPodPaired) return } - guard self.state.podState?.unfinalizedBolus?.isFinished != false else { + 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 @@ -991,15 +956,16 @@ extension OmnipodPumpManager { } } - public func readPulseLog(completion: @escaping (String) -> Void) { - // use hasSetupPod to be able to read the pulse log from a faulted Pod + public func readPulseLog(completion: @escaping (Result) -> Void) { + // use hasSetupPod to be able to read pulse log from a faulted Pod guard self.hasSetupPod else { - completion(PodCommsError.noPodPaired.localizedDescription) + completion(.failure(OmnipodPumpManagerError.noPodPaired)) return } - if self.state.podState?.isFaulted == false && self.state.podState?.unfinalizedBolus?.isFinished == false { + 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(PodCommsError.unfinalizedBolus.localizedDescription) + completion(.failure(PodCommsError.unfinalizedBolus)) return } @@ -1009,34 +975,21 @@ extension OmnipodPumpManager { case .success(let session): do { // read the most recent 50 entries from the pulse log - self.emitConfirmationBeep(session: session, beepConfigType: .bipBip) - let podInfoResponse1 = try session.readPodInfo(podInfoResponseSubType: .pulseLogRecent) - guard let podInfoPulseLogRecent = podInfoResponse1.podInfo as? PodInfoPulseLogRecent else { - self.log.error("Unable to decode for PulseLogRecent: %s", String(describing: podInfoResponse1)) - completion(PodCommsError.unexpectedResponse(response: .podInfoResponse).localizedDescription) + let beepType: BeepConfigType? = self.confirmationBeeps ? .bipBeeeeep : nil + let podInfoResponse = try session.readPodInfo(podInfoResponseSubType: .pulseLogRecent, confirmationBeepType: beepType) + 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 } - var lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry) - var str = pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber) - - // read up to the previous 50 entries from the pulse log - self.emitConfirmationBeep(session: session, beepConfigType: .bipBip) - let podInfoResponse2 = try session.readPodInfo(podInfoResponseSubType: .pulseLogPrevious) - guard let podInfoPulseLogPrevious = podInfoResponse2.podInfo as? PodInfoPulseLogPrevious else { - self.log.error("Unable to decode for PulseLogPrevious: %s", String(describing: podInfoResponse1)) - completion(PodCommsError.unexpectedResponse(response: .podInfoResponse).localizedDescription) - return - } - lastPulseNumber -= podInfoPulseLogRecent.pulseLog.count - str += pulseLogString(pulseLogEntries: podInfoPulseLogPrevious.pulseLog, lastPulseNumber: lastPulseNumber) - - self.emitConfirmationBeep(session: session, beepConfigType: .beeeeeep) - completion(str) + let lastPulseNumber = Int(podInfoPulseLogRecent.indexLastEntry) + let str = pulseLogString(pulseLogEntries: podInfoPulseLogRecent.pulseLog, lastPulseNumber: lastPulseNumber) + completion(.success(str)) } catch let error { - completion(error.localizedDescription) + completion(.failure(error)) } case .failure(let error): - completion(error.localizedDescription) + completion(.failure(error)) } } } @@ -1160,6 +1113,7 @@ extension OmnipodPumpManager: PumpManager { // MARK: Methods public func suspendDelivery(completion: @escaping (Error?) -> Void) { + let suspendTime: TimeInterval = 0 // Place holder for untimed suspends until interface is updated guard self.hasActivePod else { completion(OmnipodPumpManagerError.noPodPaired) return @@ -1186,8 +1140,9 @@ extension OmnipodPumpManager: PumpManager { state.suspendEngageState = .engaging }) - let beepType: BeepType = self.confirmationBeeps ? .beeeeeep : .noBeep - let result = session.cancelDelivery(deliveryType: .all, beepType: beepType) + // use confirmationBeepType here for confirmation beeps to avoid getting 3 beeps! + let beepType: BeepConfigType? = self.confirmationBeeps ? .beeeeeep : nil + let result = session.suspendDelivery(suspendTime: suspendTime, confirmationBeepType: beepType) switch result { case .certainFailure(let error): completion(error) @@ -1233,6 +1188,7 @@ extension OmnipodPumpManager: PumpManager { let scheduleOffset = self.state.timeZone.scheduleOffset(forDate: Date()) let beep = self.confirmationBeeps let _ = try session.resumeBasal(schedule: self.state.basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: beep, completionBeep: beep) + try session.cancelSuspendAlerts() session.dosesForStorage() { (doses) -> Bool in return self.store(doses: doses, in: session) } @@ -1278,7 +1234,7 @@ extension OmnipodPumpManager: PumpManager { return // No active pod case true?: log.default("Fetching status because pumpData is too old") - getPodStatus(storeDosesOnSuccess: true) { (response) in + getPodStatus(storeDosesOnSuccess: true, emitConfirmationBeep: false) { (response) in self.pumpDelegate.notify({ (delegate) in switch response { case .success: @@ -1413,7 +1369,7 @@ extension OmnipodPumpManager: PumpManager { } } - // when cancelling a bolus give a type 6 beeeeeep to match PDM if doing bolus confirmation beeps + // when cancelling a bolus use the built-in type 6 beeeeeep to match PDM if confirmation beeps are enabled let beeptype: BeepType = self.confirmationBeeps ? .beeeeeep : .noBeep let result = session.cancelDelivery(deliveryType: .bolus, beepType: beeptype) switch result { @@ -1457,22 +1413,14 @@ extension OmnipodPumpManager: PumpManager { } do { - let preError = self.setStateWithResult({ (state) -> PodCommsError? in - if case .some(.suspended) = state.podState?.suspendState { - self.log.info("Not enacting temp basal because podState indicates pod is suspended.") - return .podSuspended - } - - guard state.podState?.unfinalizedBolus?.isFinished != false else { - self.log.info("Not enacting temp basal because podState indicates unfinalized bolus in progress.") - return .unfinalizedBolus - } - - return nil - }) + if case .some(.suspended) = self.state.podState?.suspendState { + self.log.info("Not enacting temp basal because podState indicates pod is suspended.") + throw PodCommsError.podSuspended + } - if let error = preError { - throw error + guard self.state.podState?.unfinalizedBolus?.isFinished != false else { + self.log.info("Not enacting temp basal because podState indicates unfinalized bolus in progress.") + throw PodCommsError.unfinalizedBolus } let status: StatusResponse diff --git a/OmniKit/PumpManager/PodComms.swift b/OmniKit/PumpManager/PodComms.swift index d48e77077..3bbb9780c 100644 --- a/OmniKit/PumpManager/PodComms.swift +++ b/OmniKit/PumpManager/PodComms.swift @@ -11,6 +11,7 @@ import RileyLinkBLEKit import LoopKit import os.log +fileprivate var diagnosePairingRssi = false protocol PodCommsDelegate: class { func podComms(_ podComms: PodComms, didChange podState: PodState) @@ -60,9 +61,11 @@ class PodComms: CustomDebugStringConvertible { /// - PodCommsError.emptyResponse /// - PodCommsError.unexpectedResponse /// - PodCommsError.podChange + /// - PodCommsError.activationTimeExceeded /// - PodCommsError.rssiTooLow /// - PodCommsError.rssiTooHigh - /// - PodCommsError.activationTimeExceeded + /// - PodCommsError.diagnosticMessage + /// - PodCommsError.podIncompatible /// - MessageError.invalidCrc /// - MessageError.invalidSequence /// - MessageError.invalidAddress @@ -131,12 +134,16 @@ class PodComms: CustomDebugStringConvertible { throw PodCommsError.podChange } - // Checking RSSI + // Check the pod RSSI let maxRssiAllowed = 59 // maximum RSSI limit allowed let minRssiAllowed = 30 // minimum RSSI limit allowed if let rssi = config.rssi, let gain = config.gain { - let rssiStr = String(format: "Receiver Low Gain: %d.\nReceived Signal Strength Indicator: %d", gain, rssi) - log.default("%s", rssiStr) + let rssiStr = String(format: "RSSI: %u.\nReceiver Low Gain: %u", rssi, gain) + log.default("%@", rssiStr) + if diagnosePairingRssi { + throw PodCommsError.diagnosticMessage(str: rssiStr) + } + rssiRetries -= 1 if rssi < minRssiAllowed { log.default("RSSI value %d is less than minimum allowed value of %d, %d retries left", rssi, minRssiAllowed, rssiRetries) @@ -175,9 +182,44 @@ class PodComms: CustomDebugStringConvertible { throw PodCommsError.activationTimeExceeded } - if config.podProgressStatus == .pairingCompleted { - log.info("Version Response %{public}@ indicates pairing is complete, moving pod to configured state", String(describing: config)) - self.podState?.setupProgress = .podConfigured + // It's unlikely that Insulet will release an updated Eros pod using any different fundemental values, + // so just verify that the fundemental pod constants returned match the expected constant values in the Pod struct. + // To actually be able to handle different fundemental values in Loop things would need to be reworked to save + // these values in some persistent PodState and then make sure that everything properly works using these values. + var errorStrings: [String] = [] + if let pulseSize = config.pulseSize, pulseSize != Pod.pulseSize { + errorStrings.append(String(format: "Pod reported pulse size of %.3fU different than expected %.3fU", pulseSize, Pod.pulseSize)) + } + if let secondsPerBolusPulse = config.secondsPerBolusPulse, secondsPerBolusPulse != Pod.secondsPerBolusPulse { + errorStrings.append(String(format: "Pod reported seconds per pulse rate of %.1f different than expected %.1f", secondsPerBolusPulse, Pod.secondsPerBolusPulse)) + } + if let secondsPerPrimePulse = config.secondsPerPrimePulse, secondsPerPrimePulse != Pod.secondsPerPrimePulse { + errorStrings.append(String(format: "Pod reported seconds per prime pulse rate of %.1f different than expected %.1f", secondsPerPrimePulse, Pod.secondsPerPrimePulse)) + } + if let primeUnits = config.primeUnits, primeUnits != Pod.primeUnits { + errorStrings.append(String(format: "Pod reported prime bolus of %.2fU different than expected %.2fU", primeUnits, Pod.primeUnits)) + } + if let cannulaInsertionUnits = config.cannulaInsertionUnits, Pod.cannulaInsertionUnits != cannulaInsertionUnits { + errorStrings.append(String(format: "Pod reported cannula insertion bolus of %.2fU different than expected %.2fU", cannulaInsertionUnits, Pod.cannulaInsertionUnits)) + } + if let serviceDuration = config.serviceDuration { + if serviceDuration < Pod.serviceDuration { + errorStrings.append(String(format: "Pod reported service duration of %.0f hours shorter than expected %.0f", serviceDuration.hours, Pod.serviceDuration.hours)) + } else if serviceDuration > Pod.serviceDuration { + log.info("Pod reported service duration of %.0f hours limited to expected %.0f", serviceDuration.hours, Pod.serviceDuration.hours) + } + } + + let errMess = errorStrings.joined(separator: ".\n") + if errMess.isEmpty == false { + log.error("%@", errMess) + self.podState?.setupProgress = .podIncompatible + throw PodCommsError.podIncompatible(str: errMess) + } + + if config.podProgressStatus == .pairingCompleted && self.podState?.setupProgress.isPaired == false { + log.info("Version Response %{public}@ indicates pairing is now complete", String(describing: config)) + self.podState?.setupProgress = .podPaired } return config @@ -224,8 +266,11 @@ class PodComms: CustomDebugStringConvertible { versionResponse = try sendPairMessage(address: podState.address, transport: transport, message: message) } catch let error { if case PodCommsError.podAckedInsteadOfReturningResponse = error { - log.default("SetupPod acked instead of returning response. Moving pod to configured state.") - self.podState?.setupProgress = .podConfigured + log.default("SetupPod acked instead of returning response.") + if self.podState?.setupProgress.isPaired == false { + log.default("Moving pod to paired state.") + self.podState?.setupProgress = .podPaired + } return } log.error("SetupPod returns error %{public}@", String(describing: error)) @@ -238,7 +283,12 @@ class PodComms: CustomDebugStringConvertible { } } - func assignAddressAndSetupPod(address: UInt32, using deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void, timeZone: TimeZone, messageLogger: MessageLogger?, _ block: @escaping (_ result: SessionRunResult) -> Void) + func assignAddressAndSetupPod( + address: UInt32, + using deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void, + timeZone: TimeZone, + messageLogger: MessageLogger?, + _ block: @escaping (_ result: SessionRunResult) -> Void) { deviceSelector { (device) in guard let device = device else { @@ -259,11 +309,11 @@ class PodComms: CustomDebugStringConvertible { return } - if self.podState!.setupProgress != .podConfigured { + if self.podState!.setupProgress.isPaired == false { try self.setupPod(podState: self.podState!, timeZone: timeZone, commandSession: commandSession) } - guard self.podState!.setupProgress == .podConfigured else { + guard self.podState!.setupProgress.isPaired else { self.log.error("Unexpected podStatus setupProgress value of %{public}@", String(describing: self.podState!.setupProgress)) throw PodCommsError.invalidData } diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index 71b59a928..79e71a06e 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -33,6 +33,8 @@ public enum PodCommsError: Error { case activationTimeExceeded case rssiTooLow case rssiTooHigh + case diagnosticMessage(str: String) + case podIncompatible(str: String) } extension PodCommsError: LocalizedError { @@ -81,6 +83,10 @@ extension PodCommsError: LocalizedError { return LocalizedString("Poor signal strength", comment: "Format string for poor pod signal strength") case .rssiTooHigh: // only occurs when RileyLink is too close to the pod for reliable pairing return LocalizedString("Signal strength too high", comment: "Format string for pod signal strength too high") + case .diagnosticMessage(let str): + return str + case .podIncompatible(let str): + return str } } @@ -95,7 +101,7 @@ extension PodCommsError: LocalizedError { case .invalidData: return nil case .noResponse: - return LocalizedString("Please try repositioning the pod or the RileyLink and try again", comment: "Recovery suggestion when no response is received from pod") + return LocalizedString("Please try repositioning the RileyLink and try again", comment: "Recovery suggestion when no response is received from pod") case .emptyResponse: return nil case .podAckedInsteadOfReturningResponse: @@ -132,6 +138,19 @@ extension PodCommsError: LocalizedError { return LocalizedString("Please reposition the RileyLink relative to the pod", comment: "Recovery suggestion when pairing signal strength is too low") case .rssiTooHigh: return LocalizedString("Please reposition the RileyLink further from the pod", comment: "Recovery suggestion when pairing signal strength is too high") + case .diagnosticMessage: + return nil + case .podIncompatible: + return nil + } + } + + public var isFaulted: Bool { + switch self { + case .podFault, .activationTimeExceeded, .podIncompatible: + return true + default: + return false } } } @@ -186,6 +205,7 @@ public class PodCommsSession { /// /// - Parameters: /// - messageBlocks: The message blocks to send + /// - confirmationBeepType: If specified, type of confirmation beep to append as a separate beep message block /// - 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: @@ -196,12 +216,17 @@ public class PodCommsSession { /// - PodCommsError.nonceResyncFailed /// - MessageError /// - RileyLinkDeviceError - func send(_ messageBlocks: [MessageBlock], expectFollowOnMessage: Bool = false) throws -> T { + func send(_ messageBlocks: [MessageBlock], confirmationBeepType: BeepConfigType? = nil, expectFollowOnMessage: Bool = false) throws -> T { var triesRemaining = 2 // Retries only happen for nonce resync - var blocksToSend = messageBlocks + // If a confirmation beep type was specified & pod isn't faulted, append a beep config message block to emit the requested beep type + if let confirmationBeepType = confirmationBeepType, podState.isFaulted == false { + let confirmationBeepBlock = BeepConfigCommand(beepConfigType: confirmationBeepType, basalCompletionBeep: true, tempBasalCompletionBeep: false, bolusCompletionBeep: true) + blocksToSend += [confirmationBeepBlock] + } + if blocksToSend.contains(where: { $0 as? NonceResyncableMessageBlock != nil }) { podState.advanceToNextNonce() } @@ -271,19 +296,19 @@ public class PodCommsSession { // Returns time at which prime is expected to finish. public func prime() throws -> TimeInterval { - //4c00 00c8 0102 - - let primeDuration = TimeInterval(seconds: 55) // a bit more than (Pod.primeUnits / Pod.primeDeliveryRate) + let primeDuration: TimeInterval = .seconds(Pod.primeUnits / Pod.primeDeliveryRate) + 3 // as per PDM - // Skip following alerts if we've already done them before - if podState.setupProgress != .startingPrime { - - // The following will set Tab5[$16] to 0 during pairing, which disables $6x faults. + // If priming has never been attempted on this pod, handle the pre-prime setup tasks. + // A FaultConfig can only be done before the prime bolus or the pod will generate an 049 fault. + if podState.setupProgress.primingNeverAttempted { + // This FaultConfig command will set Tab5[$16] to 0 during pairing, which disables $6x faults let _: StatusResponse = try send([FaultConfigCommand(nonce: podState.currentNonce, tab5Sub16: 0, tab5Sub17: 0)]) + + // Set up the finish pod setup reminder alert which beeps every 5 minutes for 1 hour let finishSetupReminder = PodAlert.finishSetupReminder try configureAlerts([finishSetupReminder]) } else { - // We started prime, but didn't get confirmation somehow, so check status + // Not the first time through, check to see if prime bolus was successfully started let status: StatusResponse = try send([GetStatusCommand()]) podState.updateFromStatusResponse(status) if status.podProgressStatus == .priming || status.podProgressStatus == .primingCompleted { @@ -292,7 +317,7 @@ public class PodCommsSession { } } - // Mark 2.6U delivery with 1 second between pulses for prime + // Mark Pod.primeUnits (2.6U) bolus delivery with Pod.primeDeliveryRate (1) between pulses for prime let primeFinishTime = Date() + primeDuration podState.primeFinishTime = primeFinishTime @@ -327,10 +352,10 @@ public class PodCommsSession { } @discardableResult - private func configureAlerts(_ alerts: [PodAlert]) throws -> StatusResponse { + private func configureAlerts(_ alerts: [PodAlert], confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse { let configurations = alerts.map { $0.configuration } let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations) - let status: StatusResponse = try send([configureAlerts]) + let status: StatusResponse = try send([configureAlerts], confirmationBeepType: confirmationBeepType) for alert in alerts { podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert) } @@ -364,7 +389,8 @@ public class PodCommsSession { } public func insertCannula() throws -> TimeInterval { - let insertionWait: TimeInterval = .seconds(Pod.cannulaInsertionUnits / Pod.primeDeliveryRate) + let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra + let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate) guard let activatedAt = podState.activatedAt else { throw PodCommsError.noPodPaired @@ -394,14 +420,14 @@ public class PodCommsSession { try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm]) } - // Mark 0.5U delivery with 1 second between pulses for cannula insertion + // Mark cannulaInsertionUnits (0.5U) bolus delivery with Pod.secondsPerPrimePulse (1) between pulses for cannula insertion let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerPrimePulse) - let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: Pod.cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses) + let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses) let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, deliverySchedule: bolusSchedule) podState.setupProgress = .startingInsertCannula - let bolusExtraCommand = BolusExtraCommand(units: Pod.cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses) + let bolusExtraCommand = BolusExtraCommand(units: cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses) let status2: StatusResponse = try send([bolusScheduleCommand, bolusExtraCommand]) podState.updateFromStatusResponse(status2) @@ -450,6 +476,7 @@ public class PodCommsSession { do { let statusResponse: StatusResponse = try send([bolusScheduleCommand, bolusExtraCommand]) podState.unfinalizedBolus = UnfinalizedDose(bolusAmount: units, startTime: Date().addingTimeInterval(commsOffset), scheduledCertainty: .certain) + podState.updateFromStatusResponse(statusResponse) return DeliveryCommandResult.success(statusResponse: statusResponse) } catch PodCommsError.nonceResyncFailed { return DeliveryCommandResult.certainFailure(error: PodCommsError.nonceResyncFailed) @@ -535,28 +562,78 @@ public class PodCommsSession { return canceledDose } - // cancelDelivery() implements a smart interface to the Pod's cancel delivery command - public func cancelDelivery(deliveryType: CancelDeliveryCommand.DeliveryType, beepType: BeepType) -> CancelDeliveryResult { - var message: [MessageBlock] - - // Special case handling for a non-silent cancel all which would normally emit 3 sets of beeps! - if beepType != .noBeep && deliveryType == .all { - // For this case use two cancel commands in a one message with the 1st command silently cancelling all but the basal - // and the 2nd command cancelling only the basal with the specified beepType so there will only be a single beep sequence. - message = [CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .allButBasal, beepType: .noBeep), - CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .basal, beepType: beepType)] - } else { - message = [CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: deliveryType, beepType: beepType)] - } + // Suspends insulin delivery and sets appropriate podSuspendedReminder & suspendTimeExpired alerts. + // An expected suspend time of 0 is an untimed suspend which only uses podSuspendedReminder alert beeps. + // An expected suspend time of > 0 and <= 5 minutes will only use suspendTimeExpired alert beeps. + public func suspendDelivery(suspendTime: TimeInterval = 0, confirmationBeepType: BeepConfigType? = nil) -> CancelDeliveryResult { do { - let status: StatusResponse = try send(message) + var alertConfigurations: [AlertConfiguration] = [] - let canceledDose = handleCancelDosing(deliveryType: deliveryType, bolusNotDelivered: status.bolusNotDelivered) + // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time. + let podSuspendedReminder = PodAlert.podSuspendedReminder(active: true, suspendTime: suspendTime) + let usePodSuspendedReminder = suspendTime == 0 || suspendTime > .minutes(5) // untimed or long enough suspend + if usePodSuspendedReminder { + alertConfigurations += [podSuspendedReminder.configuration] + } + + // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed. + let suspendTimeExpired = PodAlert.suspendTimeExpired(suspendTime: suspendTime) + let useSuspendTimeExpired = suspendTime > 0 // timed suspend + if useSuspendTimeExpired { + alertConfigurations += [suspendTimeExpired.configuration] + } + let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: alertConfigurations) + let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeep) + let status: StatusResponse = try send([cancelDeliveryCommand, configureAlerts], confirmationBeepType: confirmationBeepType) + + let canceledDose = handleCancelDosing(deliveryType: .all, bolusNotDelivered: status.bolusNotDelivered) podState.updateFromStatusResponse(status) + if usePodSuspendedReminder { + podState.registerConfiguredAlert(slot: podSuspendedReminder.configuration.slot, alert: podSuspendedReminder) + } + if useSuspendTimeExpired { + podState.registerConfiguredAlert(slot: suspendTimeExpired.configuration.slot, alert: suspendTimeExpired) + } + return CancelDeliveryResult.success(statusResponse: status, canceledDose: canceledDose) + } catch PodCommsError.nonceResyncFailed { + return CancelDeliveryResult.certainFailure(error: PodCommsError.nonceResyncFailed) + } catch PodCommsError.rejectedMessage(let errorCode) { + return CancelDeliveryResult.certainFailure(error: PodCommsError.rejectedMessage(errorCode: errorCode)) + } catch let error { + podState.unfinalizedSuspend = UnfinalizedDose(suspendStartTime: Date(), scheduledCertainty: .uncertain) + return CancelDeliveryResult.uncertainFailure(error: error as? PodCommsError ?? PodCommsError.commsError(error: error)) + } + } + + // Cancels any suspend related alerts, should be called when resuming after using suspendDelivery() + @discardableResult + public 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 status = try configureAlerts([podSuspendedReminder, suspendTimeExpired]) + return status + } catch let error { + throw error + } + } + + // Cancel beeping can be done implemented using beepType (for a single delivery type) or a separate confirmation beep message block (for cancel all). + // 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 = .noBeep, confirmationBeepType: BeepConfigType? = nil) -> CancelDeliveryResult { + + do { + let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: deliveryType, beepType: beepType) + let status: StatusResponse = try send([cancelDeliveryCommand], confirmationBeepType: confirmationBeepType) + + let canceledDose = handleCancelDosing(deliveryType: deliveryType, bolusNotDelivered: status.bolusNotDelivered) + podState.updateFromStatusResponse(status) + return CancelDeliveryResult.success(statusResponse: status, canceledDose: canceledDose) } catch PodCommsError.nonceResyncFailed { return CancelDeliveryResult.certainFailure(error: PodCommsError.nonceResyncFailed) } catch PodCommsError.rejectedMessage(let errorCode) { @@ -567,12 +644,12 @@ public class PodCommsSession { } } - public func testingCommands() throws { - try cancelNone() // reads status & verifies nonce by doing a cancel none + public func testingCommands(confirmationBeepType: BeepConfigType? = nil) throws { + try cancelNone(confirmationBeepType: confirmationBeepType) // reads status & verifies nonce by doing a cancel none } - public func setTime(timeZone: TimeZone, basalSchedule: BasalSchedule, date: Date, acknowledgementBeep: Bool, completionBeep: Bool) throws -> StatusResponse { - let result = cancelDelivery(deliveryType: .all, beepType: .noBeep) + public func setTime(timeZone: TimeZone, basalSchedule: BasalSchedule, date: Date, acknowledgementBeep: Bool = false, completionBeep: Bool = false) throws -> StatusResponse { + let result = cancelDelivery(deliveryType: .all) switch result { case .certainFailure(let error): throw error @@ -617,11 +694,12 @@ public class PodCommsSession { } // use cancelDelivery with .none to get status as well as to validate & advance the nonce + // Throws PodCommsError @discardableResult - public func cancelNone() throws -> StatusResponse { + public func cancelNone(confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse { var statusResponse: StatusResponse - let cancelResult: CancelDeliveryResult = cancelDelivery(deliveryType: .none, beepType: .noBeep) + let cancelResult: CancelDeliveryResult = cancelDelivery(deliveryType: .none, confirmationBeepType: confirmationBeepType) switch cancelResult { case .certainFailure(let error): throw error @@ -634,19 +712,20 @@ public class PodCommsSession { return statusResponse } + // Throws PodCommsError @discardableResult - public func getStatus() throws -> StatusResponse { + public func getStatus(confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse { if useCancelNoneForStatus { - return try cancelNone() // functional replacement for getStatus() + return try cancelNone(confirmationBeepType: confirmationBeepType) // functional replacement for getStatus() } - let statusResponse: StatusResponse = try send([GetStatusCommand()]) + let statusResponse: StatusResponse = try send([GetStatusCommand()], confirmationBeepType: confirmationBeepType) podState.updateFromStatusResponse(statusResponse) return statusResponse } @discardableResult - public func getDetailedStatus() throws -> DetailedStatus { - let infoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: .detailedStatus)]) + public func getDetailedStatus(confirmationBeepType: BeepConfigType? = nil) throws -> DetailedStatus { + let infoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: .detailedStatus)], confirmationBeepType: confirmationBeepType) guard let detailedStatus = infoResponse.podInfo as? DetailedStatus else { throw PodCommsError.unexpectedResponse(response: .podInfoResponse) @@ -661,17 +740,19 @@ public class PodCommsSession { } @discardableResult - public func readPodInfo(podInfoResponseSubType: PodInfoResponseSubType) throws -> PodInfoResponse { - let podInfoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: podInfoResponseSubType)]) + public func readPodInfo(podInfoResponseSubType: PodInfoResponseSubType, confirmationBeepType: BeepConfigType? = nil) throws -> PodInfoResponse { + let podInfoCommand = GetStatusCommand(podInfoType: podInfoResponseSubType) + let podInfoResponse: PodInfoResponse = try send([podInfoCommand], confirmationBeepType: confirmationBeepType) return podInfoResponse } + // Can be called a second time to deactivate a given pod public func deactivatePod() throws { // Don't try to cancel if the pod hasn't completed its setup as it will either receive no response - // (pod progress state <= 2) or a create a $31 pod fault (pod progress states 3 through 7). + // (pod progress state <= 2) or creates a $31 pod fault (pod progress states 3 through 7). if podState.setupProgress == .completed && podState.fault == nil && !podState.isSuspended { - let result = cancelDelivery(deliveryType: .all, beepType: .noBeep) + let result = cancelDelivery(deliveryType: .all) switch result { case .certainFailure(let error): throw error @@ -682,6 +763,7 @@ public class PodCommsSession { } } + // if faulted read the most recent pulse log entries if podState.fault != nil { // All the dosing cleanup from the fault should have already been // handled in handlePodFault() when podState.fault was initialized. @@ -693,13 +775,12 @@ public class PodCommsSession { } } - let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) - do { + let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce) let _: StatusResponse = try send([deactivatePod]) } catch let error as PodCommsError { switch error { - case .podFault, .unexpectedResponse: + case .podFault, .activationTimeExceeded, .unexpectedResponse: break default: throw error @@ -707,9 +788,9 @@ public class PodCommsSession { } } - public func acknowledgeAlerts(alerts: AlertSet) throws -> [AlertSlot: PodAlert] { + public func acknowledgeAlerts(alerts: AlertSet, confirmationBeepType: BeepConfigType? = nil) throws -> [AlertSlot: PodAlert] { let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts) - let status: StatusResponse = try send([cmd]) + let status: StatusResponse = try send([cmd], confirmationBeepType: confirmationBeepType) podState.updateFromStatusResponse(status) return podState.activeAlerts } diff --git a/OmniKit/PumpManager/PodState.swift b/OmniKit/PumpManager/PodState.swift index 934d2bd8d..b569b49b8 100644 --- a/OmniKit/PumpManager/PodState.swift +++ b/OmniKit/PumpManager/PodState.swift @@ -10,7 +10,7 @@ import Foundation public enum SetupProgress: Int { case addressAssigned = 0 - case podConfigured + case podPaired case startingPrime case priming case settingInitialBasalSchedule @@ -19,6 +19,15 @@ public enum SetupProgress: Int { case cannulaInserting case completed case activationTimeout + case podIncompatible + + public var isPaired: Bool { + return self.rawValue >= SetupProgress.podPaired.rawValue + } + + public var primingNeverAttempted: Bool { + return self.rawValue < SetupProgress.startingPrime.rawValue + } public var primingNeeded: Bool { return self.rawValue < SetupProgress.priming.rawValue @@ -110,7 +119,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl self.configuredAlerts = [.slot7: .waitingForPairingReminder] } - public var unfinishedPairing: Bool { + public var unfinishedSetup: Bool { return setupProgress != .completed } @@ -131,7 +140,7 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl } public var isFaulted: Bool { - return fault != nil || setupProgress == .activationTimeout + return fault != nil || setupProgress == .activationTimeout || setupProgress == .podIncompatible } public mutating func advanceToNextNonce() { @@ -376,6 +385,8 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl .slot2: .shutdownImminentAlarm(0), .slot3: .expirationAlert(0), .slot4: .lowReservoirAlarm(0), + .slot5: .podSuspendedReminder(active: false, suspendTime: 0), + .slot6: .suspendTimeExpired(suspendTime: 0), .slot7: .expirationAdvisoryAlarm(alarmTime: 0, duration: 0) ] } diff --git a/OmniKitTests/MessageTests.swift b/OmniKitTests/MessageTests.swift index f68210538..10b8c670d 100644 --- a/OmniKitTests/MessageTests.swift +++ b/OmniKitTests/MessageTests.swift @@ -39,7 +39,7 @@ class MessageTests: XCTestCase { XCTAssertEqual(.aboveFiftyUnits, statusResponse.podProgressStatus) XCTAssertEqual(6.3, statusResponse.insulin, accuracy: 0.01) XCTAssertEqual(0, statusResponse.bolusNotDelivered) - XCTAssertEqual(3, statusResponse.podMessageCounter) + XCTAssertEqual(3, statusResponse.lastProgrammingMessageSeqNum) XCTAssert(statusResponse.alerts.isEmpty) XCTAssertEqual("1f00ee84300a1d18003f1800004297ff8128", msg.encoded().hexadecimalString) @@ -78,9 +78,21 @@ class MessageTests: XCTestCase { do { let config = try VersionResponse(encodedData: Data(hexadecimalString: "011502070002070002020000a64000097c279c1f08ced2")!) XCTAssertEqual(23, config.data.count) + XCTAssertEqual("2.7.0", String(describing: config.piVersion)) + XCTAssertEqual("2.7.0", String(describing: config.pmVersion)) XCTAssertEqual(0x1f08ced2, config.address) XCTAssertEqual(42560, config.lot) XCTAssertEqual(621607, config.tid) + XCTAssertEqual(2, config.productID) + XCTAssertEqual(.reminderInitialized, config.podProgressStatus) + XCTAssertEqual(2, config.gain) + XCTAssertEqual(0x1c, config.rssi) + XCTAssertNil(config.pulseSize) + XCTAssertNil(config.secondsPerBolusPulse) + XCTAssertNil(config.secondsPerPrimePulse) + XCTAssertNil(config.primeUnits) + XCTAssertNil(config.cannulaInsertionUnits) + XCTAssertNil(config.serviceDuration) } catch (let error) { XCTFail("message decoding threw error: \(error)") } @@ -91,11 +103,21 @@ class MessageTests: XCTestCase { let message = try Message(encodedData: Data(hexadecimalString: "ffffffff041d011b13881008340a5002070002070002030000a62b000447941f00ee878352")!) let config = message.messageBlocks[0] as! VersionResponse XCTAssertEqual(29, config.data.count) - XCTAssertEqual(0x1f00ee87, config.address) - XCTAssertEqual(42539, config.lot) - XCTAssertEqual(280468, config.tid) XCTAssertEqual("2.7.0", String(describing: config.piVersion)) XCTAssertEqual("2.7.0", String(describing: config.pmVersion)) + XCTAssertEqual(42539, config.lot) + XCTAssertEqual(280468, config.tid) + XCTAssertEqual(0x1f00ee87, config.address) + XCTAssertEqual(2, config.productID) + XCTAssertEqual(.pairingCompleted, config.podProgressStatus) + XCTAssertNil(config.rssi) + XCTAssertNil(config.gain) + XCTAssertEqual(Pod.pulseSize, config.pulseSize) + XCTAssertEqual(Pod.secondsPerBolusPulse, config.secondsPerBolusPulse) + XCTAssertEqual(Pod.secondsPerPrimePulse, config.secondsPerPrimePulse) + XCTAssertEqual(Pod.primeUnits, config.primeUnits) + XCTAssertEqual(Pod.cannulaInsertionUnits, config.cannulaInsertionUnits) + XCTAssertEqual(Pod.serviceDuration, config.serviceDuration) } catch (let error) { XCTFail("message decoding threw error: \(error)") } @@ -105,7 +127,21 @@ class MessageTests: XCTestCase { do { let message = try Message(encodedData: Data(hexadecimalString: "ffffffff04170115020700020700020e0000a5ad00053030971f08686301fd")!) let config = message.messageBlocks[0] as! VersionResponse + XCTAssertEqual("2.7.0", String(describing: config.piVersion)) + XCTAssertEqual("2.7.0", String(describing: config.pmVersion)) + XCTAssertEqual(0x0000a5ad, config.lot) + XCTAssertEqual(0x00053030, config.tid) + XCTAssertEqual(0x1f086863, config.address) + XCTAssertEqual(2, config.productID) XCTAssertEqual(.activationTimeExceeded, config.podProgressStatus) + XCTAssertEqual(2, config.gain) + XCTAssertEqual(0x17, config.rssi) + XCTAssertNil(config.pulseSize) + XCTAssertNil(config.secondsPerBolusPulse) + XCTAssertNil(config.secondsPerPrimePulse) + XCTAssertNil(config.primeUnits) + XCTAssertNil(config.cannulaInsertionUnits) + XCTAssertNil(config.serviceDuration) } catch (let error) { XCTFail("message decoding threw error: \(error)") } diff --git a/OmniKitTests/PodInfoTests.swift b/OmniKitTests/PodInfoTests.swift index eb591b5f9..62d53a995 100644 --- a/OmniKitTests/PodInfoTests.swift +++ b/OmniKitTests/PodInfoTests.swift @@ -21,7 +21,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(faultEvent.faultAccessingTables, false) XCTAssertEqual(faultEvent.podProgressStatus, .faultEventOccurred) XCTAssertEqual(faultEvent.errorEventInfo?.insulinStateTableCorruption, false) - XCTAssertEqual(faultEvent.errorEventInfo?.internalVariable, 1) + XCTAssertEqual(faultEvent.errorEventInfo?.occlusionType, 1) XCTAssertEqual(faultEvent.errorEventInfo?.immediateBolusInProgress, false) XCTAssertEqual(faultEvent.errorEventInfo?.podProgressStatus, .aboveFiftyUnits) } catch (let error) { @@ -127,7 +127,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.aboveFiftyUnits, decoded.podProgressStatus) XCTAssertEqual(.scheduledBasal, decoded.deliveryStatus) XCTAssertEqual(0000, decoded.bolusNotDelivered) - XCTAssertEqual(0x0a, decoded.podMessageCounter) + XCTAssertEqual(0x0a, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(.noFaults, decoded.faultEventCode.faultType) XCTAssertEqual(TimeInterval(minutes: 0x0000), decoded.faultEventTimeSinceActivation) XCTAssertNil(decoded.reservoirLevel) @@ -156,7 +156,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.inactive, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) XCTAssertEqual(0000, decoded.bolusNotDelivered) - XCTAssertEqual(9, decoded.podMessageCounter) + XCTAssertEqual(9, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(.primeOpenCountTooLow, decoded.faultEventCode.faultType) XCTAssertEqual(TimeInterval(minutes: 0x0001), decoded.faultEventTimeSinceActivation) XCTAssertNil(decoded.reservoirLevel) @@ -165,7 +165,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(00, decoded.unacknowledgedAlerts.rawValue) XCTAssertEqual(false, decoded.faultAccessingTables) XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable) + XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType) XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress) XCTAssertEqual(.primingCompleted, decoded.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b10, decoded.receiverLowGain) @@ -188,7 +188,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) XCTAssertEqual(0, decoded.bolusNotDelivered, accuracy: 0.01) - XCTAssertEqual(6, decoded.podMessageCounter) + XCTAssertEqual(6, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(.command1AParseUnexpectedFailed, decoded.faultEventCode.faultType) XCTAssertEqual(TimeInterval(minutes: 0x0000), decoded.faultEventTimeSinceActivation) XCTAssertNil(decoded.reservoirLevel) @@ -196,7 +196,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) XCTAssertEqual(false, decoded.faultAccessingTables) XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable) + XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType) XCTAssertEqual(PodProgressStatus.pairingCompleted, decoded.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b10, decoded.receiverLowGain) XCTAssertEqual(0x22, decoded.radioRSSI) @@ -218,7 +218,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) XCTAssertEqual(0, decoded.bolusNotDelivered) - XCTAssertEqual(4, decoded.podMessageCounter) + XCTAssertEqual(4, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(101.7, decoded.totalInsulinDelivered, accuracy: 0.01) XCTAssertEqual(.basalOverInfusionPulse, decoded.faultEventCode.faultType) XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) @@ -228,7 +228,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(TimeInterval(minutes: 0x0a02), decoded.timeActive) XCTAssertEqual(false, decoded.faultAccessingTables) XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable) + XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType) XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress) XCTAssertEqual(.aboveFiftyUnits, decoded.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b00, decoded.receiverLowGain) @@ -250,7 +250,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) XCTAssertEqual(0, decoded.bolusNotDelivered) - XCTAssertEqual(4, decoded.podMessageCounter) + XCTAssertEqual(4, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(101.35, decoded.totalInsulinDelivered, accuracy: 0.01) XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType) XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) @@ -260,7 +260,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(TimeInterval(minutes: 0x0e14), decoded.timeActive) XCTAssertEqual(false, decoded.faultAccessingTables) XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(1, decoded.errorEventInfo?.internalVariable) + XCTAssertEqual(1, decoded.errorEventInfo?.occlusionType) XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress) XCTAssertEqual(.aboveFiftyUnits, decoded.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b00, decoded.receiverLowGain) @@ -282,7 +282,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.inactive, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) XCTAssertEqual(0.05, decoded.bolusNotDelivered) - XCTAssertEqual(2, decoded.podMessageCounter) + XCTAssertEqual(2, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(11.8, decoded.totalInsulinDelivered, accuracy: 0.01) XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType) XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) @@ -292,7 +292,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(TimeInterval(minutes: 0x026b), decoded.timeActive) XCTAssertEqual(false, decoded.faultAccessingTables) XCTAssertEqual(false, decoded.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(1, decoded.errorEventInfo?.internalVariable) + XCTAssertEqual(1, decoded.errorEventInfo?.occlusionType) XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress) XCTAssertEqual(.aboveFiftyUnits, decoded.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b10, decoded.receiverLowGain) @@ -314,7 +314,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) XCTAssertEqual(0.00, decoded.bolusNotDelivered) - XCTAssertEqual(0, decoded.podMessageCounter) + XCTAssertEqual(0, decoded.lastProgrammingMessageSeqNum) XCTAssertEqual(0.00, decoded.totalInsulinDelivered, accuracy: 0.01) XCTAssertEqual(.resetDueToLVD, decoded.faultEventCode.faultType) XCTAssertNil(decoded.faultEventTimeSinceActivation) @@ -323,7 +323,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) XCTAssertEqual(false, decoded.faultAccessingTables) XCTAssertEqual(true, decoded.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(0, decoded.errorEventInfo?.internalVariable) + XCTAssertEqual(0, decoded.errorEventInfo?.occlusionType) XCTAssertEqual(false, decoded.errorEventInfo?.immediateBolusInProgress) XCTAssertEqual(.insertingCannula, decoded.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b10, decoded.receiverLowGain) @@ -453,7 +453,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(.faultEventOccurred, faultEvent.podProgressStatus) XCTAssertEqual(.suspended, faultEvent.deliveryStatus) XCTAssertEqual(0.00, faultEvent.bolusNotDelivered) - XCTAssertEqual(0, faultEvent.podMessageCounter) + XCTAssertEqual(0, faultEvent.lastProgrammingMessageSeqNum) XCTAssertEqual(0.00, faultEvent.totalInsulinDelivered, accuracy: 0.01) XCTAssertEqual(.resetDueToLVD, faultEvent.faultEventCode.faultType) XCTAssertNil(faultEvent.faultEventTimeSinceActivation) @@ -462,7 +462,7 @@ class PodInfoTests: XCTestCase { XCTAssertEqual(0, faultEvent.unacknowledgedAlerts.rawValue) XCTAssertEqual(false, faultEvent.faultAccessingTables) XCTAssertEqual(true, faultEvent.errorEventInfo?.insulinStateTableCorruption) - XCTAssertEqual(0, faultEvent.errorEventInfo?.internalVariable) + XCTAssertEqual(0, faultEvent.errorEventInfo?.occlusionType) XCTAssertEqual(false, faultEvent.errorEventInfo?.immediateBolusInProgress) XCTAssertEqual(.insertingCannula, faultEvent.errorEventInfo?.podProgressStatus) XCTAssertEqual(0b10, faultEvent.receiverLowGain) diff --git a/OmniKitTests/StatusTests.swift b/OmniKitTests/StatusTests.swift index 74bb11eb0..5b388b8f3 100644 --- a/OmniKitTests/StatusTests.swift +++ b/OmniKitTests/StatusTests.swift @@ -38,7 +38,7 @@ class StatusTests: XCTestCase { XCTAssertEqual(129.45, decoded.insulin, accuracy: 0.01) XCTAssertEqual(46.00, decoded.reservoirLevel) XCTAssertEqual(2.2, decoded.bolusNotDelivered) - XCTAssertEqual(9, decoded.podMessageCounter) + XCTAssertEqual(9, decoded.lastProgrammingMessageSeqNum) //XCTAssert(,decoded.alarms) } catch (let error) { XCTFail("message decoding threw error: \(error)") diff --git a/OmniKitUI/PumpManager/OmnipodHUDProvider.swift b/OmniKitUI/PumpManager/OmnipodHUDProvider.swift index a23e2d8a9..617c5a2d7 100644 --- a/OmniKitUI/PumpManager/OmnipodHUDProvider.swift +++ b/OmniKitUI/PumpManager/OmnipodHUDProvider.swift @@ -135,7 +135,7 @@ internal class OmnipodHUDProvider: NSObject, HUDProvider, PodStateObserver { updatePodLifeView() updateReservoirView() updateFaultDisplay() - pumpManager.refreshStatus() + pumpManager.refreshStatus(emitConfirmationBeep: false) } func hudDidDisappear(_ animated: Bool) { diff --git a/OmniKitUI/ViewControllers/CommandResponseViewController.swift b/OmniKitUI/ViewControllers/CommandResponseViewController.swift index 8597b9bd3..66f9103bc 100644 --- a/OmniKitUI/ViewControllers/CommandResponseViewController.swift +++ b/OmniKitUI/ViewControllers/CommandResponseViewController.swift @@ -14,30 +14,31 @@ import RileyLinkBLEKit extension CommandResponseViewController { typealias T = CommandResponseViewController - private static let successText = LocalizedString("Succeeded", comment: "A message indicating a command succeeded") - + // Returns an appropriately formatted error string or "Succeeded" if no error + private static func resultString(error: Error?) -> String { + guard let error = error else { + return LocalizedString("Succeeded", comment: "A message indicating a command succeeded") + } + + let errorStrings: [String] + if let error = error as? LocalizedError { + errorStrings = [error.errorDescription, error.failureReason, error.recoverySuggestion].compactMap { $0 } + } else { + errorStrings = [error.localizedDescription].compactMap { $0 } + } + let errorText = errorStrings.joined(separator: ". ") + + if errorText.isEmpty { + return String(describing: error) + } + return errorText + "." + } + static func changeTime(pumpManager: OmnipodPumpManager) -> T { return T { (completionHandler) -> String in pumpManager.setTime() { (error) in - let response: String - if let error = error as? LocalizedError { - let sentenceFormat = LocalizedString("%@.", comment: "Appends a full-stop to a statement") - let messageWithRecovery = [error.failureReason, error.recoverySuggestion].compactMap({ $0 }).map({ - String(format: sentenceFormat, $0) - }).joined(separator: "\n") - - if messageWithRecovery.isEmpty { - response = error.localizedDescription - } else { - response = messageWithRecovery - } - } else if let error = error { - response = error.localizedDescription - } else { - response = self.successText - } DispatchQueue.main.async { - completionHandler(response) + completionHandler(resultString(error: error)) } } return LocalizedString("Changing time…", comment: "Progress message for changing pod time.") @@ -59,7 +60,7 @@ extension CommandResponseViewController { result += String(format: LocalizedString("Delivery Status: %1$@\n", comment: "The format string for Delivery Status: (1: delivery status string)"), String(describing: status.deliveryStatus)) - result += String(format: LocalizedString("Pulses (incl. prime & insert): %1$@ U\n", comment: "The format string for total insulin pulses (1: total pulses in units)"), status.totalInsulinDelivered.twoDecimals) + result += String(format: LocalizedString("Pulse Count: %1$d\n", comment: "The format string for Pulse Count (1: pulse count)"), Int(round(status.totalInsulinDelivered / Pod.pulseSize))) result += String(format: LocalizedString("Reservoir Level: %1$@ U\n", comment: "The format string for Reservoir Level: (1: reservoir level string)"), status.reservoirLevel?.twoDecimals ?? "50+") @@ -72,7 +73,13 @@ extension CommandResponseViewController { return String(describing: slot) } } - result += String(format: LocalizedString("Alerts: %1$@\n", comment: "The format string for Alerts: (1: the alerts string)"), alertsDescription.joined(separator: ", ")) + let alertString: String + if status.unacknowledgedAlerts.isEmpty { + alertString = String(describing: status.unacknowledgedAlerts) + } else { + alertString = alertsDescription.joined(separator: ", ") + } + result += String(format: LocalizedString("Alerts: %1$@\n", comment: "The format string for Alerts: (1: the alerts string)"), alertString) result += String(format: LocalizedString("RSSI: %1$@\n", comment: "The format string for RSSI: (1: RSSI value)"), String(describing: status.radioRSSI)) @@ -81,6 +88,9 @@ extension CommandResponseViewController { if status.faultEventCode.faultType != .noFaults { result += "\n" // since we have a fault, report the additional fault related information in a separate section result += String(format: LocalizedString("Fault: %1$@\n", comment: "The format string for a fault: (1: The fault description)"), status.faultEventCode.localizedDescription) + if let refStr = status.pdmRef { + result += refStr + "\n" // add the PDM style Ref code info as a separate line + } if let previousPodProgressStatus = status.previousPodProgressStatus { result += String(format: LocalizedString("Previous pod progress: %1$@\n", comment: "The format string for previous pod progress: (1: previous pod progress string)"), String(describing: previousPodProgressStatus)) } @@ -101,7 +111,7 @@ extension CommandResponseViewController { let configuredAlerts = pumpManager.state.podState!.configuredAlerts completionHandler(podStatusString(status: status, configuredAlerts: configuredAlerts)) case .failure(let error): - completionHandler(error.localizedDescription) + completionHandler(resultString(error: error)) } } } @@ -113,7 +123,7 @@ extension CommandResponseViewController { return T { (completionHandler) -> String in pumpManager.testingCommands() { (error) in DispatchQueue.main.async { - completionHandler(error?.localizedDescription ?? self.successText) + completionHandler(resultString(error: error)) } } return LocalizedString("Testing Commands…", comment: "Progress message for testing commands.") @@ -125,9 +135,9 @@ extension CommandResponseViewController { pumpManager.playTestBeeps() { (error) in let response: String if let error = error { - response = error.localizedDescription + response = resultString(error: error) } else { - response = LocalizedString("Play test beeps command sent successfully.\n\nIf you did not hear any beeps from your pod, it's likely that the piezo speaker in your pod is broken.", comment: "Success message for play test beeps.") + response = 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.") } DispatchQueue.main.async { completionHandler(response) @@ -139,9 +149,14 @@ extension CommandResponseViewController { static func readPulseLog(pumpManager: OmnipodPumpManager) -> T { return T { (completionHandler) -> String in - pumpManager.readPulseLog() { (response) in + pumpManager.readPulseLog() { (result) in DispatchQueue.main.async { - completionHandler(response) + switch result { + case .success(let pulseLogString): + completionHandler(pulseLogString) + case .failure(let error): + completionHandler(resultString(error: error)) + } } } return LocalizedString("Reading Pulse Log…", comment: "Progress message for reading pulse log.") @@ -151,8 +166,7 @@ extension CommandResponseViewController { extension Double { var twoDecimals: String { - let reservoirLevel = self - return String(format: "%.2f", reservoirLevel) + return String(format: "%.2f", self) } } diff --git a/OmniKitUI/ViewControllers/InsertCannulaSetupViewController.swift b/OmniKitUI/ViewControllers/InsertCannulaSetupViewController.swift index e6137fdb2..3204f57b2 100644 --- a/OmniKitUI/ViewControllers/InsertCannulaSetupViewController.swift +++ b/OmniKitUI/ViewControllers/InsertCannulaSetupViewController.swift @@ -33,8 +33,6 @@ class InsertCannulaSetupViewController: SetupTableViewController { } } - private var cancelErrorCount = 0 - override func viewDidLoad() { super.viewDidLoad() @@ -61,6 +59,7 @@ class InsertCannulaSetupViewController: SetupTableViewController { case initial case startingInsertion case inserting(finishTime: CFTimeInterval) + case needsCheckInsertion case fault case ready } @@ -71,7 +70,7 @@ class InsertCannulaSetupViewController: SetupTableViewController { case .initial: activityIndicator.state = .hidden footerView.primaryButton.isEnabled = true - footerView.primaryButton.setConnectTitle() + footerView.primaryButton.setInsertCannulaTitle() case .startingInsertion: activityIndicator.state = .indeterminantProgress footerView.primaryButton.isEnabled = false @@ -80,6 +79,10 @@ class InsertCannulaSetupViewController: SetupTableViewController { activityIndicator.state = .timedProgress(finishTime: CACurrentMediaTime() + finishTime) footerView.primaryButton.isEnabled = false lastError = nil + case .needsCheckInsertion: + activityIndicator.state = .hidden + footerView.primaryButton.isEnabled = true + footerView.primaryButton.setRecheckInsertionTitle() case .fault: activityIndicator.state = .hidden footerView.primaryButton.isEnabled = true @@ -115,20 +118,35 @@ class InsertCannulaSetupViewController: SetupTableViewController { } loadingText = errorText - // If we have an error, update the continue state + // If we have an error, update the continue state depending on whether the cannula insertion was started if let podCommsError = lastError as? PodCommsError { - switch podCommsError { - case .podFault, .activationTimeExceeded: + if podCommsError.isFaulted { continueState = .fault - default: - continueState = .initial + } else { + continueState = initialOrNeedsCannulaInsertionCheck } } else if lastError != nil { - continueState = .initial + continueState = initialOrNeedsCannulaInsertionCheck } } } + // .needsCheckInsertion (if cannula insertion has been started but its completion hasn't been verified) or else .initial + private var initialOrNeedsCannulaInsertionCheck: State { + if pumpManager.state.podState?.setupProgress == .cannulaInserting { + return .needsCheckInsertion + } + return .initial + } + + // .ready (if pod setup has been verifed to be complete) or else .needsCheckInsertion + private var readyOrNeedsCannulaInsertionCheck: State { + if pumpManager.state.podState?.setupProgress == .completed { + return .ready + } + return .needsCheckInsertion + } + private func navigateToReplacePod() { performSegue(withIdentifier: "ReplacePod", sender: nil) } @@ -138,12 +156,16 @@ class InsertCannulaSetupViewController: SetupTableViewController { case .initial: continueState = .startingInsertion insertCannula() + case .needsCheckInsertion: + checkCannulaInsertionFinished() + if pumpManager.state.podState?.setupProgress == .completed { + super.continueButtonPressed(sender) + } case .ready: super.continueButtonPressed(sender) case .fault: navigateToReplacePod() - case .startingInsertion, - .inserting: + case .startingInsertion, .inserting: break } } @@ -156,6 +178,10 @@ class InsertCannulaSetupViewController: SetupTableViewController { } private func insertCannula() { + guard let podState = pumpManager.state.podState, podState.setupProgress.needsCannulaInsertion else { + self.continueState = readyOrNeedsCannulaInsertionCheck + return + } pumpManager.insertCannula() { (result) in DispatchQueue.main.async { switch(result) { @@ -164,10 +190,10 @@ class InsertCannulaSetupViewController: SetupTableViewController { let delay = finishTime if delay > 0 { DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - self.continueState = .ready + self.checkCannulaInsertionFinished() // now check if actually ready } } else { - self.continueState = .ready + self.continueState = self.readyOrNeedsCannulaInsertionCheck } case .failure(let error): self.lastError = error @@ -175,12 +201,27 @@ class InsertCannulaSetupViewController: SetupTableViewController { } } } + + private func checkCannulaInsertionFinished() { + activityIndicator.state = .indeterminantProgress + self.pumpManager.checkCannulaInsertionFinished() { (error) in + DispatchQueue.main.async { + if let error = error { + self.lastError = error + } + self.continueState = self.readyOrNeedsCannulaInsertionCheck + } + } + } } private extension SetupButton { - func setConnectTitle() { + func setInsertCannulaTitle() { setTitle(LocalizedString("Insert Cannula", comment: "Button title to insert cannula during setup"), for: .normal) } + func setRecheckInsertionTitle() { + setTitle(LocalizedString("Recheck Cannula Insertion", comment: "Button title to recheck cannula insertion during setup"), for: .normal) + } func setDeactivateTitle() { setTitle(LocalizedString("Deactivate", comment: "Button title to deactivate pod because of fault during setup"), for: .normal) } diff --git a/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift b/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift index fdc4b06c9..c54acdb56 100644 --- a/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift +++ b/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift @@ -146,7 +146,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { self.navigationItem.setRightBarButton(button, animated: false) if self.podState != nil { - refreshPodStatus() + refreshPodStatus(emitConfirmationBeep: false) } else { refreshButton.isHidden = true } @@ -157,13 +157,13 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } @objc func refreshTapped(_ sender: Any) { - refreshPodStatus() + refreshPodStatus(emitConfirmationBeep: true) } - private func refreshPodStatus() { + private func refreshPodStatus(emitConfirmationBeep: Bool) { refreshButton.alpha = 0 activityIndicator.startAnimating() - pumpManager.refreshStatus { (_) in + pumpManager.refreshStatus(emitConfirmationBeep: emitConfirmationBeep) { (_) in DispatchQueue.main.async { self.refreshButton.alpha = 1 self.activityIndicator.stopAnimating() @@ -213,19 +213,19 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { private enum Section: Int, CaseIterable { case status = 0 - case podDetails - case diagnostics case configuration case rileyLinks + case diagnostics + case podDetails case deletePumpManager } private class func sectionList(_ podState: PodState?) -> [Section] { if let podState = podState { - if podState.unfinishedPairing { + if podState.unfinishedSetup { return [.configuration, .rileyLinks] } else { - return [.status, .configuration, .rileyLinks, .podDetails, .diagnostics] + return [.status, .configuration, .rileyLinks, .diagnostics, .podDetails] } } else { return [.configuration, .rileyLinks, .deletePumpManager] @@ -249,11 +249,10 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { case playTestBeeps case readPulseLog case testCommand - case enableDisableConfirmationBeeps } private var configurationRows: [ConfigurationRow] { - if podState == nil || podState?.unfinishedPairing == true { + if podState == nil || podState?.unfinishedSetup == true { return [.replacePod] } else { return ConfigurationRow.allCases @@ -262,13 +261,14 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { private enum ConfigurationRow: Int, CaseIterable { case suspendResume = 0 + case enableDisableConfirmationBeeps case reminder case timeZoneOffset case replacePod } fileprivate enum StatusRow: Int, CaseIterable { - case activatedAt = 0 + case activeTime = 0 case expiresAt case bolus case basal @@ -302,16 +302,16 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch sections[section] { - case .podDetails: - return LocalizedString("Pod Details", comment: "The title of the device information section in settings") - case .diagnostics: - return LocalizedString("Diagnostics", comment: "The title of the configuration section in settings") - case .configuration: - return nil case .status: - return nil + return nil // No title, appears below a pod picture + case .configuration: + return LocalizedString("Configuration", comment: "The title of the configuration section in settings") case .rileyLinks: return super.tableView(tableView, titleForHeaderInSection: section) + case .diagnostics: + return LocalizedString("Diagnostics", comment: "The title of the diagnostics section in settings") + case .podDetails: + return LocalizedString("Pod Details", comment: "The title of the pod details section in settings") case .deletePumpManager: return " " // Use an empty string for more dramatic spacing } @@ -328,66 +328,77 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { switch sections[indexPath.section] { - case .podDetails: + case .status: let podState = self.podState! - switch PodDetailsRow(rawValue: indexPath.row)! { - case .podAddress: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Assigned Address", comment: "The title text for the address assigned to the pod") - cell.detailTextLabel?.text = String(format:"%04X", podState.address) - return cell - case .podLot: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Lot", comment: "The title of the cell showing the pod lot id") - cell.detailTextLabel?.text = String(format:"L%d", podState.lot) - return cell - case .podTid: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("TID", comment: "The title of the cell showing the pod TID") - cell.detailTextLabel?.text = String(format:"%07d", podState.tid) - return cell - case .piVersion: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("PI Version", comment: "The title of the cell showing the pod pi version") - cell.detailTextLabel?.text = podState.piVersion - return cell - case .pmVersion: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("PM Version", comment: "The title of the cell showing the pod pm version") - cell.detailTextLabel?.text = podState.pmVersion + let statusRow = StatusRow(rawValue: indexPath.row)! + if statusRow == .alarms { + let cell = tableView.dequeueReusableCell(withIdentifier: AlarmsTableViewCell.className, for: indexPath) as! AlarmsTableViewCell + cell.textLabel?.text = LocalizedString("Alarms", comment: "The title of the cell showing alarm status") + cell.alerts = podState.activeAlerts return cell } - case .diagnostics: - - switch Diagnostics(rawValue: indexPath.row)! { - case .readPodStatus: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Read Pod Status", comment: "The title of the command to read the pod status") - cell.accessoryType = .disclosureIndicator - return cell - case .playTestBeeps: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Play Test Beeps", comment: "The title of the command to play test beeps") - cell.accessoryType = .disclosureIndicator - return cell - case .readPulseLog: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + + switch statusRow { + case .activeTime: let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Read Pulse Log", comment: "The title of the command to read the pulse log") - cell.accessoryType = .disclosureIndicator + cell.textLabel?.text = LocalizedString("Active Time", comment: "The title of the cell showing the pod active time") + // Since the podState doesn't actually keep the pod's active time value reported in each response, + // use the following calculation to compute an active time value based on the true elapsed time. + cell.setDetailAge(podState.activatedAt?.timeIntervalSinceNow) + // To show the actual pod's active time value which should then always match the value displayed in + // "Read Pod Status", use the following alternate calculation based on the dynamically updated expiresAt time + // to have a consistent value & behavior when the pod's internal active time varies from the true elapsed time. + // cell.setDetailAge(podState.expiresAt?.addingTimeInterval(-Pod.nominalPodLife).timeIntervalSinceNow) return cell - case .testCommand: + case .expiresAt: let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Test Command", comment: "The title of the command to run the test command") - cell.accessoryType = .disclosureIndicator + if let expiresAt = podState.expiresAt { + if expiresAt.timeIntervalSinceNow > 0 { + cell.textLabel?.text = LocalizedString("Expires", comment: "The title of the cell showing the pod expiration") + } else { + cell.textLabel?.text = LocalizedString("Expired", comment: "The title of the cell showing the pod expiration after expiry") + } + } + cell.setDetailDate(podState.expiresAt, formatter: dateFormatter) return cell - case .enableDisableConfirmationBeeps: - return confirmationBeepsTableViewCell + case .bolus: + cell.textLabel?.text = LocalizedString("Bolus Delivery", comment: "The title of the cell showing pod bolus status") + + let deliveredUnits: Double? + if let dose = podState.unfinalizedBolus { + deliveredUnits = pumpManager.roundToSupportedBolusVolume(units: dose.progress * dose.units) + } else { + deliveredUnits = nil + } + + cell.setDetailBolus(suspended: podState.isSuspended, dose: podState.unfinalizedBolus, deliveredUnits: deliveredUnits) + // TODO: This timer is in the wrong context; should be part of a custom bolus progress cell +// if bolusProgressTimer == nil { +// bolusProgressTimer = Timer.scheduledTimer(withTimeInterval: .seconds(2), repeats: true) { [weak self] (_) in +// self?.tableView.reloadRows(at: [indexPath], with: .none) +// } +// } + case .basal: + cell.textLabel?.text = LocalizedString("Basal Delivery", comment: "The title of the cell showing pod basal status") + cell.setDetailBasal(suspended: podState.isSuspended, dose: podState.unfinalizedTempBasal) + case .reservoirLevel: + cell.textLabel?.text = LocalizedString("Reservoir", comment: "The title of the cell showing reservoir status") + cell.setReservoirDetail(podState.lastInsulinMeasurements) + case .deliveredInsulin: + cell.textLabel?.text = LocalizedString("Insulin Delivered", comment: "The title of the cell showing delivered insulin") + cell.setDeliveredInsulinDetail(podState.lastInsulinMeasurements) + default: + break } - case .configuration: + return cell + case .configuration: switch configurationRows[indexPath.row] { case .suspendResume: return suspendResumeTableViewCell + case .enableDisableConfirmationBeeps: + return confirmationBeepsTableViewCell case .reminder: let cell = tableView.dequeueReusableCell(withIdentifier: ExpirationReminderDateTableViewCell.className, for: indexPath) as! ExpirationReminderDateTableViewCell if let podState = podState, let reminderDate = pumpManager.expirationReminderDate { @@ -428,7 +439,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { cell.textLabel?.text = LocalizedString("Pair New Pod", comment: "The title of the command to pair new pod") } else if let podState = podState, podState.isFaulted { cell.textLabel?.text = LocalizedString("Replace Pod Now", comment: "The title of the command to replace pod when there is a pod fault") - } else if let podState = podState, podState.unfinishedPairing { + } else if let podState = podState, podState.unfinishedSetup { cell.textLabel?.text = LocalizedString("Finish pod setup", comment: "The title of the command to finish pod setup") } else { cell.textLabel?.text = LocalizedString("Replace Pod", comment: "The title of the command to replace pod") @@ -439,67 +450,63 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { return cell } - case .status: - let podState = self.podState! - let statusRow = StatusRow(rawValue: indexPath.row)! - if statusRow == .alarms { - let cell = tableView.dequeueReusableCell(withIdentifier: AlarmsTableViewCell.className, for: indexPath) as! AlarmsTableViewCell - cell.textLabel?.text = LocalizedString("Alarms", comment: "The title of the cell showing alarm status") - cell.alerts = podState.activeAlerts + case .rileyLinks: + return super.tableView(tableView, cellForRowAt: indexPath) + + case .diagnostics: + switch Diagnostics(rawValue: indexPath.row)! { + case .readPodStatus: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("Read Pod Status", comment: "The title of the command to read the pod status") + cell.accessoryType = .disclosureIndicator return cell - } else { + case .playTestBeeps: let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - - switch statusRow { - case .activatedAt: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - cell.textLabel?.text = LocalizedString("Active Time", comment: "The title of the cell showing the pod activated at time") - cell.setDetailAge(podState.activatedAt?.timeIntervalSinceNow) - return cell - case .expiresAt: - let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) - if let expiresAt = podState.expiresAt { - if expiresAt.timeIntervalSinceNow > 0 { - cell.textLabel?.text = LocalizedString("Expires", comment: "The title of the cell showing the pod expiration") - } else { - cell.textLabel?.text = LocalizedString("Expired", comment: "The title of the cell showing the pod expiration after expiry") - } - } - cell.setDetailDate(podState.expiresAt, formatter: dateFormatter) - return cell - case .bolus: - cell.textLabel?.text = LocalizedString("Bolus Delivery", comment: "The title of the cell showing pod bolus status") - - let deliveredUnits: Double? - if let dose = podState.unfinalizedBolus { - deliveredUnits = pumpManager.roundToSupportedBolusVolume(units: dose.progress * dose.units) - } else { - deliveredUnits = nil - } + cell.textLabel?.text = LocalizedString("Play Test Beeps", comment: "The title of the command to play test beeps") + cell.accessoryType = .disclosureIndicator + return cell + case .readPulseLog: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("Read Pulse Log", comment: "The title of the command to read the pulse log") + cell.accessoryType = .disclosureIndicator + return cell + case .testCommand: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("Test Command", comment: "The title of the command to run the test command") + cell.accessoryType = .disclosureIndicator + return cell + } - cell.setDetailBolus(suspended: podState.isSuspended, dose: podState.unfinalizedBolus, deliveredUnits: deliveredUnits) - // TODO: This timer is in the wrong context; should be part of a custom bolus progress cell -// if bolusProgressTimer == nil { -// bolusProgressTimer = Timer.scheduledTimer(withTimeInterval: .seconds(2), repeats: true) { [weak self] (_) in -// self?.tableView.reloadRows(at: [indexPath], with: .none) -// } -// } - case .basal: - cell.textLabel?.text = LocalizedString("Basal Delivery", comment: "The title of the cell showing pod basal status") - cell.setDetailBasal(suspended: podState.isSuspended, dose: podState.unfinalizedTempBasal) - case .reservoirLevel: - cell.textLabel?.text = LocalizedString("Reservoir", comment: "The title of the cell showing reservoir status") - cell.setReservoirDetail(podState.lastInsulinMeasurements) - case .deliveredInsulin: - cell.textLabel?.text = LocalizedString("Insulin Delivered", comment: "The title of the cell showing delivered insulin") - cell.setDeliveredInsulinDetail(podState.lastInsulinMeasurements) - default: - break - } + case .podDetails: + let podState = self.podState! + switch PodDetailsRow(rawValue: indexPath.row)! { + case .podAddress: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("Assigned Address", comment: "The title text for the address assigned to the pod") + cell.detailTextLabel?.text = String(format:"%04X", podState.address) + return cell + case .podLot: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("Lot", comment: "The title of the cell showing the pod lot id") + cell.detailTextLabel?.text = String(format:"L%d", podState.lot) + return cell + case .podTid: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("TID", comment: "The title of the cell showing the pod TID") + cell.detailTextLabel?.text = String(format:"%07d", podState.tid) + return cell + case .piVersion: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("PI Version", comment: "The title of the cell showing the pod pi version") + cell.detailTextLabel?.text = podState.piVersion + return cell + case .pmVersion: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = LocalizedString("PM Version", comment: "The title of the cell showing the pod pm version") + cell.detailTextLabel?.text = podState.pmVersion return cell } - case .rileyLinks: - return super.tableView(tableView, cellForRowAt: indexPath) + case .deletePumpManager: let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell @@ -513,8 +520,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { switch sections[indexPath.section] { - case .podDetails: - return false case .status: switch StatusRow(rawValue: indexPath.row)! { case .alarms: @@ -522,12 +527,13 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { default: return false } - case .diagnostics, .configuration, .rileyLinks, .deletePumpManager: + case .configuration, .rileyLinks, .diagnostics, .deletePumpManager: return true + case .podDetails: + return false } } - override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { if indexPath == IndexPath(row: ConfigurationRow.reminder.rawValue, section: Section.configuration.rawValue) { tableView.beginUpdates() @@ -539,30 +545,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { let sender = tableView.cellForRow(at: indexPath) switch sections[indexPath.section] { - case .podDetails: - break - case .diagnostics: - switch Diagnostics(rawValue: indexPath.row)! { - case .readPodStatus: - let vc = CommandResponseViewController.readPodStatus(pumpManager: pumpManager) - vc.title = sender?.textLabel?.text - show(vc, sender: indexPath) - case .playTestBeeps: - let vc = CommandResponseViewController.playTestBeeps(pumpManager: pumpManager) - vc.title = sender?.textLabel?.text - show(vc, sender: indexPath) - case .readPulseLog: - let vc = CommandResponseViewController.readPulseLog(pumpManager: pumpManager) - vc.title = sender?.textLabel?.text - show(vc, sender: indexPath) - case .testCommand: - let vc = CommandResponseViewController.testingCommands(pumpManager: pumpManager) - vc.title = sender?.textLabel?.text - show(vc, sender: indexPath) - case .enableDisableConfirmationBeeps: - confirmationBeepsTapped() - tableView.deselectRow(at: indexPath, animated: true) - } case .status: switch StatusRow(rawValue: indexPath.row)! { case .alarms: @@ -591,21 +573,23 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { case .suspendResume: suspendResumeTapped() tableView.deselectRow(at: indexPath, animated: true) + case .enableDisableConfirmationBeeps: + confirmationBeepsTapped() + tableView.deselectRow(at: indexPath, animated: true) case .reminder: tableView.deselectRow(at: indexPath, animated: true) tableView.endUpdates() - break case .timeZoneOffset: let vc = CommandResponseViewController.changeTime(pumpManager: pumpManager) vc.title = sender?.textLabel?.text show(vc, sender: indexPath) case .replacePod: let vc: UIViewController - if podState == nil || podState!.setupProgress.primingNeeded { - vc = PodReplacementNavigationController.instantiateNewPodFlow(pumpManager) - } else if let podState = podState, podState.isFaulted { + if let podState = podState, podState.isFaulted { vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager) - } else if let podState = podState, podState.unfinishedPairing { + } else if podState == nil || podState!.setupProgress.primingNeeded { + vc = PodReplacementNavigationController.instantiateNewPodFlow(pumpManager) + } else if let podState = podState, podState.unfinishedSetup { vc = PodReplacementNavigationController.instantiateInsertCannulaFlow(pumpManager) } else { vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager) @@ -619,6 +603,27 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { let device = devicesDataSource.devices[indexPath.row] let vc = RileyLinkDeviceTableViewController(device: device) self.show(vc, sender: sender) + case .diagnostics: + switch Diagnostics(rawValue: indexPath.row)! { + case .readPodStatus: + let vc = CommandResponseViewController.readPodStatus(pumpManager: pumpManager) + vc.title = sender?.textLabel?.text + show(vc, sender: indexPath) + case .playTestBeeps: + let vc = CommandResponseViewController.playTestBeeps(pumpManager: pumpManager) + vc.title = sender?.textLabel?.text + show(vc, sender: indexPath) + case .readPulseLog: + let vc = CommandResponseViewController.readPulseLog(pumpManager: pumpManager) + vc.title = sender?.textLabel?.text + show(vc, sender: indexPath) + case .testCommand: + let vc = CommandResponseViewController.testingCommands(pumpManager: pumpManager) + vc.title = sender?.textLabel?.text + show(vc, sender: indexPath) + } + case .podDetails: + break case .deletePumpManager: let confirmVC = UIAlertController(pumpManagerDeletionHandler: { self.pumpManager.notifyDelegateOfDeactivation { @@ -636,25 +641,20 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { override func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? { switch sections[indexPath.section] { - case .podDetails, .status: + case .status: break - case .diagnostics: - switch Diagnostics(rawValue: indexPath.row)! { - case .enableDisableConfirmationBeeps: - break - case .readPodStatus, .playTestBeeps, .readPulseLog, .testCommand: - tableView.reloadRows(at: [indexPath], with: .fade) - } case .configuration: switch configurationRows[indexPath.row] { case .reminder, .suspendResume: break - case .timeZoneOffset, .replacePod: + case .enableDisableConfirmationBeeps, .timeZoneOffset, .replacePod: tableView.reloadRows(at: [indexPath], with: .fade) } case .rileyLinks: break - case .deletePumpManager: + case .diagnostics: + tableView.reloadRows(at: [indexPath], with: .fade) + case .podDetails, .deletePumpManager: break } @@ -685,8 +685,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } private func confirmationBeepsTapped() { - let confirmationBeeps: Bool = pumpManager.confirmationBeeps - func done() { DispatchQueue.main.async { [weak self] in if let self = self { @@ -697,27 +695,21 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } confirmationBeepsTableViewCell.isLoading = true - if confirmationBeeps { - pumpManager.setConfirmationBeeps(enabled: false, completion: { (error) in - if let error = error { - DispatchQueue.main.async { - let title = LocalizedString("Error disabling confirmation beeps", comment: "The alert title for disable confirmation beeps error") - self.present(UIAlertController(with: error, title: title), animated: true) - } - } - done() - }) - } else { - pumpManager.setConfirmationBeeps(enabled: true, completion: { (error) in - if let error = error { - DispatchQueue.main.async { - let title = LocalizedString("Error enabling confirmation beeps", comment: "The alert title for enable confirmation beeps error") - self.present(UIAlertController(with: error, title: title), animated: true) + let confirmationBeeps = !pumpManager.confirmationBeeps + pumpManager.setConfirmationBeeps(enabled: confirmationBeeps, completion: { (error) in + if let error = error { + DispatchQueue.main.async { + let title: String + if confirmationBeeps { + title = LocalizedString("Error enabling confirmation beeps", comment: "The alert title for enable confirmation beeps error") + } else { + title = LocalizedString("Error disabling confirmation beeps", comment: "The alert title for disable confirmation beeps error") } + self.present(UIAlertController(with: error, title: title), animated: true) } - done() - }) - } + } + done() + }) } } @@ -771,7 +763,7 @@ extension OmnipodSettingsViewController: PodStateObserver { return } - let reloadRows: [StatusRow] = [.bolus, .basal, .reservoirLevel, .deliveredInsulin] + let reloadRows: [StatusRow] = [.activeTime, .bolus, .basal, .reservoirLevel, .deliveredInsulin] self.tableView.reloadRows(at: reloadRows.map({ IndexPath(row: $0.rawValue, section: statusIdx) }), with: .none) if oldState?.activeAlerts != state?.activeAlerts, @@ -949,8 +941,6 @@ private extension UITableViewCell { self.detailTextLabel?.text = String(format: LocalizedString("%@ U of %@ U (%@)", comment: "Format string for bolus progress. (1: The delivered amount) (2: The programmed amount) (3: the percent progress)"), deliveredUnits, units, progressStr) } } - - } func setDeliveredInsulinDetail(_ measurements: PodInsulinMeasurements?) { diff --git a/OmniKitUI/ViewControllers/PairPodSetupViewController.swift b/OmniKitUI/ViewControllers/PairPodSetupViewController.swift index 64e78bf7b..b2757cb3d 100644 --- a/OmniKitUI/ViewControllers/PairPodSetupViewController.swift +++ b/OmniKitUI/ViewControllers/PairPodSetupViewController.swift @@ -154,10 +154,9 @@ class PairPodSetupViewController: SetupTableViewController { // If we have an error, update the continue state if let podCommsError = lastError as? PodCommsError { - switch podCommsError { - case .podFault, .activationTimeExceeded: + if podCommsError.isFaulted { continueState = .fault - default: + } else { continueState = .initial } } else if lastError != nil { diff --git a/OmniKitUI/ViewControllers/ReplacePodViewController.swift b/OmniKitUI/ViewControllers/ReplacePodViewController.swift index 5bc7ac5de..63746543a 100644 --- a/OmniKitUI/ViewControllers/ReplacePodViewController.swift +++ b/OmniKitUI/ViewControllers/ReplacePodViewController.swift @@ -16,7 +16,8 @@ class ReplacePodViewController: SetupTableViewController { enum PodReplacementReason { case normal case activationTimeout - case fault(_ faultCode: FaultEventCode) + case podIncompatible + case fault(_ podFault: DetailedStatus) case canceledPairingBeforeApplication case canceledPairing } @@ -29,8 +30,14 @@ class ReplacePodViewController: SetupTableViewController { break // Text set in interface builder case .activationTimeout: instructionsLabel.text = LocalizedString("Activation time exceeded. The pod must be deactivated before pairing with a new one. Please deactivate and discard pod.", comment: "Instructions when deactivating pod that didn't complete activation in time.") - case .fault(let faultCode): - instructionsLabel.text = String(format: LocalizedString("%1$@. Insulin delivery has stopped. Please deactivate and remove pod.", comment: "Format string providing instructions for replacing pod due to a fault. (1: The fault description)"), faultCode.localizedDescription) + case .podIncompatible: + instructionsLabel.text = LocalizedString("Unable to use incompatible pod. The pod must be deactivated before pairing with a new one. Please deactivate and discard pod.", comment: "Instructions when deactivating an incompatible pod") + case .fault(let podFault): + var faultDescription = podFault.faultEventCode.localizedDescription + if let refString = podFault.pdmRef { + faultDescription += String(format: " (%@)", refString) + } + instructionsLabel.text = String(format: LocalizedString("%1$@. Insulin delivery has stopped. Please deactivate and remove pod.", comment: "Format string providing instructions for replacing pod due to a fault. (1: The fault description)"), faultDescription) case .canceledPairingBeforeApplication: instructionsLabel.text = LocalizedString("Incompletely set up pod must be deactivated before pairing with a new one. Please deactivate and discard pod.", comment: "Instructions when deactivating pod that has been paired, but not attached.") case .canceledPairing: @@ -49,8 +56,12 @@ class ReplacePodViewController: SetupTableViewController { if podFault.podProgressStatus == .activationTimeExceeded { self.replacementReason = .activationTimeout } else { - self.replacementReason = .fault(podFault.faultEventCode) + self.replacementReason = .fault(podFault) } + } else if podState?.setupProgress == .activationTimeout { + self.replacementReason = .activationTimeout + } else if podState?.setupProgress == .podIncompatible { + self.replacementReason = .podIncompatible } else if podState?.setupProgress.primingNeeded == true { self.replacementReason = .canceledPairingBeforeApplication } else if podState?.setupProgress.needsCannulaInsertion == true {