From e14f9da6edc1f6710a0ab5c96a4a2ce52bc80432 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 26 Apr 2018 22:01:22 +0200 Subject: [PATCH 01/16] Add a setting to limit maximum Insulin on Board This is useful to prevent multiple bolus' to create accidentally very high insulin on board values. It is primarly a safety feature, e.g. if someone enters too many carbs by using multiple apps. --- DoseMathTests/DoseMathTests.swift | 261 ++++++++++++++++-- Loop/Extensions/NSUserDefaults.swift | 1 + Loop/Managers/DoseMath.swift | 30 +- Loop/Managers/LoopDataManager.swift | 93 +++++-- Loop/Models/LoopSettings.swift | 4 + .../SettingsTableViewController.swift | 19 +- .../TextFieldTableViewController.swift | 16 +- 7 files changed, 369 insertions(+), 55 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index f54990834b..988d84ae1b 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -101,6 +101,14 @@ class RecommendTempBasalTests: XCTestCase { return TimeInterval(hours: 4) } + var insulinOnBoard: Double { + return 0 + } + + var maxInsulinOnBoard: Double { + return 25 + } + func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") @@ -112,6 +120,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -129,6 +139,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -151,6 +163,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: lastTempBasal ) @@ -169,6 +183,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -190,6 +206,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: lastTempBasal ) @@ -217,6 +235,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: lastTempBasal ) @@ -231,6 +251,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -248,6 +270,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -266,6 +290,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -287,6 +313,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: lastTempBasal ) @@ -305,13 +333,74 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) - + // Basal 0.8, 1.1 units required -> 2.2 units extra for 30 minutes XCTAssertEqual(3.0, dose!.unitsPerHour) XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) } + func testFlatAndHighLimitIob() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + insulinOnBoard: 0, + maxInsulinOnBoard: 1, + lastTempBasal: nil + ) + // Basal 0.8, 1.1 units required, limited to 1 -> 2 units extra for 30 minutes = 2.8 + XCTAssertEqual(2.8, dose!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) + } + + func testFlatAndHighLimitIobWithOnboard() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + insulinOnBoard: 1.5, + maxInsulinOnBoard: 2, + lastTempBasal: nil + ) + // Basal 0.8, 1.1 units required, limited to 0.5 -> 1 units extra for 30 minutes = 1.8 + XCTAssertEqual(1.8, dose!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) + } + + func testFlatAndHighLimitIobExceeded() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + let dose = glucose.recommendedTempBasal( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxBasalRate: maxBasalRate, + insulinOnBoard: 2.5, + maxInsulinOnBoard: 2, + lastTempBasal: nil + ) + + XCTAssertNil(dose) + } + func testHighAndFalling() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") @@ -323,6 +412,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -341,6 +432,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -359,6 +452,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -376,6 +471,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -394,6 +491,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -412,6 +511,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -429,6 +530,8 @@ class RecommendTempBasalTests: XCTestCase { model: insulinModel, basalRates: basalRateSchedule, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, lastTempBasal: nil ) @@ -487,6 +590,14 @@ class RecommendBolusTests: XCTestCase { return TimeInterval(hours: 4) } + var insulinOnBoard: Double { + return 0 + } + + var maxInsulinOnBoard: Double { + return 25 + } + func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") @@ -497,7 +608,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -513,7 +626,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -529,7 +644,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -545,7 +662,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -561,7 +680,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(1.575, dose.amount) @@ -573,6 +694,78 @@ class RecommendBolusTests: XCTestCase { } } + func testStartLowEndHighLimitIob() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + + let dose = glucose.recommendedBolus( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + pendingInsulin: 0, + maxBolus: maxBolus, + insulinOnBoard: 0, + maxInsulinOnBoard: 1.3 + ) + + XCTAssertEqual(1.3, dose.amount) + + if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! { + XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60) + } else { + XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)") + } + } + + func testStartLowEndHighLimitIobWithOnboard() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + + let dose = glucose.recommendedBolus( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + pendingInsulin: 0, + maxBolus: maxBolus, + insulinOnBoard: 1.0, + maxInsulinOnBoard: 1.3 + ) + + XCTAssertEqual(0.3, dose.amount) + + if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! { + XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60) + } else { + XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)") + } + } + + func testStartLowEndHighLimitIobExceeded() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + + let dose = glucose.recommendedBolus( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + pendingInsulin: 0, + maxBolus: maxBolus, + insulinOnBoard: 2.0, + maxInsulinOnBoard: 1.3 + ) + + XCTAssertEqual(0, dose.amount) + + if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! { + XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60) + } else { + XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)") + } + } + func testStartBelowSuspendThresholdEndHigh() { // 60 - 200 mg/dL let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") @@ -584,7 +777,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -607,7 +802,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -629,7 +826,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(1.4, dose.amount) @@ -647,7 +846,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 1, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0.575, dose.amount) @@ -663,7 +864,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) @@ -679,7 +882,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(1.575, dose.amount, accuracy: 1.0 / 40.0) @@ -695,7 +900,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0.325, dose.amount, accuracy: 1.0 / 40.0) @@ -711,7 +918,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0.325, dose.amount, accuracy: 1.0 / 40.0) @@ -725,7 +934,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0.8, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount, accuracy: .ulpOfOne) @@ -741,7 +952,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: ExponentialInsulinModel(actionDuration: 21600.0, peakActivityTime: 4500.0), pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0.275, dose.amount) @@ -757,7 +970,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: self.insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(1.25, dose.amount) @@ -772,7 +987,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(1.25, dose.amount, accuracy: 1.0 / 40.0) @@ -788,7 +1005,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0.0, dose.amount) @@ -804,7 +1023,9 @@ class RecommendBolusTests: XCTestCase { sensitivity: insulinSensitivitySchedule, model: insulinModel, pendingInsulin: 0, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard ) XCTAssertEqual(0, dose.amount) diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index 88101cb1b5..a1a9fcde96 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -168,6 +168,7 @@ extension UserDefaults { glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus, + maximumInsulinOnBoard: nil, suspendThreshold: suspendThreshold, retrospectiveCorrectionEnabled: bool(forKey: "com.loudnate.Loop.RetrospectiveCorrectionEnabled") ) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 10c0872e8f..400f68af20 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -39,15 +39,20 @@ extension InsulinCorrection { /// - Parameters: /// - scheduledBasalRate: The scheduled basal rate at the time the correction is delivered /// - maxBasalRate: The maximum allowed basal rate + /// - insulinOnBoard: The current insulin on board + /// - maxInsulinOnBoard: The maximum insulin allowed /// - duration: The duration of the temporary basal /// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in basal delivery /// - Returns: A temp basal recommendation fileprivate func asTempBasal( scheduledBasalRate: Double, maxBasalRate: Double, + insulinOnBoard: Double, + maxInsulinOnBoard: Double, duration: TimeInterval, minimumProgrammableIncrementPerUnit: Double ) -> TempBasalRecommendation { + let units = Swift.min(self.units, Swift.max(0, maxInsulinOnBoard - insulinOnBoard)) var rate = units / (duration / TimeInterval(hours: 1)) // units/hour switch self { case .aboveRange, .inRange, .entirelyBelowRange: @@ -85,15 +90,20 @@ extension InsulinCorrection { /// - Parameters: /// - pendingInsulin: The number of units expected to be delivered, but not yet reflected in the correction /// - maxBolus: The maximum allowable bolus value in units + /// - insulinOnBoard: The current insulin on board + /// - maxInsulinOnBoard: The maximum insulin allowed /// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in bolus delivery /// - Returns: A bolus recommendation fileprivate func asBolus( pendingInsulin: Double, maxBolus: Double, + insulinOnBoard: Double, + maxInsulinOnBoard: Double, minimumProgrammableIncrementPerUnit: Double ) -> BolusRecommendation { - var units = self.units - pendingInsulin - units = Swift.min(maxBolus, Swift.max(0, units)) + let netUnits = self.units - pendingInsulin + var units = Swift.min(maxBolus, Swift.max(0, netUnits)) + units = Swift.min(units, Swift.max(0, maxInsulinOnBoard - insulinOnBoard)) units = round(units * minimumProgrammableIncrementPerUnit) / minimumProgrammableIncrementPerUnit return BolusRecommendation( @@ -347,6 +357,8 @@ extension Collection where Iterator.Element == GlucoseValue { /// - model: The insulin absorption model /// - basalRates: The schedule of basal rates /// - maxBasalRate: The maximum allowed basal rate + /// - insulinOnBoard: The current insulin on board + /// - maxInsulinOnBoard: The maximum insulin allowed /// - lastTempBasal: The previously set temp basal /// - duration: The duration of the temporary basal /// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in basal delivery @@ -360,7 +372,10 @@ extension Collection where Iterator.Element == GlucoseValue { model: InsulinModel, basalRates: BasalRateSchedule, maxBasalRate: Double, + insulinOnBoard: Double, + maxInsulinOnBoard: Double, lastTempBasal: DoseEntry?, + lowerOnly: Bool = false, // only lower the basal, never raise duration: TimeInterval = .minutes(30), minimumProgrammableIncrementPerUnit: Double = 40, continuationInterval: TimeInterval = .minutes(11) @@ -386,6 +401,8 @@ extension Collection where Iterator.Element == GlucoseValue { let temp = correction?.asTempBasal( scheduledBasalRate: scheduledBasalRate, maxBasalRate: maxBasalRate, + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, duration: duration, minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit ) @@ -408,6 +425,8 @@ extension Collection where Iterator.Element == GlucoseValue { /// - model: The insulin absorption model /// - pendingInsulin: The number of units expected to be delivered, but not yet reflected in the correction /// - maxBolus: The maximum bolus to return + /// - insulinOnBoard: The current insulin on board + /// - maxInsulinOnBoard: The maximum insulin allowed /// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in bolus delivery /// - Returns: A bolus recommendation func recommendedBolus( @@ -418,7 +437,8 @@ extension Collection where Iterator.Element == GlucoseValue { model: InsulinModel, pendingInsulin: Double, maxBolus: Double, - minimumProgrammableIncrementPerUnit: Double = 40 + insulinOnBoard: Double, + maxInsulinOnBoard: Double ) -> BolusRecommendation { guard let correction = self.insulinCorrection( to: correctionRange, @@ -433,7 +453,9 @@ extension Collection where Iterator.Element == GlucoseValue { var bolus = correction.asBolus( pendingInsulin: pendingInsulin, maxBolus: maxBolus, - minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit + insulinOnBoard: insulinOnBoard, + maxInsulinOnBoard: maxInsulinOnBoard, + minimumProgrammableIncrementPerUnit: 40 ) // Handle the "current BG below target" notice here diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 75abd492c3..c4661049e0 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -573,6 +573,26 @@ final class LoopDataManager { updateGroup.leave() } } + + if insulinOnBoard == nil { + updateGroup.enter() + let now = Date() + doseStore.getInsulinOnBoardValues(start: retrospectiveStart, end: now) { (result) in + switch result { + case .success(let value): + if let recentValue = value.closestPriorToDate(now) { + self.insulinOnBoard = recentValue + } else { + self.insulinOnBoard = InsulinValue(startDate: now, value: 0.0) + } + case .failure(let error): + NSLog("getInsulinOnBoardValues - error: \(error)") + self.logger.error(error) + self.insulinOnBoard = nil + } + updateGroup.leave() + } + } _ = updateGroup.wait(timeout: .distantFuture) @@ -690,10 +710,18 @@ final class LoopDataManager { } } private var insulinEffect: [GlucoseEffect]? { + didSet { + predictedGlucose = nil + insulinOnBoard = nil + } + } + + fileprivate var insulinOnBoard: InsulinValue? { didSet { predictedGlucose = nil } } + private var glucoseMomentumEffect: [GlucoseEffect]? { didSet { predictedGlucose = nil @@ -876,6 +904,7 @@ final class LoopDataManager { guard let maxBasal = settings.maximumBasalRatePerHour, + let maximumInsulinOnBoard = settings.maximumInsulinOnBoard, let glucoseTargetRange = settings.glucoseTargetRangeSchedule, let insulinSensitivity = insulinSensitivitySchedule, let basalRates = basalRateSchedule, @@ -883,6 +912,15 @@ final class LoopDataManager { else { throw LoopError.configurationError("Check settings") } + + guard let insulinOnBoard = insulinOnBoard + else { + throw LoopError.missingDataError(details: "Insulin on Board not available (updatePredictedGlucoseAndRecommendedBasal)", recovery: "Pump data up to date?") + } + +// guard cgmCalibrated else { +// throw LoopError.missingDataError(details: "CGM", recovery: "CGM Recently calibrated") +// } guard lastRequestedBolus == nil, // Don't recommend changes if a bolus was just set @@ -893,6 +931,8 @@ final class LoopDataManager { model: model, basalRates: basalRates, maxBasalRate: maxBasal, + insulinOnBoard: insulinOnBoard.value, + maxInsulinOnBoard: maximumInsulinOnBoard, lastTempBasal: lastTempBasal ) else { @@ -914,13 +954,19 @@ final class LoopDataManager { guard let predictedGlucose = predictedGlucose, let maxBolus = settings.maximumBolus, + let maximumInsulinOnBoard = settings.maximumInsulinOnBoard, let glucoseTargetRange = settings.glucoseTargetRangeSchedule, let insulinSensitivity = insulinSensitivitySchedule, let model = insulinModelSettings?.model else { throw LoopError.configurationError("Check Settings") } - + + guard let insulinOnBoard = insulinOnBoard + else { + throw LoopError.missingDataError(details: "Insulin on Board not available (recommendBolus)", recovery: "Pump data up to date?") + } + guard let glucoseDate = predictedGlucose.first?.startDate else { throw LoopError.missingDataError(details: "No glucose data found", recovery: "Check your CGM source") } @@ -937,7 +983,9 @@ final class LoopDataManager { sensitivity: insulinSensitivity, model: model, pendingInsulin: pendingInsulin, - maxBolus: maxBolus + maxBolus: maxBolus, + insulinOnBoard: insulinOnBoard.value, + maxInsulinOnBoard: maximumInsulinOnBoard ) return recommendation @@ -979,6 +1027,8 @@ protocol LoopState { /// The last-calculated carbs on board var carbsOnBoard: CarbValue? { get } + var insulinOnBoard: InsulinValue? { get } + /// An error in the current state of the loop, or one that happened during the last attempt to loop. var error: Error? { get } @@ -1034,6 +1084,11 @@ extension LoopDataManager { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) return loopDataManager.carbsOnBoard } + + var insulinOnBoard: InsulinValue? { + dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) + return loopDataManager.insulinOnBoard + } var error: Error? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) @@ -1115,6 +1170,7 @@ extension LoopDataManager { "## LoopDataManager", "settings: \(String(reflecting: manager.settings))", "insulinCounteractionEffects: \(String(reflecting: manager.insulinCounteractionEffects))", + "insulinOnBoard: \(String(describing: state.insulinOnBoard))", "predictedGlucose: \(state.predictedGlucose ?? [])", "retrospectivePredictedGlucose: \(state.retrospectivePredictedGlucose ?? [])", "recommendedTempBasal: \(String(describing: state.recommendedTempBasal))", @@ -1125,45 +1181,24 @@ extension LoopDataManager { "lastTempBasal: \(String(describing: state.lastTempBasal))", "carbsOnBoard: \(String(describing: state.carbsOnBoard))" ] - var loopError = state.error - - // TODO: this should be moved to doseStore.generateDiagnosticReport - self.doseStore.insulinOnBoard(at: Date()) { (result) in - let insulinOnBoard: InsulinValue? - - switch result { - case .success(let value): - insulinOnBoard = value - case .failure(let error): - insulinOnBoard = nil - - if loopError == nil { - loopError = error - } - } - - entries.append("insulinOnBoard: \(String(describing: insulinOnBoard))") - entries.append("error: \(String(describing: loopError))") + self.glucoseStore.generateDiagnosticReport { (report) in + entries.append(report) entries.append("") - self.glucoseStore.generateDiagnosticReport { (report) in + self.carbStore.generateDiagnosticReport { (report) in entries.append(report) entries.append("") - self.carbStore.generateDiagnosticReport { (report) in + self.doseStore.generateDiagnosticReport { (report) in entries.append(report) entries.append("") - self.doseStore.generateDiagnosticReport { (report) in - entries.append(report) - entries.append("") - - completion(entries.joined(separator: "\n")) - } + completion(entries.joined(separator: "\n")) } } } + } } } diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index 24d374a153..d4b0b2ee0f 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -19,6 +19,8 @@ struct LoopSettings { var maximumBasalRatePerHour: Double? var maximumBolus: Double? + + var maximumInsulinOnBoard: Double? var suspendThreshold: GlucoseThreshold? = nil @@ -65,6 +67,7 @@ extension LoopSettings: RawRepresentable { self.maximumBasalRatePerHour = rawValue["maximumBasalRatePerHour"] as? Double + self.maximumInsulinOnBoard = rawValue["maximumInsulinOnBoard"] as? Double self.maximumBolus = rawValue["maximumBolus"] as? Double if let rawThreshold = rawValue["minimumBGGuard"] as? GlucoseThreshold.RawValue { @@ -85,6 +88,7 @@ extension LoopSettings: RawRepresentable { raw["glucoseTargetRangeSchedule"] = glucoseTargetRangeSchedule?.rawValue raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour + raw["maximumInsulinOnBoard"] = maximumInsulinOnBoard raw["maximumBolus"] = maximumBolus raw["minimumBGGuard"] = suspendThreshold?.rawValue diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index 660b64fe18..da5ceae27e 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -106,6 +106,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case insulinSensitivity case maxBasal case maxBolus + case maxInsulinOnBoard } fileprivate enum ServiceRow: Int, CaseCountable { @@ -345,6 +346,14 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } else { configCell.detailTextLabel?.text = TapToSetString } + case .maxInsulinOnBoard: + configCell.textLabel?.text = NSLocalizedString("Maximum IOB", comment: "The title text for the maximum insulin on board value") + + if let maxInsulinOnBoard = dataManager.loopManager.settings.maximumInsulinOnBoard { + configCell.detailTextLabel?.text = "\(valueNumberFormatter.string(from: NSNumber(value: maxInsulinOnBoard))!) U" + } else { + configCell.detailTextLabel?.text = TapToSetString + } } return configCell @@ -484,7 +493,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case .configuration: let row = ConfigurationRow(rawValue: indexPath.row)! switch row { - case .maxBasal, .maxBolus: + case .maxBasal, .maxBolus, .maxInsulinOnBoard: let vc: LoopKit.TextFieldTableViewController switch row { @@ -492,6 +501,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu vc = .maxBasal(dataManager.loopManager.settings.maximumBasalRatePerHour) case .maxBolus: vc = .maxBolus(dataManager.loopManager.settings.maximumBolus) + case .maxInsulinOnBoard: + vc = .maxInsulinOnBoard(dataManager.loopManager.settings.maximumInsulinOnBoard) default: fatalError() } @@ -1012,6 +1023,12 @@ extension SettingsTableViewController: LoopKit.TextFieldTableViewControllerDeleg } else { dataManager.loopManager.settings.maximumBolus = nil } + case .maxInsulinOnBoard: + if let value = controller.value, let units = valueNumberFormatter.number(from: value)?.doubleValue { + dataManager.loopManager.settings.maximumInsulinOnBoard = units + } else { + dataManager.loopManager.settings.maximumInsulinOnBoard = nil + } default: assertionFailure() } diff --git a/Loop/View Controllers/TextFieldTableViewController.swift b/Loop/View Controllers/TextFieldTableViewController.swift index c343def6be..08afe249f0 100644 --- a/Loop/View Controllers/TextFieldTableViewController.swift +++ b/Loop/View Controllers/TextFieldTableViewController.swift @@ -71,5 +71,19 @@ extension TextFieldTableViewController { } return vc - } + } + + static func maxInsulinOnBoard(_ value: Double?) -> T { + let vc = T() + + vc.placeholder = NSLocalizedString("Enter a number of units", comment: "The placeholder text instructing users how to enter a maximum iob") + vc.keyboardType = .decimalPad + vc.unit = NSLocalizedString("Units", comment: "The unit string for units") + + if let maxIOB = value { + vc.value = valueNumberFormatter.string(from: NSNumber(value: maxIOB)) + } + + return vc + } } From fb22de4382c6afe57d86dd12df305b0752336879 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 26 Apr 2018 22:18:53 +0200 Subject: [PATCH 02/16] Cosmetics - Revert accidental change - Clarify the exceed IOB intention - Remove some merge artifacts --- DoseMathTests/DoseMathTests.swift | 3 ++- Loop/Managers/DoseMath.swift | 6 +++--- Loop/Managers/LoopDataManager.swift | 9 +++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 988d84ae1b..73e0d7e313 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -397,7 +397,8 @@ class RecommendTempBasalTests: XCTestCase { maxInsulinOnBoard: 2, lastTempBasal: nil ) - + // If the IOB is exceeded the rate is limited to the default + // basal rate. XCTAssertNil(dose) } diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 400f68af20..e8331c59c6 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -375,7 +375,6 @@ extension Collection where Iterator.Element == GlucoseValue { insulinOnBoard: Double, maxInsulinOnBoard: Double, lastTempBasal: DoseEntry?, - lowerOnly: Bool = false, // only lower the basal, never raise duration: TimeInterval = .minutes(30), minimumProgrammableIncrementPerUnit: Double = 40, continuationInterval: TimeInterval = .minutes(11) @@ -438,7 +437,8 @@ extension Collection where Iterator.Element == GlucoseValue { pendingInsulin: Double, maxBolus: Double, insulinOnBoard: Double, - maxInsulinOnBoard: Double + maxInsulinOnBoard: Double, + minimumProgrammableIncrementPerUnit: Double = 40 ) -> BolusRecommendation { guard let correction = self.insulinCorrection( to: correctionRange, @@ -455,7 +455,7 @@ extension Collection where Iterator.Element == GlucoseValue { maxBolus: maxBolus, insulinOnBoard: insulinOnBoard, maxInsulinOnBoard: maxInsulinOnBoard, - minimumProgrammableIncrementPerUnit: 40 + minimumProgrammableIncrementPerUnit: minimumProgrammableIncrementPerUnit ) // Handle the "current BG below target" notice here diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index c4661049e0..2cb9fc116b 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -913,14 +913,11 @@ final class LoopDataManager { throw LoopError.configurationError("Check settings") } - guard let insulinOnBoard = insulinOnBoard - else { + guard let + insulinOnBoard = insulinOnBoard + else { throw LoopError.missingDataError(details: "Insulin on Board not available (updatePredictedGlucoseAndRecommendedBasal)", recovery: "Pump data up to date?") } - -// guard cgmCalibrated else { -// throw LoopError.missingDataError(details: "CGM", recovery: "CGM Recently calibrated") -// } guard lastRequestedBolus == nil, // Don't recommend changes if a bolus was just set From 800215a85e402faa41ff825b8caab27a469d0486 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 26 Apr 2018 22:28:29 +0200 Subject: [PATCH 03/16] Two more cosmetic fixes. --- Loop/Managers/LoopDataManager.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 2cb9fc116b..c050a51f83 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -586,7 +586,6 @@ final class LoopDataManager { self.insulinOnBoard = InsulinValue(startDate: now, value: 0.0) } case .failure(let error): - NSLog("getInsulinOnBoardValues - error: \(error)") self.logger.error(error) self.insulinOnBoard = nil } @@ -916,7 +915,7 @@ final class LoopDataManager { guard let insulinOnBoard = insulinOnBoard else { - throw LoopError.missingDataError(details: "Insulin on Board not available (updatePredictedGlucoseAndRecommendedBasal)", recovery: "Pump data up to date?") + throw LoopError.missingDataError(details: "Insulin on Board not available (updatePredictedGlucoseAndRecommendedBasal)", recovery: "Pump data up to date?") } guard From 77d501c622658e08709a138b921d6f5ef8fae64b Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 26 Apr 2018 22:53:28 +0200 Subject: [PATCH 04/16] Refactor Bolus Recommendation Make Bolus recommendation part of Loop update and don't allow external calls to it. The data doesn't change in any case and update() is called in all places where we want a Bolus recommendation. This is in preparation of automated Bolus code, which needs consistent Bolus and Basal data. --- Loop/Managers/LoopDataManager.swift | 81 ++++++++++++++--------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 75abd492c3..a80f70deee 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -299,7 +299,7 @@ final class LoopDataManager { do { try self.update() - completion(.success(try self.recommendBolus())) + completion(.success(self.recommendedBolus?.recommendation)) } catch let error { completion(.failure(error)) } @@ -586,7 +586,7 @@ final class LoopDataManager { if predictedGlucose == nil { do { - try updatePredictedGlucoseAndRecommendedBasal() + try updatePredictedGlucoseAndRecommendedBasalAndBolus() } catch let error { logger.error(error) @@ -717,6 +717,7 @@ final class LoopDataManager { fileprivate var predictedGlucose: [GlucoseValue]? { didSet { recommendedTempBasal = nil + recommendedBolus = nil } } fileprivate var retrospectivePredictedGlucose: [GlucoseValue]? { @@ -726,6 +727,8 @@ final class LoopDataManager { } fileprivate var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? + fileprivate var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? + fileprivate var carbsOnBoard: CarbValue? fileprivate var lastTempBasal: DoseEntry? @@ -841,7 +844,7 @@ final class LoopDataManager { /// - LoopError.glucoseTooOld /// - LoopError.missingDataError /// - LoopError.pumpDataTooOld - private func updatePredictedGlucoseAndRecommendedBasal() throws { + private func updatePredictedGlucoseAndRecommendedBasalAndBolus() throws { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) guard let glucose = glucoseStore.latestGlucose else { @@ -879,14 +882,25 @@ final class LoopDataManager { let glucoseTargetRange = settings.glucoseTargetRangeSchedule, let insulinSensitivity = insulinSensitivitySchedule, let basalRates = basalRateSchedule, + let maxBolus = settings.maximumBolus, let model = insulinModelSettings?.model else { throw LoopError.configurationError("Check settings") } + + let pendingInsulin = try self.getPendingInsulin() - guard - lastRequestedBolus == nil, // Don't recommend changes if a bolus was just set - let tempBasal = predictedGlucose.recommendedTempBasal( + guard lastRequestedBolus == nil + else { + // Don't recommend changes if a bolus was just requested. + // Sending additional pump commands is not going to be + // successful in any case. + recommendedBolus = nil + recommendedTempBasal = nil + return + } + + let tempBasal = predictedGlucose.recommendedTempBasal( to: glucoseTargetRange, suspendThreshold: settings.suspendThreshold?.quantity, sensitivity: insulinSensitivity, @@ -895,42 +909,13 @@ final class LoopDataManager { maxBasalRate: maxBasal, lastTempBasal: lastTempBasal ) - else { + + if let temp = tempBasal { + recommendedTempBasal = (recommendation: temp, date: startDate) + } else { recommendedTempBasal = nil - return - } - - recommendedTempBasal = (recommendation: tempBasal, date: Date()) - } - - /// - Returns: A bolus recommendation from the current data - /// - Throws: - /// - LoopError.configurationError - /// - LoopError.glucoseTooOld - /// - LoopError.missingDataError - fileprivate func recommendBolus() throws -> BolusRecommendation { - dispatchPrecondition(condition: .onQueue(dataAccessQueue)) - - guard - let predictedGlucose = predictedGlucose, - let maxBolus = settings.maximumBolus, - let glucoseTargetRange = settings.glucoseTargetRangeSchedule, - let insulinSensitivity = insulinSensitivitySchedule, - let model = insulinModelSettings?.model - else { - throw LoopError.configurationError("Check Settings") - } - - guard let glucoseDate = predictedGlucose.first?.startDate else { - throw LoopError.missingDataError(details: "No glucose data found", recovery: "Check your CGM source") } - - guard abs(glucoseDate.timeIntervalSinceNow) <= recencyInterval else { - throw LoopError.glucoseTooOld(date: glucoseDate) - } - - let pendingInsulin = try self.getPendingInsulin() - + let recommendation = predictedGlucose.recommendedBolus( to: glucoseTargetRange, suspendThreshold: settings.suspendThreshold?.quantity, @@ -939,8 +924,7 @@ final class LoopDataManager { pendingInsulin: pendingInsulin, maxBolus: maxBolus ) - - return recommendation + recommendedBolus = (recommendation: recommendation, date: startDate) } /// *This method should only be called from the `dataAccessQueue`* @@ -997,6 +981,8 @@ protocol LoopState { /// The recommended temp basal based on predicted glucose var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? { get } + var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? { get } + /// The retrospective prediction over a recent period of glucose samples var retrospectivePredictedGlucose: [GlucoseValue]? { get } @@ -1064,6 +1050,11 @@ extension LoopDataManager { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) return loopDataManager.recommendedTempBasal } + + var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? { + dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) + return loopDataManager.recommendedBolus + } var retrospectivePredictedGlucose: [GlucoseValue]? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) @@ -1075,7 +1066,10 @@ extension LoopDataManager { } func recommendBolus() throws -> BolusRecommendation { - return try loopDataManager.recommendBolus() + if let bolus = loopDataManager.recommendedBolus { + return bolus.recommendation + } + throw LoopError.missingDataError(details: "Recommended Bolus data not available.", recovery: "Check you loop state.") } } @@ -1118,6 +1112,7 @@ extension LoopDataManager { "predictedGlucose: \(state.predictedGlucose ?? [])", "retrospectivePredictedGlucose: \(state.retrospectivePredictedGlucose ?? [])", "recommendedTempBasal: \(String(describing: state.recommendedTempBasal))", + "recommendedBolus: \(String(describing: state.recommendedBolus))", "lastBolus: \(String(describing: manager.lastRequestedBolus))", "lastGlucoseChange: \(String(describing: manager.lastGlucoseChange))", "retrospectiveGlucoseChange: \(String(describing: manager.retrospectiveGlucoseChange))", From e32732798fd756943ea10320dd346ca8713094d4 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 26 Apr 2018 23:00:18 +0200 Subject: [PATCH 05/16] Remove recommendBolus function from LoopState The recommendedBolus variable contains the same information. --- Loop/Managers/LoopDataManager.swift | 16 ---------------- Loop/Managers/NightscoutDataManager.swift | 10 +--------- Loop/Managers/WatchDataManager.swift | 2 +- .../BolusViewController+LoopDataManager.swift | 2 +- 4 files changed, 3 insertions(+), 27 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index a80f70deee..da836e102d 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -994,15 +994,6 @@ protocol LoopState { /// - Returns: An timeline of predicted glucose values /// - Throws: LoopError.missingDataError if prediction cannot be computed func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] - - /// Calculates a recommended bolus based on predicted glucose - /// - /// - Returns: A bolus recommendation - /// - Throws: An error describing why a bolus couldnʼt be computed - /// - LoopError.configurationError - /// - LoopError.glucoseTooOld - /// - LoopError.missingDataError - func recommendBolus() throws -> BolusRecommendation } @@ -1064,13 +1055,6 @@ extension LoopDataManager { func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] { return try loopDataManager.predictGlucose(using: inputs) } - - func recommendBolus() throws -> BolusRecommendation { - if let bolus = loopDataManager.recommendedBolus { - return bolus.recommendation - } - throw LoopError.missingDataError(details: "Recommended Bolus data not available.", recovery: "Check you loop state.") - } } /// Executes a closure with access to the current state of the loop. diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift index 5e7706cce4..a773507f84 100644 --- a/Loop/Managers/NightscoutDataManager.swift +++ b/Loop/Managers/NightscoutDataManager.swift @@ -43,15 +43,7 @@ final class NightscoutDataManager { var loopError = state.error let recommendedBolus: Double? - do { - recommendedBolus = try state.recommendBolus().amount - } catch let error { - recommendedBolus = nil - - if loopError == nil { - loopError = error - } - } + recommendedBolus = state.recommendedBolus?.recommendation.amount let carbsOnBoard = state.carbsOnBoard let predictedGlucose = state.predictedGlucose diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 44255cd80f..a20a326050 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -125,7 +125,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { context.reservoir = reservoir?.unitVolume context.loopLastRunDate = state.lastLoopCompleted - context.recommendedBolusDose = try? state.recommendBolus().amount + context.recommendedBolusDose = state.recommendedBolus?.recommendation.amount context.maxBolus = manager.settings.maximumBolus if let glucoseTargetRangeSchedule = manager.settings.glucoseTargetRangeSchedule { diff --git a/Loop/View Controllers/BolusViewController+LoopDataManager.swift b/Loop/View Controllers/BolusViewController+LoopDataManager.swift index 280d333530..84246ef882 100644 --- a/Loop/View Controllers/BolusViewController+LoopDataManager.swift +++ b/Loop/View Controllers/BolusViewController+LoopDataManager.swift @@ -20,7 +20,7 @@ extension BolusViewController { if let recommendation = recommendation { bolusRecommendation = recommendation } else { - bolusRecommendation = try? state.recommendBolus() + bolusRecommendation = state.recommendedBolus?.recommendation } manager.doseStore.insulinOnBoard(at: Date()) { (result) in From 25817d9915bbb2d5f70b8dda0fe83e75028c693e Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 May 2018 21:18:25 +0200 Subject: [PATCH 06/16] Factor out automated Bolus code Depends on wip/bolus and wip/iob branch. Still needs cleanup and testing. --- Loop/Extensions/NSUserDefaults.swift | 1 + Loop/Managers/DeviceDataManager.swift | 89 ++++++++++-- Loop/Managers/LoopDataManager.swift | 137 +++++++++++++++++- Loop/Models/LoopSettings.swift | 19 +++ .../SettingsTableViewController.swift | 16 +- 5 files changed, 242 insertions(+), 20 deletions(-) diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index a1a9fcde96..c64f587c09 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -165,6 +165,7 @@ extension UserDefaults { let settings = LoopSettings( dosingEnabled: bool(forKey: "com.loudnate.Naterade.DosingEnabled"), + bolusEnabled: false, glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 146ecc5d08..610193cef8 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -407,12 +407,10 @@ final class DeviceDataManager { return } + ops.runSession(withName: "Fetch Pump History", using: device) { (session) in do { - // TODO: This should isn't safe to access synchronously - let startDate = self.loopManager.doseStore.pumpEventQueryAfterDate - - let (events, model) = try session.getHistoryEvents(since: startDate) + let (events, model) = try session.getHistoryEvents(since: self.loopManager.doseStore.pumpEventQueryAfterDate) self.loopManager.addPumpEvents(events, from: model) { (error) in if let error = error { self.logger.addError("Failed to store history: \(error)", fromSource: "DoseStore") @@ -512,21 +510,26 @@ final class DeviceDataManager { } } + private var bolusInProgress = false + /// TODO: Isolate to queue /// Send a bolus command and handle the result /// /// - parameter units: The number of units to deliver /// - parameter completion: A clsure called after the command is complete. This closure takes a single argument: /// - error: An error describing why the command failed - func enactBolus(units: Double, at startDate: Date = Date(), completion: @escaping (_ error: Error?) -> Void) { + func enactBolus(units: Double, at startDate: Date = Date(), quiet : Bool = false, completion: @escaping (_ error: Error?) -> Void) { + let notify = { (error: Error?) -> Void in if let error = error { - NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate) + if !quiet { + NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate) + } } - + self.bolusInProgress = false completion(error) } - + guard units > 0 else { notify(nil) return @@ -536,9 +539,27 @@ final class DeviceDataManager { notify(LoopError.configurationError("Pump ID")) return } - + + guard !bolusInProgress else { + notify(LoopError.invalidData(details: "Bolus already in progress")) + bolusInProgress = true // notify alwasy set this to false, so reset to true... + return + } + bolusInProgress = true + // If we don't have recent pump data, or the pump was recently rewound, read new pump data before bolusing. - let shouldReadReservoir = isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-6)) + var shouldReadReservoir = isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-10)) + if loopManager.doseStore.lastReservoirVolumeDrop < 0 { + notify(LoopError.invalidData(details: "Last Reservoir drop negative.")) + shouldReadReservoir = true + } else if let reservoir = loopManager.doseStore.lastReservoirValue, reservoir.startDate.timeIntervalSinceNow <= + -loopManager.recencyInterval { + notify(LoopError.pumpDataTooOld(date: reservoir.startDate)) + shouldReadReservoir = true + } else if loopManager.doseStore.lastReservoirValue == nil { + notify(LoopError.missingDataError(details: "Reservoir Value missing", recovery: "Keep phone close.")) + shouldReadReservoir = true + } ops.runSession(withName: "Bolus", using: rileyLinkManager.firstConnectedDevice) { (session) in guard let session = session else { @@ -831,9 +852,9 @@ extension DeviceDataManager: DoseStoreDelegate { extension DeviceDataManager: LoopDataManagerDelegate { func loopDataManager( - _ manager: LoopDataManager, - didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), - completion: @escaping (_ result: Result) -> Void + _ manager: LoopDataManager, + didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), + completion: @escaping (_ result: Result) -> Void ) { guard let pumpOps = pumpOps else { completion(.failure(LoopError.configurationError("Pump ID"))) @@ -871,11 +892,53 @@ extension DeviceDataManager: LoopDataManagerDelegate { value: response.rate, unit: .unitsPerHour ))) + } catch let error { notify(.failure(error)) } } } + + func loopDataManager(_ manager: LoopDataManager, didRecommendBolus bolus: (recommendation: BolusRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) { + + enactBolus(units: bolus.recommendation.amount, quiet: true) { (error) in + if let error = error { + completion(.failure(error)) + } else { + let now = Date() + completion(.success(DoseEntry( + type: .bolus, + startDate: now, + endDate: now, + value: bolus.recommendation.amount, + unit: .units + ))) + } + + } + } + + func loopDataManager(_ manager: LoopDataManager, uploadTreatments treatments: [NightscoutTreatment], completion: @escaping (Result<[String]>) -> Void) { + + guard let uploader = remoteDataManager.nightscoutService.uploader else { + completion(.failure(LoopError.configurationError("Nightscout not configured"))) + return + } + + uploader.upload(treatments) { (result) in + switch result { + case .success(let objects): + completion(.success(objects)) + case .failure(let error): + let logger = DiagnosticLogger.shared!.forCategory("NightscoutUploader") + logger.error(error) + NSLog("UPLOADING delegate failed \(error)") + completion(.failure(error)) + + } + } + + } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 0dd069d203..aa0087a419 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -86,6 +86,7 @@ final class LoopDataManager { self.dataAccessQueue.async { self.carbEffect = nil self.carbsOnBoard = nil + self.lastCarbChange = Date() self.notify(forChange: .carbs) } } @@ -325,7 +326,11 @@ final class LoopDataManager { /// - date: The date the bolus was requested func addRequestedBolus(units: Double, at date: Date, completion: (() -> Void)?) { dataAccessQueue.async { - self.lastRequestedBolus = (units: units, date: date) + NSLog("addRequestedBolus: \(units) \(date)") + self.recommendedBolus = nil + self.lastPendingBolus = nil + self.lastFailedBolus = nil + self.lastRequestedBolus = (units: units, date: date, reservoir: self.doseStore.lastReservoirValue) self.notify(forChange: .bolus) completion?() @@ -338,17 +343,40 @@ final class LoopDataManager { /// - units: The bolus amount, in units /// - date: The date the bolus was enacted func addConfirmedBolus(units: Double, at date: Date, completion: (() -> Void)?) { - self.doseStore.addPendingPumpEvent(.enactedBolus(units: units, at: date)) { + let event = NewPumpEvent.enactedBolus(units: units, at: date) + NSLog("addConfirmedBolus: \(units) \(date)") + self.doseStore.addPendingPumpEvent(event) { self.dataAccessQueue.async { + let requestDate = self.lastRequestedBolus?.date ?? date + self.lastPendingBolus = (units: units, date: requestDate, reservoir: self.doseStore.lastReservoirValue, event: event) self.lastRequestedBolus = nil + self.lastFailedBolus = nil + self.lastAutomaticBolus = date // keep this as a date, irrespective of automatic or not + self.recommendedBolus = nil self.insulinEffect = nil - self.notify(forChange: .bolus) + self.notify(forChange: .bolus) + do { + try self.update() + } catch let error { + NSLog("addConfirmedBolus: Update after confirmed bolus failed \(error)") + } completion?() } } } + func addFailedBolus(units: Double, at date: Date, error: Error, completion: (() -> Void)?) { + dataAccessQueue.async { + NSLog("addFailedBolus: \(units) \(date) \(error)") + self.lastFailedBolus = (units: units, date: date, error: error) + self.lastPendingBolus = nil + self.recommendedBolus = nil + self.notify(forChange: .bolus) + completion?() + } + } + /// Adds and stores new pump events /// /// - Parameters: @@ -433,11 +461,23 @@ final class LoopDataManager { if let error = error { self.logger.error(error) } else { - self.lastLoopCompleted = Date() + if self.settings.bolusEnabled { + // Have to do a bolus first. + self.setAutomatedBolus { (error) -> Void in + if let error = error { + self.logger.error(error) + } else { + self.lastLoopCompleted = Date() + } + } + } else { + // No automatic Bolus, we are done. + self.lastLoopCompleted = Date() + } } self.notify(forChange: .tempBasal) } - + // Delay the notification until we know the result of the temp basal return } else { @@ -759,7 +799,11 @@ final class LoopDataManager { fileprivate var carbsOnBoard: CarbValue? fileprivate var lastTempBasal: DoseEntry? - fileprivate var lastRequestedBolus: (units: Double, date: Date)? + + fileprivate var lastRequestedBolus: (units: Double, date: Date, reservoir: ReservoirValue?)? + fileprivate var lastPendingBolus: (units: Double, date: Date, reservoir: ReservoirValue?, event: NewPumpEvent)? + fileprivate var lastFailedBolus: (units: Double, date: Date, error: Error)? + fileprivate var lastLoopCompleted: Date? { didSet { NotificationManager.scheduleLoopNotRunningNotifications() @@ -994,6 +1038,78 @@ final class LoopDataManager { } } } + + /// *This method should only be called from the `dataAccessQueue`* + private var lastAutomaticBolus : Date? = nil + private var lastCarbChange : Date? = nil + + private func roundInsulinUnits(_ units: Double) -> Double { + return round(units * settings.insulinIncrementPerUnit)/settings.insulinIncrementPerUnit + } + + private func setAutomatedBolus(_ completion: @escaping (_ error: Error?) -> Void) { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + + guard let recommendedBolus = self.recommendedBolus else { + completion(nil) + NSLog("setAutomatedBolus - recommendation not available") + return + } + + let safeAmount = roundInsulinUnits(recommendedBolus.recommendation.amount * settings.automatedBolusRatio) + if safeAmount < settings.automatedBolusThreshold { + completion(nil) + NSLog("setAutomatedBolus - recommendation below threshold") + return + } + + guard abs(recommendedBolus.date.timeIntervalSinceNow) < TimeInterval(minutes: 5) else { + completion(LoopError.recommendationExpired(date: recommendedBolus.date)) + NSLog("setAutomatedBolus - recommendation too old") + return + } + + if let lastAutomaticBolus = self.lastAutomaticBolus, abs(lastAutomaticBolus.timeIntervalSinceNow) < settings.automaticBolusInterval { + NSLog("setAutomatedBolus - last automatic bolus too close") + completion(nil) + return + } + + if let carbChange = lastCarbChange { + guard abs(carbChange.timeIntervalSinceNow) > TimeInterval(minutes: 2) else { + NSLog("setAutomatedBolus - last carbchange too close") + completion(nil) + return + } + } + + // TODO lastPendingBolus is never cleared, thus we need to check for the date here. + if lastRequestedBolus != nil { + NSLog("setAutomatedBolus - lastRequestedBolus or lastPendingBolus still in progress \(String(describing: lastRequestedBolus)) \(String(describing: lastPendingBolus))") + completion(nil) + return + } + + // Copy bolus with "safe" ratio + let automatedBolus = (recommendation: BolusRecommendation(amount: safeAmount , pendingInsulin: recommendedBolus.recommendation.pendingInsulin, notice: recommendedBolus.recommendation.notice ), date: recommendedBolus.date) + self.recommendedBolus = nil + lastAutomaticBolus = Date() + + delegate.loopDataManager(self, didRecommendBolus: automatedBolus) { (result) in + self.dataAccessQueue.async { + switch result { + case .success(let bolus): + NSLog("setAutomatedBolus - success: \(bolus)") + self.recommendedBolus = nil + completion(nil) + case .failure(let error): + completion(error) + } + } + } + } + + } @@ -1191,4 +1307,13 @@ protocol LoopDataManagerDelegate: class { /// - completion: A closure called once on completion /// - result: The enacted basal func loopDataManager(_ manager: LoopDataManager, didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) -> Void + + /// Informs the delegate that an immediate bolus is recommended + /// + /// - Parameters: + /// - manager: The manager + /// - bolus: The recommended bolus + /// - completion: A closure called once on completion + /// - result: The enacted bolus + func loopDataManager(_ manager: LoopDataManager, didRecommendBolus bolus: (recommendation: BolusRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) -> Void } diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index d4b0b2ee0f..b9e7f8bb63 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -12,6 +12,8 @@ import RileyLinkBLEKit struct LoopSettings { var dosingEnabled = false + var bolusEnabled = false + let dynamicCarbAbsorptionEnabled = true var glucoseTargetRangeSchedule: GlucoseRangeSchedule? @@ -25,6 +27,18 @@ struct LoopSettings { var suspendThreshold: GlucoseThreshold? = nil var retrospectiveCorrectionEnabled = true + + // Not configurable through UI + let automatedBolusThreshold: Double = 0.2 + let automatedBolusRatio: Double = 0.7 + let automaticBolusInterval: TimeInterval = TimeInterval(minutes: 3) + let absorptionRate: Double = 20 + + let minimumRecommendedBolus: Double = 0.2 + let insulinIncrementPerUnit: Double = 10 // 0.1 steps in basal and bolus + + let absorptionTimeOverrun = 1.0 + } @@ -60,6 +74,10 @@ extension LoopSettings: RawRepresentable { if let dosingEnabled = rawValue["dosingEnabled"] as? Bool { self.dosingEnabled = dosingEnabled } + + if let bolusEnabled = rawValue["bolusEnabled"] as? Bool { + self.bolusEnabled = bolusEnabled + } if let rawValue = rawValue["glucoseTargetRangeSchedule"] as? GlucoseRangeSchedule.RawValue { self.glucoseTargetRangeSchedule = GlucoseRangeSchedule(rawValue: rawValue) @@ -83,6 +101,7 @@ extension LoopSettings: RawRepresentable { var raw: RawValue = [ "version": LoopSettings.version, "dosingEnabled": dosingEnabled, + "bolusEnabled": bolusEnabled, "retrospectiveCorrectionEnabled": retrospectiveCorrectionEnabled ] diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index da5ceae27e..c7050c63c2 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -80,6 +80,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu fileprivate enum LoopRow: Int, CaseCountable { case dosing = 0 + case bolus case preferredInsulinDataSource case diagnostic } @@ -191,6 +192,15 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu switchCell.switch?.addTarget(self, action: #selector(dosingEnabledChanged(_:)), for: .valueChanged) + return switchCell + case .bolus: + let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell + + switchCell.switch?.isOn = dataManager.loopManager.settings.bolusEnabled + switchCell.textLabel?.text = NSLocalizedString("Automated Bolus", comment: "The title text for the automated bolus enabled switch cell") + + switchCell.switch?.addTarget(self, action: #selector(bolusEnabledChanged(_:)), for: .valueChanged) + return switchCell case .preferredInsulinDataSource: let cell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) @@ -640,7 +650,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu vc.title = sender?.textLabel?.text show(vc, sender: sender) - case .dosing: + case .dosing, .bolus: break } case .services: @@ -703,6 +713,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu @objc private func dosingEnabledChanged(_ sender: UISwitch) { dataManager.loopManager.settings.dosingEnabled = sender.isOn } + + @objc private func bolusEnabledChanged(_ sender: UISwitch) { + dataManager.loopManager.settings.bolusEnabled = sender.isOn + } @objc private func reloadDevices() { self.dataManager.rileyLinkManager.getDevices { (devices) in From 1107c6ec759631b9e6684c930a89fc770cd68471 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 May 2018 21:27:09 +0200 Subject: [PATCH 07/16] Remove merge artifacts. --- Loop/Managers/DeviceDataManager.swift | 41 +++------------------------ Loop/Models/LoopSettings.swift | 10 ++----- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 610193cef8..877f8f8b3d 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -510,8 +510,6 @@ final class DeviceDataManager { } } - private var bolusInProgress = false - /// TODO: Isolate to queue /// Send a bolus command and handle the result /// @@ -526,7 +524,6 @@ final class DeviceDataManager { NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate) } } - self.bolusInProgress = false completion(error) } @@ -539,14 +536,7 @@ final class DeviceDataManager { notify(LoopError.configurationError("Pump ID")) return } - - guard !bolusInProgress else { - notify(LoopError.invalidData(details: "Bolus already in progress")) - bolusInProgress = true // notify alwasy set this to false, so reset to true... - return - } - bolusInProgress = true - + // If we don't have recent pump data, or the pump was recently rewound, read new pump data before bolusing. var shouldReadReservoir = isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-10)) if loopManager.doseStore.lastReservoirVolumeDrop < 0 { @@ -852,9 +842,9 @@ extension DeviceDataManager: DoseStoreDelegate { extension DeviceDataManager: LoopDataManagerDelegate { func loopDataManager( - _ manager: LoopDataManager, - didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), - completion: @escaping (_ result: Result) -> Void + _ manager: LoopDataManager, + didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), + completion: @escaping (_ result: Result) -> Void ) { guard let pumpOps = pumpOps else { completion(.failure(LoopError.configurationError("Pump ID"))) @@ -892,7 +882,6 @@ extension DeviceDataManager: LoopDataManagerDelegate { value: response.rate, unit: .unitsPerHour ))) - } catch let error { notify(.failure(error)) } @@ -917,28 +906,6 @@ extension DeviceDataManager: LoopDataManagerDelegate { } } - - func loopDataManager(_ manager: LoopDataManager, uploadTreatments treatments: [NightscoutTreatment], completion: @escaping (Result<[String]>) -> Void) { - - guard let uploader = remoteDataManager.nightscoutService.uploader else { - completion(.failure(LoopError.configurationError("Nightscout not configured"))) - return - } - - uploader.upload(treatments) { (result) in - switch result { - case .success(let objects): - completion(.success(objects)) - case .failure(let error): - let logger = DiagnosticLogger.shared!.forCategory("NightscoutUploader") - logger.error(error) - NSLog("UPLOADING delegate failed \(error)") - completion(.failure(error)) - - } - } - - } } diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index b9e7f8bb63..c9f324c8ce 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -28,17 +28,11 @@ struct LoopSettings { var retrospectiveCorrectionEnabled = true - // Not configurable through UI + // Not configurable through UI, but might be nice. let automatedBolusThreshold: Double = 0.2 let automatedBolusRatio: Double = 0.7 - let automaticBolusInterval: TimeInterval = TimeInterval(minutes: 3) - let absorptionRate: Double = 20 - + let automaticBolusInterval: TimeInterval = TimeInterval(minutes: 7) let minimumRecommendedBolus: Double = 0.2 - let insulinIncrementPerUnit: Double = 10 // 0.1 steps in basal and bolus - - let absorptionTimeOverrun = 1.0 - } From 634cf143c0c89e6e2887a2f552649f6aef63c2a3 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 May 2018 22:09:42 +0200 Subject: [PATCH 08/16] Fix merged data to actually give a bolus recommendation and deliver the bolus. --- Loop/Managers/DeviceDataManager.swift | 5 ++- Loop/Managers/DoseMath.swift | 5 +++ Loop/Managers/LoopDataManager.swift | 50 +++++++++------------------ 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 877f8f8b3d..60348ab51a 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -410,7 +410,10 @@ final class DeviceDataManager { ops.runSession(withName: "Fetch Pump History", using: device) { (session) in do { - let (events, model) = try session.getHistoryEvents(since: self.loopManager.doseStore.pumpEventQueryAfterDate) + // TODO: This should isn't safe to access synchronously + let startDate = self.loopManager.doseStore.pumpEventQueryAfterDate + + let (events, model) = try session.getHistoryEvents(since: startDate) self.loopManager.addPumpEvents(events, from: model) { (error) in if let error = error { self.logger.addError("Failed to store history: \(error)", fromSource: "DoseStore") diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index e8331c59c6..3afbdffdb6 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -375,6 +375,7 @@ extension Collection where Iterator.Element == GlucoseValue { insulinOnBoard: Double, maxInsulinOnBoard: Double, lastTempBasal: DoseEntry?, + lowerOnly: Bool = false, duration: TimeInterval = .minutes(30), minimumProgrammableIncrementPerUnit: Double = 40, continuationInterval: TimeInterval = .minutes(11) @@ -397,6 +398,10 @@ extension Collection where Iterator.Element == GlucoseValue { maxBasalRate = scheduledBasalRate } + if lowerOnly { + maxBasalRate = scheduledBasalRate + } + let temp = correction?.asTempBasal( scheduledBasalRate: scheduledBasalRate, maxBasalRate: maxBasalRate, diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index aa0087a419..67a84e5100 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -328,8 +328,6 @@ final class LoopDataManager { dataAccessQueue.async { NSLog("addRequestedBolus: \(units) \(date)") self.recommendedBolus = nil - self.lastPendingBolus = nil - self.lastFailedBolus = nil self.lastRequestedBolus = (units: units, date: date, reservoir: self.doseStore.lastReservoirValue) self.notify(forChange: .bolus) @@ -347,10 +345,7 @@ final class LoopDataManager { NSLog("addConfirmedBolus: \(units) \(date)") self.doseStore.addPendingPumpEvent(event) { self.dataAccessQueue.async { - let requestDate = self.lastRequestedBolus?.date ?? date - self.lastPendingBolus = (units: units, date: requestDate, reservoir: self.doseStore.lastReservoirValue, event: event) self.lastRequestedBolus = nil - self.lastFailedBolus = nil self.lastAutomaticBolus = date // keep this as a date, irrespective of automatic or not self.recommendedBolus = nil self.insulinEffect = nil @@ -366,17 +361,6 @@ final class LoopDataManager { } } - func addFailedBolus(units: Double, at date: Date, error: Error, completion: (() -> Void)?) { - dataAccessQueue.async { - NSLog("addFailedBolus: \(units) \(date) \(error)") - self.lastFailedBolus = (units: units, date: date, error: error) - self.lastPendingBolus = nil - self.recommendedBolus = nil - self.notify(forChange: .bolus) - completion?() - } - } - /// Adds and stores new pump events /// /// - Parameters: @@ -795,15 +779,13 @@ final class LoopDataManager { fileprivate var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? fileprivate var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? - + fileprivate var carbsOnBoard: CarbValue? fileprivate var lastTempBasal: DoseEntry? - + fileprivate var lastRequestedBolus: (units: Double, date: Date, reservoir: ReservoirValue?)? - fileprivate var lastPendingBolus: (units: Double, date: Date, reservoir: ReservoirValue?, event: NewPumpEvent)? - fileprivate var lastFailedBolus: (units: Double, date: Date, error: Error)? - + fileprivate var lastLoopCompleted: Date? { didSet { NotificationManager.scheduleLoopNotRunningNotifications() @@ -973,6 +955,7 @@ final class LoopDataManager { // Don't recommend changes if a bolus was just requested. // Sending additional pump commands is not going to be // successful in any case. + NSLog("updatePredictedGlucoseAndRecommendedBasalAndBolus - previous Bolus still in progress") recommendedBolus = nil recommendedTempBasal = nil return @@ -987,16 +970,16 @@ final class LoopDataManager { maxBasalRate: maxBasal, insulinOnBoard: insulinOnBoard.value, maxInsulinOnBoard: maximumInsulinOnBoard, - lastTempBasal: lastTempBasal + lastTempBasal: lastTempBasal, + lowerOnly: settings.bolusEnabled ) if let temp = tempBasal { recommendedTempBasal = (recommendation: temp, date: startDate) } else { recommendedTempBasal = nil - return } - + let recommendation = predictedGlucose.recommendedBolus( to: glucoseTargetRange, suspendThreshold: settings.suspendThreshold?.quantity, @@ -1038,15 +1021,14 @@ final class LoopDataManager { } } } - - /// *This method should only be called from the `dataAccessQueue`* + + // Keep track of last automatic bolus to space them out private var lastAutomaticBolus : Date? = nil + + // Keeps track of last carb change to prevent bolus immediately afterwards. private var lastCarbChange : Date? = nil - - private func roundInsulinUnits(_ units: Double) -> Double { - return round(units * settings.insulinIncrementPerUnit)/settings.insulinIncrementPerUnit - } - + + /// *This method should only be called from the `dataAccessQueue`* private func setAutomatedBolus(_ completion: @escaping (_ error: Error?) -> Void) { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) @@ -1056,7 +1038,7 @@ final class LoopDataManager { return } - let safeAmount = roundInsulinUnits(recommendedBolus.recommendation.amount * settings.automatedBolusRatio) + let safeAmount = round(recommendedBolus.recommendation.amount * settings.automatedBolusRatio * 10) / 10 if safeAmount < settings.automatedBolusThreshold { completion(nil) NSLog("setAutomatedBolus - recommendation below threshold") @@ -1083,9 +1065,9 @@ final class LoopDataManager { } } - // TODO lastPendingBolus is never cleared, thus we need to check for the date here. + // TODO lastRequestedBolus might not ever get cleared, thus we need to check for the date here. if lastRequestedBolus != nil { - NSLog("setAutomatedBolus - lastRequestedBolus or lastPendingBolus still in progress \(String(describing: lastRequestedBolus)) \(String(describing: lastPendingBolus))") + NSLog("setAutomatedBolus - lastRequestedBolus still in progress \(String(describing: lastRequestedBolus))") completion(nil) return } From a8ec1ff69c731005857e4684030784fda16c8998 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 May 2018 22:28:30 +0200 Subject: [PATCH 09/16] Document new lowerOnly parameter --- Loop/Managers/DoseMath.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 3afbdffdb6..c1902d36df 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -360,6 +360,7 @@ extension Collection where Iterator.Element == GlucoseValue { /// - insulinOnBoard: The current insulin on board /// - maxInsulinOnBoard: The maximum insulin allowed /// - lastTempBasal: The previously set temp basal + /// - lowerOnly: Only return lower basal rates, never higher /// - duration: The duration of the temporary basal /// - minimumProgrammableIncrementPerUnit: The smallest fraction of a unit supported in basal delivery /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command From 9ae7b9ccd5cac1c95b68ca9a23c05fb99299724c Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 May 2018 22:32:02 +0200 Subject: [PATCH 10/16] Document parameters to enactBolus and add a thought about a critical section. --- Loop/Managers/DeviceDataManager.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 60348ab51a..f90764462a 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -517,6 +517,8 @@ final class DeviceDataManager { /// Send a bolus command and handle the result /// /// - parameter units: The number of units to deliver + /// - parameter startDate: The start of the bolus. + /// - parameter quiet: Do not produce a notification of failure to the user. /// - parameter completion: A clsure called after the command is complete. This closure takes a single argument: /// - error: An error describing why the command failed func enactBolus(units: Double, at startDate: Date = Date(), quiet : Bool = false, completion: @escaping (_ error: Error?) -> Void) { @@ -561,6 +563,9 @@ final class DeviceDataManager { } if shouldReadReservoir { + // TODO it might be safer to return here and not give a Bolus + // forcing the recalculation of recommendedBolus. The new + // data might have invalidated the old recommendation. do { let reservoir = try session.getRemainingInsulin() From 9e0af1c4bc537b816639855156e5a13f2db3c6fb Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 May 2018 22:49:44 +0200 Subject: [PATCH 11/16] Remove unused setting. --- Loop/Models/LoopSettings.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index c9f324c8ce..34cb61a9d5 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -32,7 +32,6 @@ struct LoopSettings { let automatedBolusThreshold: Double = 0.2 let automatedBolusRatio: Double = 0.7 let automaticBolusInterval: TimeInterval = TimeInterval(minutes: 7) - let minimumRecommendedBolus: Double = 0.2 } From 7540620b7e7418c7baee22f4ffef5f78482ac668 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 Jun 2018 12:01:34 +0200 Subject: [PATCH 12/16] Add maximum Insulin on board protection to Bolus dialog as well. --- .../BolusViewController+LoopDataManager.swift | 5 +++++ Loop/View Controllers/BolusViewController.swift | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/Loop/View Controllers/BolusViewController+LoopDataManager.swift b/Loop/View Controllers/BolusViewController+LoopDataManager.swift index 84246ef882..e215aa2882 100644 --- a/Loop/View Controllers/BolusViewController+LoopDataManager.swift +++ b/Loop/View Controllers/BolusViewController+LoopDataManager.swift @@ -13,6 +13,7 @@ extension BolusViewController { func configureWithLoopManager(_ manager: LoopDataManager, recommendation: BolusRecommendation?, glucoseUnit: HKUnit) { manager.getLoopState { (manager, state) in let maximumBolus = manager.settings.maximumBolus + let maximumInsulinOnBoard = manager.settings.maximumInsulinOnBoard let activeCarbohydrates = state.carbsOnBoard?.quantity.doubleValue(for: .gram()) let bolusRecommendation: BolusRecommendation? @@ -38,6 +39,10 @@ extension BolusViewController { self.maxBolus = maxBolus } + if let maxInsulinOnBoard = maximumInsulinOnBoard { + self.maxInsulinOnBoard = maxInsulinOnBoard + } + self.glucoseUnit = glucoseUnit self.activeInsulin = activeInsulin self.activeCarbohydrates = activeCarbohydrates diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index 58d01a89c2..f3d0a77444 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -116,6 +116,7 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex var maxBolus: Double = 25 + var maxInsulinOnBoard: Double = 0 private(set) var bolus: Double? @@ -186,6 +187,15 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex return } + if maxInsulinOnBoard > 0 { + guard bolus + (activeInsulin ?? 0) <= maxInsulinOnBoard else { + NSLog("BolusViewController - maxIOB") + presentAlertController(withTitle: NSLocalizedString("Would exceed Maximum Insulin on Board", comment: "The title of the alert describing a maximum insulin on board validation error"), message: String(format: NSLocalizedString("The insulin on board amount is %@ Units", comment: "Body of the alert describing a maximum iob validation error. (1: The localized max iob value)"), + bolusUnitsFormatter.string(from: NSNumber(value: maxInsulinOnBoard)) ?? "")) + return + } + } + let context = LAContext() if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) { From f2a66b6e41ccc4998a100557d07a548bae6e1836 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 Jun 2018 12:15:16 +0200 Subject: [PATCH 13/16] Improve error message for IOB validation. --- Loop/View Controllers/BolusViewController.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index f3d0a77444..ce0dd173ad 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -187,11 +187,16 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex return } + let iob = activeInsulin ?? 0 if maxInsulinOnBoard > 0 { - guard bolus + (activeInsulin ?? 0) <= maxInsulinOnBoard else { + guard bolus + iob <= maxInsulinOnBoard else { NSLog("BolusViewController - maxIOB") - presentAlertController(withTitle: NSLocalizedString("Would exceed Maximum Insulin on Board", comment: "The title of the alert describing a maximum insulin on board validation error"), message: String(format: NSLocalizedString("The insulin on board amount is %@ Units", comment: "Body of the alert describing a maximum iob validation error. (1: The localized max iob value)"), - bolusUnitsFormatter.string(from: NSNumber(value: maxInsulinOnBoard)) ?? "")) + presentAlertController(withTitle: NSLocalizedString("Exceeds Maximum Insulin on Board", comment: "The title of the alert describing a maximum insulin on board validation error"), message: String(format: NSLocalizedString("The insulin on board amount is %@ Units. Together with the entered value of %@ Units it exceeds the configured maximum of %@ Units.", comment: "Body of the alert describing a maximum iob validation error. (1: The bolus value, 2: The IOB value, 3: The maximum IOB permitted)"), + bolusUnitsFormatter.string(from: NSNumber(value: iob)) ?? "", + bolusUnitsFormatter.string(from: NSNumber(value: bolus)) ?? "", + bolusUnitsFormatter.string(from: NSNumber(value: maxInsulinOnBoard)) ?? "" + )) + return } } From 353e9d593e0977af4a909bb3501a764872eed149 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jun 2018 23:40:46 +0200 Subject: [PATCH 14/16] Fix test code after merge --- DoseMathTests/DoseMathTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 7df9cd335f..9d807c9df5 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -712,7 +712,7 @@ class RecommendBolusTests: XCTestCase { XCTAssertEqual(1.3, dose.amount) if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! { - XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60) + XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60) } else { XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)") } @@ -736,7 +736,7 @@ class RecommendBolusTests: XCTestCase { XCTAssertEqual(0.3, dose.amount) if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! { - XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60) + XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60) } else { XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)") } @@ -760,7 +760,7 @@ class RecommendBolusTests: XCTestCase { XCTAssertEqual(0, dose.amount) if case BolusRecommendationNotice.currentGlucoseBelowTarget(let glucose) = dose.notice! { - XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter()), 60) + XCTAssertEqual(glucose.quantity.doubleValue(for: .milligramsPerDeciliter), 60) } else { XCTFail("Expected currentGlucoseBelowTarget, but got \(dose.notice!)") } From b0fb27e646b3d43e279ad812bcf8ab27bba98227 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jun 2018 23:44:15 +0200 Subject: [PATCH 15/16] Fix merge with latest upstream/dev --- Loop/Managers/DeviceDataManager.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 9343ef67ca..0b61ea0a0b 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -562,13 +562,15 @@ final class DeviceDataManager { return } + guard let recencyInterval = loopManager?.settings.recencyInterval else { + notify(LoopError.configurationError("LoopManager")) + return + } + // If we don't have recent pump data, or the pump was recently rewound, read new pump data before bolusing. var shouldReadReservoir = isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-10)) - if loopManager.doseStore.lastReservoirVolumeDrop < 0 { - notify(LoopError.invalidData(details: "Last Reservoir drop negative.")) - shouldReadReservoir = true - } else if let reservoir = loopManager.doseStore.lastReservoirValue, reservoir.startDate.timeIntervalSinceNow <= - -loopManager.recencyInterval { + if let reservoir = loopManager.doseStore.lastReservoirValue, reservoir.startDate.timeIntervalSinceNow <= + -recencyInterval { notify(LoopError.pumpDataTooOld(date: reservoir.startDate)) shouldReadReservoir = true } else if loopManager.doseStore.lastReservoirValue == nil { From d60744d9224697244117ac682074e8a9acb41dff Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 24 Jun 2018 21:03:28 +0200 Subject: [PATCH 16/16] Re-add diagnostic report entries which were accidentally removed during the merge --- Loop/Managers/LoopDataManager.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 376ccbf744..512b04f624 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -1235,6 +1235,21 @@ extension LoopDataManager { var entries = [ "## LoopDataManager", "settings: \(String(reflecting: manager.settings))", + + "insulinCounteractionEffects: [", + "* GlucoseEffectVelocity(start, end, mg/dL/min)", + manager.insulinCounteractionEffects.reduce(into: "", { (entries, entry) in + entries.append("* \(entry.startDate), \(entry.endDate), \(entry.quantity.doubleValue(for: GlucoseEffectVelocity.unit))\n") + }), + "]", + + "predictedGlucose: [", + "* PredictedGlucoseValue(start, mg/dL)", + (state.predictedGlucose ?? []).reduce(into: "", { (entries, entry) in + entries.append("* \(entry.startDate), \(entry.quantity.doubleValue(for: .milligramsPerDeciliter))\n") + }), + "]", + "retrospectivePredictedGlucose: \(state.retrospectivePredictedGlucose ?? [])", "glucoseMomentumEffect: \(manager.glucoseMomentumEffect ?? [])", "retrospectiveGlucoseEffect: \(manager.retrospectiveGlucoseEffect)",