From 1486ffebaa12de74336bf7fab1b32fd60f5d4310 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 23 Sep 2020 21:56:22 -0500 Subject: [PATCH] Use detailed status for read status command (#625) * Use detailed status for read status command --- .travis.yml | 2 +- OmniKit/MessageTransport/Message.swift | 9 +- ...oFaultEvent.swift => DetailedStatus.swift} | 24 +- .../MessageBlocks/PodInfo.swift | 6 +- .../MessageBlocks/StatusResponse.swift | 6 +- OmniKit/Model/FaultEventCode.swift | 2 + OmniKit/PumpManager/OmnipodPumpManager.swift | 69 +---- OmniKit/PumpManager/PodCommsSession.swift | 22 +- OmniKit/PumpManager/PodState.swift | 12 +- OmniKitTests/MessageTests.swift | 2 +- OmniKitTests/PodInfoTests.swift | 54 ++-- OmniKitTests/StatusTests.swift | 6 +- .../CommandResponseViewController.swift | 63 +++- .../OmnipodSettingsViewController.swift | 293 +++++++++++------- .../ReplacePodViewController.swift | 2 +- RileyLink.xcodeproj/project.pbxproj | 12 +- 16 files changed, 340 insertions(+), 244 deletions(-) rename OmniKit/MessageTransport/MessageBlocks/{PodInfoFaultEvent.swift => DetailedStatus.swift} (89%) diff --git a/.travis.yml b/.travis.yml index 503bd8488..f3f42ccb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ cache: - Carthage before_script: - - carthage bootstrap + - carthage bootstrap --cache-builds script: - set -o pipefail && xcodebuild -project RileyLink.xcodeproj -scheme Shared build -destination 'name=iPhone 8' test | xcpretty diff --git a/OmniKit/MessageTransport/Message.swift b/OmniKit/MessageTransport/Message.swift index 72f2d4551..a33bdf515 100644 --- a/OmniKit/MessageTransport/Message.swift +++ b/OmniKit/MessageTransport/Message.swift @@ -89,13 +89,14 @@ struct Message { return data } - var fault: PodInfoFaultEvent? { + var fault: DetailedStatus? { if messageBlocks.count > 0 && messageBlocks[0].blockType == .podInfoResponse, let infoResponse = messageBlocks[0] as? PodInfoResponse, - infoResponse.podInfoResponseSubType == .faultEvents, - let fault = infoResponse.podInfo as? PodInfoFaultEvent + infoResponse.podInfoResponseSubType == .detailedStatus, + let detailedStatus = infoResponse.podInfo as? DetailedStatus, + detailedStatus.faultEventCode.faultType != .noFaults || detailedStatus.podProgressStatus == .activationTimeExceeded { - return fault + return detailedStatus } else { return nil } diff --git a/OmniKit/MessageTransport/MessageBlocks/PodInfoFaultEvent.swift b/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift similarity index 89% rename from OmniKit/MessageTransport/MessageBlocks/PodInfoFaultEvent.swift rename to OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift index e2c564819..8db79f01e 100644 --- a/OmniKit/MessageTransport/MessageBlocks/PodInfoFaultEvent.swift +++ b/OmniKit/MessageTransport/MessageBlocks/DetailedStatus.swift @@ -1,5 +1,5 @@ // -// PodInfoFaultEvent.swift +// DetailedStatus.swift // OmniKit // // Created by Pete Schwamb on 2/23/18. @@ -8,18 +8,18 @@ import Foundation -public struct PodInfoFaultEvent : PodInfo, Equatable { +public struct DetailedStatus : PodInfo, Equatable { // CMD 1 2 3 4 5 6 7 8 9 10 1112 1314 1516 17 18 19 20 21 2223 // DATA 0 1 2 3 4 5 6 7 8 910 1112 1314 15 16 17 18 19 2021 // 02 16 02 0J 0K LLLL MM NNNN PP QQQQ RRRR SSSS TT UU VV WW 0X YYYY - public var podInfoType: PodInfoResponseSubType = .faultEvents + public var podInfoType: PodInfoResponseSubType = .detailedStatus public let podProgressStatus: PodProgressStatus public let deliveryStatus: DeliveryStatus - public let insulinNotDelivered: Double + public let bolusNotDelivered: Double public let podMessageCounter: UInt8 public let totalInsulinDelivered: Double - public let currentStatus: FaultEventCode + public let faultEventCode: FaultEventCode public let faultEventTimeSinceActivation: TimeInterval? public let reservoirLevel: Double? public let timeActive: TimeInterval @@ -46,13 +46,13 @@ public struct PodInfoFaultEvent : PodInfo, Equatable { self.deliveryStatus = DeliveryStatus(rawValue: encodedData[2] & 0xf)! - self.insulinNotDelivered = Pod.pulseSize * Double((Int(encodedData[3] & 0x3) << 8) | Int(encodedData[4])) + self.bolusNotDelivered = Pod.pulseSize * Double((Int(encodedData[3] & 0x3) << 8) | Int(encodedData[4])) self.podMessageCounter = encodedData[5] self.totalInsulinDelivered = Pod.pulseSize * Double((Int(encodedData[6]) << 8) | Int(encodedData[7])) - self.currentStatus = FaultEventCode(rawValue: encodedData[8]) + self.faultEventCode = FaultEventCode(rawValue: encodedData[8]) let minutesSinceActivation = encodedData[9...10].toBigEndian(UInt16.self) if minutesSinceActivation != 0xffff { @@ -97,18 +97,18 @@ public struct PodInfoFaultEvent : PodInfo, Equatable { } } -extension PodInfoFaultEvent: CustomDebugStringConvertible { +extension DetailedStatus: CustomDebugStringConvertible { public typealias RawValue = Data public var debugDescription: String { return [ - "## PodInfoFaultEvent", + "## DetailedStatus", "* rawHex: \(data.hexadecimalString)", "* podProgressStatus: \(podProgressStatus)", "* deliveryStatus: \(deliveryStatus.description)", - "* insulinNotDelivered: \(insulinNotDelivered.twoDecimals) U", + "* bolusNotDelivered: \(bolusNotDelivered.twoDecimals) U", "* podMessageCounter: \(podMessageCounter)", "* totalInsulinDelivered: \(totalInsulinDelivered.twoDecimals) U", - "* currentStatus: \(currentStatus.description)", + "* faultEventCode: \(faultEventCode.description)", "* faultEventTimeSinceActivation: \(faultEventTimeSinceActivation?.stringValue ?? "none")", "* reservoirLevel: \(reservoirLevel?.twoDecimals ?? "50+") U", "* timeActive: \(timeActive.stringValue)", @@ -125,7 +125,7 @@ extension PodInfoFaultEvent: CustomDebugStringConvertible { } } -extension PodInfoFaultEvent: RawRepresentable { +extension DetailedStatus: RawRepresentable { public init?(rawValue: Data) { do { try self.init(encodedData: rawValue) diff --git a/OmniKit/MessageTransport/MessageBlocks/PodInfo.swift b/OmniKit/MessageTransport/MessageBlocks/PodInfo.swift index 0f11c5370..f7ea230d0 100644 --- a/OmniKit/MessageTransport/MessageBlocks/PodInfo.swift +++ b/OmniKit/MessageTransport/MessageBlocks/PodInfo.swift @@ -18,7 +18,7 @@ public protocol PodInfo { public enum PodInfoResponseSubType: UInt8, Equatable { case normal = 0x00 case configuredAlerts = 0x01 - case faultEvents = 0x02 + case detailedStatus = 0x02 // Returned on any pod fault case dataLog = 0x03 case fault = 0x05 case pulseLogRecent = 0x50 // dumps up to 50 entries data from the pulse log @@ -30,8 +30,8 @@ public enum PodInfoResponseSubType: UInt8, Equatable { return StatusResponse.self as! PodInfo.Type case .configuredAlerts: return PodInfoConfiguredAlerts.self - case .faultEvents: - return PodInfoFaultEvent.self + case .detailedStatus: + return DetailedStatus.self case .dataLog: return PodInfoDataLog.self case .fault: diff --git a/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift b/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift index 28911d1d3..cb60b8c89 100644 --- a/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift +++ b/OmniKit/MessageTransport/MessageBlocks/StatusResponse.swift @@ -52,7 +52,7 @@ public struct StatusResponse : MessageBlock { public let timeActive: TimeInterval public let reservoirLevel: Double? public let insulin: Double - public let insulinNotDelivered: Double + public let bolusNotDelivered: Double public let podMessageCounter: UInt8 public let alerts: AlertSet @@ -86,7 +86,7 @@ public struct StatusResponse : MessageBlock { self.podMessageCounter = (encodedData[4] >> 3) & 0xf - self.insulinNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit + self.bolusNotDelivered = Double((Int(encodedData[4] & 0x3) << 8) | Int(encodedData[5])) / Pod.pulsesPerUnit self.alerts = AlertSet(rawValue: ((encodedData[6] & 0x7f) << 1) | (encodedData[7] >> 7)) @@ -101,7 +101,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), undelivered:\(insulinNotDelivered), seq:\(podMessageCounter), alerts:\(alerts))" + return "StatusResponse(deliveryStatus:\(deliveryStatus), progressStatus:\(podProgressStatus), timeActive:\(timeActive.stringValue), reservoirLevel:\(String(describing: reservoirLevel)), delivered:\(insulin), bolusNotDelivered:\(bolusNotDelivered), seq:\(podMessageCounter), alerts:\(alerts))" } } diff --git a/OmniKit/Model/FaultEventCode.swift b/OmniKit/Model/FaultEventCode.swift index edda00946..69823485b 100644 --- a/OmniKit/Model/FaultEventCode.swift +++ b/OmniKit/Model/FaultEventCode.swift @@ -394,6 +394,8 @@ public struct FaultEventCode: CustomStringConvertible, Equatable { public var localizedDescription: String { if let faultType = faultType { switch faultType { + case .noFaults: + return LocalizedString("No faults", comment: "Description for Fault Event Code .noFaults") case .reservoirEmpty: return LocalizedString("Empty reservoir", comment: "Description for Empty reservoir pod fault") case .exceededMaximumPodLife80Hrs: diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index 5c76d4e4b..5ca20fe22 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -473,14 +473,14 @@ extension OmnipodPumpManager { // MARK: Testing #if targetEnvironment(simulator) - private func jumpStartPod(address: UInt32, lot: UInt32, tid: UInt32, fault: PodInfoFaultEvent? = nil, startDate: Date? = nil, mockFault: Bool) { + 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.activatedAt = start podState.expiresAt = start + .hours(72) - let fault = mockFault ? try? PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d0000000e00c36a020703ff020900002899080082")!) : nil + let fault = mockFault ? try? DetailedStatus(encodedData: Data(hexadecimalString: "020d0000000e00c36a020703ff020900002899080082")!) : nil podState.fault = fault self.podComms = PodComms(podState: podState) @@ -502,7 +502,7 @@ extension OmnipodPumpManager { let mockCommsErrorDuringPairing = true DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + .seconds(2)) { self.jumpStartPod(address: 0x1f0b3557, lot: 40505, tid: 6439, mockFault: mockFaultDuringPairing) - let fault: PodInfoFaultEvent? = self.setStateWithResult({ (state) in + let fault: DetailedStatus? = self.setStateWithResult({ (state) in state.podState?.setupProgress = .priming return state.podState?.fault }) @@ -599,7 +599,7 @@ extension OmnipodPumpManager { DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + mockDelay) { let result = self.setStateWithResult({ (state) -> PumpManagerResult in // Mock fault - // let fault = try! PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d0000000e00c36a020703ff020900002899080082")!) + // let fault = try! DetailedStatus(encodedData: Data(hexadecimalString: "020d0000000e00c36a020703ff020900002899080082")!) // self.state.podState?.fault = fault // return .failure(PodCommsError.podFault(fault: fault)) @@ -907,74 +907,25 @@ extension OmnipodPumpManager { #endif } - private func podStatusString(status: StatusResponse) -> String { - var result, str: String - var delivered: Double - - let formatter = DateComponentsFormatter() - formatter.unitsStyle = .full - formatter.allowedUnits = [.day, .hour, .minute] - if let timeStr = formatter.string(from: status.timeActive) { - str = timeStr - } else { - str = String(format: LocalizedString("%1$@ minutes", comment: "The format string for minutes (1: number of minutes string)"), String(describing: Int(status.timeActive / 60))) - } - result = String(format: LocalizedString("Pod Active: %1$@\n", comment: "The format string for Pod Active: (1: Pod active time string)"), str) - - result += String(format: LocalizedString("Delivery Status: %1$@\n", comment: "The format string for Delivery Status: (1: delivery status string)"), String(describing: status.deliveryStatus)) - - if let lastInsulinMeasurements = self.state.podState?.lastInsulinMeasurements { - delivered = lastInsulinMeasurements.delivered - } else { - delivered = status.insulin - } - result += String(format: LocalizedString("Total Insulin Delivered: %1$@ U\n", comment: "The format string for Total Insulin Delivered: (1: total insulin delivered string)"), delivered.twoDecimals) - - result += String(format: LocalizedString("Reservoir Level: %1$@ U\n", comment: "The format string for Reservoir Level: (1: reservoir level string)"), status.reservoirLevel?.twoDecimals ?? "50+") - - result += String(format: LocalizedString("Last Bolus Not Delivered: %1$@ U\n", comment: "The format string for Last Bolus Not Delivered: (1: bolus not delivered string)"), status.insulinNotDelivered.twoDecimals) - - if let podState = self.state.podState, - podState.activeAlerts.startIndex != podState.activeAlerts.endIndex - { - // generate a more helpful string with the actual alert names - str = String(describing: podState.activeAlerts) - } else { - str = String(describing: status.alerts) - } - result += String(format: LocalizedString("Alerts: %1$@\n", comment: "The format string for Alerts: (1: the alerts string)"), str) - - return result - } - - public func readPodStatus(completion: @escaping (String) -> Void) { + 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(PodCommsError.noPodPaired.errorDescription!) + completion(.failure(PodCommsError.noPodPaired)) return } let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice podComms.runSession(withName: "Read pod status", using: rileyLinkSelector) { (result) in - - let reportError = { (errorStr: String) in - self.log.error("Failed to read pod status: %{public}@", errorStr) - completion(errorStr) - } - do { switch result { case .success(let session): - let status = try session.getStatus() - session.dosesForStorage({ (doses) -> Bool in - self.store(doses: doses, in: session) - }) - completion(self.podStatusString(status: status)) + let faultStatus = try session.getDetailedStatus() + completion(.success(faultStatus)) case .failure(let error): - reportError(String(describing: error)) + completion(.failure(error)) } } catch let error { - reportError(error.localizedDescription) + completion(.failure(error)) } } } diff --git a/OmniKit/PumpManager/PodCommsSession.swift b/OmniKit/PumpManager/PodCommsSession.swift index a32b54b66..ec1d17707 100644 --- a/OmniKit/PumpManager/PodCommsSession.swift +++ b/OmniKit/PumpManager/PodCommsSession.swift @@ -26,7 +26,7 @@ public enum PodCommsError: Error { case unfinalizedTempBasal case nonceResyncFailed case podSuspended - case podFault(fault: PodInfoFaultEvent) + case podFault(fault: DetailedStatus) case commsError(error: Error) case podChange case activationTimeExceeded @@ -66,7 +66,7 @@ extension PodCommsError: LocalizedError { case .podSuspended: return LocalizedString("Pod is suspended", comment: "Error message action could not be performed because pod is suspended") case .podFault(let fault): - let faultDescription = String(describing: fault.currentStatus) + let faultDescription = String(describing: fault.faultEventCode) return String(format: LocalizedString("Pod Fault: %1$@", comment: "Format string for pod fault code"), faultDescription) case .commsError(let error): return error.localizedDescription @@ -158,7 +158,7 @@ public class PodCommsSession { } // Will throw either PodCommsError.podFault or PodCommsError.activationTimeExceeded - private func throwPodFault(fault: PodInfoFaultEvent) throws { + private func throwPodFault(fault: DetailedStatus) throws { if self.podState.fault == nil { self.podState.fault = fault // save the first fault returned } @@ -166,7 +166,7 @@ public class PodCommsSession { if fault.deliveryStatus == .suspended { let now = Date() podState.unfinalizedTempBasal?.cancel(at: now) - podState.unfinalizedBolus?.cancel(at: now, withRemaining: fault.insulinNotDelivered) + podState.unfinalizedBolus?.cancel(at: now, withRemaining: fault.bolusNotDelivered) } if fault.podProgressStatus == .activationTimeExceeded { // avoids a confusing "No fault" error when activation time is exceeded @@ -531,7 +531,7 @@ public class PodCommsSession { do { let status: StatusResponse = try send(message) - let canceledDose = handleCancelDosing(deliveryType: deliveryType, bolusNotDelivered: status.insulinNotDelivered) + let canceledDose = handleCancelDosing(deliveryType: deliveryType, bolusNotDelivered: status.bolusNotDelivered) podState.updateFromStatusResponse(status) @@ -619,6 +619,16 @@ public class PodCommsSession { podState.updateFromStatusResponse(statusResponse) return statusResponse } + + @discardableResult + public func getDetailedStatus() throws -> DetailedStatus { + let infoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: .detailedStatus)]) + + guard let faultEvent = infoResponse.podInfo as? DetailedStatus else { + throw PodCommsError.unexpectedResponse(response: .podInfoResponse) + } + return faultEvent + } @discardableResult public func readPulseLogsRequest(podInfoResponseSubType: PodInfoResponseSubType) throws -> PodInfoResponse { @@ -655,7 +665,7 @@ public class PodCommsSession { if let fault = podState.fault { // Be sure to clean up the dosing info in case cancelDelivery() wasn't called // (or if it was called and it had a fault return) & then read the pulse log. - handleCancelDosing(deliveryType: .all, bolusNotDelivered: fault.insulinNotDelivered) + handleCancelDosing(deliveryType: .all, bolusNotDelivered: fault.bolusNotDelivered) do { // read the most recent pulse log entries for later analysis, but don't throw on error let podInfoCommand = GetStatusCommand(podInfoType: .pulseLogRecent) diff --git a/OmniKit/PumpManager/PodState.swift b/OmniKit/PumpManager/PodState.swift index dcc7a49f5..80ec4827c 100644 --- a/OmniKit/PumpManager/PodState.swift +++ b/OmniKit/PumpManager/PodState.swift @@ -76,11 +76,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl return false } - public var fault: PodInfoFaultEvent? + public var fault: DetailedStatus? public var messageTransportState: MessageTransportState public var primeFinishTime: Date? public var setupProgress: SetupProgress - var configuredAlerts: [AlertSlot: PodAlert] + public var configuredAlerts: [AlertSlot: PodAlert] public var activeAlerts: [AlertSlot: PodAlert] { var active = [AlertSlot: PodAlert]() @@ -318,8 +318,11 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl self.finalizedDoses = [] } - if let rawFault = rawValue["fault"] as? PodInfoFaultEvent.RawValue { - self.fault = PodInfoFaultEvent(rawValue: rawFault) + if let rawFault = rawValue["fault"] as? DetailedStatus.RawValue, + let fault = DetailedStatus(rawValue: rawFault), + fault.faultEventCode.faultType != .noFaults + { + self.fault = fault } else { self.fault = nil } @@ -423,7 +426,6 @@ public struct PodState: RawRepresentable, Equatable, CustomDebugStringConvertibl rawValue["setupUnitsDelivered"] = setupUnitsDelivered } - if configuredAlerts.count > 0 { let rawConfiguredAlerts = Dictionary(uniqueKeysWithValues: configuredAlerts.map { slot, alarm in (String(describing: slot.rawValue), alarm.rawValue) }) diff --git a/OmniKitTests/MessageTests.swift b/OmniKitTests/MessageTests.swift index 41db4e7b4..0a10e3bfa 100644 --- a/OmniKitTests/MessageTests.swift +++ b/OmniKitTests/MessageTests.swift @@ -38,7 +38,7 @@ class MessageTests: XCTestCase { XCTAssertEqual(.normal, statusResponse.deliveryStatus) XCTAssertEqual(.aboveFiftyUnits, statusResponse.podProgressStatus) XCTAssertEqual(6.3, statusResponse.insulin, accuracy: 0.01) - XCTAssertEqual(0, statusResponse.insulinNotDelivered) + XCTAssertEqual(0, statusResponse.bolusNotDelivered) XCTAssertEqual(3, statusResponse.podMessageCounter) XCTAssert(statusResponse.alerts.isEmpty) diff --git a/OmniKitTests/PodInfoTests.swift b/OmniKitTests/PodInfoTests.swift index c08ae85f2..059603055 100644 --- a/OmniKitTests/PodInfoTests.swift +++ b/OmniKitTests/PodInfoTests.swift @@ -16,8 +16,8 @@ class PodInfoTests: XCTestCase { do { // Decode let infoResponse = try PodInfoResponse(encodedData: Data(hexadecimalString: "0216020d0000000000ab6a038403ff03860000285708030d0000")!) - XCTAssertEqual(infoResponse.podInfoResponseSubType, .faultEvents) - let faultEvent = infoResponse.podInfo as! PodInfoFaultEvent + XCTAssertEqual(infoResponse.podInfoResponseSubType, .detailedStatus) + let faultEvent = infoResponse.podInfo as! DetailedStatus XCTAssertEqual(faultEvent.faultAccessingTables, false) XCTAssertEqual(faultEvent.logEventErrorType, LogEventErrorCode(rawValue: 2)) } catch (let error) { @@ -118,13 +118,13 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 08 01 0000 0a 0038 00 0000 03ff 0087 00 00 00 95 ff 0000 do { // Decode - let decoded = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "02080100000a003800000003ff008700000095ff0000")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + let decoded = try DetailedStatus(encodedData: Data(hexadecimalString: "02080100000a003800000003ff008700000095ff0000")!) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) XCTAssertEqual(.aboveFiftyUnits, decoded.podProgressStatus) XCTAssertEqual(.normal, decoded.deliveryStatus) - XCTAssertEqual(0000, decoded.insulinNotDelivered) + XCTAssertEqual(0000, decoded.bolusNotDelivered) XCTAssertEqual(0x0a, decoded.podMessageCounter) - XCTAssertEqual(.noFaults, decoded.currentStatus.faultType) + XCTAssertEqual(.noFaults, decoded.faultEventCode.faultType) XCTAssertEqual(0000, decoded.faultEventTimeSinceActivation) XCTAssertEqual(nil, decoded.reservoirLevel) XCTAssertEqual(8100, decoded.timeActive) @@ -147,13 +147,13 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 0f 00 0000 09 0034 5c 0001 03ff 0001 00 00 05 ae 05 6029 do { // Decode - let decoded = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020f0000000900345c000103ff0001000005ae056029")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + let decoded = try DetailedStatus(encodedData: Data(hexadecimalString: "020f0000000900345c000103ff0001000005ae056029")!) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) XCTAssertEqual(.inactive, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) - XCTAssertEqual(0000, decoded.insulinNotDelivered) + XCTAssertEqual(0000, decoded.bolusNotDelivered) XCTAssertEqual(9, decoded.podMessageCounter) - XCTAssertEqual(.primeOpenCountTooLow, decoded.currentStatus.faultType) + XCTAssertEqual(.primeOpenCountTooLow, decoded.faultEventCode.faultType) XCTAssertEqual(60, decoded.faultEventTimeSinceActivation) XCTAssertEqual(nil, decoded.reservoirLevel) XCTAssertEqual(TimeInterval(minutes: 1), decoded.timeActive) @@ -177,13 +177,13 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 0d 00 0000 06 0000 8f 0000 03ff 0000 00 00 03 a2 03 86a0 do { // Decode - let decoded = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d0000000600008f000003ff0000000003a20386a0")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + let decoded = try DetailedStatus(encodedData: Data(hexadecimalString: "020d0000000600008f000003ff0000000003a20386a0")!) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) - XCTAssertEqual(0, decoded.insulinNotDelivered, accuracy: 0.01) + XCTAssertEqual(0, decoded.bolusNotDelivered, accuracy: 0.01) XCTAssertEqual(6, decoded.podMessageCounter) - XCTAssertEqual(.command1AParseUnexpectedFailed, decoded.currentStatus.faultType) + XCTAssertEqual(.command1AParseUnexpectedFailed, decoded.faultEventCode.faultType) XCTAssertEqual(0000*60, decoded.faultEventTimeSinceActivation) XCTAssertEqual(nil, decoded.reservoirLevel) XCTAssertEqual(0, decoded.timeActive) // timeActive converts minutes to seconds @@ -206,14 +206,14 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 0d 00 0000 04 07f2 86 09ff 03ff 0a02 00 00 08 23 08 0000 do { // Decode - let decoded = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d0000000407f28609ff03ff0a0200000823080000")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + let decoded = try DetailedStatus(encodedData: Data(hexadecimalString: "020d0000000407f28609ff03ff0a0200000823080000")!) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) - XCTAssertEqual(0, decoded.insulinNotDelivered) + XCTAssertEqual(0, decoded.bolusNotDelivered) XCTAssertEqual(4, decoded.podMessageCounter) XCTAssertEqual(101.7, decoded.totalInsulinDelivered, accuracy: 0.01) - XCTAssertEqual(.basalOverInfusionPulse, decoded.currentStatus.faultType) + XCTAssertEqual(.basalOverInfusionPulse, decoded.faultEventCode.faultType) XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) XCTAssertEqual(2559 * 60, decoded.faultEventTimeSinceActivation) //09ff XCTAssertEqual("1 day plus 18:39", decoded.faultEventTimeSinceActivation?.stringValue) @@ -235,14 +235,14 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 0d 00 0000 04 07eb 6a 0e0c 03ff 0e14 00 00 28 17 08 0000 do { // Decode - let decoded = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d0000000407eb6a0e0c03ff0e1400002817080000")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + let decoded = try DetailedStatus(encodedData: Data(hexadecimalString: "020d0000000407eb6a0e0c03ff0e1400002817080000")!) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) XCTAssertEqual(.faultEventOccurred, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) - XCTAssertEqual(0, decoded.insulinNotDelivered) + XCTAssertEqual(0, decoded.bolusNotDelivered) XCTAssertEqual(4, decoded.podMessageCounter) XCTAssertEqual(101.35, decoded.totalInsulinDelivered, accuracy: 0.01) - XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.currentStatus.faultType) + XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType) XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) XCTAssertEqual(3596 * 60, decoded.faultEventTimeSinceActivation) //09ff XCTAssertEqual("2 days plus 11:56", decoded.faultEventTimeSinceActivation?.stringValue) @@ -264,14 +264,14 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 0f 00 0001 02 00ec 6a 0268 03ff 026b 00 00 28 a7 08 2023 do { // Decode - let decoded = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020f0000010200ec6a026803ff026b000028a7082023")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + let decoded = try DetailedStatus(encodedData: Data(hexadecimalString: "020f0000010200ec6a026803ff026b000028a7082023")!) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) XCTAssertEqual(.inactive, decoded.podProgressStatus) XCTAssertEqual(.suspended, decoded.deliveryStatus) - XCTAssertEqual(0.05, decoded.insulinNotDelivered) + XCTAssertEqual(0.05, decoded.bolusNotDelivered) XCTAssertEqual(2, decoded.podMessageCounter) XCTAssertEqual(11.8, decoded.totalInsulinDelivered, accuracy: 0.01) - XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.currentStatus.faultType) + XCTAssertEqual(.occlusionCheckAboveThreshold, decoded.faultEventCode.faultType) XCTAssertEqual(0, decoded.unacknowledgedAlerts.rawValue) XCTAssertEqual(616 * 60, decoded.faultEventTimeSinceActivation) //09ff XCTAssertEqual("10:16", decoded.faultEventTimeSinceActivation?.stringValue) @@ -369,7 +369,7 @@ class PodInfoTests: XCTestCase { // 02 16 // 02 0d 00 0000 00 0000 12 ffff 03ff 0000 00 00 87 92 07 0000 do { // Decode - let faultEvent = try PodInfoFaultEvent(encodedData: Data(hexadecimalString: "020d00000000000012ffff03ff000000008792070000")!) + let faultEvent = try DetailedStatus(encodedData: Data(hexadecimalString: "020d00000000000012ffff03ff000000008792070000")!) XCTAssertEqual(faultEvent.faultAccessingTables, false) XCTAssertNil(faultEvent.faultEventTimeSinceActivation) diff --git a/OmniKitTests/StatusTests.swift b/OmniKitTests/StatusTests.swift index 053b8d2dd..0526bae3a 100644 --- a/OmniKitTests/StatusTests.swift +++ b/OmniKitTests/StatusTests.swift @@ -37,7 +37,7 @@ class StatusTests: XCTestCase { XCTAssertEqual(.fiftyOrLessUnits, decoded.podProgressStatus) XCTAssertEqual(129.45, decoded.insulin, accuracy: 0.01) XCTAssertEqual(46.00, decoded.reservoirLevel) - XCTAssertEqual(2.2, decoded.insulinNotDelivered) + XCTAssertEqual(2.2, decoded.bolusNotDelivered) XCTAssertEqual(9, decoded.podMessageCounter) //XCTAssert(,decoded.alarms) } catch (let error) { @@ -64,12 +64,12 @@ class StatusTests: XCTestCase { // 0e 01 02 do { // Encode - let encoded = GetStatusCommand(podInfoType: .faultEvents) + let encoded = GetStatusCommand(podInfoType: .detailedStatus) XCTAssertEqual("0e0102", encoded.data.hexadecimalString) // Decode let decoded = try GetStatusCommand(encodedData: Data(hexadecimalString: "0e0102")!) - XCTAssertEqual(.faultEvents, decoded.podInfoType) + XCTAssertEqual(.detailedStatus, decoded.podInfoType) } catch (let error) { XCTFail("message decoding threw error: \(error)") } diff --git a/OmniKitUI/ViewControllers/CommandResponseViewController.swift b/OmniKitUI/ViewControllers/CommandResponseViewController.swift index 280b3ecbc..ed93085d3 100644 --- a/OmniKitUI/ViewControllers/CommandResponseViewController.swift +++ b/OmniKitUI/ViewControllers/CommandResponseViewController.swift @@ -43,12 +43,64 @@ extension CommandResponseViewController { return LocalizedString("Changing time…", comment: "Progress message for changing pod time.") } } + + private static func podStatusString(status: DetailedStatus, configuredAlerts: [AlertSlot: PodAlert]) -> String { + var result, str: String + + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .full + formatter.allowedUnits = [.day, .hour, .minute] + if let timeStr = formatter.string(from: status.timeActive) { + str = timeStr + } else { + str = String(format: LocalizedString("%1$@ minutes", comment: "The format string for minutes (1: number of minutes string)"), String(describing: Int(status.timeActive / 60))) + } + result = String(format: LocalizedString("Pod Active: %1$@\n", comment: "The format string for Pod Active: (1: Pod active time string)"), str) + + 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("Total Insulin Delivered: %1$@ U\n", comment: "The format string for Total Insulin Delivered: (1: total insulin delivered string)"), status.totalInsulinDelivered.twoDecimals) + + result += String(format: LocalizedString("Reservoir Level: %1$@ U\n", comment: "The format string for Reservoir Level: (1: reservoir level string)"), status.reservoirLevel?.twoDecimals ?? "50+") + + result += String(format: LocalizedString("Last Bolus Not Delivered: %1$@ U\n", comment: "The format string for Last Bolus Not Delivered: (1: bolus not delivered string)"), status.bolusNotDelivered.twoDecimals) + + let alertsDescription = status.unacknowledgedAlerts.map { (slot) -> String in + if let podAlert = configuredAlerts[slot] { + return String(describing: podAlert) + } else { + return String(describing: slot) + } + } + result += String(format: LocalizedString("Alerts: %1$@\n", comment: "The format string for Alerts: (1: the alerts string)"), alertsDescription.joined(separator: ", ")) + + result += String(format: LocalizedString("RSSI: %1$@\n", comment: "The format string for RSSI: (1: RSSI value)"), String(describing: status.radioRSSI)) + + result += String(format: LocalizedString("Receiver Low Gain: %1$@\n", comment: "The format string for receiverLowGain: (1: receiverLowGain)"), String(describing: status.receiverLowGain)) + + 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) + result += String(format: LocalizedString("Previous pod progress: %1$@\n", comment: "The format string for previous pod progress: (1: previous pod progress string)"), String(describing: status.previousPodProgressStatus)) + if let faultEventTimeSinceActivation = status.faultEventTimeSinceActivation, let faultTimeStr = formatter.string(from: faultEventTimeSinceActivation) { + result += String(format: LocalizedString("Fault time: %1$@\n", comment: "The format string for fault time: (1: fault time string)"), faultTimeStr) + } + } + + return result + } static func readPodStatus(pumpManager: OmnipodPumpManager) -> T { return T { (completionHandler) -> String in - pumpManager.readPodStatus() { (response) in + pumpManager.readPodStatus() { (result) in DispatchQueue.main.async { - completionHandler(response) + switch result { + case .success(let status): + let configuredAlerts = pumpManager.state.podState!.configuredAlerts + completionHandler(podStatusString(status: status, configuredAlerts: configuredAlerts)) + case .failure(let error): + completionHandler(error.localizedDescription) + } } } return LocalizedString("Read Pod Status…", comment: "Progress message for reading Pod status.") @@ -95,3 +147,10 @@ extension CommandResponseViewController { } } +extension Double { + var twoDecimals: String { + let reservoirLevel = self + return String(format: "%.2f", reservoirLevel) + } +} + diff --git a/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift b/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift index 9d0f0fed6..864157e6b 100644 --- a/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift +++ b/OmniKitUI/ViewControllers/OmnipodSettingsViewController.swift @@ -33,10 +33,18 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { var statusError: Error? - var podState: PodState? + var podState: PodState? { + didSet { + refreshButton.isHidden = !refreshAvailable + } + } var pumpManagerStatus: PumpManagerStatus? + var refreshAvailable: Bool { + return podState != nil + } + private var bolusProgressTimer: Timer? init(pumpManager: OmnipodPumpManager) { @@ -56,10 +64,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { fatalError("init(coder:) has not been implemented") } - public var podImage: UIImage? { - return UIImage(named: "PodLarge", in: Bundle(for: OmnipodSettingsViewController.self), compatibleWith: nil)! - } - lazy var suspendResumeTableViewCell: SuspendResumeTableViewCell = { let cell = SuspendResumeTableViewCell(style: .default, reuseIdentifier: nil) cell.basalDeliveryState = pumpManager.status.basalDeliveryState @@ -71,6 +75,9 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { cell.updateTextLabel(enabled: pumpManager.confirmationBeeps) return cell }() + + var activityIndicator: UIActivityIndicatorView! + var refreshButton: UIButton! override func viewDidLoad() { super.viewDidLoad() @@ -87,10 +94,46 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className) tableView.register(AlarmsTableViewCell.self, forCellReuseIdentifier: AlarmsTableViewCell.className) tableView.register(ExpirationReminderDateTableViewCell.nib(), forCellReuseIdentifier: ExpirationReminderDateTableViewCell.className) - + + let podImage = UIImage(named: "PodLarge", in: Bundle(for: OmnipodSettingsViewController.self), compatibleWith: nil)! let imageView = UIImageView(image: podImage) imageView.contentMode = .center imageView.frame.size.height += 18 // feels right + + let activityIndicatorStyle: UIActivityIndicatorView.Style + if #available(iOSApplicationExtension 13.0, *) { + activityIndicatorStyle = .medium + } else { + activityIndicatorStyle = .white + } + activityIndicator = UIActivityIndicatorView(style: activityIndicatorStyle) + activityIndicator.hidesWhenStopped = true + + imageView.addSubview(activityIndicator) + + refreshButton = UIButton(type: .custom) + if #available(iOSApplicationExtension 13.0, *) { + let medConfig = UIImage.SymbolConfiguration(pointSize: 21, weight: .bold, scale: .medium) + refreshButton.setImage(UIImage(systemName: "arrow.clockwise", withConfiguration: medConfig), for: .normal) + refreshButton.tintColor = .systemFill + } + refreshButton.addTarget(self, action: #selector(refreshTapped(_:)), for: .touchUpInside) + imageView.isUserInteractionEnabled = true + imageView.addSubview(refreshButton) + + let margin: CGFloat = 15 + + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + refreshButton.translatesAutoresizingMaskIntoConstraints = false + let parent = imageView.layoutMarginsGuide + NSLayoutConstraint.activate([ + activityIndicator.trailingAnchor.constraint(equalTo: parent.trailingAnchor, constant: -margin), + activityIndicator.bottomAnchor.constraint(equalTo: parent.bottomAnchor, constant: -margin), + refreshButton.centerYAnchor.constraint(equalTo: activityIndicator.centerYAnchor), + refreshButton.centerXAnchor.constraint(equalTo: activityIndicator.centerXAnchor), + ]) + + tableView.tableHeaderView = imageView if #available(iOSApplicationExtension 13.0, *) { @@ -101,11 +144,32 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:))) self.navigationItem.setRightBarButton(button, animated: false) + + if self.podState != nil { + refreshPodStatus() + } else { + refreshButton.isHidden = true + } } @objc func doneTapped(_ sender: Any) { done() } + + @objc func refreshTapped(_ sender: Any) { + refreshPodStatus() + } + + private func refreshPodStatus() { + refreshButton.isHidden = true + activityIndicator.startAnimating() + pumpManager.refreshStatus { (_) in + DispatchQueue.main.async { + self.refreshButton.isHidden = false + self.activityIndicator.stopAnimating() + } + } + } private func done() { if let nav = navigationController as? SettingsNavigationViewController { @@ -124,8 +188,10 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } } - if let configSectionIdx = self.sections.firstIndex(of: .configuration) { - self.tableView.reloadRows(at: [IndexPath(row: ConfigurationRow.reminder.rawValue, section: configSectionIdx)], with: .none) + if let configSectionIdx = self.sections.firstIndex(of: .configuration), + let replacePodRowIdx = self.configurationRows.firstIndex(of: .replacePod) + { + self.tableView.reloadRows(at: [IndexPath(row: replacePodRowIdx, section: configSectionIdx)], with: .none) } super.viewWillAppear(animated) @@ -146,10 +212,10 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { // MARK: - Data Source private enum Section: Int, CaseIterable { - case podDetails = 0 - case actions + case status = 0 + case podDetails + case diagnostics case configuration - case status case rileyLinks case deletePumpManager } @@ -157,12 +223,12 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { private class func sectionList(_ podState: PodState?) -> [Section] { if let podState = podState { if podState.unfinishedPairing { - return [.actions, .rileyLinks] + return [.rileyLinks, .diagnostics] } else { - return [.podDetails, .actions, .configuration, .status, .rileyLinks] + return [.status, .configuration, .rileyLinks, .podDetails, .diagnostics] } } else { - return [.actions, .rileyLinks, .deletePumpManager] + return [.configuration, .rileyLinks, .deletePumpManager] } } @@ -171,40 +237,40 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } private enum PodDetailsRow: Int, CaseIterable { - case activatedAt = 0 - case expiresAt - case podAddress + case podAddress = 0 case podLot case podTid case piVersion case pmVersion } - private enum ActionsRow: Int, CaseIterable { - case suspendResume = 0 - case readPodStatus + private enum Diagnostics: Int, CaseIterable { + case readPodStatus = 0 case playTestBeeps case readPulseLog case testCommand - case replacePod + case enableDisableConfirmationBeeps } - private var actions: [ActionsRow] { + private var configurationRows: [ConfigurationRow] { if podState == nil || podState?.unfinishedPairing == true { return [.replacePod] } else { - return ActionsRow.allCases + return ConfigurationRow.allCases } } private enum ConfigurationRow: Int, CaseIterable { - case reminder = 0 + case suspendResume = 0 + case reminder case timeZoneOffset - case enableDisableConfirmationBeeps + case replacePod } fileprivate enum StatusRow: Int, CaseIterable { - case bolus = 0 + case activatedAt = 0 + case expiresAt + case bolus case basal case alarms case reservoirLevel @@ -221,10 +287,10 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { switch sections[section] { case .podDetails: return PodDetailsRow.allCases.count - case .actions: - return actions.count + case .diagnostics: + return Diagnostics.allCases.count case .configuration: - return ConfigurationRow.allCases.count + return configurationRows.count case .status: return StatusRow.allCases.count case .rileyLinks: @@ -237,13 +303,13 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch sections[section] { case .podDetails: - return LocalizedString("Device Information", comment: "The title of the device information section in settings") - case .actions: - return nil + 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 LocalizedString("Configuration", comment: "The title of the configuration section in settings") + return nil case .status: - return LocalizedString("Status", comment: "The title of the status section in settings") + return nil case .rileyLinks: return super.tableView(tableView, titleForHeaderInSection: section) case .deletePumpManager: @@ -255,7 +321,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { switch sections[section] { case .rileyLinks: return super.tableView(tableView, viewForHeaderInSection: section) - case .podDetails, .actions, .configuration, .status, .deletePumpManager: + case .podDetails, .diagnostics, .configuration, .status, .deletePumpManager: return nil } } @@ -265,22 +331,6 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { case .podDetails: let podState = self.podState! switch PodDetailsRow(rawValue: indexPath.row)! { - 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 .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") @@ -307,11 +357,9 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { cell.detailTextLabel?.text = podState.pmVersion return cell } - case .actions: + case .diagnostics: - switch actions[indexPath.row] { - case .suspendResume: - return suspendResumeTableViewCell + 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") @@ -332,25 +380,14 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { cell.textLabel?.text = LocalizedString("Test Command", comment: "The title of the command to run the test command") cell.accessoryType = .disclosureIndicator return cell - case .replacePod: - let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell - if podState == nil { - 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 { - 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") - cell.tintColor = .deleteColor - } - - cell.isEnabled = true - return cell + case .enableDisableConfirmationBeeps: + return confirmationBeepsTableViewCell } case .configuration: - switch ConfigurationRow(rawValue: indexPath.row)! { + switch configurationRows[indexPath.row] { + case .suspendResume: + return suspendResumeTableViewCell case .reminder: let cell = tableView.dequeueReusableCell(withIdentifier: ExpirationReminderDateTableViewCell.className, for: indexPath) as! ExpirationReminderDateTableViewCell if let podState = podState, let reminderDate = pumpManager.expirationReminderDate { @@ -380,8 +417,21 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } cell.accessoryType = .disclosureIndicator return cell - case .enableDisableConfirmationBeeps: - return confirmationBeepsTableViewCell + case .replacePod: + let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell + if podState == nil { + 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 { + 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") + cell.tintColor = .deleteColor + } + + cell.isEnabled = true + return cell } case .status: @@ -396,6 +446,22 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { 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") @@ -451,7 +517,7 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { default: return false } - case .actions, .configuration, .rileyLinks, .deletePumpManager: + case .diagnostics, .configuration, .rileyLinks, .deletePumpManager: return true } } @@ -470,11 +536,8 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { switch sections[indexPath.section] { case .podDetails: break - case .actions: - switch actions[indexPath.row] { - case .suspendResume: - suspendResumeTapped() - tableView.deselectRow(at: indexPath, animated: true) + case .diagnostics: + switch Diagnostics(rawValue: indexPath.row)! { case .readPodStatus: let vc = CommandResponseViewController.readPodStatus(pumpManager: pumpManager) vc.title = sender?.textLabel?.text @@ -491,30 +554,18 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { let vc = CommandResponseViewController.testingCommands(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 { - vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager) - } else if let podState = podState, podState.unfinishedPairing { - vc = PodReplacementNavigationController.instantiateInsertCannulaFlow(pumpManager) - } else { - vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager) - } - if var completionNotifying = vc as? CompletionNotifying { - completionNotifying.completionDelegate = self - } - self.navigationController?.present(vc, animated: true, completion: nil) + case .enableDisableConfirmationBeeps: + confirmationBeepsTapped() + tableView.deselectRow(at: indexPath, animated: true) } case .status: switch StatusRow(rawValue: indexPath.row)! { case .alarms: if let cell = tableView.cellForRow(at: indexPath) as? AlarmsTableViewCell { - cell.isLoading = true - cell.isEnabled = false let activeSlots = AlertSet(slots: Array(cell.alerts.keys)) if activeSlots.count > 0 { + cell.isLoading = true + cell.isEnabled = false pumpManager.acknowledgeAlerts(activeSlots) { (updatedAlerts) in DispatchQueue.main.async { cell.isLoading = false @@ -525,12 +576,16 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { } } } + tableView.deselectRow(at: indexPath, animated: true) } default: break } case .configuration: - switch ConfigurationRow(rawValue: indexPath.row)! { + switch configurationRows[indexPath.row] { + case .suspendResume: + suspendResumeTapped() + tableView.deselectRow(at: indexPath, animated: true) case .reminder: tableView.deselectRow(at: indexPath, animated: true) tableView.endUpdates() @@ -539,9 +594,21 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { let vc = CommandResponseViewController.changeTime(pumpManager: pumpManager) vc.title = sender?.textLabel?.text show(vc, sender: indexPath) - case .enableDisableConfirmationBeeps: - confirmationBeepsTapped() - tableView.deselectRow(at: indexPath, animated: true) + case .replacePod: + let vc: UIViewController + if podState == nil || podState!.setupProgress.primingNeeded { + vc = PodReplacementNavigationController.instantiateNewPodFlow(pumpManager) + } else if let podState = podState, podState.isFaulted { + vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager) + } else if let podState = podState, podState.unfinishedPairing { + vc = PodReplacementNavigationController.instantiateInsertCannulaFlow(pumpManager) + } else { + vc = PodReplacementNavigationController.instantiatePodReplacementFlow(pumpManager) + } + if var completionNotifying = vc as? CompletionNotifying { + completionNotifying.completionDelegate = self + } + self.navigationController?.present(vc, animated: true, completion: nil) } case .rileyLinks: let device = devicesDataSource.devices[indexPath.row] @@ -566,18 +633,18 @@ class OmnipodSettingsViewController: RileyLinkSettingsViewController { switch sections[indexPath.section] { case .podDetails, .status: break - case .actions: - switch ActionsRow(rawValue: indexPath.row)! { - case .suspendResume, .replacePod: + case .diagnostics: + switch Diagnostics(rawValue: indexPath.row)! { + case .enableDisableConfirmationBeeps: break case .readPodStatus, .playTestBeeps, .readPulseLog, .testCommand: tableView.reloadRows(at: [indexPath], with: .fade) } case .configuration: - switch ConfigurationRow(rawValue: indexPath.row)! { - case .reminder, .enableDisableConfirmationBeeps: + switch configurationRows[indexPath.row] { + case .reminder, .suspendResume: break - case .timeZoneOffset: + case .timeZoneOffset, .replacePod: tableView.reloadRows(at: [indexPath], with: .fade) } case .rileyLinks: @@ -665,7 +732,7 @@ extension OmnipodSettingsViewController: RadioSelectionTableViewControllerDelega switch sections[indexPath.section] { case .configuration: - switch ConfigurationRow(rawValue: indexPath.row)! { + switch configurationRows[indexPath.row] { default: assertionFailure() } @@ -682,7 +749,7 @@ extension OmnipodSettingsViewController: PodStateObserver { let newSections = OmnipodSettingsViewController.sectionList(state) let sectionsChanged = OmnipodSettingsViewController.sectionList(self.podState) != newSections - let oldActionsCount = self.actions.count + let oldConfigurationRowsCount = self.configurationRows.count let oldState = self.podState self.podState = state @@ -690,7 +757,7 @@ extension OmnipodSettingsViewController: PodStateObserver { self.devicesDataSource.devicesSectionIndex = self.sections.firstIndex(of: .rileyLinks)! self.tableView.reloadData() } else { - if oldActionsCount != self.actions.count, let idx = newSections.firstIndex(of: .actions) { + if oldConfigurationRowsCount != self.configurationRows.count, let idx = newSections.firstIndex(of: .configuration) { self.tableView.reloadSections([idx], with: .fade) } } @@ -775,7 +842,7 @@ class AlarmsTableViewCell: LoadingTableViewCell { } private func updateColor() { - if alerts == .none { + if alerts.count == 0 { detailTextLabel?.textColor = defaultDetailColor } else { detailTextLabel?.textColor = tintColor @@ -795,7 +862,11 @@ class AlarmsTableViewCell: LoadingTableViewCell { var alerts = [AlertSlot: PodAlert]() { didSet { updateColor() - detailTextLabel?.text = alerts.map { slot, alert in String.init(describing: alert) }.joined(separator: ", ") + if alerts.isEmpty { + detailTextLabel?.text = LocalizedString("None", comment: "Alerts detail when no alerts unacknowledged") + } else { + detailTextLabel?.text = alerts.map { slot, alert in String.init(describing: alert) }.joined(separator: ", ") + } } } diff --git a/OmniKitUI/ViewControllers/ReplacePodViewController.swift b/OmniKitUI/ViewControllers/ReplacePodViewController.swift index cb9b4c1c2..5bc7ac5de 100644 --- a/OmniKitUI/ViewControllers/ReplacePodViewController.swift +++ b/OmniKitUI/ViewControllers/ReplacePodViewController.swift @@ -49,7 +49,7 @@ class ReplacePodViewController: SetupTableViewController { if podFault.podProgressStatus == .activationTimeExceeded { self.replacementReason = .activationTimeout } else { - self.replacementReason = .fault(podFault.currentStatus) + self.replacementReason = .fault(podFault.faultEventCode) } } else if podState?.setupProgress.primingNeeded == true { self.replacementReason = .canceledPairingBeforeApplication diff --git a/RileyLink.xcodeproj/project.pbxproj b/RileyLink.xcodeproj/project.pbxproj index 954ff7db9..0188bf852 100644 --- a/RileyLink.xcodeproj/project.pbxproj +++ b/RileyLink.xcodeproj/project.pbxproj @@ -463,7 +463,7 @@ C1BB12A521CB5654009A29B5 /* PodInfoConfiguredAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C06B252150371700B602AD /* PodInfoConfiguredAlerts.swift */; }; C1BB12A621CB5654009A29B5 /* PodInfoDataLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E54AB52156B2D500E319B8 /* PodInfoDataLog.swift */; }; C1BB12A721CB5654009A29B5 /* PodInfoFault.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EDD474215AFFF300A103D1 /* PodInfoFault.swift */; }; - C1BB12A821CB5654009A29B5 /* PodInfoFaultEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE336D214ED01200888876 /* PodInfoFaultEvent.swift */; }; + C1BB12A821CB5654009A29B5 /* DetailedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE336D214ED01200888876 /* DetailedStatus.swift */; }; C1BB12A921CB5654009A29B5 /* PodInfoPulseLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D065F215D76E40072157B /* PodInfoPulseLog.swift */; }; C1BB12AB21CB5654009A29B5 /* PodInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFAFA5213323E800C50C1D /* PodInfoResponse.swift */; }; C1BB12AD21CB5654009A29B5 /* SetInsulinScheduleCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FFAFB2213323E800C50C1D /* SetInsulinScheduleCommand.swift */; }; @@ -612,7 +612,7 @@ E9EE3368214ECFF900888876 /* StatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE3367214ECFF900888876 /* StatusTests.swift */; }; E9EE336A214ED00400888876 /* BolusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE3369214ED00400888876 /* BolusTests.swift */; }; E9EE336E214ED01200888876 /* PodInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE336B214ED01200888876 /* PodInfo.swift */; }; - E9EE3370214ED01200888876 /* PodInfoFaultEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE336D214ED01200888876 /* PodInfoFaultEvent.swift */; }; + E9EE3370214ED01200888876 /* DetailedStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EE336D214ED01200888876 /* DetailedStatus.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1523,7 +1523,7 @@ E9EE3367214ECFF900888876 /* StatusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusTests.swift; sourceTree = ""; }; E9EE3369214ED00400888876 /* BolusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusTests.swift; sourceTree = ""; }; E9EE336B214ED01200888876 /* PodInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfo.swift; sourceTree = ""; }; - E9EE336D214ED01200888876 /* PodInfoFaultEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PodInfoFaultEvent.swift; sourceTree = ""; }; + E9EE336D214ED01200888876 /* DetailedStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailedStatus.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2582,7 +2582,7 @@ E9C06B252150371700B602AD /* PodInfoConfiguredAlerts.swift */, E9E54AB52156B2D500E319B8 /* PodInfoDataLog.swift */, E9EDD474215AFFF300A103D1 /* PodInfoFault.swift */, - E9EE336D214ED01200888876 /* PodInfoFaultEvent.swift */, + E9EE336D214ED01200888876 /* DetailedStatus.swift */, E95D065F215D76E40072157B /* PodInfoPulseLog.swift */, C1FFAFA5213323E800C50C1D /* PodInfoResponse.swift */, C1FFAFB2213323E800C50C1D /* SetInsulinScheduleCommand.swift */, @@ -3971,7 +3971,7 @@ C1BB12BB21CB5767009A29B5 /* LocalizedString.swift in Sources */, C1BB12BE21CB57AA009A29B5 /* BasalSchedule.swift in Sources */, C1BB129F21CB5654009A29B5 /* DeactivatePodCommand.swift in Sources */, - C1BB12A821CB5654009A29B5 /* PodInfoFaultEvent.swift in Sources */, + C1BB12A821CB5654009A29B5 /* DetailedStatus.swift in Sources */, C1BB128821CB5603009A29B5 /* main.swift in Sources */, C1BB12B921CB571C009A29B5 /* Pod.swift in Sources */, C1BB12B021CB5654009A29B5 /* TempBasalExtraCommand.swift in Sources */, @@ -4047,7 +4047,7 @@ C1FFAFBE213323E900C50C1D /* CRC16.swift in Sources */, D807D7DA228913EC006BCDF0 /* BeepConfigCommand.swift in Sources */, C1FFAFCD213323E900C50C1D /* SetupPodCommand.swift in Sources */, - E9EE3370214ED01200888876 /* PodInfoFaultEvent.swift in Sources */, + E9EE3370214ED01200888876 /* DetailedStatus.swift in Sources */, E9EE336E214ED01200888876 /* PodInfo.swift in Sources */, C1FFAFD2213323E900C50C1D /* PodComms.swift in Sources */, C1FFB01921332A7100C50C1D /* IdentifiableClass.swift in Sources */,