diff --git a/.travis.yml b/.travis.yml index c582b6b5a0..6e609f5fad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: objective-c -osx_image: xcode11 +osx_image: xcode12 # xcode_sdk: iphonesimulator11 # xcode_project: Loop.xcodeproj # xcode_scheme: Loop diff --git a/Cartfile b/Cartfile index ca66f13586..4565a561f5 100644 --- a/Cartfile +++ b/Cartfile @@ -3,5 +3,5 @@ github "LoopKit/CGMBLEKit" "dev" github "i-schuetz/SwiftCharts" == 0.6.5 github "LoopKit/dexcom-share-client-swift" "dev" github "LoopKit/G4ShareSpy" "dev" -github "ps2/rileylink_ios" "dev" +github "ps2/rileylink_ios" "carthage-pin" github "LoopKit/Amplitude-iOS" "decreepify" diff --git a/Cartfile.resolved b/Cartfile.resolved index b6cc1373e7..8cbcf921c6 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -5,4 +5,4 @@ github "LoopKit/LoopKit" "5f459ee56ecd17c740c5fa0142353f3eb5b6120d" github "LoopKit/MKRingProgressView" "f548a5c64832be2d37d7c91b5800e284887a2a0a" github "LoopKit/dexcom-share-client-swift" "68ea5d08588e00bf148518b126416b8352dbef64" github "i-schuetz/SwiftCharts" "0.6.5" -github "ps2/rileylink_ios" "4ca67f4ac2dd5b15c6b7e19c448eb2d52b9592a7" +github "ps2/rileylink_ios" "1d0ca309f3131ee58da26f31d89eecad8781c77b" diff --git a/Common/Extensions/NSBundle.swift b/Common/Extensions/NSBundle.swift index b5f2011274..1d2d751019 100644 --- a/Common/Extensions/NSBundle.swift +++ b/Common/Extensions/NSBundle.swift @@ -17,9 +17,17 @@ extension Bundle { var bundleDisplayName: String { return object(forInfoDictionaryKey: "CFBundleDisplayName") as! String } + + var featureSpecifier: String? { + return object(forInfoDictionaryKey: "com.loopkit.Loop.featureSpecifier") as? String + } var localizedNameAndVersion: String { - return String(format: NSLocalizedString("%1$@ v%2$@", comment: "The format string for the app name and version number. (1: bundle name)(2: bundle version)"), bundleDisplayName, shortVersionString) + var displayName = bundleDisplayName + if let featureSpecifier = featureSpecifier { + displayName += " (\(featureSpecifier))" + } + return String(format: NSLocalizedString("%1$@ v%2$@", comment: "The format string for the app name and version number. (1: bundle name)(2: bundle version)"), displayName, shortVersionString) } private var mainAppBundleIdentifier: String? { diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 6637784f2b..8af70a17db 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -119,6 +119,25 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertNil(dose) } + + func testNoChangeAutomaticBolusing() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose) + } + func testNoChangeOverrideActive() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") @@ -138,6 +157,27 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(0.8, dose!.unitsPerHour, accuracy: 1.0 / 40.0) XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) } + + func testNoChangeOverrideActiveAutomaticBolusing() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil, + isBasalRateScheduleOverrideActive: true + ) + + XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration) + XCTAssertEqual(0, dose!.bolusUnits) + } func testStartHighEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") @@ -178,6 +218,94 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(0, dose!.unitsPerHour) XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration) } + + func testStartHighEndInRangeAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + + var dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose) + + // Cancel existing temp basal + let lastTempBasal = DoseEntry( + type: .tempBasal, + startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)), + endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)), + value: 0.125, + unit: .unitsPerHour + ) + + dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: lastTempBasal + ) + + XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration) + XCTAssertEqual(0, dose!.bolusUnits) + } + + func testStartHighEndInRangeAutomaticBolusWithOverride() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") + + var dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil, + isBasalRateScheduleOverrideActive: true + ) + + XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration) + + // Continue existing temp basal + let lastTempBasal = DoseEntry( + type: .tempBasal, + startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)), + endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)), + value: 0.8, + unit: .unitsPerHour + ) + + dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: lastTempBasal, + isBasalRateScheduleOverrideActive: true + ) + + XCTAssertNil(dose) + } + func testStartLowEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") @@ -217,6 +345,49 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(0, dose!.unitsPerHour) XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration) } + + func testStartLowEndInRangeAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") + + var dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose) + + let lastTempBasal = DoseEntry( + type: .tempBasal, + startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)), + endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)), + value: 1.225, + unit: .unitsPerHour + ) + + dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: lastTempBasal + ) + + XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration) + XCTAssertEqual(0, dose!.bolusUnits) + } + func testCorrectLowAtMin() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min") @@ -258,6 +429,49 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertNil(dose) } + func testCorrectLowAtMinAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_correct_low_at_min") + + // Cancel existing dose + let lastTempBasal = DoseEntry( + type: .tempBasal, + startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -21)), + endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 9)), + value: 0.125, + unit: .unitsPerHour + ) + + var dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: lastTempBasal + ) + + XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration) + XCTAssertEqual(0, dose!.bolusUnits) + + dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose) + } + func testStartHighEndLow() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") @@ -276,6 +490,26 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) } + func testStartHighEndLowAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration) + XCTAssertEqual(0, dose!.bolusUnits) + } + func testStartLowEndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") @@ -292,6 +526,7 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertNil(dose) + // Cancel last temp let lastTempBasal = DoseEntry( type: .tempBasal, startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)), @@ -315,6 +550,48 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(TimeInterval(minutes: 0), dose!.duration) } + func testStartLowEndHighAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") + + var dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose) + + let lastTempBasal = DoseEntry( + type: .tempBasal, + startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)), + endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)), + value: 1.225, + unit: .unitsPerHour + ) + + dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: lastTempBasal + ) + + XCTAssertEqual(0, dose!.basalAdjustment!.unitsPerHour) + XCTAssertEqual(TimeInterval(minutes: 0), dose!.basalAdjustment!.duration) + XCTAssertEqual(0, dose!.bolusUnits) + } + func testFlatAndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") @@ -332,6 +609,73 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(3.0, dose!.unitsPerHour) XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) } + + func testFlatAndHighAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(0.85, dose!.bolusUnits, accuracy: 1.0 / 40.0) + } + + func testFlatAndHighAutomaticBolusWithOverride() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") + + var dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil, + isBasalRateScheduleOverrideActive: true + ) + + XCTAssertEqual(0.8, dose!.basalAdjustment!.unitsPerHour, accuracy: 1.0 / 40.0) + XCTAssertEqual(TimeInterval(minutes: 30), dose!.basalAdjustment!.duration) + XCTAssertEqual(0.85, dose!.bolusUnits, accuracy: 1.0 / 40.0) + + // Continue temp + let lastTempBasal = DoseEntry( + type: .tempBasal, + startDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: -11)), + endDate: glucose.first!.startDate.addingTimeInterval(TimeInterval(minutes: 19)), + value: 0.8, + unit: .unitsPerHour + ) + + dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: lastTempBasal, + isBasalRateScheduleOverrideActive: true + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(0.85, dose!.bolusUnits, accuracy: 1.0 / 40.0) + + } + func testHighAndFalling() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") @@ -350,6 +694,25 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(1.60, dose!.unitsPerHour, accuracy: 1.0 / 40.0) XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) } + + func testHighAndFallingAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(0.2, dose!.bolusUnits, accuracy: 1.0 / 40.0) + } func testInRangeAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") @@ -369,6 +732,25 @@ class RecommendTempBasalTests: XCTestCase { XCTAssertEqual(TimeInterval(minutes: 30), dose!.duration) } + func testInRangeAndRisingAutomaticBolus() { + let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") + + let dose = glucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: glucose.first!.startDate, + suspendThreshold: suspendThreshold.quantity, + sensitivity: insulinSensitivitySchedule, + model: insulinModel, + basalRates: basalRateSchedule, + maxAutomaticBolus: 5, + partialApplicationFactor: 0.5, + lastTempBasal: nil + ) + + XCTAssertNil(dose!.basalAdjustment) + XCTAssertEqual(0.2, dose!.bolusUnits, accuracy: 1.0 / 40.0) + } + func testHighAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") @@ -513,7 +895,7 @@ class RecommendBolusTests: XCTestCase { func testNoChange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_no_change_glucose") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -529,7 +911,7 @@ class RecommendBolusTests: XCTestCase { func testStartHighEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_in_range") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -545,7 +927,7 @@ class RecommendBolusTests: XCTestCase { func testStartLowEndInRange() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_in_range") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -561,7 +943,7 @@ class RecommendBolusTests: XCTestCase { func testStartHighEndLow() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_high_end_low") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -577,7 +959,7 @@ class RecommendBolusTests: XCTestCase { func testStartLowEndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -601,7 +983,7 @@ class RecommendBolusTests: XCTestCase { // 60 - 200 mg/dL let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 70), @@ -624,7 +1006,7 @@ class RecommendBolusTests: XCTestCase { // 60 - 200 mg/dL let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: nil, // Expected to default to 90 @@ -646,7 +1028,7 @@ class RecommendBolusTests: XCTestCase { func testDroppingBelowRangeThenRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_dropping_then_rising") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -665,7 +1047,7 @@ class RecommendBolusTests: XCTestCase { func testStartLowEndHighWithPendingBolus() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_low_end_high") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -682,7 +1064,7 @@ class RecommendBolusTests: XCTestCase { func testStartVeryLowEndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_start_very_low_end_high") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -698,7 +1080,7 @@ class RecommendBolusTests: XCTestCase { func testFlatAndHigh() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_flat_and_high") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -714,7 +1096,7 @@ class RecommendBolusTests: XCTestCase { func testHighAndFalling() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_falling") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -730,7 +1112,7 @@ class RecommendBolusTests: XCTestCase { func testInRangeAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_in_range_and_rising") - var dose = glucose.recommendedBolus( + var dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -744,7 +1126,7 @@ class RecommendBolusTests: XCTestCase { // Less existing temp - dose = glucose.recommendedBolus( + dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -760,7 +1142,7 @@ class RecommendBolusTests: XCTestCase { func testStartLowEndJustAboveRange() { let glucose = loadGlucoseValueFixture("recommended_temp_start_low_end_just_above_range") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0), @@ -777,7 +1159,7 @@ class RecommendBolusTests: XCTestCase { func testHighAndRising() { let glucose = loadGlucoseValueFixture("recommend_temp_basal_high_and_rising") - var dose = glucose.recommendedBolus( + var dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -792,7 +1174,7 @@ class RecommendBolusTests: XCTestCase { // Use mmol sensitivity value let insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: HKUnit.millimolesPerLiter, dailyItems: [RepeatingScheduleValue(startTime: 0.0, value: 10.0 / 3)])! - dose = glucose.recommendedBolus( + dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -808,7 +1190,7 @@ class RecommendBolusTests: XCTestCase { func testRiseAfterDIA() { let glucose = loadGlucoseValueFixture("far_future_high_bg_forecast") - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, at: glucose.first!.startDate, suspendThreshold: suspendThreshold.quantity, @@ -825,7 +1207,7 @@ class RecommendBolusTests: XCTestCase { func testNoInputGlucose() { let glucose: [GlucoseFixtureValue] = [] - let dose = glucose.recommendedBolus( + let dose = glucose.recommendedManualBolus( to: glucoseTargetRange, suspendThreshold: suspendThreshold.quantity, sensitivity: insulinSensitivitySchedule, diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index f1f1b4297c..a7cd19b260 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ isa = PBXAggregateTarget; buildConfigurationList = 432CF87820D8B8380066B889 /* Build configuration list for PBXAggregateTarget "Cartfile" */; buildPhases = ( - 432CF88220D8BCD90066B889 /* Homebrew & Carthage Setup */, 432CF87B20D8B8490066B889 /* Build Carthage Dependencies */, ); dependencies = ( @@ -367,13 +366,14 @@ C136AA2423109CC6008A320D /* LoopPlugins.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* LoopPlugins.swift */; }; C13BAD941E8009B000050CB5 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */; }; C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */; }; + C15C5D3C23B7211100C1B247 /* DosingStrategySelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15C5D3B23B7211100C1B247 /* DosingStrategySelectionViewController.swift */; }; C165B8CE23302C5D0004112E /* RemoteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C165B8CD23302C5D0004112E /* RemoteCommand.swift */; }; C16DA84222E8E112008624C2 /* LoopPlugins.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* LoopPlugins.swift */; }; C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824991E1999FA00D9D25C /* CaseCountable.swift */; }; C17824A01E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.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 */; }; + C17824A51E1AD4D100D9D25C /* ManualBolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */; }; + C17824A61E1AF91F00D9D25C /* ManualBolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */; }; C1814B86225E507C008D2D8E /* Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1814B85225E507C008D2D8E /* Sequence.swift */; }; C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; }; C19E96DF23D275F8003F79B0 /* LoopCompletionFreshness.swift in Sources */ = {isa = PBXBuildFile; fileRef = C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */; }; @@ -1079,12 +1079,13 @@ C12CB9B823106A6300F84978 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Intents.strings; sourceTree = ""; }; C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_very_low_end_in_range.json; sourceTree = ""; }; C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealBolusNightscoutTreatment.swift; sourceTree = ""; }; + C15C5D3B23B7211100C1B247 /* DosingStrategySelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DosingStrategySelectionViewController.swift; sourceTree = ""; }; C165B8CD23302C5D0004112E /* RemoteCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommand.swift; sourceTree = ""; }; C16DA84122E8E112008624C2 /* LoopPlugins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopPlugins.swift; sourceTree = ""; }; C17824991E1999FA00D9D25C /* CaseCountable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseCountable.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 = ""; }; + C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManualBolusRecommendation.swift; sourceTree = ""; }; C1814B85225E507C008D2D8E /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = ""; }; C18A491222FCC22800FDA733 /* build-derived-assets.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "build-derived-assets.sh"; sourceTree = ""; }; C18A491322FCC22900FDA733 /* make_scenario.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = make_scenario.py; sourceTree = ""; }; @@ -1094,6 +1095,7 @@ C19E96DD23D2733F003F79B0 /* LoopCompletionFreshness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopCompletionFreshness.swift; sourceTree = ""; }; C1A3EED1235233E1007672E3 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = ""; }; C1A3EED323523551007672E3 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = ""; }; + C1BFEC2125B754C20017CEA8 /* carthage.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = carthage.sh; 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 = ""; }; C1D197FE232CF92D0096D646 /* capture-build-details.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = ""; }; C1D289B422F90A52003FFBD9 /* BasalDeliveryState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDeliveryState.swift; sourceTree = ""; }; @@ -1289,7 +1291,7 @@ children = ( 43511CDD21FD80AD00566C63 /* RetrospectiveCorrection */, 43880F961D9D8052009061A8 /* ServiceAuthentication */, - C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */, + C17824A41E1AD4D100D9D25C /* ManualBolusRecommendation.swift */, 43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */, 436A0DA41D236A2A00104B24 /* LoopError.swift */, 430B29942041F5CB00BA9F93 /* LoopSettings+Loop.swift */, @@ -1615,6 +1617,7 @@ 43E3449E1B9D68E900C85C07 /* StatusTableViewController.swift */, 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */, 89CA2B31226C18B8004D9350 /* TestingScenariosTableViewController.swift */, + C15C5D3B23B7211100C1B247 /* DosingStrategySelectionViewController.swift */, ); path = "View Controllers"; sourceTree = ""; @@ -1888,6 +1891,7 @@ C18A491122FCC20B00FDA733 /* Scripts */ = { isa = PBXGroup; children = ( + C1BFEC2125B754C20017CEA8 /* carthage.sh */, C1D197FE232CF92D0096D646 /* capture-build-details.sh */, C125F31A22FE7CE200FD0545 /* copy-frameworks.sh */, C18A491222FCC22800FDA733 /* build-derived-assets.sh */, @@ -2417,21 +2421,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\n\nif [ -f $PROJECT_DIR/.gitmodules ]; then\n echo \"Skipping checkout due to presence of .gitmodules file\"\n if [ $ACTION = \"install\" ]; then\n echo \"You're installing: Make sure to keep all submodules up-to-date and run carthage build after changes.\"\n fi\nelse\n echo \"Bootstrapping carthage dependencies\"\n\n xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)\n trap 'rm -f \"$xcconfig\"' INT TERM HUP EXIT\n\n# For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise\n# the build will fail on lipo due to duplicate architectures.\n# Xcode 12 Beta 3:\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A8169g = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\n# Xcode 12 beta 4\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A8179i = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\n# Xcode 12 beta 5\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A8189h = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\n# Xcode 12 beta 6\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A8189n = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\n# Xcode 12 GM\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A7208 = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\n# Xcode 12 GM 2\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A7209 = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\n# Xcode 12.0.1\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12A7300 = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig\necho 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig\necho 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig\necho 'ONLY_ACTIVE_ARCH=NO' >> $xcconfig\necho 'VALID_ARCHS = $(inherited) x86_64' >> $xcconfig\nexport XCODE_XCCONFIG_FILE=\"$xcconfig\"\necho $XCODE_XCCONFIG_FILE\n cat $XCODE_XCCONFIG_FILE\n unset LLVM_TARGET_TRIPLE_SUFFIX\n /usr/local/bin/carthage bootstrap --project-directory \"$SRCROOT\" --cache-builds\nfi\n"; - }; - 432CF88220D8BCD90066B889 /* Homebrew & Carthage Setup */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Homebrew & Carthage Setup"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if ! [ -x \"$(command -v brew)\" ]; then\n # Install Homebrew\n ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\nfi\n\nif brew ls carthage > /dev/null; then\n brew upgrade carthage || echo \"Continuing…\"\nelse\n brew install carthage\nfi\n"; + shellScript = "\n\nif [ -f $PROJECT_DIR/.gitmodules ]; then\n echo \"Skipping checkout due to presence of .gitmodules file\"\n if [ $ACTION = \"install\" ]; then\n echo \"You're installing: Make sure to keep all submodules up-to-date and run carthage build after changes.\"\n fi\nelse\n echo \"Bootstrapping carthage dependencies\"\n ./Scripts/carthage.sh bootstrap --project-directory \"$SRCROOT\" --platform ios,watchos --cache-builds --verbose\nfi\n"; }; 43D9FFE221EAE40600AF44BF /* Copy Frameworks with Carthage */ = { isa = PBXShellScriptBuildPhase; @@ -2620,7 +2610,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C17824A51E1AD4D100D9D25C /* BolusRecommendation.swift in Sources */, + C17824A51E1AD4D100D9D25C /* ManualBolusRecommendation.swift in Sources */, 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */, 43441A9C1EDB34810087958C /* StatusExtensionContext+LoopKit.swift in Sources */, 43C05CC521EC29E3006FB252 /* TextFieldTableViewCell.swift in Sources */, @@ -2707,6 +2697,7 @@ 43C3B6EC20B650A80026CAFA /* SettingsImageTableViewCell.swift in Sources */, 89ADE13B226BFA0F0067222B /* TestingScenariosManager.swift in Sources */, 4F7E8ACB20E2ACB500AEA65E /* WatchPredictedGlucose.swift in Sources */, + C15C5D3C23B7211100C1B247 /* DosingStrategySelectionViewController.swift in Sources */, 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */, 4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */, 435CB6231F37967800C320C7 /* InsulinModelSettingsViewController.swift in Sources */, @@ -2876,7 +2867,7 @@ 43E2D8D41D20BF42004DA55F /* DoseMathTests.swift in Sources */, C11C87DE1E21EAAD00BB71D3 /* HKUnit.swift in Sources */, C13BAD941E8009B000050CB5 /* NumberFormatter.swift in Sources */, - C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */, + C17824A61E1AF91F00D9D25C /* ManualBolusRecommendation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Loop/Base.lproj/Localizable.strings b/Loop/Base.lproj/Localizable.strings index bc11213827..36f81aa726 100644 --- a/Loop/Base.lproj/Localizable.strings +++ b/Loop/Base.lproj/Localizable.strings @@ -249,9 +249,13 @@ /* The error message when invalid data was encountered. (1: details of invalid data) */ "Invalid data: %1$@" = "Invalid data: %1$@"; + /* The title text for the issue report cell */ "Issue Report" = "Issue Report"; +/* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ +"prediction-description-retrospective-correction" = "Predicted: %1$@\nActual: %2$@ (%3$@)"; + /* Glucose HUD accessibility hint */ "Launches CGM app" = "Launches CGM app"; diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index 07ba662f49..0a2b8d1792 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -59,6 +59,7 @@ + @@ -1100,7 +1101,7 @@ - + @@ -1307,6 +1308,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Loop/Info.plist b/Loop/Info.plist index 50b90a4d07..2bbed86cb6 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -2,6 +2,8 @@ + com.loopkit.Loop.featureSpecifier + Automatic Bolusing AppGroupIdentifier $(APP_GROUP_IDENTIFIER) CFBundleDevelopmentRegion diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 7f512b0d30..83f400a68f 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -16,6 +16,9 @@ import UserNotifications final class DeviceDataManager { private let queue = DispatchQueue(label: "com.loopkit.DeviceManagerQueue", qos: .utility) + + fileprivate let dosingQueue: DispatchQueue = DispatchQueue(label: "com.loopkit.DeviceManagerDosingQueue", qos: .utility) + private let log = DiagnosticLogger.shared.forCategory("DeviceManager") @@ -661,34 +664,71 @@ extension DeviceDataManager: LoopDataManagerDelegate { guard let pumpManager = pumpManager else { return units } - - return pumpManager.roundToSupportedBolusVolume(units: units) + + let rounded = ([0.0] + pumpManager.supportedBolusVolumes).enumerated().min( by: { abs($0.1 - units) < abs($1.1 - units) } )!.1 + self.log.default("Rounded \(units) to \(rounded)") + + return rounded } func loopDataManager( _ manager: LoopDataManager, - didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), - completion: @escaping (_ result: Result) -> Void + didRecommend automaticDose: (recommendation: AutomaticDoseRecommendation, date: Date), + completion: @escaping (_ error: Error?) -> Void ) { guard let pumpManager = pumpManager else { - completion(.failure(LoopError.configurationError(.pumpManager))) + completion(LoopError.configurationError(.pumpManager)) return } + + dosingQueue.async { + let doseDispatchGroup = DispatchGroup() + + var tempBasalError: Error? = nil + var bolusError: Error? = nil + + if let basalAdjustment = automaticDose.recommendation.basalAdjustment { + self.log.default("LoopManager did recommend basal change") + + doseDispatchGroup.enter() + pumpManager.enactTempBasal(unitsPerHour: basalAdjustment.unitsPerHour, for: basalAdjustment.duration, completion: { result in + switch result { + case .failure(let error): + tempBasalError = error + default: + break + } + doseDispatchGroup.leave() + }) + } + + doseDispatchGroup.wait() + + guard tempBasalError == nil else { + completion(tempBasalError) + return + } - log.default("LoopManager did recommend basal change") - - pumpManager.enactTempBasal( - unitsPerHour: basal.recommendation.unitsPerHour, - for: basal.recommendation.duration, - completion: { result in - switch result { - case .success(let doseEntry): - completion(.success(doseEntry)) - case .failure(let error): - completion(.failure(error)) + if automaticDose.recommendation.bolusUnits > 0 { + self.log.default("LoopManager did recommend bolus dose") + doseDispatchGroup.enter() + pumpManager.enactBolus(units: automaticDose.recommendation.bolusUnits, at: Date(), willRequest: { (dose) in + self.log.default("PumpManager willRequest bolus") + }) { (result) in + switch result { + case .failure(let error): + bolusError = error + default: + self.log.default("PumpManager issued bolus command") + break + } + doseDispatchGroup.leave() } } - ) + + doseDispatchGroup.wait() + completion(bolusError) + } } } diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index b10e4450c0..ff6f429f9b 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -31,6 +31,18 @@ extension InsulinCorrection { return 0 } } + + fileprivate func asBolus( + partialApplicationFactor: Double, + maxBolusUnits: Double, + volumeRounder: ((Double) -> Double)? + ) -> Double { + + let partialDose = units * partialApplicationFactor + + return Swift.min(Swift.max(0, volumeRounder?(partialDose) ?? partialDose),maxBolusUnits) + } + /// Determines the temp basal over `duration` needed to perform the correction. /// @@ -63,7 +75,7 @@ extension InsulinCorrection { duration: duration ) } - + private var bolusRecommendationNotice: BolusRecommendationNotice? { switch self { case .suspend(min: let minimum): @@ -86,16 +98,16 @@ extension InsulinCorrection { /// - maxBolus: The maximum allowable bolus value in units /// - volumeRounder: The smallest fraction of a unit supported in bolus delivery /// - Returns: A bolus recommendation - fileprivate func asBolus( + fileprivate func asManualBolus( pendingInsulin: Double, maxBolus: Double, volumeRounder: ((Double) -> Double)? - ) -> BolusRecommendation { + ) -> ManualBolusRecommendation { var units = self.units - pendingInsulin units = Swift.min(maxBolus, Swift.max(0, units)) units = volumeRounder?(units) ?? units - return BolusRecommendation( + return ManualBolusRecommendation( amount: units, pendingInsulin: pendingInsulin, notice: bolusRecommendationNotice @@ -103,7 +115,6 @@ extension InsulinCorrection { } } - struct TempBasalRecommendation: Equatable { let unitsPerHour: Double let duration: TimeInterval @@ -114,7 +125,6 @@ struct TempBasalRecommendation: Equatable { } } - extension TempBasalRecommendation { /// Equates the recommended rate with another rate /// @@ -164,6 +174,11 @@ extension TempBasalRecommendation { } } +struct AutomaticDoseRecommendation: Equatable { + let basalAdjustment: TempBasalRecommendation? + let bolusUnits: Double +} + /// Computes a total insulin amount necessary to correct a glucose differential at a given sensitivity /// @@ -291,7 +306,7 @@ extension Collection where Element: GlucoseValue { return nil } - // Choose either the minimum glucose or eventual glocse as the correction delta + // Choose either the minimum glucose or eventual glucose as the correction delta let minGlucoseTargets = correctionRange.quantityRange(at: min.startDate) let eventualGlucoseTargets = correctionRange.quantityRange(at: eventual.startDate) @@ -395,6 +410,88 @@ extension Collection where Element: GlucoseValue { scheduledBasalRateMatchesPump: !isBasalRateScheduleOverrideActive ) } + + /// Recommends a dose suitable for automatic enactment. Uses boluses for high corrections, and temp basals for low corrections. + /// + /// Returns nil if the normal scheduled basal, or active temporary basal, is sufficient. + /// + /// - Parameters: + /// - correctionRange: The schedule of correction ranges + /// - date: The date at which the temp basal would be scheduled, defaults to now + /// - suspendThreshold: A glucose value causing a recommendation of no insulin if any prediction falls below + /// - sensitivity: The schedule of insulin sensitivities + /// - model: The insulin absorption model + /// - basalRates: The schedule of basal rates + /// - maxBasalRate: The maximum allowed basal rate + /// - lastTempBasal: The previously set temp basal + /// - rateRounder: Closure that rounds recommendation to nearest supported rate. If nil, no rounding is performed + /// - isBasalRateScheduleOverrideActive: A flag describing whether a basal rate schedule override is in progress + /// - duration: The duration of the temporary basal + /// - continuationInterval: The duration of time before an ongoing temp basal should be continued with a new command + /// - Returns: The recommended dosing, if one could be computed + func recommendedAutomaticDose( + to correctionRange: GlucoseRangeSchedule, + at date: Date = Date(), + suspendThreshold: HKQuantity?, + sensitivity: InsulinSensitivitySchedule, + model: InsulinModel, + basalRates: BasalRateSchedule, + maxAutomaticBolus: Double, + partialApplicationFactor: Double, + lastTempBasal: DoseEntry?, + volumeRounder: ((Double) -> Double)? = nil, + rateRounder: ((Double) -> Double)? = nil, + isBasalRateScheduleOverrideActive: Bool = false, + duration: TimeInterval = .minutes(30), + continuationInterval: TimeInterval = .minutes(11) + ) -> AutomaticDoseRecommendation? { + guard let correction = self.insulinCorrection( + to: correctionRange, + at: date, + suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound, + sensitivity: sensitivity.quantity(at: date), + model: model + ) else { + return nil + } + + let scheduledBasalRate = basalRates.value(at: date) + var maxAutomaticBolus = maxAutomaticBolus + + if case .aboveRange(min: let min, correcting: _, minTarget: let doseThreshold, units: _) = correction, + min.quantity < doseThreshold + { + maxAutomaticBolus = 0 + } + + var temp: TempBasalRecommendation? = correction.asTempBasal( + scheduledBasalRate: scheduledBasalRate, + maxBasalRate: scheduledBasalRate, + duration: duration, + rateRounder: rateRounder + ) + + temp = temp?.ifNecessary( + at: date, + scheduledBasalRate: scheduledBasalRate, + lastTempBasal: lastTempBasal, + continuationInterval: continuationInterval, + scheduledBasalRateMatchesPump: !isBasalRateScheduleOverrideActive + ) + + let bolusUnits = correction.asBolus( + partialApplicationFactor: partialApplicationFactor, + maxBolusUnits: maxAutomaticBolus, + volumeRounder: volumeRounder + ) + + if temp != nil || bolusUnits > 0 { + return AutomaticDoseRecommendation(basalAdjustment: temp, bolusUnits: bolusUnits) + } + + return nil + } + /// Recommends a bolus to conform a glucose prediction timeline to a correction range /// @@ -408,7 +505,7 @@ extension Collection where Element: GlucoseValue { /// - maxBolus: The maximum bolus to return /// - volumeRounder: Closure that rounds recommendation to nearest supported bolus volume. If nil, no rounding is performed /// - Returns: A bolus recommendation - func recommendedBolus( + func recommendedManualBolus( to correctionRange: GlucoseRangeSchedule, at date: Date = Date(), suspendThreshold: HKQuantity?, @@ -417,7 +514,7 @@ extension Collection where Element: GlucoseValue { pendingInsulin: Double, maxBolus: Double, volumeRounder: ((Double) -> Double)? = nil - ) -> BolusRecommendation { + ) -> ManualBolusRecommendation { guard let correction = self.insulinCorrection( to: correctionRange, at: date, @@ -425,10 +522,10 @@ extension Collection where Element: GlucoseValue { sensitivity: sensitivity.quantity(at: date), model: model ) else { - return BolusRecommendation(amount: 0, pendingInsulin: pendingInsulin) + return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin) } - var bolus = correction.asBolus( + var bolus = correction.asManualBolus( pendingInsulin: pendingInsulin, maxBolus: maxBolus, volumeRounder: volumeRounder diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index b57ae6ab84..f9c064886e 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -207,10 +207,11 @@ final class LoopDataManager { private var retrospectiveGlucoseDiscrepanciesSummed: [GlucoseChange]? + fileprivate var predictedGlucose: [PredictedGlucoseValue]? { didSet { - recommendedTempBasal = nil - recommendedBolus = nil + recommendedDose = nil + recommendedManualBolus = nil predictedGlucoseIncludingPendingInsulin = nil } } @@ -219,9 +220,9 @@ final class LoopDataManager { private var recentCarbEntries: [StoredCarbEntry]? - fileprivate var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? + fileprivate var recommendedDose: (recommendation: AutomaticDoseRecommendation, date: Date)? - fileprivate var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? + fileprivate var recommendedManualBolus: (recommendation: ManualBolusRecommendation, date: Date)? fileprivate var carbsOnBoard: CarbValue? @@ -534,7 +535,7 @@ extension LoopDataManager { /// - carbEntry: The new carb value /// - completion: A closure called once upon completion /// - result: The bolus recommendation - func addCarbEntryAndRecommendBolus(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry? = nil, completion: @escaping (_ result: Result) -> Void) { + func addCarbEntryAndRecommendBolus(_ carbEntry: NewCarbEntry, replacing replacingEntry: StoredCarbEntry? = nil, completion: @escaping (_ result: Result) -> Void) { let addCompletion: (CarbStoreResult) -> Void = { (result) in self.dataAccessQueue.async { switch result { @@ -548,7 +549,7 @@ extension LoopDataManager { do { try self.update() - completion(.success(self.recommendedBolus?.recommendation)) + completion(.success(self.recommendedManualBolus?.recommendation)) } catch let error { completion(.failure(error)) } @@ -587,8 +588,8 @@ extension LoopDataManager { self.dataAccessQueue.async { self.logger.debug("bolusConfirmed") self.lastRequestedBolus = nil - self.recommendedBolus = nil - self.recommendedTempBasal = nil + self.recommendedManualBolus = nil + self.recommendedDose = nil self.insulinEffect = nil self.notify(forChange: .bolus) @@ -666,9 +667,9 @@ extension LoopDataManager { // Actions - func enactRecommendedTempBasal(_ completion: @escaping (_ error: Error?) -> Void) { + func enactRecommendedDose(_ completion: @escaping (_ error: Error?) -> Void) { dataAccessQueue.async { - self.setRecommendedTempBasal(completion) + self.enactDose(completion) } } @@ -688,7 +689,7 @@ extension LoopDataManager { try self.update() if self.settings.dosingEnabled { - self.setRecommendedTempBasal { (error) -> Void in + self.enactDose { (error) -> Void in self.lastLoopError = error if let error = error { @@ -1006,7 +1007,7 @@ extension LoopDataManager { } /// - Throws: LoopError.missingDataError - fileprivate func recommendBolus(forPrediction predictedGlucose: [Sample]) throws -> BolusRecommendation? { + fileprivate func recommendBolus(forPrediction predictedGlucose: [Sample]) throws -> ManualBolusRecommendation? { guard let glucose = glucoseStore.latestGlucose else { throw LoopError.missingDataError(.glucose) } @@ -1056,7 +1057,7 @@ extension LoopDataManager { return self.delegate?.loopDataManager(self, roundBolusVolume: units) ?? units } - return predictedGlucose.recommendedBolus( + return predictedGlucose.recommendedManualBolus( to: glucoseTargetRange, suspendThreshold: settings.suspendThreshold?.quantity, sensitivity: insulinSensitivity, @@ -1208,6 +1209,7 @@ extension LoopDataManager { let rateRounder = { (_ rate: Double) in return self.delegate?.loopDataManager(self, roundBasalRate: rate) ?? rate } + let lastTempBasal: DoseEntry? @@ -1217,32 +1219,57 @@ extension LoopDataManager { lastTempBasal = nil } - let tempBasal = predictedGlucose.recommendedTempBasal( - to: glucoseTargetRange, - at: predictedGlucose[0].startDate, - suspendThreshold: settings.suspendThreshold?.quantity, - sensitivity: insulinSensitivity, - model: model, - basalRates: basalRates, - maxBasalRate: maxBasal, - lastTempBasal: lastTempBasal, - rateRounder: rateRounder, - isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true - ) + let dosingRecommendation: AutomaticDoseRecommendation? - if let temp = tempBasal { + switch settings.dosingStrategy { + case .automaticBolus: + let volumeRounder = { (_ units: Double) in + return self.delegate?.loopDataManager(self, roundBolusVolume: units) ?? units + } + + dosingRecommendation = predictedGlucose.recommendedAutomaticDose( + to: glucoseTargetRange, + at: predictedGlucose[0].startDate, + suspendThreshold: settings.suspendThreshold?.quantity, + sensitivity: insulinSensitivity, + model: model, + basalRates: basalRates, + maxAutomaticBolus: maxBolus * settings.bolusPartialApplicationFactor, + partialApplicationFactor: settings.bolusPartialApplicationFactor, + lastTempBasal: lastTempBasal, + volumeRounder: volumeRounder, + rateRounder: rateRounder, + isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true + ) + case .tempBasalOnly: + let temp = predictedGlucose.recommendedTempBasal( + to: glucoseTargetRange, + at: predictedGlucose[0].startDate, + suspendThreshold: settings.suspendThreshold?.quantity, + sensitivity: insulinSensitivity, + model: model, + basalRates: basalRates, + maxBasalRate: maxBasal, + lastTempBasal: lastTempBasal, + rateRounder: rateRounder, + isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true + ) + dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp, bolusUnits: 0) + } + + if let dosingRecommendation = dosingRecommendation { self.logger.default("Current basal state: \(String(describing: basalDeliveryState))") - self.logger.default("Recommending temp basal: \(temp) at \(startDate)") - recommendedTempBasal = (recommendation: temp, date: startDate) + self.logger.default("Recommending dose: \(dosingRecommendation) at \(startDate)") + recommendedDose = (recommendation: dosingRecommendation, date: startDate) } else { - recommendedTempBasal = nil + recommendedDose = nil } let volumeRounder = { (_ units: Double) in return self.delegate?.loopDataManager(self, roundBolusVolume: units) ?? units } - let recommendation = predictedGlucoseIncludingPendingInsulin.recommendedBolus( + let recommendation = predictedGlucoseIncludingPendingInsulin.recommendedManualBolus( to: glucoseTargetRange, at: predictedGlucose[0].startDate, suspendThreshold: settings.suspendThreshold?.quantity, @@ -1252,33 +1279,32 @@ extension LoopDataManager { maxBolus: maxBolus, volumeRounder: volumeRounder ) - recommendedBolus = (recommendation: recommendation, date: startDate) - self.logger.debug("Recommending bolus: \(String(describing: recommendedBolus))") + recommendedManualBolus = (recommendation: recommendation, date: startDate) + self.logger.debug("Recommending manual bolus: \(String(describing: recommendedManualBolus))") } /// *This method should only be called from the `dataAccessQueue`* - private func setRecommendedTempBasal(_ completion: @escaping (_ error: Error?) -> Void) { + private func enactDose(_ completion: @escaping (_ error: Error?) -> Void) { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) - guard let recommendedTempBasal = self.recommendedTempBasal else { + guard let recommendedDose = self.recommendedDose else { completion(nil) return } - guard abs(recommendedTempBasal.date.timeIntervalSinceNow) < TimeInterval(minutes: 5) else { - completion(LoopError.recommendationExpired(date: recommendedTempBasal.date)) + guard abs(recommendedDose.date.timeIntervalSinceNow) < TimeInterval(minutes: 5) else { + completion(LoopError.recommendationExpired(date: recommendedDose.date)) + return + } + + if case .suspended = basalDeliveryState { + completion(LoopError.pumpSuspended) return } - delegate?.loopDataManager(self, didRecommendBasalChange: recommendedTempBasal) { (result) in + delegate?.loopDataManager(self, didRecommend: recommendedDose) { (error) in self.dataAccessQueue.async { - switch result { - case .success: - self.recommendedTempBasal = nil - completion(nil) - case .failure(let error): - completion(error) - } + completion(error) } } } @@ -1303,9 +1329,9 @@ protocol LoopState { var predictedGlucoseIncludingPendingInsulin: [PredictedGlucoseValue]? { get } /// The recommended temp basal based on predicted glucose - var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? { get } + var recommendedAutomaticDose: (recommendation: AutomaticDoseRecommendation, date: Date)? { get } - var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? { get } + var recommendedBolus: (recommendation: ManualBolusRecommendation, date: Date)? { get } /// The difference in predicted vs actual glucose over a recent period var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? { get } @@ -1330,7 +1356,7 @@ protocol LoopState { /// - Parameter predictedGlucose: A timeline of predicted glucose values /// - Returns: A bolus recommendation, or `nil` if not applicable /// - Throws: LoopError.missingDataError if recommendation cannot be computed - func recommendBolus(forPrediction predictedGlucose: [Sample]) throws -> BolusRecommendation? + func recommendBolus(forPrediction predictedGlucose: [Sample]) throws -> ManualBolusRecommendation? /// Computes the carbs on board, taking into account an unstored carb entry /// - Parameters: @@ -1389,20 +1415,20 @@ extension LoopDataManager { return loopDataManager.predictedGlucoseIncludingPendingInsulin } - var recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? { + var recommendedAutomaticDose: (recommendation: AutomaticDoseRecommendation, date: Date)? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) guard loopDataManager.lastRequestedBolus == nil else { return nil } - return loopDataManager.recommendedTempBasal + return loopDataManager.recommendedDose } - var recommendedBolus: (recommendation: BolusRecommendation, date: Date)? { + var recommendedBolus: (recommendation: ManualBolusRecommendation, date: Date)? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) guard loopDataManager.lastRequestedBolus == nil else { return nil } - return loopDataManager.recommendedBolus + return loopDataManager.recommendedManualBolus } var retrospectiveGlucoseDiscrepancies: [GlucoseChange]? { @@ -1419,7 +1445,7 @@ extension LoopDataManager { return try loopDataManager.predictGlucose(using: inputs, potentialBolus: potentialBolus, potentialCarbEntry: potentialCarbEntry, replacingCarbEntry: replacedCarbEntry, includingPendingInsulin: includingPendingInsulin) } - func recommendBolus(forPrediction predictedGlucose: [Sample]) throws -> BolusRecommendation? { + func recommendBolus(forPrediction predictedGlucose: [Sample]) throws -> ManualBolusRecommendation? { return try loopDataManager.recommendBolus(forPrediction: predictedGlucose) } @@ -1509,7 +1535,7 @@ extension LoopDataManager { "glucoseMomentumEffect: \(manager.glucoseMomentumEffect ?? [])", "retrospectiveGlucoseEffect: \(manager.retrospectiveGlucoseEffect)", - "recommendedTempBasal: \(String(describing: state.recommendedTempBasal))", + "recommendedTempBasal: \(String(describing: state.recommendedAutomaticDose))", "recommendedBolus: \(String(describing: state.recommendedBolus))", "lastBolus: \(String(describing: manager.lastRequestedBolus))", "lastLoopCompleted: \(String(describing: manager.lastLoopCompleted))", @@ -1558,8 +1584,8 @@ protocol LoopDataManagerDelegate: class { /// - manager: The manager /// - basal: The new recommended basal /// - completion: A closure called once on completion - /// - result: The enacted basal - func loopDataManager(_ manager: LoopDataManager, didRecommendBasalChange basal: (recommendation: TempBasalRecommendation, date: Date), completion: @escaping (_ result: Result) -> Void) -> Void + /// - error: Set if an error occurred while issuing dosing commands + func loopDataManager(_ manager: LoopDataManager, didRecommend automaticDose: (recommendation: AutomaticDoseRecommendation, date: Date), completion: @escaping (_ error: Error?) -> Void) -> Void /// Asks the delegate to round a recommended basal rate to a supported rate /// @@ -1572,7 +1598,7 @@ protocol LoopDataManagerDelegate: class { /// /// - Parameters: /// - units: The recommended bolus in U - /// - Returns: a supported bolus volume in U. The volume returned should not be larger than the passed in rate. + /// - Returns: a supported bolus volume in U. The volume returned should be the nearest deliverable volume. func loopDataManager(_ manager: LoopDataManager, roundBolusVolume units: Double) -> Double } diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift index dd4af48c0c..e2d020d398 100644 --- a/Loop/Managers/NightscoutDataManager.swift +++ b/Loop/Managers/NightscoutDataManager.swift @@ -179,7 +179,7 @@ final class NightscoutDataManager { let carbsOnBoard = state.carbsOnBoard let predictedGlucose = state.predictedGlucoseIncludingPendingInsulin - let recommendedTempBasal = state.recommendedTempBasal + let recommendedTempBasal = state.recommendedAutomaticDose manager.doseStore.insulinOnBoard(at: Date()) { (result) in let insulinOnBoard: InsulinValue? @@ -199,8 +199,8 @@ final class NightscoutDataManager { insulinOnBoard: insulinOnBoard, carbsOnBoard: carbsOnBoard, predictedGlucose: predictedGlucose, - recommendedTempBasal: recommendedTempBasal, - recommendedBolus: recommendedBolus, + recommendedAutomaticDose: recommendedTempBasal, + recommendedManualBolus: recommendedBolus, loopError: loopError ) @@ -219,8 +219,8 @@ final class NightscoutDataManager { insulinOnBoard: InsulinValue? = nil, carbsOnBoard: CarbValue? = nil, predictedGlucose: [GlucoseValue]? = nil, - recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? = nil, - recommendedBolus: Double? = nil, + recommendedAutomaticDose: (recommendation: AutomaticDoseRecommendation, date: Date)? = nil, + recommendedManualBolus: Double? = nil, loopError: Error? = nil) { @@ -255,9 +255,9 @@ final class NightscoutDataManager { } let recommended: RecommendedTempBasal? - - if let (recommendation: recommendation, date: date) = recommendedTempBasal { - recommended = RecommendedTempBasal(timestamp: date, rate: recommendation.unitsPerHour, duration: recommendation.duration) + + if let (recommendation: recommendation, date: date) = recommendedAutomaticDose, let basalAdjustment = recommendation.basalAdjustment { + recommended = RecommendedTempBasal(timestamp: date, rate: basalAdjustment.unitsPerHour, duration: basalAdjustment.duration) } else { recommended = nil } @@ -277,7 +277,7 @@ final class NightscoutDataManager { //this is the only pill that has the option to modify the text //to do that pass a different name value instead of loopName - let loopStatus = LoopStatus(name: loopName, version: loopVersion, timestamp: statusTime, iob: iob, cob: cob, predicted: predicted, recommendedTempBasal: recommended, recommendedBolus: recommendedBolus, enacted: loopEnacted, failureReason: loopError) + let loopStatus = LoopStatus(name: loopName, version: loopVersion, timestamp: statusTime, iob: iob, cob: cob, predicted: predicted, recommendedTempBasal: recommended, recommendedBolus: recommendedManualBolus, enacted: loopEnacted, failureReason: loopError) let pumpStatus: NightscoutUploadKit.PumpStatus? diff --git a/Loop/Managers/TestingScenariosManager.swift b/Loop/Managers/TestingScenariosManager.swift index 7a5fafe1ee..e011adcfae 100644 --- a/Loop/Managers/TestingScenariosManager.swift +++ b/Loop/Managers/TestingScenariosManager.swift @@ -154,14 +154,18 @@ extension TestingScenariosManagerRequirements { private func stepForward(_ scenario: TestingScenario, completion: @escaping (TestingScenario) -> Void) { deviceManager.loopManager.getLoopState { _, state in var scenario = scenario - guard let recommendedTemp = state.recommendedTempBasal?.recommendation else { + guard let dose = state.recommendedAutomaticDose?.recommendation else { scenario.stepForward(by: .minutes(5)) completion(scenario) return } - scenario.stepForward(unitsPerHour: recommendedTemp.unitsPerHour, duration: recommendedTemp.duration) - completion(scenario) + // TODO: Handle bolus + + if let basalAdjustment = dose.basalAdjustment { + scenario.stepForward(unitsPerHour: basalAdjustment.unitsPerHour, duration: basalAdjustment.duration) + completion(scenario) + } } } diff --git a/Loop/Models/LoopError.swift b/Loop/Models/LoopError.swift index 55527bf86f..e427d3f298 100644 --- a/Loop/Models/LoopError.swift +++ b/Loop/Models/LoopError.swift @@ -91,6 +91,9 @@ enum LoopError: Error { // Invalid Data case invalidData(details: String) + + // Pump Suspended + case pumpSuspended } @@ -130,7 +133,8 @@ extension LoopError: LocalizedError { return String(format: NSLocalizedString("Recommendation expired: %1$@ old", comment: "The error message when a recommendation has expired. (1: age of recommendation in minutes)"), minutes) case .invalidData(let details): return String(format: NSLocalizedString("Invalid data: %1$@", comment: "The error message when invalid data was encountered. (1: details of invalid data)"), details) - + case .pumpSuspended: + return NSLocalizedString("Pump Suspended", comment: "The error message when loop failed because the pump was encountered.") } } } diff --git a/Loop/Models/BolusRecommendation.swift b/Loop/Models/ManualBolusRecommendation.swift similarity index 92% rename from Loop/Models/BolusRecommendation.swift rename to Loop/Models/ManualBolusRecommendation.swift index 74165a6b33..6611df2921 100644 --- a/Loop/Models/BolusRecommendation.swift +++ b/Loop/Models/ManualBolusRecommendation.swift @@ -65,7 +65,7 @@ extension BolusRecommendationNotice: Equatable { } -struct BolusRecommendation { +struct ManualBolusRecommendation { let amount: Double let pendingInsulin: Double var notice: BolusRecommendationNotice? @@ -78,12 +78,12 @@ struct BolusRecommendation { } -extension BolusRecommendation: Comparable { - static func ==(lhs: BolusRecommendation, rhs: BolusRecommendation) -> Bool { +extension ManualBolusRecommendation: Comparable { + static func ==(lhs: ManualBolusRecommendation, rhs: ManualBolusRecommendation) -> Bool { return lhs.amount == rhs.amount } - static func <(lhs: BolusRecommendation, rhs: BolusRecommendation) -> Bool { + static func <(lhs: ManualBolusRecommendation, rhs: ManualBolusRecommendation) -> Bool { return lhs.amount < rhs.amount } } diff --git a/Loop/View Controllers/BolusViewController+LoopDataManager.swift b/Loop/View Controllers/BolusViewController+LoopDataManager.swift new file mode 100644 index 0000000000..dc7745fc13 --- /dev/null +++ b/Loop/View Controllers/BolusViewController+LoopDataManager.swift @@ -0,0 +1,51 @@ +// +// BolusViewController+LoopDataManager.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit + + +extension BolusViewController { + func configureWithLoopManager(_ manager: LoopDataManager, recommendation: ManualBolusRecommendation?, glucoseUnit: HKUnit) { + manager.getLoopState { (manager, state) in + let maximumBolus = manager.settings.maximumBolus + + let activeCarbohydrates = state.carbsOnBoard?.quantity.doubleValue(for: .gram()) + let bolusRecommendation: ManualBolusRecommendation? + + if let recommendation = recommendation { + bolusRecommendation = recommendation + } else { + bolusRecommendation = state.recommendedBolus?.recommendation + } + + print("BolusViewController: recommendation = \(String(describing: bolusRecommendation))") + + manager.doseStore.insulinOnBoard(at: Date()) { (result) in + let activeInsulin: Double? + + switch result { + case .success(let value): + activeInsulin = value.value + case .failure: + activeInsulin = nil + } + + DispatchQueue.main.async { + if let maxBolus = maximumBolus { + self.maxBolus = maxBolus + } + + self.glucoseUnit = glucoseUnit + self.activeInsulin = activeInsulin + self.activeCarbohydrates = activeCarbohydrates + self.bolusRecommendation = bolusRecommendation + } + } + } + } +} diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index ba21caa0cb..370e098f92 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -140,7 +140,7 @@ final class BolusViewController: ChartsTableViewController, IdentifiableClass, U private var computedInitialBolusRecommendation = false - var bolusRecommendation: BolusRecommendation? = nil { + var bolusRecommendation: ManualBolusRecommendation? = nil { didSet { let amount = bolusRecommendation?.amount ?? 0 recommendedBolusAmountLabel?.text = bolusUnitsFormatter.string(from: amount) diff --git a/Loop/View Controllers/DosingStrategySelectionViewController.swift b/Loop/View Controllers/DosingStrategySelectionViewController.swift new file mode 100644 index 0000000000..5cf5173f80 --- /dev/null +++ b/Loop/View Controllers/DosingStrategySelectionViewController.swift @@ -0,0 +1,93 @@ +// +// DosingStrategySelectionViewController.swift +// Loop +// +// Created by Pete Schwamb on 12/27/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopCore +import LoopKit +import LoopUI + + +protocol DosingStrategySelectionViewControllerDelegate: class { + func dosingStrategySelectionViewControllerDidChangeValue(_ controller: DosingStrategySelectionViewController) +} + + +class DosingStrategySelectionViewController: UITableViewController, IdentifiableClass { + + /// The currently-selected strategy. + var dosingStrategy: DosingStrategy? + + var initialDosingStrategy: DosingStrategy? + + weak var delegate: DosingStrategySelectionViewControllerDelegate? + + // MARK: - UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 91 + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + // Record the configured dosingStrategy for change tracking + initialDosingStrategy = dosingStrategy + } + + override func viewWillDisappear(_ animated: Bool) { + // Notify observers if the strategy changed since viewDidAppear + if dosingStrategy != initialDosingStrategy { + delegate?.dosingStrategySelectionViewControllerDidChangeValue(self) + } + + super.viewWillDisappear(animated) + } + + // MARK: - UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return DosingStrategy.allCases.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let strategy = DosingStrategy(rawValue: indexPath.row)! + let cell = tableView.dequeueReusableCell(withIdentifier: TitleSubtitleTextFieldTableViewCell.className, for: indexPath) as! TitleSubtitleTextFieldTableViewCell + let isSelected = strategy == dosingStrategy + cell.tintColor = isSelected ? nil : .clear + cell.titleLabel.text = strategy.title + cell.subtitleLabel.text = strategy.subtitle + return cell + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + let selectedStrategy = DosingStrategy(rawValue: indexPath.row)! + dosingStrategy = selectedStrategy + + for strategy in DosingStrategy.allCases { + guard let cell = tableView.cellForRow(at: IndexPath(row: strategy.rawValue, section: 0)) as? TitleSubtitleTextFieldTableViewCell else { + continue + } + + let isSelected = dosingStrategy == strategy + cell.tintColor = isSelected ? nil : .clear + } + + tableView.deselectRow(at: indexPath, animated: true) + } +} diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index d7b6c4bd4e..c8a0af8513 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -70,6 +70,7 @@ final class SettingsTableViewController: UITableViewController { case basalRate case deliveryLimits case insulinModel + case dosingStrategy case carbRatio case insulinSensitivity } @@ -100,6 +101,9 @@ final class SettingsTableViewController: UITableViewController { vc.insulinSensitivitySchedule = insulinSensitivitySchedule } + vc.delegate = self + case let vc as DosingStrategySelectionViewController: + vc.dosingStrategy = dataManager.loopManager.settings.dosingStrategy vc.delegate = self default: break @@ -265,6 +269,9 @@ final class SettingsTableViewController: UITableViewController { } else { configCell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString } + case .dosingStrategy: + configCell.textLabel?.text = NSLocalizedString("Dosing Strategy", comment: "The title text for the dosing strategy setting row") + configCell.detailTextLabel?.text = dataManager.loopManager.settings.dosingStrategy.title case .deliveryLimits: configCell.textLabel?.text = NSLocalizedString("Delivery Limits", comment: "Title text for delivery limits") @@ -507,6 +514,8 @@ final class SettingsTableViewController: UITableViewController { } case .insulinModel: performSegue(withIdentifier: InsulinModelSettingsViewController.className, sender: sender) + case .dosingStrategy: + performSegue(withIdentifier: DosingStrategySelectionViewController.className, sender: sender) case .deliveryLimits: let vc = DeliveryLimitSettingsTableViewController(style: .grouped) @@ -794,6 +803,30 @@ extension SettingsTableViewController: InsulinModelSettingsViewControllerDelegat } } +extension SettingsTableViewController: DosingStrategySelectionViewControllerDelegate { + func dosingStrategySelectionViewControllerDidChangeValue(_ controller: DosingStrategySelectionViewController) { + guard let indexPath = self.tableView.indexPathForSelectedRow else { + return + } + + switch sections[indexPath.section] { + case .configuration: + switch ConfigurationRow(rawValue: indexPath.row)! { + case .dosingStrategy: + if let strategy = controller.dosingStrategy { + dataManager.loopManager.settings.dosingStrategy = strategy + } + + tableView.reloadRows(at: [indexPath], with: .none) + default: + assertionFailure() + } + default: + assertionFailure() + } + } +} + extension SettingsTableViewController: LoopKitUI.TextFieldTableViewControllerDelegate { func textFieldTableViewControllerDidEndEditing(_ controller: LoopKitUI.TextFieldTableViewController) { diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 79d1162aa0..2fb9cd795e 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -300,7 +300,7 @@ final class StatusTableViewController: ChartsTableViewController { reloading = true let reloadGroup = DispatchGroup() - var newRecommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? + var newRecommendedTempBasal: (recommendation: AutomaticDoseRecommendation, date: Date)? var glucoseValues: [StoredGlucoseSample]? var predictedGlucoseValues: [GlucoseValue]? var iobValues: [InsulinValue]? @@ -347,7 +347,7 @@ final class StatusTableViewController: ChartsTableViewController { lastLoopCompleted! < Date(timeIntervalSinceNow: .minutes(-6)) || !manager.settings.dosingEnabled { - newRecommendedTempBasal = state.recommendedTempBasal + newRecommendedTempBasal = state.recommendedAutomaticDose } if currentContext.contains(.carbs) { @@ -504,7 +504,7 @@ final class StatusTableViewController: ChartsTableViewController { } // Show/hide the table view rows - let statusRowMode = self.determineStatusRowMode(recommendedTempBasal: newRecommendedTempBasal) + let statusRowMode = self.determineStatusRowMode(recommendedDose: newRecommendedTempBasal) self.updateHUDandStatusRows(statusRowMode: statusRowMode, newSize: currentContext.newSize, animated: animated) @@ -569,7 +569,7 @@ final class StatusTableViewController: ChartsTableViewController { private enum StatusRowMode { case hidden - case recommendedTempBasal(tempBasal: TempBasalRecommendation, at: Date, enacting: Bool) + case recommendedDose(dose: AutomaticDoseRecommendation, at: Date, enacting: Bool) case scheduleOverrideEnabled(TemporaryScheduleOverride) case enactingBolus case bolusing(dose: DoseEntry) @@ -588,7 +588,7 @@ final class StatusTableViewController: ChartsTableViewController { private var statusRowMode = StatusRowMode.hidden - private func determineStatusRowMode(recommendedTempBasal: (recommendation: TempBasalRecommendation, date: Date)? = nil) -> StatusRowMode { + private func determineStatusRowMode(recommendedDose: (recommendation: AutomaticDoseRecommendation, date: Date)? = nil) -> StatusRowMode { let statusRowMode: StatusRowMode if case .initiating = bolusState { @@ -601,8 +601,8 @@ final class StatusTableViewController: ChartsTableViewController { statusRowMode = .pumpSuspended(resuming: true) } else if case .inProgress(let dose) = bolusState, dose.endDate.timeIntervalSinceNow > 0 { statusRowMode = .bolusing(dose: dose) - } else if let (recommendation: tempBasal, date: date) = recommendedTempBasal { - statusRowMode = .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: false) + } else if let (recommendation: dose, date: date) = recommendedDose { + statusRowMode = .recommendedDose(dose: dose, at: date, enacting: false) } else if let scheduleOverride = deviceManager.loopManager.settings.scheduleOverride, scheduleOverride.context != .preMeal && scheduleOverride.context != .legacyWorkout, !scheduleOverride.hasFinished() @@ -646,15 +646,15 @@ final class StatusTableViewController: ChartsTableViewController { switch (statusWasVisible, statusIsVisible) { case (true, true): switch (oldStatusRowMode, self.statusRowMode) { - case (.recommendedTempBasal(tempBasal: let oldTempBasal, at: let oldDate, enacting: let wasEnacting), - .recommendedTempBasal(tempBasal: let newTempBasal, at: let newDate, enacting: let isEnacting)): + case (.recommendedDose(dose: let oldDose, at: let oldDate, enacting: let wasEnacting), + .recommendedDose(dose: let newDose, at: let newDate, enacting: let isEnacting)): // Ensure we have a change - guard oldTempBasal != newTempBasal || oldDate != newDate || wasEnacting != isEnacting else { + guard oldDose != newDose || oldDate != newDate || wasEnacting != isEnacting else { break } // If the rate or date change, reload the row - if oldTempBasal != newTempBasal || oldDate != newDate { + if oldDose != newDose || oldDate != newDate { self.tableView.reloadRows(at: [statusIndexPath], with: animated ? .fade : .none) } else if let cell = tableView.cellForRow(at: statusIndexPath) { // If only the enacting state changed, update the activity indicator @@ -809,14 +809,28 @@ final class StatusTableViewController: ChartsTableViewController { cell.subtitleLabel?.text = nil cell.accessoryView = nil return cell - case .recommendedTempBasal(tempBasal: let tempBasal, at: let date, enacting: let enacting): + case .recommendedDose(dose: let dose, at: let date, enacting: let enacting): let cell = getTitleSubtitleCell() let timeFormatter = DateFormatter() timeFormatter.dateStyle = .none timeFormatter.timeStyle = .short - cell.titleLabel.text = NSLocalizedString("Recommended Basal", comment: "The title of the cell displaying a recommended temp basal value") - cell.subtitleLabel?.text = String(format: NSLocalizedString("%1$@ U/hour @ %2$@", comment: "The format for recommended temp basal rate and time. (1: localized rate number)(2: localized time)"), NumberFormatter.localizedString(from: NSNumber(value: tempBasal.unitsPerHour), number: .decimal), timeFormatter.string(from: date)) + //cell.titleLabel.text = NSLocalizedString("Recommended Dose", comment: "The title of the cell displaying a recommended dose") + + var text: String + + if let basalAdjustment = dose.basalAdjustment, dose.bolusUnits == 0 { + cell.titleLabel.text = NSLocalizedString("Recommended Basal", comment: "The title of the cell displaying a recommended temp basal value") + text = String(format: NSLocalizedString("%1$@ U/hour", comment: "The format for recommended temp basal rate and time. (1: localized rate number)"), NumberFormatter.localizedString(from: NSNumber(value: basalAdjustment.unitsPerHour), number: .decimal)) + } else { + let bolusUnitsStr = quantityFormatter.string(from: HKQuantity(unit: .internationalUnit(), doubleValue: dose.bolusUnits), for: .internationalUnit()) ?? "" + cell.titleLabel.text = NSLocalizedString("Recommended Auto-Bolus", comment: "The title of the cell displaying a recommended automatic bolus value") + text = String(format: NSLocalizedString("%1$@ ", comment: "The format for recommended bolus string. (1: localized bolus volume)" ), bolusUnitsStr) + } + text += String(format: NSLocalizedString(" @ %1$@", comment: "The format for dose recommendation time. (1: localized time)"), timeFormatter.string(from: date)) + + cell.subtitleLabel.text = text + cell.selectionStyle = .default if enacting { @@ -982,10 +996,10 @@ final class StatusTableViewController: ChartsTableViewController { tableView.deselectRow(at: indexPath, animated: true) switch statusRowMode { - case .recommendedTempBasal(tempBasal: let tempBasal, at: let date, enacting: let enacting) where !enacting: - self.updateHUDandStatusRows(statusRowMode: .recommendedTempBasal(tempBasal: tempBasal, at: date, enacting: true), newSize: nil, animated: true) + case .recommendedDose(dose: let dose, at: let date, enacting: let enacting) where !enacting: + self.updateHUDandStatusRows(statusRowMode: .recommendedDose(dose: dose, at: date, enacting: true), newSize: nil, animated: true) - self.deviceManager.loopManager.enactRecommendedTempBasal { (error) in + self.deviceManager.loopManager.enactRecommendedDose { (error) in DispatchQueue.main.async { self.updateHUDandStatusRows(statusRowMode: .hidden, newSize: nil, animated: true) diff --git a/Loop/en.lproj/Localizable.strings b/Loop/en.lproj/Localizable.strings index bc11213827..917c5426c9 100644 --- a/Loop/en.lproj/Localizable.strings +++ b/Loop/en.lproj/Localizable.strings @@ -252,6 +252,9 @@ /* The title text for the issue report cell */ "Issue Report" = "Issue Report"; +/* Format string describing retrospective glucose prediction comparison. (1: Predicted glucose)(2: Actual glucose)(3: difference) */ +"prediction-description-retrospective-correction" = "Predicted: %1$@\nActual: %2$@ (%3$@)"; + /* Glucose HUD accessibility hint */ "Launches CGM app" = "Launches CGM app"; diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift index 2b23925b33..581b77a6ca 100644 --- a/LoopCore/LoopSettings.swift +++ b/LoopCore/LoopSettings.swift @@ -8,6 +8,31 @@ import LoopKit import HealthKit +public enum DosingStrategy: Int, CaseIterable { + case tempBasalOnly + case automaticBolus +} + +public extension DosingStrategy { + var title: String { + switch self { + case .tempBasalOnly: + return NSLocalizedString("Temp Basal Only", comment: "Title string for temp basal only dosing strategy") + case .automaticBolus: + return NSLocalizedString("Automatic Bolus", comment: "Title string for automatic bolus dosing strategy") + } + } + + var subtitle: String { + switch self { + case .tempBasalOnly: + return NSLocalizedString("Loop will dose via temp basals, limited by your max temp basal setting.", comment: "Description string for temp basal only dosing strategy") + case .automaticBolus: + return NSLocalizedString("Loop will automatically bolus when bg is predicted to be higher than target range, and will use temp basals when bg is predicted to be lower than target range.", comment: "Description string for automatic bolus dosing strategy") + } + } +} + public struct LoopSettings: Equatable { public var dosingEnabled = false @@ -32,6 +57,8 @@ public struct LoopSettings: Equatable { public var suspendThreshold: GlucoseThreshold? = nil public let retrospectiveCorrectionEnabled = true + + public var dosingStrategy: DosingStrategy = .tempBasalOnly /// The interval over which to aggregate changes in glucose for retrospective correction public let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) @@ -49,6 +76,8 @@ public struct LoopSettings: Equatable { public let defaultWatchCarbPickerValue = 15 // grams public let defaultWatchBolusPickerValue = 1.0 // % + + public let bolusPartialApplicationFactor = 0.4 // % // MARK - Display settings @@ -234,6 +263,12 @@ extension LoopSettings: RawRepresentable { if let rawThreshold = rawValue["minimumBGGuard"] as? GlucoseThreshold.RawValue { self.suspendThreshold = GlucoseThreshold(rawValue: rawThreshold) } + + if let rawDosingStrategy = rawValue["dosingStrategy"] as? DosingStrategy.RawValue, + let dosingStrategy = DosingStrategy(rawValue: rawDosingStrategy) { + self.dosingStrategy = dosingStrategy + } + } public var rawValue: RawValue { @@ -250,6 +285,7 @@ extension LoopSettings: RawRepresentable { raw["maximumBasalRatePerHour"] = maximumBasalRatePerHour raw["maximumBolus"] = maximumBolus raw["minimumBGGuard"] = suspendThreshold?.rawValue + raw["dosingStrategy"] = dosingStrategy.rawValue return raw } diff --git a/Scripts/carthage.sh b/Scripts/carthage.sh new file mode 100755 index 0000000000..43734c9b53 --- /dev/null +++ b/Scripts/carthage.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# +# This script exists due to an issue with Carthage. The script comes from https://github.com/Carthage/Carthage/issues/3019#issuecomment-665136323 + +# carthage.sh +# Usage example: ./carthage.sh build --platform iOS + +set -euo pipefail + +xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) +trap 'rm -f "$xcconfig"' INT TERM HUP EXIT + +# For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise +# the build will fail on lipo due to duplicate architectures. +echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = arm64 arm64e armv7 armv7s armv6 armv8' >> $xcconfig +echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig + +export XCODE_XCCONFIG_FILE="$xcconfig" +"${SRCROOT}/bin/carthage" "$@" diff --git a/Scripts/copy-frameworks.sh b/Scripts/copy-frameworks.sh index added33f8b..76a2f0826f 100755 --- a/Scripts/copy-frameworks.sh +++ b/Scripts/copy-frameworks.sh @@ -38,4 +38,4 @@ for COUNTER in $(seq 0 $(($SCRIPT_INPUT_FILE_COUNT - 1))); do done echo "Copy Frameworks with Carthage" -carthage copy-frameworks +"${SRCROOT}/bin/carthage" copy-frameworks diff --git a/bin/carthage b/bin/carthage new file mode 100755 index 0000000000..e77f60a5bd Binary files /dev/null and b/bin/carthage differ