Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
*/
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.configurationError))
completion(.failure(LoopError.connectionError))
return
}

Expand All @@ -366,8 +366,9 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
clock.timeZone = ops.pumpState.timeZone

guard let date = clock.date else {
self.logger.addError("Could not interpret pump clock: \(clock)", fromSource: "RileyLink")
completion(.failure(LoopError.configurationError))
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))
Expand Down Expand Up @@ -435,7 +436,7 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
}

guard let ops = device.ops else {
completion(LoopError.configurationError)
completion(LoopError.configurationError("PumpOps"))
return
}

Expand Down
42 changes: 24 additions & 18 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ final class LoopDataManager {
private func getPendingInsulin() throws -> Double {

guard let basalRates = deviceDataManager.basalRateSchedule else {
throw LoopError.configurationError
throw LoopError.configurationError("Basal Rate Schedule")
}

let pendingTempBasalInsulin: Double
Expand All @@ -301,7 +301,7 @@ final class LoopDataManager {
guard let
glucose = self.deviceDataManager.glucoseStore?.latestGlucose
else {
resultsHandler(nil, LoopError.missingDataError("Cannot predict glucose due to missing input data"))
resultsHandler(nil, LoopError.missingDataError(details: "Cannot predict glucose due to missing input data", recovery: "Check your CGM data source"))
return
}

Expand Down Expand Up @@ -433,7 +433,7 @@ final class LoopDataManager {

private func updateCarbEffect(_ completionHandler: @escaping (_ effects: [GlucoseEffect]?, _ error: Error?) -> Void) {
guard let effectStartDate = effectStartDate else {
completionHandler(nil, LoopError.missingDataError("Glucose data not available"))
completionHandler(nil, LoopError.missingDataError(details: "Glucose data not available", recovery: "Check your CGM data source"))
return
}

Expand All @@ -446,7 +446,7 @@ final class LoopDataManager {
completionHandler(effects, error)
}
} else {
completionHandler(nil, LoopError.missingDataError("CarbStore not available"))
completionHandler(nil, LoopError.missingDataError(details: "CarbStore not available", recovery: nil))
}
}

Expand All @@ -462,7 +462,7 @@ final class LoopDataManager {

private func updateGlucoseMomentumEffect(_ completionHandler: @escaping (_ effects: [GlucoseEffect]?, _ error: Error?) -> Void) {
guard let glucoseStore = deviceDataManager.glucoseStore else {
completionHandler(nil, LoopError.missingDataError("GlucoseStore not available"))
completionHandler(nil, LoopError.missingDataError(details: "GlucoseStore not available", recovery: nil))
return
}
glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in
Expand All @@ -485,7 +485,7 @@ final class LoopDataManager {
let insulinEffect = self.insulinEffect
else {
self.retrospectivePredictedGlucose = nil
throw LoopError.missingDataError("Cannot retrospect glucose due to missing input data")
throw LoopError.missingDataError(details: "Cannot retrospect glucose due to missing input data", recovery: nil)
}

guard let change = glucoseChange else {
Expand Down Expand Up @@ -525,14 +525,14 @@ final class LoopDataManager {
glucose = self.deviceDataManager.glucoseStore?.latestGlucose
else {
self.predictedGlucose = nil
throw LoopError.missingDataError("Glucose")
throw LoopError.missingDataError(details: "Glucose", recovery: "Check your CGM data source")
}

guard let
pumpStatusDate = self.deviceDataManager.doseStore.lastReservoirValue?.startDate
else {
self.predictedGlucose = nil
throw LoopError.missingDataError("Reservoir")
throw LoopError.missingDataError(details: "Reservoir", recovery: "Check that your pump is in range")
}

let startDate = Date()
Expand All @@ -556,7 +556,7 @@ final class LoopDataManager {
let insulinEffect = self.insulinEffect else
{
self.predictedGlucose = nil
throw LoopError.missingDataError("Glucose effects")
throw LoopError.missingDataError(details: "Glucose effects", recovery: nil)
}

var error: Error?
Expand Down Expand Up @@ -604,14 +604,17 @@ final class LoopDataManager {
self.predictedGlucose = retrospectiveCorrectionEnabled ? predictionWithRetrospectiveEffect : prediction
self.predictedGlucoseWithoutMomentum = predictionWithoutMomentum

guard let minimumBGGuard = self.deviceDataManager.minimumBGGuard else {
throw LoopError.configurationError("Minimum BG Guard")
}

guard let
maxBasal = deviceDataManager.maximumBasalRatePerHour,
let glucoseTargetRange = deviceDataManager.glucoseTargetRangeSchedule,
let insulinSensitivity = deviceDataManager.insulinSensitivitySchedule,
let basalRates = deviceDataManager.basalRateSchedule,
let minimumBGGuard = deviceDataManager.minimumBGGuard
let basalRates = deviceDataManager.basalRateSchedule
else {
error = LoopError.configurationError
error = LoopError.configurationError("Check settings")
throw error!
}

Expand Down Expand Up @@ -655,27 +658,30 @@ final class LoopDataManager {
}
}
} else {
resultsHandler(nil, LoopError.configurationError)
resultsHandler(nil, LoopError.configurationError("Carb Store"))
}
}

private func recommendBolus() throws -> BolusRecommendation {
guard let minimumBGGuard = self.deviceDataManager.minimumBGGuard else {
throw LoopError.configurationError("Minimum BG Guard")
}

guard let
glucose = self.predictedGlucose,
let glucoseWithoutMomentum = self.predictedGlucoseWithoutMomentum,
let maxBolus = self.deviceDataManager.maximumBolus,
let glucoseTargetRange = self.deviceDataManager.glucoseTargetRangeSchedule,
let insulinSensitivity = self.deviceDataManager.insulinSensitivitySchedule,
let basalRates = self.deviceDataManager.basalRateSchedule,
let minimumBGGuard = self.deviceDataManager.minimumBGGuard
let basalRates = self.deviceDataManager.basalRateSchedule
else {
throw LoopError.configurationError
throw LoopError.configurationError("Check Settings")
}

let recencyInterval = TimeInterval(minutes: 15)

guard let glucoseDate = glucose.first?.startDate else {
throw LoopError.missingDataError("No glucose data found")
throw LoopError.missingDataError(details: "No glucose data found", recovery: "Check your CGM source")
}

guard abs(glucoseDate.timeIntervalSinceNow) <= recencyInterval else {
Expand Down Expand Up @@ -737,7 +743,7 @@ final class LoopDataManager {
}

guard let ops = device.ops else {
resultsHandler(false, LoopError.configurationError)
resultsHandler(false, LoopError.configurationError("PumpOps"))
return
}

Expand Down
27 changes: 21 additions & 6 deletions Loop/Models/LoopError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ enum LoopError: Error {
case communicationError

// Missing or unexpected configuration values
case configurationError
case configurationError(String)

// No connected devices, or failure during device connection
case connectionError

// Missing required data to perform an action
case missingDataError(String)
// Missing data required to perform an action
case missingDataError(details: String, recovery: String?)

// Glucose data is too old to perform action
case glucoseTooOld(date: Date)
Expand All @@ -29,10 +29,22 @@ enum LoopError: Error {

// Recommendation Expired
case recommendationExpired(date: Date)

// Invalid Data
case invalidData(details: String)
}

extension LoopError: LocalizedError {

public var recoverySuggestion: String? {
switch self {
case .missingDataError(_, let recovery):
return recovery;
default:
return nil;
}
}

public var errorDescription: String? {

let formatter = DateComponentsFormatter()
Expand All @@ -42,11 +54,11 @@ extension LoopError: LocalizedError {
switch self {
case .communicationError:
return NSLocalizedString("Communication Error", comment: "The error message displayed after a communication error.")
case .configurationError:
return NSLocalizedString("Configuration Error", comment: "The error message displayed for configuration errors.")
case .configurationError(let details):
return String(format: NSLocalizedString("Configuration Error: %1$@", comment: "The error message displayed for configuration errors. (1: configuration error details)"), details)
case .connectionError:
return NSLocalizedString("No connected devices, or failure during device connection", comment: "The error message displayed for device connection errors.")
case .missingDataError(let details):
case .missingDataError(let details, _):
return String(format: NSLocalizedString("Missing data: %1$@", comment: "The error message for missing data. (1: missing data details)"), details)
case .glucoseTooOld(let date):
let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? ""
Expand All @@ -57,6 +69,9 @@ extension LoopError: LocalizedError {
case .recommendationExpired(let date):
let minutes = formatter.string(from: -date.timeIntervalSinceNow) ?? ""
return String(format: NSLocalizedString("Recommendation expired: %1$@ old", comment: "The error message when a recommendation has expired. (1: age of recommendation in minutes)"), minutes)
case .invalidData(let details):
return String(format: NSLocalizedString("Invalid data: %1$@", comment: "The error message when invalid data was encountered. (1: details of invalid data)"), details)

}
}
}
Expand Down
15 changes: 13 additions & 2 deletions Loop/View Controllers/StatusTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -771,12 +771,15 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize

@IBOutlet weak var hudView: HUDView! {
didSet {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openCGMApp(_:)))
glucoseHUD.addGestureRecognizer(tapGestureRecognizer)
let glucoseTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openCGMApp(_:)))
glucoseHUD.addGestureRecognizer(glucoseTapGestureRecognizer)

if cgmAppURL != nil {
glucoseHUD.accessibilityHint = NSLocalizedString("Launches CGM app", comment: "Glucose HUD accessibility hint")
}
let statusTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showLastError(_:)))
loopCompletionHUD.addGestureRecognizer(statusTapGestureRecognizer)
loopCompletionHUD.accessibilityHint = NSLocalizedString("Show last loop error", comment: "Loop Completion HUD accessibility hint")
}
}

Expand All @@ -802,6 +805,14 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
}
}

@objc private func showLastError(_: Any) {
self.dataManager.loopManager.getLoopStatus { (_, _, _, _, _, _, _, error) -> Void in
if let error = error {
self.presentAlertController(with: error)
}
}
}

@objc private func openCGMApp(_: Any) {
if let url = cgmAppURL {
UIApplication.shared.open(url)
Expand Down