From 90e7ddeb6550858fd6dc3923ccfeee414ae755f9 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 31 Aug 2017 23:28:28 -0500 Subject: [PATCH 1/2] Device errors --- Loop.xcodeproj/project.pbxproj | 4 + Loop/Managers/DeviceDataManager.swift | 83 +++++++++---------- Loop/Managers/LoopDataManager.swift | 1 + .../CommandResponseViewController.swift | 1 + .../StatusTableViewController.swift | 14 +++- 5 files changed, 56 insertions(+), 47 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 73fe92bba3..be7f7a6161 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; }; 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; 4302F4E51D4EA75100F0FCAF /* DoseStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */; }; + 43045E581F25AC1700FD9CE1 /* RileyLinkDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43045E571F25AC1700FD9CE1 /* RileyLinkDeviceManager.swift */; }; 43076BF31DFDBC4B0012A723 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43076BF21DFDBC4B0012A723 /* it.lproj */; }; 4309786C1E73D2F500BEBC82 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 4309786B1E73D2F500BEBC82 /* it.lproj */; }; 4309786E1E73DAD100BEBC82 /* CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4309786D1E73DAD100BEBC82 /* CGM.swift */; }; @@ -371,6 +372,7 @@ 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoseStore.swift; sourceTree = ""; }; + 43045E571F25AC1700FD9CE1 /* RileyLinkDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RileyLinkDeviceManager.swift; sourceTree = ""; }; 43076BF21DFDBC4B0012A723 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; 4309786B1E73D2F500BEBC82 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; 4309786D1E73DAD100BEBC82 /* CGM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGM.swift; sourceTree = ""; }; @@ -958,6 +960,7 @@ C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */, 43C094491CACCC73001F6403 /* NotificationManager.swift */, 432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */, + 43045E571F25AC1700FD9CE1 /* RileyLinkDeviceManager.swift */, 430C1ABC1E5568A80067F1AE /* StatusChartsManager+LoopKit.swift */, 4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */, 4328E0341CFC0AE100E199AA /* WatchDataManager.swift */, @@ -1533,6 +1536,7 @@ 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, 435CB6271F37AE5600C320C7 /* WalshInsulinModel.swift in Sources */, 43E344A41B9E1B1C00C85C07 /* NSUserDefaults.swift in Sources */, + 43045E581F25AC1700FD9CE1 /* RileyLinkDeviceManager.swift in Sources */, 43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */, C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */, 435400321C9F745500D5819C /* BolusSuggestionUserInfo.swift in Sources */, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 10af3d58e3..d112d53ad9 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -38,6 +38,15 @@ final class DeviceDataManager { var latestPumpStatus: RileyLinkKit.PumpStatus? + private(set) var lastError: (date: Date, error: Error)? + + fileprivate func setLastError(error: Error) { + DispatchQueue.main.async { // Synchronize writes + self.lastError = (date: Date(), error: error) + // TODO: Notify observers of change + } + } + // Returns a value in the range 0 - 1 var pumpBatteryChargeRemaining: Double? { get { @@ -159,8 +168,7 @@ final class DeviceDataManager { // Gather PumpStatus from MySentry packet let pumpStatus: NightscoutUploadKit.PumpStatus? - if let pumpDate = pumpDateComponents.date, let pumpID = pumpID { - + if let pumpID = pumpID { let batteryStatus = BatteryStatus(percent: status.batteryRemainingPercent) let iobStatus = IOBStatus(timestamp: pumpDate, iob: status.iob) @@ -189,7 +197,10 @@ final class DeviceDataManager { switch result { case .newData(let values): self.loopManager.addGlucose(values, from: self.cgmManager?.device) - case .noData, .error: + case .noData: + break + case .error(let error): + self.setLastError(error: error) break } } @@ -217,11 +228,16 @@ final class DeviceDataManager { loopManager.addReservoirValue(units, at: date) { (result) in switch result { case .failure(let error): + self.setLastError(error: error) self.logger.addError(error, fromSource: "DoseStore") case .success(let (newValue, lastValue, areStoredValuesContinuous)): // Run a loop as long as we have fresh, reliable pump data. if self.preferredInsulinDataSource == .pumpHistory || !areStoredValuesContinuous { self.fetchPumpHistory { (error) in + if let error = error { + self.setLastError(error: error) + } + if error == nil || areStoredValuesContinuous { self.loopManager.loop() } @@ -258,8 +274,9 @@ final class DeviceDataManager { /// - Parameters: /// - completion: A closure called once upon completion /// - error: An error describing why the fetch and/or store failed - private func fetchPumpHistory(_ completion: @escaping (_ error: Error?) -> Void) { + fileprivate func fetchPumpHistory(_ completion: @escaping (_ error: Error?) -> Void) { guard let device = rileyLinkManager.firstConnectedDevice else { + completion(LoopError.connectionError) return } @@ -284,39 +301,6 @@ final class DeviceDataManager { } } - /** - Read the pump's current state, including reservoir and clock - - - parameter completion: A closure called after the command is complete. This closure takes a single Result argument: - - Success(status, date): The pump status, and the resolved date according to the pump's clock - - Failure(error): An error describing why the command failed - */ - private func readPumpData(_ completion: @escaping (RileyLinkKit.Either<(status: RileyLinkKit.PumpStatus, date: Date), Error>) -> Void) { - guard let device = rileyLinkManager.firstConnectedDevice, let ops = device.ops else { - completion(.failure(LoopError.connectionError)) - return - } - - ops.readPumpStatus { (result) in - switch result { - case .success(let status): - var clock = status.clock - clock.timeZone = ops.pumpState.timeZone - - guard let date = clock.date else { - let errorStr = "Could not interpret pump clock: \(clock)" - self.logger.addError(errorStr, fromSource: "RileyLink") - completion(.failure(LoopError.invalidData(details: errorStr))) - return - } - completion(.success((status: status, date: date))) - case .failure(let error): - self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink") - completion(.failure(error)) - } - } - } - private func pumpDataIsStale() -> Bool { // How long should we wait before we poll for new pump data? let pumpStatusAgeTolerance = rileyLinkManager.idleListeningEnabled ? TimeInterval(minutes: 11) : TimeInterval(minutes: 4) @@ -330,6 +314,7 @@ final class DeviceDataManager { */ fileprivate func assertCurrentPumpData() { guard let device = rileyLinkManager.firstConnectedDevice else { + self.setLastError(error: LoopError.connectionError) return } @@ -339,7 +324,7 @@ final class DeviceDataManager { return } - readPumpData { (result) in + rileyLinkManager.readPumpData { (result) in let nsPumpStatus: NightscoutUploadKit.PumpStatus? switch result { case .success(let (status, date)): @@ -352,6 +337,8 @@ final class DeviceDataManager { nsPumpStatus = NightscoutUploadKit.PumpStatus(clock: date, pumpID: status.pumpID, iob: nil, battery: battery, suspended: status.suspended, bolusing: status.bolusing, reservoir: status.reservoir) case .failure(let error): + self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink") + self.setLastError(error: error) self.troubleshootPumpComms(using: device) self.nightscoutDataManager.uploadLoopStatus(loopError: error) nsPumpStatus = nil @@ -406,7 +393,7 @@ final class DeviceDataManager { loopManager.doseStore.lastReservoirVolumeDrop < 0 || loopManager.doseStore.lastReservoirValue!.startDate.timeIntervalSinceNow <= TimeInterval(minutes: -6) { - readPumpData { (result) in + rileyLinkManager.readPumpData { (result) in switch result { case .success(let (status, date)): self.loopManager.addReservoirValue(status.reservoir, at: date) { (result) in @@ -425,6 +412,8 @@ final class DeviceDataManager { default: notify(error) } + + self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink") } } } else { @@ -449,6 +438,7 @@ final class DeviceDataManager { case .failure(let error): self.logger.addError("Device \(device.name ?? "") auto-tune failed with error: \(error)", fromSource: "RileyLink") self.rileyLinkManager.deprioritizeDevice(device: device) + self.setLastError(error: error) } } } else { @@ -660,7 +650,10 @@ extension DeviceDataManager: CGMManagerDelegate { loopManager.addGlucose(values, from: manager.device) { _ in self.assertCurrentPumpData() } - case .noData, .error: + case .noData: + break + case .error(let error): + self.setLastError(error: error) self.assertCurrentPumpData() } } @@ -721,6 +714,12 @@ extension DeviceDataManager: LoopDataManagerDelegate { value: body.rate, unit: .unitsPerHour ))) + + // If we haven't fetched history in a while (preferredInsulinDataSource == .reservoir), + // let's try to do so while the pump radio is on. + if self.loopManager.doseStore.lastAddedPumpEvents.timeIntervalSinceNow < .minutes(-4) { + self.fetchPumpHistory { (_) in } + } case .failure(let error): completion(.failure(error)) } @@ -736,15 +735,13 @@ extension DeviceDataManager: CustomDebugStringConvertible { "## DeviceDataManager", "launchDate: \(launchDate)", "cgm: \(String(describing: cgm))", + "lastError: \(String(describing: lastError))", "latestPumpStatusFromMySentry: \(String(describing: latestPumpStatusFromMySentry))", "pumpState: \(String(reflecting: pumpState))", "preferredInsulinDataSource: \(preferredInsulinDataSource)", cgmManager != nil ? String(reflecting: cgmManager!) : "", String(reflecting: rileyLinkManager), String(reflecting: statusExtensionManager!), - "", - "## NSUserDefaults", - String(reflecting: UserDefaults.standard.dictionaryRepresentation()) ].joined(separator: "\n") } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 1148a0575e..735c07f6a0 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1084,6 +1084,7 @@ extension LoopDataManager { entries.append("insulinOnBoard: \(String(describing: insulinOnBoard))") entries.append("error: \(String(describing: loopError))") + entries.append("") self.glucoseStore.generateDiagnosticReport { (report) in entries.append(report) diff --git a/Loop/View Controllers/CommandResponseViewController.swift b/Loop/View Controllers/CommandResponseViewController.swift index 5ea4ceacc4..5fe9ba345b 100644 --- a/Loop/View Controllers/CommandResponseViewController.swift +++ b/Loop/View Controllers/CommandResponseViewController.swift @@ -18,6 +18,7 @@ extension CommandResponseViewController { completionHandler([ "Use the Share button above save this diagnostic report to aid investigating your problem. Issues can be filed at https://github.com/LoopKit/Loop/issues.", "Generated: \(Date())", + "", String(reflecting: dataManager), "", report, diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 2516ebc60a..777911bf24 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -126,6 +126,8 @@ final class StatusTableViewController: ChartsTableViewController { } } + private var lastLoopError: Error? + private var refreshContext = RefreshContext.all private var bolusState: BolusState? { @@ -163,6 +165,7 @@ final class StatusTableViewController: ChartsTableViewController { let reloadGroup = DispatchGroup() var lastLoopCompleted: Date? + var lastLoopError: Error? var lastReservoirValue: ReservoirValue? var lastTempBasal: DoseEntry? var newRecommendedTempBasal: LoopDataManager.TempBasalRecommendation? @@ -209,6 +212,7 @@ final class StatusTableViewController: ChartsTableViewController { lastTempBasal = state.lastTempBasal lastLoopCompleted = state.lastLoopCompleted + lastLoopError = state.error if let lastPoint = self.charts.predictedGlucosePoints.last?.y { self.eventualGlucoseDescription = String(describing: lastPoint) @@ -319,6 +323,7 @@ final class StatusTableViewController: ChartsTableViewController { // Loop completion HUD self.hudView.loopCompletionHUD.lastLoopCompleted = lastLoopCompleted + self.lastLoopError = lastLoopError // Net basal rate HUD let date = lastTempBasal?.startDate ?? Date() @@ -814,10 +819,11 @@ final class StatusTableViewController: ChartsTableViewController { } @objc private func showLastError(_: Any) { - self.deviceManager.loopManager.getLoopState { (_, state) in - if let error = state.error { - self.presentAlertController(with: error) - } + // First, check whether we have a device error after the most recent completion date + if let deviceError = deviceManager.lastError, deviceError.date > (hudView.loopCompletionHUD.lastLoopCompleted ?? .distantPast) { + self.presentAlertController(with: deviceError.error) + } else if let lastLoopError = lastLoopError { + self.presentAlertController(with: lastLoopError) } } From 187712f46a89715d5510ab9c6b04c36d3c027e3e Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 31 Aug 2017 23:28:48 -0500 Subject: [PATCH 2/2] Device errors --- Loop/Managers/RileyLinkDeviceManager.swift | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Loop/Managers/RileyLinkDeviceManager.swift diff --git a/Loop/Managers/RileyLinkDeviceManager.swift b/Loop/Managers/RileyLinkDeviceManager.swift new file mode 100644 index 0000000000..99e0d05afb --- /dev/null +++ b/Loop/Managers/RileyLinkDeviceManager.swift @@ -0,0 +1,42 @@ +// +// RileyLinkDeviceManager.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import RileyLinkKit + + +extension RileyLinkDeviceManager { + /// Read the pump's current state, including reservoir and clock + /// + /// - Parameter completion: A closure called after the command is complete. This closure takes a single Result argument: + /// - Parameter result: + /// - success(status, date): The pump status, and the resolved date according to the pump's clock + /// - failure(error): An error describing why the command failed + func readPumpData(_ completion: @escaping (_ result: RileyLinkKit.Either<(status: RileyLinkKit.PumpStatus, date: Date), Error>) -> Void) { + guard let device = firstConnectedDevice, let ops = device.ops else { + completion(.failure(LoopError.connectionError)) + return + } + + ops.readPumpStatus { (result) in + switch result { + case .success(let status): + var clock = status.clock + clock.timeZone = ops.pumpState.timeZone + + guard let date = clock.date else { + let errorStr = "Could not interpret pump clock: \(clock)" + completion(.failure(LoopError.invalidData(details: errorStr))) + return + } + completion(.success((status: status, date: date))) + case .failure(let error): + completion(.failure(error)) + } + } + } + +}