From b0ed4a3521fc870dd9b58c4714a8709db493140c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 1 Jan 2017 19:57:54 -0600 Subject: [PATCH 01/37] Configurable minimum bg guard. --- DoseMathTests/DoseMathTests.swift | 120 +++++++++++++----- ...nd_temp_basal_start_very_low_end_high.json | 7 + Loop.xcodeproj/project.pbxproj | 18 +++ Loop/Extensions/CaseCountable.swift | 19 +++ Loop/Extensions/NSUserDefaults.swift | 15 +++ Loop/Managers/AnalyticsManager.swift | 4 + Loop/Managers/DeviceDataManager.swift | 7 + Loop/Managers/DoseMath.swift | 16 +-- Loop/Managers/LoopDataManager.swift | 12 +- Loop/Models/GlucoseThreshold.swift | 41 ++++++ .../GlucoseThresholdTableViewController.swift | 51 ++++++++ .../SettingsTableViewController.swift | 54 ++++++-- .../TextFieldTableViewController.swift | 5 +- 13 files changed, 310 insertions(+), 59 deletions(-) create mode 100644 DoseMathTests/Fixtures/recommend_temp_basal_start_very_low_end_high.json create mode 100644 Loop/Extensions/CaseCountable.swift create mode 100644 Loop/Models/GlucoseThreshold.swift create mode 100644 Loop/View Controllers/GlucoseThresholdTableViewController.swift diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 4a52f5b772..5bafc90f03 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -87,6 +87,10 @@ class RecommendTempBasalTests: XCTestCase { var insulinSensitivitySchedule: InsulinSensitivitySchedule { return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliterUnit(), dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])! } + + var minimumBGGuard: GlucoseThreshold { + return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliterUnit(), value: 55) + } func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") @@ -97,7 +101,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertNil(dose) @@ -112,7 +117,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertNil(dose) @@ -132,7 +138,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose!.rate) @@ -148,7 +155,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertNil(dose) @@ -167,7 +175,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose!.rate) @@ -192,7 +201,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose!.rate) @@ -205,7 +215,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertNil(dose) @@ -224,7 +235,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose!.rate) @@ -240,7 +252,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose!.rate) @@ -257,7 +270,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertNil(dose) @@ -276,7 +290,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose!.rate) @@ -292,7 +307,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(3.0, dose!.rate) @@ -308,7 +324,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(1.425, dose!.rate, accuracy: 1.0 / 40.0) @@ -324,7 +341,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(1.475, dose!.rate, accuracy: 1.0 / 40.0) @@ -340,7 +358,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: self.insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(3.0, dose!.rate) @@ -355,7 +374,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(2.975, dose!.rate, accuracy: 1.0 / 40.0) @@ -371,7 +391,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: self.insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0.0, dose!.rate) @@ -385,7 +406,8 @@ class RecommendTempBasalTests: XCTestCase { maxBasalRate: maxBasalRate, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertNil(dose) @@ -430,6 +452,10 @@ class RecommendBolusTests: XCTestCase { var insulinSensitivitySchedule: InsulinSensitivitySchedule { return InsulinSensitivitySchedule(unit: HKUnit.milligramsPerDeciliterUnit(), dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 60.0)])! } + + var minimumBGGuard: GlucoseThreshold { + return GlucoseThreshold(unit: HKUnit.milligramsPerDeciliterUnit(), value: 55) + } func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") @@ -440,7 +466,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose) @@ -455,7 +482,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose) @@ -475,7 +503,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose) @@ -490,7 +519,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose) @@ -505,7 +535,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(0, dose) @@ -520,9 +551,26 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) + XCTAssertEqual(1.325, dose) + } + + func testStartVeryLowEndHigh() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_very_low_end_high") + + let dose = DoseMath.recommendBolusFromPredictedGlucose(glucose, + atDate: glucose.first!.startDate, + lastTempBasal: nil, + maxBolus: maxBolus, + glucoseTargetRange: glucoseTargetRange, + insulinSensitivity: insulinSensitivitySchedule, + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard + ) + XCTAssertEqual(0, dose) } @@ -535,7 +583,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(1.333, dose, accuracy: 1.0 / 40.0) @@ -550,7 +599,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(0.067, dose, accuracy: 1.0 / 40.0) @@ -565,7 +615,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(0.083, dose, accuracy: 1.0 / 40.0) @@ -585,7 +636,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(0, dose, accuracy: 1e-13) @@ -605,7 +657,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(0.083, dose, accuracy: 1.0 / 40.0) @@ -620,7 +673,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: self.insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqual(1.0, dose) @@ -634,7 +688,8 @@ class RecommendBolusTests: XCTestCase { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard ) XCTAssertEqualWithAccuracy(1.0, dose, accuracy: 1.0 / 40.0) @@ -642,7 +697,8 @@ class RecommendBolusTests: XCTestCase { func testNoInputGlucose() { let dose = DoseMath.recommendBolusFromPredictedGlucose([], lastTempBasal: nil, maxBolus: 4, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, - basalRateSchedule: basalRateSchedule) + basalRateSchedule: basalRateSchedule, + minimumBGGuard: minimumBGGuard) XCTAssertEqual(0, dose) } diff --git a/DoseMathTests/Fixtures/recommend_temp_basal_start_very_low_end_high.json b/DoseMathTests/Fixtures/recommend_temp_basal_start_very_low_end_high.json new file mode 100644 index 0000000000..fed8e6c7b1 --- /dev/null +++ b/DoseMathTests/Fixtures/recommend_temp_basal_start_very_low_end_high.json @@ -0,0 +1,7 @@ + [ + {"date": "2015-07-19T18:00:00", "amount": 40}, + {"date": "2015-07-19T18:30:00", "amount": 50}, + {"date": "2015-07-19T19:00:00", "amount": 80}, + {"date": "2015-07-19T19:30:00", "amount": 160}, + {"date": "2015-07-19T20:00:00", "amount": 200} + ] diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 104363a65b..9dc86ed282 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -180,6 +180,11 @@ C10428971D17BAD400DD539A /* NightscoutUploadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */; }; C12F21A71DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json in Resources */ = {isa = PBXBuildFile; fileRef = C12F21A61DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json */; }; C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */; }; + C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824991E1999FA00D9D25C /* CaseCountable.swift */; }; + C178249E1E19B62300D9D25C /* GlucoseThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */; }; + C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */; }; + C17824A11E19E8C200D9D25C /* GlucoseThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */; }; + C17824A31E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json in Resources */ = {isa = PBXBuildFile; fileRef = C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */; }; C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884621D51A7A400405663 /* BatteryIndicator.swift */; }; C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; }; C1C73EF71DE3D0230022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73EF91DE3D0230022FC89 /* InfoPlist.strings */; }; @@ -471,6 +476,10 @@ C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NightscoutUploadKit.framework; path = Carthage/Build/iOS/NightscoutUploadKit.framework; sourceTree = ""; }; C12F21A61DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_tamp_basal_very_low_end_in_range.json; sourceTree = ""; }; C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealBolusNightscoutTreatment.swift; sourceTree = ""; }; + C17824991E1999FA00D9D25C /* CaseCountable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseCountable.swift; sourceTree = ""; }; + C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseThreshold.swift; sourceTree = ""; }; + C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseThresholdTableViewController.swift; sourceTree = ""; }; + C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_start_very_low_end_high.json; sourceTree = ""; }; C17884621D51A7A400405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = ""; }; C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = ""; }; C1C73EF81DE3D0230022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -593,6 +602,7 @@ 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */, 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */, 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */, + C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */, ); path = Models; sourceTree = ""; @@ -734,6 +744,7 @@ 43E2D8EA1D20C0DB004DA55F /* recommend_temp_basal_start_low_end_high.json */, 43E2D8EB1D20C0DB004DA55F /* recommend_temp_basal_start_low_end_in_range.json */, C12F21A61DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json */, + C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */, ); path = Fixtures; sourceTree = ""; @@ -757,6 +768,7 @@ 43F41C361D3BF32400C11ED6 /* UIAlertController.swift */, 437CEEE31CDE5C0A003C8C80 /* UIImage.swift */, 434FF1ED1CF27EEF000DB779 /* UITableViewCell.swift */, + C17824991E1999FA00D9D25C /* CaseCountable.swift */, ); path = Extensions; sourceTree = ""; @@ -776,6 +788,7 @@ 43F5C2DA1B92A5E1003EB13D /* SettingsTableViewController.swift */, 43E3449E1B9D68E900C85C07 /* StatusTableViewController.swift */, 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */, + C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -1205,6 +1218,7 @@ files = ( 43E2D8F21D20C0DB004DA55F /* recommend_temp_basal_no_change_glucose.json in Resources */, 43E2D8F61D20C0DB004DA55F /* recommend_temp_basal_start_low_end_in_range.json in Resources */, + C17824A31E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json in Resources */, 43E2D8F41D20C0DB004DA55F /* recommend_temp_basal_start_high_end_low.json in Resources */, 43E2D8EF1D20C0DB004DA55F /* recommend_temp_basal_high_and_falling.json in Resources */, 43E2D8ED1D20C0DB004DA55F /* recommend_temp_basal_correct_low_at_min.json in Resources */, @@ -1306,10 +1320,12 @@ 43E3449F1B9D68E900C85C07 /* StatusTableViewController.swift in Sources */, 43DBF0531C93EC8200B3C386 /* DeviceDataManager.swift in Sources */, 43E2D8C81D208D5B004DA55F /* KeychainManager+Loop.swift in Sources */, + C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */, 4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */, 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */, 43DBF0591C93F73800B3C386 /* CarbEntryTableViewController.swift in Sources */, 43DE925C1C547A20001FFDE1 /* WatchContext.swift in Sources */, + C178249E1E19B62300D9D25C /* GlucoseThreshold.swift in Sources */, 43F41C351D3B623800C11ED6 /* ChartPointsTouchHighlightLayerViewCache.swift in Sources */, 43EB40861C82646A00472A8C /* StatusChartManager.swift in Sources */, C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */, @@ -1321,6 +1337,7 @@ 4315D2871CA5CC3B00589052 /* CarbEntryEditTableViewController.swift in Sources */, 43F5173D1D713DB0000FA422 /* RadioSelectionTableViewController.swift in Sources */, 4331E0781C85302200FBE832 /* CGPoint.swift in Sources */, + C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */, 43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */, 4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */, 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */, @@ -1392,6 +1409,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C17824A11E19E8C200D9D25C /* GlucoseThreshold.swift in Sources */, 43E2D8DC1D20C049004DA55F /* DoseMath.swift in Sources */, 43E2D8DB1D20C03B004DA55F /* NSTimeInterval.swift in Sources */, 43E2D8D41D20BF42004DA55F /* DoseMathTests.swift in Sources */, diff --git a/Loop/Extensions/CaseCountable.swift b/Loop/Extensions/CaseCountable.swift new file mode 100644 index 0000000000..1c8c494893 --- /dev/null +++ b/Loop/Extensions/CaseCountable.swift @@ -0,0 +1,19 @@ +// +// CaseCountable.swift +// Loop +// +// Created by Pete Schwamb on 1/1/17. +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import Foundation + +public protocol CaseCountable: RawRepresentable {} + +public extension CaseCountable where RawValue: Integer { + static var count: Int { + var i: RawValue = 0 + while let new = Self(rawValue: i) { i = new.rawValue.advanced(by: 1) } + return Int(i.toIntMax()) + } +} diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index cc13e51eb5..790c7216b2 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -9,6 +9,7 @@ import Foundation import LoopKit import MinimedKit +import HealthKit extension UserDefaults { @@ -31,6 +32,7 @@ extension UserDefaults { case PumpTimeZone = "com.loudnate.Naterade.PumpTimeZone" case RetrospectiveCorrectionEnabled = "com.loudnate.Loop.RetrospectiveCorrectionEnabled" case BatteryChemistry = "com.loopkit.Loop.BatteryChemistry" + case MinimumBGGuard = "com.loopkit.Loop.MinimumBGGuard" } var basalRateSchedule: BasalRateSchedule? { @@ -244,5 +246,18 @@ extension UserDefaults { } } } + + var minimumBGGuard: GlucoseThreshold? { + get { + if let rawValue = dictionary(forKey: Key.MinimumBGGuard.rawValue) { + return GlucoseThreshold(rawValue: rawValue) + } else { + return nil + } + } + set { + set(newValue?.rawValue, forKey: Key.MinimumBGGuard.rawValue) + } + } } diff --git a/Loop/Managers/AnalyticsManager.swift b/Loop/Managers/AnalyticsManager.swift index 51ef938d64..046e64a100 100644 --- a/Loop/Managers/AnalyticsManager.swift +++ b/Loop/Managers/AnalyticsManager.swift @@ -106,6 +106,10 @@ final class AnalyticsManager { func didChangeMaximumBolus() { logEvent("Maximum bolus change") } + + func didChangeMinimumBGGuard() { + logEvent("Minimum BG Guard change") + } // MARK: - Loop Events diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 4dad875a39..17126c5845 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -817,6 +817,13 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto AnalyticsManager.sharedManager.didChangeGlucoseTargetRangeSchedule() } } + + var minimumBGGuard: GlucoseThreshold? = UserDefaults.standard.minimumBGGuard { + didSet { + UserDefaults.standard.minimumBGGuard = minimumBGGuard + AnalyticsManager.sharedManager.didChangeMinimumBGGuard() + } + } var workoutModeEnabled: Bool? { guard let range = glucoseTargetRangeSchedule else { diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index a7eec3743e..acd47c7eed 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -51,6 +51,7 @@ struct DoseMath { - parameter glucoseTargetRange: The schedule of target glucose ranges - parameter insulinSensitivity: The schedule of insulin sensitivities, in Units of insulin per glucose-unit - parameter basalRateSchedule: The schedule of basal rates + - parameter minimumBGGuard: Loop will always 0 temp if minBG is less than or equal to this value. - returns: The recommended basal rate and duration */ @@ -60,7 +61,8 @@ struct DoseMath { maxBasalRate: Double, glucoseTargetRange: GlucoseRangeSchedule, insulinSensitivity: InsulinSensitivitySchedule, - basalRateSchedule: BasalRateSchedule + basalRateSchedule: BasalRateSchedule, + minimumBGGuard: GlucoseThreshold ) -> (rate: Double, duration: TimeInterval)? { guard glucose.count > 1 else { return nil @@ -77,9 +79,7 @@ struct DoseMath { var rate: Double? var duration = TimeInterval(minutes: 30) - let alwaysLowTempBGThreshold: Double = 55 // mg/dL - - if minGlucose.quantity.doubleValue(for: HKUnit.milligramsPerDeciliterUnit()) <= alwaysLowTempBGThreshold { + if minGlucose.quantity <= minimumBGGuard.quantity { rate = 0 } else if minGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) < minGlucoseTargets.minValue && eventualGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) <= eventualGlucoseTargets.minValue { let targetGlucose = HKQuantity(unit: glucoseTargetRange.unit, doubleValue: (minGlucoseTargets.minValue + minGlucoseTargets.maxValue) / 2) @@ -140,6 +140,7 @@ struct DoseMath { - parameter glucoseTargetRange: The schedule of target glucose ranges - parameter insulinSensitivity: The schedule of insulin sensitivities, in Units of insulin per glucose-unit - parameter basalRateSchedule: The schedule of basal rates + - parameter minimumBGGuard: If minBG is less than or equal to this value, no recommendation will be made - returns: The recommended bolus */ @@ -149,7 +150,8 @@ struct DoseMath { maxBolus: Double, glucoseTargetRange: GlucoseRangeSchedule, insulinSensitivity: InsulinSensitivitySchedule, - basalRateSchedule: BasalRateSchedule + basalRateSchedule: BasalRateSchedule, + minimumBGGuard: GlucoseThreshold ) -> Double { guard glucose.count > 1 else { return 0 @@ -159,10 +161,8 @@ struct DoseMath { let minGlucose = glucose.min { $0.quantity < $1.quantity }! let eventualGlucoseTargets = glucoseTargetRange.value(at: eventualGlucose.startDate) - // Use between to opt-out of the override. - let minGlucoseTargets = glucoseTargetRange.between(start: minGlucose.startDate, end: minGlucose.startDate).first!.value - guard minGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) >= minGlucoseTargets.minValue else { + guard minGlucose.quantity >= minimumBGGuard.quantity else { return 0 } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 394ebe78cb..6fbd107c89 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -545,7 +545,8 @@ final class LoopDataManager { maxBasal = deviceDataManager.maximumBasalRatePerHour, let glucoseTargetRange = deviceDataManager.glucoseTargetRangeSchedule, let insulinSensitivity = deviceDataManager.insulinSensitivitySchedule, - let basalRates = deviceDataManager.basalRateSchedule + let basalRates = deviceDataManager.basalRateSchedule, + let minimumBGGuard = deviceDataManager.minimumBGGuard else { error = LoopError.missingDataError("Loop configuration data not set") throw error! @@ -559,7 +560,8 @@ final class LoopDataManager { maxBasalRate: maxBasal, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivity, - basalRateSchedule: basalRates + basalRateSchedule: basalRates, + minimumBGGuard: minimumBGGuard ) else { recommendedTempBasal = nil @@ -600,7 +602,8 @@ final class LoopDataManager { let maxBolus = self.deviceDataManager.maximumBolus, let glucoseTargetRange = self.deviceDataManager.glucoseTargetRangeSchedule, let insulinSensitivity = self.deviceDataManager.insulinSensitivitySchedule, - let basalRates = self.deviceDataManager.basalRateSchedule + let basalRates = self.deviceDataManager.basalRateSchedule, + let minimumBGGuard = self.deviceDataManager.minimumBGGuard else { throw LoopError.missingDataError("Bolus prediction and configuration data not found") } @@ -622,7 +625,8 @@ final class LoopDataManager { maxBolus: maxBolus, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivity, - basalRateSchedule: basalRates + basalRateSchedule: basalRates, + minimumBGGuard: minimumBGGuard ) - pendingBolusAmount) } diff --git a/Loop/Models/GlucoseThreshold.swift b/Loop/Models/GlucoseThreshold.swift new file mode 100644 index 0000000000..19607343ed --- /dev/null +++ b/Loop/Models/GlucoseThreshold.swift @@ -0,0 +1,41 @@ +// +// GlucoseThreshold.swift +// Loop +// +// Created by Pete Schwamb on 1/1/17. +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import Foundation +import HealthKit + +struct GlucoseThreshold: RawRepresentable { + typealias RawValue = [String: Any] + + let value: Double + let unit: HKUnit + + public var quantity: HKQuantity { + return HKQuantity(unit: unit, doubleValue: value) + } + + public init(unit: HKUnit, value: Double) { + self.value = value + self.unit = unit + } + + init?(rawValue: RawValue) { + guard let unitsStr = rawValue["units"] as? String, let value = rawValue["value"] as? Double else { + return nil + } + self.unit = HKUnit(from: unitsStr) + self.value = value + } + + var rawValue: RawValue { + return [ + "value": value, + "units": unit.unitString + ] + } +} diff --git a/Loop/View Controllers/GlucoseThresholdTableViewController.swift b/Loop/View Controllers/GlucoseThresholdTableViewController.swift new file mode 100644 index 0000000000..291fd6bdd0 --- /dev/null +++ b/Loop/View Controllers/GlucoseThresholdTableViewController.swift @@ -0,0 +1,51 @@ +// +// GlucoseThresholdTableViewController.swift +// Loop +// +// Created by Pete Schwamb on 1/1/17. +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import Foundation + +import UIKit +import LoopKit +import HealthKit + + +final class GlucoseThresholdTableViewController: TextFieldTableViewController { + + public let glucoseUnits: HKUnit + + private static let valueNumberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + + return formatter + }() + + init(threshold: Double?, glucoseUnits: HKUnit) { + self.glucoseUnits = glucoseUnits + + super.init(style: .grouped) + + placeholder = NSLocalizedString("Enter minimum BG guard", comment: "The placeholder text instructing users to enter a minimum BG guard") + keyboardType = .decimalPad + contextHelp = NSLocalizedString("When current or forecasted BG is below miminum BG guard, Loop will not recommend a bolus, and will issue temporary basal rates of 0U/hr.", comment: "Instructions on entering minimum BG threshold") + + unit = glucoseUnits.glucoseUnitDisplayString + + if let threshold = threshold { + value = GlucoseThresholdTableViewController.valueNumberFormatter.string(from: NSNumber(value: threshold)) + } + + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index 5250817f37..271ffe973c 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -76,28 +76,25 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } } - fileprivate enum Section: Int { + fileprivate enum Section: Int, CaseCountable { case loop = 0 case devices case configuration case services - - static let count = 4 } - fileprivate enum LoopRow: Int { + fileprivate enum LoopRow: Int, CaseCountable { case dosing = 0 case preferredInsulinDataSource case diagnostic - - static let count = 3 } - fileprivate enum ConfigurationRow: Int { + fileprivate enum ConfigurationRow: Int, CaseCountable { case pumpID = 0 case transmitterID case receiverEnabled case glucoseTargetRange + case minimumBGGuard case insulinActionDuration case basalRate case carbRatio @@ -105,17 +102,13 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case maxBasal case maxBolus case batteryChemistry - - static let count = 11 } - fileprivate enum ServiceRow: Int { + fileprivate enum ServiceRow: Int, CaseCountable { case share = 0 case nightscout case mLab case amplitude - - static let count = 4 } fileprivate lazy var valueNumberFormatter: NumberFormatter = { @@ -127,7 +120,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu return formatter }() - + // MARK: - UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { @@ -243,6 +236,14 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } else { configCell.detailTextLabel?.text = TapToSetString } + case .minimumBGGuard: + configCell.textLabel?.text = NSLocalizedString("Minimum BG Guard", comment: "The title text for the minimum bg guard setting") + + if let minimumBGGuard = dataManager.minimumBGGuard { + configCell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@ %2$@/U", comment: "Format string for minimum bg guard. (1: value)(2: bg unit)"), minimumBGGuard.value, minimumBGGuard.unit.glucoseUnitDisplayString) + } else { + configCell.detailTextLabel?.text = TapToSetString + } case .insulinActionDuration: configCell.textLabel?.text = NSLocalizedString("Insulin Action Duration", comment: "The title text for the insulin action duration value") @@ -464,6 +465,24 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } else { show(scheduleVC, sender: sender) } + case .minimumBGGuard: + if let minBGGuard = dataManager.minimumBGGuard { + let vc = GlucoseThresholdTableViewController(threshold: minBGGuard.value, glucoseUnits: minBGGuard.unit) + vc.delegate = self + self.show(vc, sender: sender) + } else if let glucoseStore = dataManager.glucoseStore { + glucoseStore.preferredUnit({ (unit, error) -> Void in + DispatchQueue.main.async { + if let error = error { + self.presentAlertController(with: error) + } else if let unit = unit { + let vc = GlucoseThresholdTableViewController(threshold: nil, glucoseUnits: unit) + vc.delegate = self + self.show(vc, sender: sender) + } + } + }) + } case .receiverEnabled: break case .batteryChemistry: @@ -589,6 +608,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if let controller = controller as? GlucoseRangeScheduleTableViewController { dataManager.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, workoutRange: controller.workoutRange, timeZone: controller.timeZone) } + case .minimumBGGuard: + if let controller = controller as? GlucoseRangeScheduleTableViewController { + dataManager.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, workoutRange: controller.workoutRange, timeZone: controller.timeZone) + } case let row: if let controller = controller as? DailyQuantityScheduleTableViewController { switch row { @@ -677,6 +700,11 @@ extension SettingsTableViewController: TextFieldTableViewControllerDelegate { } else { dataManager.maximumBolus = nil } + case .minimumBGGuard: + if let controller = controller as? GlucoseThresholdTableViewController, + let value = controller.value, let minBGGuard = valueNumberFormatter.number(from: value)?.doubleValue { + dataManager.minimumBGGuard = GlucoseThreshold(unit: controller.glucoseUnits, value: minBGGuard) + } default: assertionFailure() } diff --git a/Loop/View Controllers/TextFieldTableViewController.swift b/Loop/View Controllers/TextFieldTableViewController.swift index 501add03b0..43d28c9087 100644 --- a/Loop/View Controllers/TextFieldTableViewController.swift +++ b/Loop/View Controllers/TextFieldTableViewController.swift @@ -7,12 +7,13 @@ // import LoopKit +import HealthKit /// Convenience static constructors used to contain common configuration extension TextFieldTableViewController { typealias T = TextFieldTableViewController - + private static let valueNumberFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -84,5 +85,5 @@ extension TextFieldTableViewController { } return vc - } + } } From b8c34d8adb7f54d0334b13e6127aef24820264dc Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sun, 1 Jan 2017 21:02:49 -0600 Subject: [PATCH 02/37] Fix display issues, and clear out stored value when user deletes entry via interface --- .../SettingsTableViewController.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index 271ffe973c..7560b5b2e2 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -240,7 +240,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu configCell.textLabel?.text = NSLocalizedString("Minimum BG Guard", comment: "The title text for the minimum bg guard setting") if let minimumBGGuard = dataManager.minimumBGGuard { - configCell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@ %2$@/U", comment: "Format string for minimum bg guard. (1: value)(2: bg unit)"), minimumBGGuard.value, minimumBGGuard.unit.glucoseUnitDisplayString) + let value = valueNumberFormatter.string(from: NSNumber(value: minimumBGGuard.value)) ?? "-" + configCell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@ %2$@", comment: "Format string for minimum bg guard. (1: value)(2: bg unit)"), value, minimumBGGuard.unit.glucoseUnitDisplayString) } else { configCell.detailTextLabel?.text = TapToSetString } @@ -469,6 +470,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if let minBGGuard = dataManager.minimumBGGuard { let vc = GlucoseThresholdTableViewController(threshold: minBGGuard.value, glucoseUnits: minBGGuard.unit) vc.delegate = self + vc.indexPath = indexPath + vc.title = sender?.textLabel?.text self.show(vc, sender: sender) } else if let glucoseStore = dataManager.glucoseStore { glucoseStore.preferredUnit({ (unit, error) -> Void in @@ -478,6 +481,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } else if let unit = unit { let vc = GlucoseThresholdTableViewController(threshold: nil, glucoseUnits: unit) vc.delegate = self + vc.indexPath = indexPath + vc.title = sender?.textLabel?.text self.show(vc, sender: sender) } } @@ -608,10 +613,6 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if let controller = controller as? GlucoseRangeScheduleTableViewController { dataManager.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, workoutRange: controller.workoutRange, timeZone: controller.timeZone) } - case .minimumBGGuard: - if let controller = controller as? GlucoseRangeScheduleTableViewController { - dataManager.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, workoutRange: controller.workoutRange, timeZone: controller.timeZone) - } case let row: if let controller = controller as? DailyQuantityScheduleTableViewController { switch row { @@ -704,6 +705,8 @@ extension SettingsTableViewController: TextFieldTableViewControllerDelegate { if let controller = controller as? GlucoseThresholdTableViewController, let value = controller.value, let minBGGuard = valueNumberFormatter.number(from: value)?.doubleValue { dataManager.minimumBGGuard = GlucoseThreshold(unit: controller.glucoseUnits, value: minBGGuard) + } else { + dataManager.minimumBGGuard = nil } default: assertionFailure() From f6366c2218178b840bbd7f3e95f2417b7d69a53d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 2 Jan 2017 15:32:41 -0600 Subject: [PATCH 03/37] Return recommendation structure --- DoseMathTests/DoseMathTests.swift | 68 ++++++++++++++----- Loop.xcodeproj/project.pbxproj | 12 +++- .../Complication - WatchApp.xcscheme | 2 +- .../xcschemes/DoseMathTests.xcscheme | 2 +- .../xcschemes/Loop Status Extension.xcscheme | 2 +- .../xcshareddata/xcschemes/Loop.xcscheme | 2 +- .../xcshareddata/xcschemes/LoopTests.xcscheme | 2 +- .../Notification - WatchApp.xcscheme | 2 +- .../xcshareddata/xcschemes/WatchApp.xcscheme | 2 +- Loop/Managers/DoseMath.swift | 40 ++++++++--- Loop/Managers/LoopDataManager.swift | 20 +++--- Loop/Models/BolusRecommendation.swift | 26 +++++++ 12 files changed, 135 insertions(+), 45 deletions(-) create mode 100644 Loop/Models/BolusRecommendation.swift diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 5bafc90f03..5945549e48 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -467,10 +467,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) } func testStartHighEndInRange() { @@ -483,10 +484,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) // Don't consider net-negative temp basal let lastTempBasal = DoseEntry( @@ -504,10 +506,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) } func testStartLowEndInRange() { @@ -520,10 +523,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) } func testStartHighEndLow() { @@ -536,10 +540,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) } func testStartLowEndHigh() { @@ -552,10 +557,28 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(1.325, dose) + XCTAssertEqual(1.325, dose.amount) + } + + func testStartLowEndHighWithPendingBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + + let dose = DoseMath.recommendBolusFromPredictedGlucose(glucose, + atDate: glucose.first!.startDate, + lastTempBasal: nil, + maxBolus: maxBolus, + glucoseTargetRange: glucoseTargetRange, + insulinSensitivity: insulinSensitivitySchedule, + basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 1, + minimumBGGuard: minimumBGGuard + ) + + XCTAssertEqual(0.325, dose.amount) } func testStartVeryLowEndHigh() { @@ -568,10 +591,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) } func testFlatAndHigh() { @@ -584,10 +608,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqualWithAccuracy(1.333, dose, accuracy: 1.0 / 40.0) + XCTAssertEqualWithAccuracy(1.333, dose.amount, accuracy: 1.0 / 40.0) } func testHighAndFalling() { @@ -600,10 +625,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqualWithAccuracy(0.067, dose, accuracy: 1.0 / 40.0) + XCTAssertEqualWithAccuracy(0.067, dose.amount, accuracy: 1.0 / 40.0) } func testInRangeAndRising() { @@ -616,10 +642,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqualWithAccuracy(0.083, dose, accuracy: 1.0 / 40.0) + XCTAssertEqualWithAccuracy(0.083, dose.amount, accuracy: 1.0 / 40.0) // Less existing temp var lastTempBasal = DoseEntry( @@ -637,10 +664,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqualWithAccuracy(0, dose, accuracy: 1e-13) + XCTAssertEqualWithAccuracy(0, dose.amount, accuracy: 1e-13) // But not a finished temp lastTempBasal = DoseEntry( @@ -658,10 +686,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqualWithAccuracy(0.083, dose, accuracy: 1.0 / 40.0) + XCTAssertEqualWithAccuracy(0.083, dose.amount, accuracy: 1.0 / 40.0) } func testHighAndRising() { @@ -674,10 +703,11 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: self.insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqual(1.0, dose) + XCTAssertEqual(1.0, dose.amount) // Use mmol sensitivity value let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: HKUnit.millimolesPerLiterUnit(), dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 10.0 / 3)])! @@ -689,18 +719,24 @@ class RecommendBolusTests: XCTestCase { glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard ) - XCTAssertEqualWithAccuracy(1.0, dose, accuracy: 1.0 / 40.0) + XCTAssertEqualWithAccuracy(1.0, dose.amount, accuracy: 1.0 / 40.0) } func testNoInputGlucose() { - let dose = DoseMath.recommendBolusFromPredictedGlucose([], lastTempBasal: nil, maxBolus: 4, glucoseTargetRange: glucoseTargetRange, insulinSensitivity: insulinSensitivitySchedule, + let dose = DoseMath.recommendBolusFromPredictedGlucose([], + lastTempBasal: nil, + maxBolus: 4, + glucoseTargetRange: glucoseTargetRange, + insulinSensitivity: insulinSensitivitySchedule, basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, minimumBGGuard: minimumBGGuard) - XCTAssertEqual(0, dose) + XCTAssertEqual(0, dose.amount) } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 9dc86ed282..958ebebfc3 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -185,6 +185,8 @@ C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */; }; C17824A11E19E8C200D9D25C /* GlucoseThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */; }; C17824A31E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json in Resources */ = {isa = PBXBuildFile; fileRef = C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */; }; + C17824A51E1AD4D100D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; }; + C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; }; C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884621D51A7A400405663 /* BatteryIndicator.swift */; }; C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; }; C1C73EF71DE3D0230022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73EF91DE3D0230022FC89 /* InfoPlist.strings */; }; @@ -480,6 +482,7 @@ C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseThreshold.swift; sourceTree = ""; }; C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseThresholdTableViewController.swift; sourceTree = ""; }; C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_start_very_low_end_high.json; sourceTree = ""; }; + C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusRecommendation.swift; sourceTree = ""; }; C17884621D51A7A400405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = ""; }; C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = ""; }; C1C73EF81DE3D0230022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -603,6 +606,7 @@ 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */, 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */, C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */, + C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */, ); path = Models; sourceTree = ""; @@ -1081,7 +1085,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0820; ORGANIZATIONNAME = "LoopKit Authors"; TargetAttributes = { 43776F8B1B8022E90074EA36 = { @@ -1294,6 +1298,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C17824A51E1AD4D100D9D25C /* BolusRecommendation.swift in Sources */, 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */, 434F54571D287FDB002A9274 /* NibLoadable.swift in Sources */, 4315D28A1CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift in Sources */, @@ -1413,6 +1418,7 @@ 43E2D8DC1D20C049004DA55F /* DoseMath.swift in Sources */, 43E2D8DB1D20C03B004DA55F /* NSTimeInterval.swift in Sources */, 43E2D8D41D20BF42004DA55F /* DoseMathTests.swift in Sources */, + C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1900,7 +1906,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1927,7 +1933,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme index bf7d22a5ca..e9d45e8f1f 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme @@ -1,6 +1,6 @@ Double { + ) -> BolusRecommendation { guard glucose.count > 1 else { - return 0 + return BolusRecommendation(amount: 0) } let eventualGlucose = glucose.last! @@ -163,24 +165,42 @@ struct DoseMath { let eventualGlucoseTargets = glucoseTargetRange.value(at: eventualGlucose.startDate) guard minGlucose.quantity >= minimumBGGuard.quantity else { - return 0 + return BolusRecommendation(amount: 0) } let targetGlucose = eventualGlucoseTargets.maxValue let currentSensitivity = insulinSensitivity.quantity(at: date).doubleValue(for: glucoseTargetRange.unit) - var doseUnits = (eventualGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) - targetGlucose) / currentSensitivity - + let doseUnits = (eventualGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) - targetGlucose) / currentSensitivity + + let pendingTempBasalInsulin: Double + if let lastTempBasal = lastTempBasal, lastTempBasal.unit == .unitsPerHour && lastTempBasal.endDate > date { let normalBasalRate = basalRateSchedule.value(at: date) let remainingTime = lastTempBasal.endDate.timeIntervalSince(date) let remainingUnits = (lastTempBasal.value - normalBasalRate) * remainingTime / TimeInterval(hours: 1) - doseUnits -= max(0, remainingUnits) + pendingTempBasalInsulin = max(0, remainingUnits) + } else { + pendingTempBasalInsulin = 0 } - - doseUnits = round(doseUnits * 40) / 40 - - return min(maxBolus, max(0, doseUnits)) + + // All outstanding potential insulin delivery + let pendingInsulin = pendingTempBasalInsulin + pendingBolusAmount + + // Round to pump accuracy increments + let roundedAmount = round(max(0, (doseUnits - pendingInsulin)) * 40) / 40 + + // Cap at max bolus amount + let cappedAmount = min(maxBolus, max(0, roundedAmount)) + + + // if minGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) < eventualGlucoseTargets.minValue && + // eventualGlucoseTargets{ + // + // } + + + return BolusRecommendation(amount: cappedAmount, notice: nil, minBG: minGlucose, eventualBG: eventualGlucose, pendingInsulin: pendingInsulin) } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 6fbd107c89..7c3800399a 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -619,15 +619,17 @@ final class LoopDataManager { } let pendingBolusAmount: Double = lastBolus?.units ?? 0 - - return max(0, DoseMath.recommendBolusFromPredictedGlucose(glucose, - lastTempBasal: self.lastTempBasal, - maxBolus: maxBolus, - glucoseTargetRange: glucoseTargetRange, - insulinSensitivity: insulinSensitivity, - basalRateSchedule: basalRates, - minimumBGGuard: minimumBGGuard - ) - pendingBolusAmount) + + let bolusRecommendation = DoseMath.recommendBolusFromPredictedGlucose(glucose, + lastTempBasal: self.lastTempBasal, + maxBolus: maxBolus, + glucoseTargetRange: glucoseTargetRange, + insulinSensitivity: insulinSensitivity, + basalRateSchedule: basalRates, + pendingBolusAmount: pendingBolusAmount, + minimumBGGuard: minimumBGGuard) + + return bolusRecommendation.amount } func getRecommendedBolus(_ resultsHandler: @escaping (_ units: Double?, _ error: Error?) -> Void) { diff --git a/Loop/Models/BolusRecommendation.swift b/Loop/Models/BolusRecommendation.swift new file mode 100644 index 0000000000..182c39f07a --- /dev/null +++ b/Loop/Models/BolusRecommendation.swift @@ -0,0 +1,26 @@ +// +// BolusRecommendation.swift +// Loop +// +// Created by Pete Schwamb on 1/2/17. +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit + +struct BolusRecommendation { + let amount: Double + let notice: String? + let minBG: GlucoseValue? + let eventualBG: GlucoseValue? + let pendingInsulin: Double? + + init(amount: Double, notice: String? = nil, minBG: GlucoseValue? = nil, eventualBG: GlucoseValue? = nil, pendingInsulin: Double? = nil) { + self.amount = amount + self.notice = notice + self.minBG = minBG + self.eventualBG = eventualBG + self.pendingInsulin = pendingInsulin + } +} From 887969983999b043ed6be6f7729cd86e0009ba1b Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 2 Jan 2017 23:44:45 -0600 Subject: [PATCH 04/37] Adding more context to bolus screen --- DoseMathTests/DoseMathTests.swift | 23 +++ ...mmend_temp_basal_dropping_then_rising.json | 7 + Loop.xcodeproj/project.pbxproj | 6 + Loop/Base.lproj/Main.storyboard | 157 +++++++++++++++--- Loop/Managers/DoseMath.swift | 32 +++- Loop/Managers/LoopDataManager.swift | 12 +- Loop/Managers/NightscoutDataManager.swift | 4 +- Loop/Managers/WatchDataManager.swift | 8 +- Loop/Models/BolusRecommendation.swift | 10 +- .../BolusViewController.swift | 88 +++++++++- .../StatusTableViewController.swift | 20 ++- 11 files changed, 307 insertions(+), 60 deletions(-) create mode 100644 DoseMathTests/Fixtures/recommend_temp_basal_dropping_then_rising.json diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 5945549e48..55cf08cbaa 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -562,8 +562,31 @@ class RecommendBolusTests: XCTestCase { ) XCTAssertEqual(1.325, dose.amount) + XCTAssertEqual("Current glucose is below target range, but is predicted to rise.", dose.notice!) } + func testDroppingBelowRangeThenRising() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_dropping_then_rising") + + let dose = DoseMath.recommendBolusFromPredictedGlucose(glucose, + atDate: glucose.first!.startDate, + lastTempBasal: nil, + maxBolus: maxBolus, + glucoseTargetRange: glucoseTargetRange, + insulinSensitivity: insulinSensitivitySchedule, + basalRateSchedule: basalRateSchedule, + pendingBolusAmount: 0, + minimumBGGuard: minimumBGGuard + ) + + XCTAssertEqual(1.325, dose.amount) + XCTAssertEqual("Glucose is predicted to be 80 mg/dL at 6:30 PM and eventually rise above target.", dose.notice!) + + + + } + + func testStartLowEndHighWithPendingBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") diff --git a/DoseMathTests/Fixtures/recommend_temp_basal_dropping_then_rising.json b/DoseMathTests/Fixtures/recommend_temp_basal_dropping_then_rising.json new file mode 100644 index 0000000000..9643057fc0 --- /dev/null +++ b/DoseMathTests/Fixtures/recommend_temp_basal_dropping_then_rising.json @@ -0,0 +1,7 @@ +[ + {"date": "2015-07-19T18:00:00", "amount": 90}, + {"date": "2015-07-19T18:30:00", "amount": 80}, + {"date": "2015-07-19T19:00:00", "amount": 100}, + {"date": "2015-07-19T19:30:00", "amount": 160}, + {"date": "2015-07-19T20:00:00", "amount": 200} + ] diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 958ebebfc3..a08573ed3d 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -189,6 +189,8 @@ C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; }; C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884621D51A7A400405663 /* BatteryIndicator.swift */; }; C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; }; + C1C6591A1E1B1F430025CC58 /* NSNumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0E7A1D7DE13400D6475D /* NSNumberFormatter.swift */; }; + C1C6591C1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json in Resources */ = {isa = PBXBuildFile; fileRef = C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */; }; C1C73EF71DE3D0230022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73EF91DE3D0230022FC89 /* InfoPlist.strings */; }; C1C73F021DE3D0250022FC89 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F041DE3D0250022FC89 /* Localizable.strings */; }; C1C73F081DE3D0260022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F0A1DE3D0260022FC89 /* InfoPlist.strings */; }; @@ -485,6 +487,7 @@ C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusRecommendation.swift; sourceTree = ""; }; C17884621D51A7A400405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = ""; }; C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = ""; }; + C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_dropping_then_rising.json; sourceTree = ""; }; C1C73EF81DE3D0230022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; C1C73F031DE3D0250022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; C1C73F091DE3D0260022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -749,6 +752,7 @@ 43E2D8EB1D20C0DB004DA55F /* recommend_temp_basal_start_low_end_in_range.json */, C12F21A61DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json */, C17824A21E19EAB600D9D25C /* recommend_temp_basal_start_very_low_end_high.json */, + C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */, ); path = Fixtures; sourceTree = ""; @@ -1230,6 +1234,7 @@ C12F21A71DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json in Resources */, 43E2D8F11D20C0DB004DA55F /* recommend_temp_basal_in_range_and_rising.json in Resources */, 43E2D8EE1D20C0DB004DA55F /* recommend_temp_basal_flat_and_high.json in Resources */, + C1C6591C1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json in Resources */, 43E2D8F31D20C0DB004DA55F /* recommend_temp_basal_start_high_end_in_range.json in Resources */, 43E2D8F51D20C0DB004DA55F /* recommend_temp_basal_start_low_end_high.json in Resources */, 43E2D8EC1D20C0DB004DA55F /* read_selected_basal_profile.json in Resources */, @@ -1418,6 +1423,7 @@ 43E2D8DC1D20C049004DA55F /* DoseMath.swift in Sources */, 43E2D8DB1D20C03B004DA55F /* NSTimeInterval.swift in Sources */, 43E2D8D41D20BF42004DA55F /* DoseMathTests.swift in Sources */, + C1C6591A1E1B1F430025CC58 /* NSNumberFormatter.swift in Sources */, C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index 09ca7666af..70719a3db2 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - - + + - + @@ -20,10 +20,10 @@ - + - + - + - +