From a5382ffb11a476933a219f0fa8d3b4589b046203 Mon Sep 17 00:00:00 2001 From: dm61 Date: Mon, 1 Jan 2018 23:37:22 -0700 Subject: [PATCH 1/8] initial dosing threshold below suspend threshold --- Loop/Managers/DoseMath.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 10c0872e8f..d9c1d3ff1a 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -196,6 +196,15 @@ private func targetGlucoseValue(percentEffectDuration: Double, minValue: Double, // The inflection point in time: before it we use minValue, after it we linearly blend from minValue to maxValue let useMinValueUntilPercent = 0.5 + // Allow dosing below minValue during initial interval here set to 10% of effect duration + let useInitialValueUntilPercent = 0.1 + // Set initial threshold to 10 pts below minValue, which normally equals suspend threshold + let initialValue = minValue - 10.0 + + guard percentEffectDuration > useInitialValueUntilPercent else { + return initialValue + } + guard percentEffectDuration > useMinValueUntilPercent else { return minValue } @@ -246,10 +255,14 @@ extension Collection where Iterator.Element == GlucoseValue { continue } - // If any predicted value is below the suspend threshold, return immediately - guard prediction.quantity >= suspendThreshold else { + // (modified: If any predicted value is below the suspend threshold, return immediately) + // Allow dosing at a threshold below suspend threshold, here set to 10 mg/dL below + let predicted_bg = prediction.quantity + let initialThreshold = suspendThresholdValue - 10.0 + guard predicted_bg >= initialThreshold else { return .suspend(min: prediction) } + // Update range statistics if minGlucose == nil || prediction.quantity < minGlucose!.quantity { From 71401d935171e8d22049ea7d689aa4edf2ed446b Mon Sep 17 00:00:00 2001 From: dm61 Date: Tue, 2 Jan 2018 13:02:10 -0700 Subject: [PATCH 2/8] Allow dosing below suspend threshold Allow dosing above an initialThreshold, which can be below suspendThreshold. For dosing decisions, initialThreshold applies to an initial portion of effect duration. All changes are in DoseMath. initialThreshold is hard coded (in two locations) to 10 mg/dL below suspendThreshold, and is used during initial 10% of effect duration. --- Loop/Managers/DoseMath.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index d9c1d3ff1a..e9e3151989 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -196,9 +196,10 @@ private func targetGlucoseValue(percentEffectDuration: Double, minValue: Double, // The inflection point in time: before it we use minValue, after it we linearly blend from minValue to maxValue let useMinValueUntilPercent = 0.5 - // Allow dosing below minValue during initial interval here set to 10% of effect duration + // Allow dosing below minValue during initial interval set below to 10% of + // effect duration, so nominally 0.1*6*60 min = 36 min let useInitialValueUntilPercent = 0.1 - // Set initial threshold to 10 pts below minValue, which normally equals suspend threshold + // Set initial threshold to 10 mg/dL pts below minValue (which normally equals suspend threshold) let initialValue = minValue - 10.0 guard percentEffectDuration > useInitialValueUntilPercent else { @@ -255,8 +256,8 @@ extension Collection where Iterator.Element == GlucoseValue { continue } - // (modified: If any predicted value is below the suspend threshold, return immediately) - // Allow dosing at a threshold below suspend threshold, here set to 10 mg/dL below + // (current Loop: If any predicted value is below the suspend threshold, return immediately) + // modified to allow dosing above initialThreshold, here set to 10 mg/dL below suspendThreshold let predicted_bg = prediction.quantity let initialThreshold = suspendThresholdValue - 10.0 guard predicted_bg >= initialThreshold else { @@ -274,6 +275,7 @@ extension Collection where Iterator.Element == GlucoseValue { let time = prediction.startDate.timeIntervalSince(date) // Compute the target value as a function of time since the dose started + // Target initially dropped to InitialThreshold, see changes in targetGlucoseValue let targetValue = targetGlucoseValue( percentEffectDuration: time / model.effectDuration, minValue: suspendThresholdValue, @@ -390,11 +392,12 @@ extension Collection where Iterator.Element == GlucoseValue { var maxBasalRate = maxBasalRate // TODO: Allow `highBasalThreshold` to be a configurable setting - if case .aboveRange(min: let min, correcting: _, minTarget: let highBasalThreshold, units: _)? = correction, - min.quantity < highBasalThreshold - { - maxBasalRate = scheduledBasalRate - } + // Allow high temping below minTarget + // if case .aboveRange(min: let min, correcting: _, minTarget: let highBasalThreshold, units: _)? = correction, + // min.quantity < highBasalThreshold + // { + // maxBasalRate = scheduledBasalRate + //} let temp = correction?.asTempBasal( scheduledBasalRate: scheduledBasalRate, From fed2217ce7a61c4975e1d1fa650b85e3f93314f2 Mon Sep 17 00:00:00 2001 From: dm61 Date: Tue, 2 Jan 2018 16:28:30 -0700 Subject: [PATCH 3/8] correct predicted_bg calculation --- Loop/Managers/DoseMath.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index e9e3151989..87a648d7c7 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -258,7 +258,7 @@ extension Collection where Iterator.Element == GlucoseValue { // (current Loop: If any predicted value is below the suspend threshold, return immediately) // modified to allow dosing above initialThreshold, here set to 10 mg/dL below suspendThreshold - let predicted_bg = prediction.quantity + let predicted_bg = prediction.quantity.doubleValue(for: unit) let initialThreshold = suspendThresholdValue - 10.0 guard predicted_bg >= initialThreshold else { return .suspend(min: prediction) @@ -389,10 +389,10 @@ extension Collection where Iterator.Element == GlucoseValue { ) let scheduledBasalRate = basalRates.value(at: date) - var maxBasalRate = maxBasalRate + let maxBasalRate = maxBasalRate // TODO: Allow `highBasalThreshold` to be a configurable setting - // Allow high temping below minTarget + // Allow high temping below minTarget but above suspend threshold // if case .aboveRange(min: let min, correcting: _, minTarget: let highBasalThreshold, units: _)? = correction, // min.quantity < highBasalThreshold // { From 9b7b261e53b0b39e48737502a7f81796020ee14b Mon Sep 17 00:00:00 2001 From: dm61 Date: Sun, 7 Jan 2018 09:17:54 -0700 Subject: [PATCH 4/8] use initial threshold only for boluses --- Loop/Managers/DoseMath.swift | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 87a648d7c7..bf614729a2 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -192,15 +192,16 @@ private func insulinCorrectionUnits(fromValue: Double, toValue: Double, effected /// - minValue: The minimum (starting) target value /// - maxValue: The maximum (eventual) target value /// - Returns: A target value somewhere between the minimum and maximum -private func targetGlucoseValue(percentEffectDuration: Double, minValue: Double, maxValue: Double) -> Double { +private func targetGlucoseValue(percentEffectDuration: Double, + initialValue: Double, + minValue: Double, maxValue: Double) -> Double { + // The inflection point in time: before it we use minValue, after it we linearly blend from minValue to maxValue let useMinValueUntilPercent = 0.5 - // Allow dosing below minValue during initial interval set below to 10% of - // effect duration, so nominally 0.1*6*60 min = 36 min - let useInitialValueUntilPercent = 0.1 - // Set initial threshold to 10 mg/dL pts below minValue (which normally equals suspend threshold) - let initialValue = minValue - 10.0 + // Allow bolus dosing below minValue during initial interval set to 15% of + // effect duration, so nominally 0.15*6*60 min = 54 min + let useInitialValueUntilPercent = 0.15 guard percentEffectDuration > useInitialValueUntilPercent else { return initialValue @@ -234,6 +235,7 @@ extension Collection where Iterator.Element == GlucoseValue { private func insulinCorrection( to correctionRange: GlucoseRangeSchedule, at date: Date, + initialThreshold: HKQuantity, suspendThreshold: HKQuantity, sensitivity: HKQuantity, model: InsulinModel @@ -248,6 +250,7 @@ extension Collection where Iterator.Element == GlucoseValue { let unit = correctionRange.unit let sensitivityValue = sensitivity.doubleValue(for: unit) + let initialThresholdValue = initialThreshold.doubleValue(for: unit) let suspendThresholdValue = suspendThreshold.doubleValue(for: unit) // For each prediction above target, determine the amount of insulin necessary to correct glucose based on the modeled effectiveness of the insulin at that time @@ -257,10 +260,8 @@ extension Collection where Iterator.Element == GlucoseValue { } // (current Loop: If any predicted value is below the suspend threshold, return immediately) - // modified to allow dosing above initialThreshold, here set to 10 mg/dL below suspendThreshold - let predicted_bg = prediction.quantity.doubleValue(for: unit) - let initialThreshold = suspendThresholdValue - 10.0 - guard predicted_bg >= initialThreshold else { + // allow dosing above initial threshold, which is below suspend threshold for boluses + guard prediction.quantity >= initialThreshold else { return .suspend(min: prediction) } @@ -275,10 +276,11 @@ extension Collection where Iterator.Element == GlucoseValue { let time = prediction.startDate.timeIntervalSince(date) // Compute the target value as a function of time since the dose started - // Target initially dropped to InitialThreshold, see changes in targetGlucoseValue + // Target value initially dropped to InitialThreshold let targetValue = targetGlucoseValue( percentEffectDuration: time / model.effectDuration, - minValue: suspendThresholdValue, + initialValue: initialThresholdValue, + minValue: suspendThresholdValue, maxValue: correctionRange.value(at: prediction.startDate).averageValue ) @@ -383,6 +385,8 @@ extension Collection where Iterator.Element == GlucoseValue { let correction = self.insulinCorrection( to: correctionRange, at: date, + // for temps, initial threshold equals suspend threshold + initialThreshold: suspendThreshold ?? correctionRange.minQuantity(at: date), suspendThreshold: suspendThreshold ?? correctionRange.minQuantity(at: date), sensitivity: sensitivity.quantity(at: date), model: model @@ -392,7 +396,7 @@ extension Collection where Iterator.Element == GlucoseValue { let maxBasalRate = maxBasalRate // TODO: Allow `highBasalThreshold` to be a configurable setting - // Allow high temping below minTarget but above suspend threshold + // (dm61 commented out lines bellow to allow high temping below minTarget but above suspend threshold) // if case .aboveRange(min: let min, correcting: _, minTarget: let highBasalThreshold, units: _)? = correction, // min.quantity < highBasalThreshold // { @@ -439,6 +443,8 @@ extension Collection where Iterator.Element == GlucoseValue { guard let correction = self.insulinCorrection( to: correctionRange, at: date, + // for boluses, initial threshold is below suspend threshold + initialThreshold: HKQuantity(unit: HKUnit.milligramsPerDeciliter(), doubleValue: 60), suspendThreshold: suspendThreshold ?? correctionRange.minQuantity(at: date), sensitivity: sensitivity.quantity(at: date), model: model From 3341988db220eaa3521e8fc6655f388b814b95e0 Mon Sep 17 00:00:00 2001 From: ddaniels1 Date: Sun, 1 Apr 2018 12:21:35 -0700 Subject: [PATCH 5/8] Configurable Bolus Threshold --- Loop.xcodeproj/project.pbxproj | 8 +++ Loop/Extensions/NSUserDefaults.swift | 11 +++- Loop/Managers/AnalyticsManager.swift | 5 ++ Loop/Managers/DoseMath.swift | 3 +- Loop/Managers/LoopDataManager.swift | 1 + Loop/Models/BolusThreshold.swift | 51 +++++++++++++++++++ Loop/Models/LoopSettings.swift | 7 ++- .../BolusThresholdViewController.swift | 44 ++++++++++++++++ .../SettingsTableViewController.swift | 48 +++++++++++++++++ 9 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 Loop/Models/BolusThreshold.swift create mode 100644 Loop/View Controllers/BolusThresholdViewController.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 2ae66a22c0..9f97861c32 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -236,6 +236,8 @@ 4FF4D1001E18374700846527 /* WatchContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF4D0FF1E18374700846527 /* WatchContext.swift */; }; 4FF4D1011E18375000846527 /* WatchContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF4D0FF1E18374700846527 /* WatchContext.swift */; }; 540DED971E14C75F002B2491 /* EnliteSensorDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */; }; + 6AB322972070BD40001EE2C0 /* BolusThreshold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB322962070BD40001EE2C0 /* BolusThreshold.swift */; }; + 6AB322992070BE8B001EE2C0 /* BolusThresholdViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB322982070BE8B001EE2C0 /* BolusThresholdViewController.swift */; }; 7D7076351FE06EDE004AC8EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076371FE06EDE004AC8EA /* Localizable.strings */; }; 7D70763A1FE06EDF004AC8EA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D70763C1FE06EDF004AC8EA /* InfoPlist.strings */; }; 7D70763F1FE06EDF004AC8EA /* ckcomplication.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D7076411FE06EDF004AC8EA /* ckcomplication.strings */; }; @@ -593,6 +595,8 @@ 4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+StatusExtension.swift"; sourceTree = ""; }; 4FF4D0FF1E18374700846527 /* WatchContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchContext.swift; sourceTree = ""; }; 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnliteSensorDisplayable.swift; sourceTree = ""; }; + 6AB322962070BD40001EE2C0 /* BolusThreshold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusThreshold.swift; sourceTree = ""; }; + 6AB322982070BE8B001EE2C0 /* BolusThresholdViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusThresholdViewController.swift; sourceTree = ""; }; 7D68AAA91FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 7D68AAAA1FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Main.strings; sourceTree = ""; }; 7D68AAAB1FE2DB0A00522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainInterface.strings; sourceTree = ""; }; @@ -762,6 +766,7 @@ 43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */, 4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */, C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */, + 6AB322962070BD40001EE2C0 /* BolusThreshold.swift */, 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */, 436A0DA41D236A2A00104B24 /* LoopError.swift */, 43D848B11E7DF42500DADCBC /* LoopSettings.swift */, @@ -966,6 +971,7 @@ 43A51E201EB6DBDD000736CC /* ChartsTableViewController.swift */, 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */, C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */, + 6AB322982070BE8B001EE2C0 /* BolusThresholdViewController.swift */, 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */, 435CB6221F37967800C320C7 /* InsulinModelSettingsViewController.swift */, 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */, @@ -1621,6 +1627,7 @@ 43BFF0BC1E45C80600FF19A9 /* UIColor+Loop.swift in Sources */, 43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */, 4F08DE9D1E81D0E9006741EA /* StatusChartsManager+LoopKit.swift in Sources */, + 6AB322992070BE8B001EE2C0 /* BolusThresholdViewController.swift in Sources */, 434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */, 439BED2A1E76093C00B0AED5 /* CGMManager.swift in Sources */, C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */, @@ -1639,6 +1646,7 @@ 4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */, 43D381621EBD9759007F8C8F /* HeaderValuesTableViewCell.swift in Sources */, 43BFF0C51E465A2D00FF19A9 /* UIColor+HIG.swift in Sources */, + 6AB322972070BD40001EE2C0 /* BolusThreshold.swift in Sources */, 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */, 437CCAE01D285C7B0075D2C3 /* ServiceAuthentication.swift in Sources */, 4FC8C8011DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift in Sources */, diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index 98515b8fec..eb347283fd 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -114,6 +114,7 @@ extension UserDefaults { removeObject(forKey: "com.loudnate.Naterade.MaximumBasalRatePerHour") removeObject(forKey: "com.loudnate.Naterade.MaximumBolus") removeObject(forKey: "com.loopkit.Loop.MinimumBGGuard") + removeObject(forKey: "com.loopkit.Loop.MinimumBolusGuard") removeObject(forKey: "com.loudnate.Loop.RetrospectiveCorrectionEnabled") } @@ -130,7 +131,14 @@ extension UserDefaults { } else { suspendThreshold = nil } - + + let bolusThreshold: BolusThreshold? + if let rawValue = dictionary(forKey: "com.loopkit.Loop.MinimumBolusGuard") { + bolusThreshold = BolusThreshold(rawValue: rawValue) + } else { + bolusThreshold = nil + } + var maximumBasalRatePerHour: Double? = double(forKey: "com.loudnate.Naterade.MaximumBasalRatePerHour") if maximumBasalRatePerHour! <= 0 { maximumBasalRatePerHour = nil @@ -147,6 +155,7 @@ extension UserDefaults { maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus, suspendThreshold: suspendThreshold, + bolusThreshold: bolusThreshold, retrospectiveCorrectionEnabled: bool(forKey: "com.loudnate.Loop.RetrospectiveCorrectionEnabled") ) self.loopSettings = settings diff --git a/Loop/Managers/AnalyticsManager.swift b/Loop/Managers/AnalyticsManager.swift index 999bf15b5c..b8e6f517db 100644 --- a/Loop/Managers/AnalyticsManager.swift +++ b/Loop/Managers/AnalyticsManager.swift @@ -111,6 +111,11 @@ final class AnalyticsManager: IdentifiableClass { if newValue.suspendThreshold != oldValue.suspendThreshold { logEvent("Minimum BG Guard change") } + + if newValue.bolusThreshold != oldValue.bolusThreshold { + logEvent("Bolus BG Guard change") + } + } // MARK: - Loop Events diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index bf614729a2..4cc59a0534 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -434,6 +434,7 @@ extension Collection where Iterator.Element == GlucoseValue { to correctionRange: GlucoseRangeSchedule, at date: Date = Date(), suspendThreshold: HKQuantity?, + bolusThreshold: HKQuantity?, sensitivity: InsulinSensitivitySchedule, model: InsulinModel, pendingInsulin: Double, @@ -444,7 +445,7 @@ extension Collection where Iterator.Element == GlucoseValue { to: correctionRange, at: date, // for boluses, initial threshold is below suspend threshold - initialThreshold: HKQuantity(unit: HKUnit.milligramsPerDeciliter(), doubleValue: 60), + initialThreshold: bolusThreshold ?? correctionRange.minQuantity(at: date), suspendThreshold: suspendThreshold ?? correctionRange.minQuantity(at: date), sensitivity: sensitivity.quantity(at: date), model: model diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index fcf0f16ac9..4f4121bb4d 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -934,6 +934,7 @@ final class LoopDataManager { let recommendation = predictedGlucose.recommendedBolus( to: glucoseTargetRange, suspendThreshold: settings.suspendThreshold?.quantity, + bolusThreshold: settings.bolusThreshold?.quantity, sensitivity: insulinSensitivity, model: model, pendingInsulin: pendingInsulin, diff --git a/Loop/Models/BolusThreshold.swift b/Loop/Models/BolusThreshold.swift new file mode 100644 index 0000000000..211cc73128 --- /dev/null +++ b/Loop/Models/BolusThreshold.swift @@ -0,0 +1,51 @@ +// +// BolusThreshold.swift +// Loop +// +// Created by David Daniels on 3/25/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + + + +import Foundation +import HealthKit + +struct BolusThreshold: 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 + ] + } +} + + +extension BolusThreshold: Equatable { + static func ==(lhs: BolusThreshold, rhs: BolusThreshold) -> Bool { + return lhs.value == rhs.value + } +} + diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index b01a463683..a4e0e05712 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -20,6 +20,8 @@ struct LoopSettings { var maximumBolus: Double? var suspendThreshold: GlucoseThreshold? = nil + + var bolusThreshold: BolusThreshold? = nil var retrospectiveCorrectionEnabled = true } @@ -63,7 +65,9 @@ extension LoopSettings: RawRepresentable { if let rawThreshold = rawValue["minimumBGGuard"] as? GlucoseThreshold.RawValue { self.suspendThreshold = GlucoseThreshold(rawValue: rawThreshold) } - + if let rawBolusThreshold = rawValue["minimumBolusGuard"] as? BolusThreshold.RawValue { + self.bolusThreshold = BolusThreshold(rawValue: rawBolusThreshold) + } if let retrospectiveCorrectionEnabled = rawValue["retrospectiveCorrectionEnabled"] as? Bool { self.retrospectiveCorrectionEnabled = retrospectiveCorrectionEnabled } @@ -80,6 +84,7 @@ extension LoopSettings: RawRepresentable { raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour raw["maximumBolus"] = maximumBolus raw["minimumBGGuard"] = suspendThreshold?.rawValue + raw["BolusBGGuard"] = bolusThreshold?.rawValue return raw } diff --git a/Loop/View Controllers/BolusThresholdViewController.swift b/Loop/View Controllers/BolusThresholdViewController.swift new file mode 100644 index 0000000000..a843266838 --- /dev/null +++ b/Loop/View Controllers/BolusThresholdViewController.swift @@ -0,0 +1,44 @@ +// +// BolusThresholdViewController.swift +// Loop +// +// Created by David Daniels on 3/26/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + + + +import Foundation + +import UIKit +import LoopKit +import HealthKit + + +final class BolusThresholdTableViewController: TextFieldTableViewController { + + public let glucoseUnit: HKUnit + + init(threshold: Double?, glucoseUnit: HKUnit) { + self.glucoseUnit = glucoseUnit + + super.init(style: .grouped) + + placeholder = NSLocalizedString("Enter bolus threshold", comment: "The placeholder text instructing users to enter a bolus threshold") + keyboardType = .decimalPad + contextHelp = NSLocalizedString("When current or forecasted glucose is below the bolus threshold, Loop will not recommend a bolus.", comment: "Explanation of bolus threshold") + + unit = glucoseUnit.glucoseUnitDisplayString + + if let threshold = threshold { + value = NumberFormatter.glucoseFormatter(for: glucoseUnit).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 4c530cff8b..a75260f21f 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -116,6 +116,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu fileprivate enum ConfigurationRow: Int, CaseCountable { case glucoseTargetRange = 0 case suspendThreshold + case bolusThreshold case insulinModel case basalRate case carbRatio @@ -335,6 +336,18 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu let value = valueNumberFormatter.string(from: NSNumber(value: suspendThreshold.value)) ?? "-" configCell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@ %2$@", comment: "Format string for current suspend threshold. (1: value)(2: bg unit)"), value, suspendThreshold.unit.glucoseUnitDisplayString) } else { + + configCell.detailTextLabel?.text = TapToSetString + } + case .bolusThreshold: + configCell.textLabel?.text = NSLocalizedString("Bolus Threshold", comment: "The title text in settings") + + if let bolusThreshold = dataManager.loopManager.settings.bolusThreshold { + let value = valueNumberFormatter.string(from: NSNumber(value: bolusThreshold.value)) ?? "-" + configCell.detailTextLabel?.text = String(format: NSLocalizedString("%1$@ %2$@", comment: "Format string for current bolus threshold. (1: value)(2: bg unit)"), value, bolusThreshold.unit.glucoseUnitDisplayString) + } else { + + configCell.detailTextLabel?.text = TapToSetString } case .insulinModel: @@ -615,12 +628,39 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } case .insulinModel: performSegue(withIdentifier: InsulinModelSettingsViewController.className, sender: sender) + + + case .bolusThreshold: + + if let minBolusGuard = dataManager.loopManager.settings.bolusThreshold { + let vc = BolusThresholdTableViewController (threshold: minBolusGuard.value, glucoseUnit: minBolusGuard.unit) + vc.delegate = self + vc.indexPath = indexPath + vc.title = sender?.textLabel?.text + self.show(vc, sender: sender) + } else { + dataManager.loopManager.glucoseStore.preferredUnit { (unit, error) -> Void in + DispatchQueue.main.async { + if let error = error { + self.presentAlertController(with: error) + } else if let unit = unit { + let vc = BolusThresholdTableViewController (threshold: nil, glucoseUnit: unit) + vc.delegate = self + vc.indexPath = indexPath + vc.title = sender?.textLabel?.text + self.show(vc, sender: sender) + } + } + } } + } + case .devices: let vc = RileyLinkDeviceTableViewController() vc.device = dataManager.rileyLinkManager.devices[indexPath.row] show(vc, sender: sender) + case .loop: switch LoopRow(rawValue: indexPath.row)! { case .preferredInsulinDataSource: @@ -947,6 +987,14 @@ extension SettingsTableViewController: TextFieldTableViewControllerDelegate { } else { dataManager.loopManager.settings.suspendThreshold = nil } + + case .bolusThreshold: + if let controller = controller as? BolusThresholdTableViewController, + let value = controller.value, let minBolusGuard = valueNumberFormatter.number(from: value)?.doubleValue { + dataManager.loopManager.settings.bolusThreshold = BolusThreshold(unit: controller.glucoseUnit, value: minBolusGuard) + } else { + dataManager.loopManager.settings.bolusThreshold = nil + } case .maxBasal: if let value = controller.value, let rate = valueNumberFormatter.number(from: value)?.doubleValue { dataManager.loopManager.settings.maximumBasalRatePerHour = rate From 935b6f047d802d3c76479a9d3fc19cb45971de14 Mon Sep 17 00:00:00 2001 From: ddaniels1 Date: Sun, 1 Apr 2018 14:18:05 -0700 Subject: [PATCH 6/8] Swift 4 updates --- Loop/Extensions/NSUserDefaults.swift | 2 +- Loop/Managers/CGM/CGMManager.swift | 2 +- Loop/Managers/CGM/EnliteCGMManager.swift | 2 +- .../RadioSelectionTableViewController.swift | 4 ++-- LoopUI/Managers/StatusChartsManager.swift | 12 ++++++------ LoopUI/Views/ChartPointsContextFillLayer.swift | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index eb347283fd..aa9646e1ef 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -193,7 +193,7 @@ extension UserDefaults { guard let rawValue = array(forKey: Key.insulinCounteractionEffects.rawValue) as? [GlucoseEffectVelocity.RawValue] else { return nil } - return rawValue.flatMap { + return rawValue.compactMap { GlucoseEffectVelocity(rawValue: $0) } } diff --git a/Loop/Managers/CGM/CGMManager.swift b/Loop/Managers/CGM/CGMManager.swift index 64615f545a..82424119b3 100644 --- a/Loop/Managers/CGM/CGMManager.swift +++ b/Loop/Managers/CGM/CGMManager.swift @@ -38,7 +38,7 @@ protocol CGMManagerDelegate: class { protocol CGMManager: CustomDebugStringConvertible { - weak var delegate: CGMManagerDelegate? { get set } + var delegate: CGMManagerDelegate? { get set } /// Whether the device is capable of waking the app var providesBLEHeartbeat: Bool { get } diff --git a/Loop/Managers/CGM/EnliteCGMManager.swift b/Loop/Managers/CGM/EnliteCGMManager.swift index 247137c803..876810e9fc 100644 --- a/Loop/Managers/CGM/EnliteCGMManager.swift +++ b/Loop/Managers/CGM/EnliteCGMManager.swift @@ -40,7 +40,7 @@ final class EnliteCGMManager: CGMManager { _ = deviceManager.remoteDataManager.nightscoutService.uploader?.processGlucoseEvents(events, source: device.device.deviceURI) - if let latestSensorEvent = events.flatMap({ $0.glucoseEvent as? RelativeTimestampedGlucoseEvent }).last { + if let latestSensorEvent = events.compactMap({ $0.glucoseEvent as? RelativeTimestampedGlucoseEvent }).last { self.sensorState = EnliteSensorDisplayable(latestSensorEvent) } diff --git a/Loop/View Controllers/RadioSelectionTableViewController.swift b/Loop/View Controllers/RadioSelectionTableViewController.swift index fc13d26427..1b119e30cb 100644 --- a/Loop/View Controllers/RadioSelectionTableViewController.swift +++ b/Loop/View Controllers/RadioSelectionTableViewController.swift @@ -78,7 +78,7 @@ extension RadioSelectionTableViewController { let vc = T() vc.selectedIndex = value.rawValue - vc.options = (0..<2).flatMap({ InsulinDataSource(rawValue: $0) }).map { String(describing: $0) } + vc.options = (0..<2).compactMap({ InsulinDataSource(rawValue: $0) }).map { String(describing: $0) } vc.contextHelp = NSLocalizedString("Insulin delivery can be determined from the pump by either interpreting the event history or comparing the reservoir volume over time. Reading event history allows for a more accurate status graph and uploading up-to-date treatment data to Nightscout, at the cost of faster pump battery drain and the possibility of a higher radio error rate compared to reading only reservoir volume. If the selected source cannot be used for any reason, the system will attempt to fall back to the other option.", comment: "Instructions on selecting an insulin data source") return vc @@ -88,7 +88,7 @@ extension RadioSelectionTableViewController { let vc = T() vc.selectedIndex = value.rawValue - vc.options = (0..<2).flatMap({ BatteryChemistryType(rawValue: $0) }).map { String(describing: $0) } + vc.options = (0..<2).compactMap({ BatteryChemistryType(rawValue: $0) }).map { String(describing: $0) } vc.contextHelp = NSLocalizedString("Alkaline and Lithium batteries decay at differing rates. Alkaline tend to have a linear voltage drop over time whereas lithium cell batteries tend to maintain voltage until halfway through their lifespan. Under normal usage in a Non-MySentry compatible Minimed (x22/x15) insulin pump running Loop, Alkaline batteries last approximately 4 to 5 days. Lithium batteries last between 1-2 weeks. This selection will use different battery voltage decay rates for each of the battery chemistry types and alert the user when a battery is approximately 8 to 10 hours from failure.", comment: "Instructions on selecting battery chemistry type") return vc diff --git a/LoopUI/Managers/StatusChartsManager.swift b/LoopUI/Managers/StatusChartsManager.swift index 8019c33085..476990b437 100644 --- a/LoopUI/Managers/StatusChartsManager.swift +++ b/LoopUI/Managers/StatusChartsManager.swift @@ -417,7 +417,7 @@ public final class StatusChartsManager { frame: frame, innerFrame: innerFrame, settings: chartSettings, - layers: layers.flatMap { $0 } + layers: layers.compactMap { $0 } ) } @@ -487,7 +487,7 @@ public final class StatusChartsManager { iobLine, ] - return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.flatMap { $0 }) + return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.compactMap { $0 }) } public func cobChartWithFrame(_ frame: CGRect) -> Chart? { @@ -544,7 +544,7 @@ public final class StatusChartsManager { cobLine ] - return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.flatMap { $0 }) + return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.compactMap { $0 }) } public func doseChartWithFrame(_ frame: CGRect) -> Chart? { @@ -632,7 +632,7 @@ public final class StatusChartsManager { bolusLayer ] - return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.flatMap { $0 }) + return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.compactMap { $0 }) } // MARK: - Carb Effect @@ -765,7 +765,7 @@ public final class StatusChartsManager { frame: frame, innerFrame: innerFrame, settings: chartSettings, - layers: layers.flatMap { $0 } + layers: layers.compactMap { $0 } ) } @@ -893,7 +893,7 @@ public final class StatusChartsManager { frame: frame, innerFrame: coordsSpace.chartInnerFrame, settings: chartSettings, - layers: layers.flatMap { $0 } + layers: layers.compactMap { $0 } ) } diff --git a/LoopUI/Views/ChartPointsContextFillLayer.swift b/LoopUI/Views/ChartPointsContextFillLayer.swift index a2824336ce..220c5b9480 100644 --- a/LoopUI/Views/ChartPointsContextFillLayer.swift +++ b/LoopUI/Views/ChartPointsContextFillLayer.swift @@ -59,7 +59,7 @@ final class ChartPointsFillsLayer: ChartCoordsSpaceLayer { let fills: [ChartPointsFill] init?(xAxis: ChartAxis, yAxis: ChartAxis, fills: [ChartPointsFill?]) { - self.fills = fills.flatMap({ $0 }) + self.fills = fills.compactMap({ $0 }) guard fills.count > 0 else { return nil From e63d89091dd73400a27c9b3d4e7ec3ca53796c3a Mon Sep 17 00:00:00 2001 From: ddaniels1 Date: Sun, 1 Apr 2018 22:48:53 -0700 Subject: [PATCH 7/8] Swift 4 upgrade --- Common/Models/StatusExtensionContext.swift | 4 ++-- Loop.xcodeproj/project.pbxproj | 6 +++++- .../xcschemes/Complication - WatchApp.xcscheme | 4 +--- .../xcshareddata/xcschemes/DoseMathTests.xcscheme | 4 +--- .../xcshareddata/xcschemes/Loop Status Extension.xcscheme | 4 +--- Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme | 8 +++----- Loop.xcodeproj/xcshareddata/xcschemes/LoopTests.xcscheme | 4 +--- .../xcschemes/Notification - WatchApp.xcscheme | 4 +--- Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme | 4 +--- 9 files changed, 16 insertions(+), 26 deletions(-) diff --git a/Common/Models/StatusExtensionContext.swift b/Common/Models/StatusExtensionContext.swift index 9430895822..3847b9da20 100644 --- a/Common/Models/StatusExtensionContext.swift +++ b/Common/Models/StatusExtensionContext.swift @@ -287,7 +287,7 @@ struct StatusExtensionContext: RawRepresentable { } if let rawValue = rawValue["glucose"] as? [GlucoseContext.RawValue] { - glucose = rawValue.flatMap({return GlucoseContext(rawValue: $0)}) + glucose = rawValue.compactMap({return GlucoseContext(rawValue: $0)}) } if let rawValue = rawValue["predictedGlucose"] as? PredictedGlucoseContext.RawValue { @@ -311,7 +311,7 @@ struct StatusExtensionContext: RawRepresentable { activeInsulin = rawValue["activeInsulin"] as? Double if let rawValue = rawValue["targetRanges"] as? [DatedRangeContext.RawValue] { - targetRanges = rawValue.flatMap({return DatedRangeContext(rawValue: $0)}) + targetRanges = rawValue.compactMap({return DatedRangeContext(rawValue: $0)}) } if let rawValue = rawValue["temporaryOverride"] as? DatedRangeContext.RawValue { diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 9f97861c32..7d2d409ad7 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -1339,7 +1339,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = "LoopKit Authors"; TargetAttributes = { 43776F8B1B8022E90074EA36 = { @@ -2011,12 +2011,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -2076,12 +2078,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme index 2d94260fca..ab563e3f5a 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/Complication - WatchApp.xcscheme @@ -1,6 +1,6 @@ @@ -61,7 +60,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "" selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme index b2c46e209e..301b74132b 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/Loop.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -57,7 +56,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/LoopTests.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/LoopTests.xcscheme index b74e4c3f3e..337f33d7fa 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/LoopTests.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/LoopTests.xcscheme @@ -1,6 +1,6 @@ Date: Mon, 2 Apr 2018 00:55:37 -0700 Subject: [PATCH 8/8] LoopSettings Edit to store entered BolusThreshold when app shuts down --- Loop/Models/LoopSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index a4e0e05712..7ad304782e 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -84,7 +84,7 @@ extension LoopSettings: RawRepresentable { raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour raw["maximumBolus"] = maximumBolus raw["minimumBGGuard"] = suspendThreshold?.rawValue - raw["BolusBGGuard"] = bolusThreshold?.rawValue + raw["minimumBolusGuard"] = bolusThreshold?.rawValue return raw }