@@ -10,6 +10,7 @@ import Foundation
1010import HealthKit
1111import LoopKit
1212import LoopCore
13+ import Combine
1314
1415
1516final class LoopDataManager {
@@ -33,6 +34,8 @@ final class LoopDataManager {
3334
3435 private let logger : CategoryLogger
3536
37+ private var loopSubscription : AnyCancellable ?
38+
3639 // References to registered notification center observers
3740 private var notificationObservers : [ Any ] = [ ]
3841
@@ -68,7 +71,8 @@ final class LoopDataManager {
6871 defaultAbsorptionTimes: LoopSettings . defaultCarbAbsorptionTimes,
6972 carbRatioSchedule: carbRatioSchedule,
7073 insulinSensitivitySchedule: insulinSensitivitySchedule,
71- overrideHistory: overrideHistory
74+ overrideHistory: overrideHistory,
75+ carbAbsorptionModel: . adaptiveRateNonlinear
7276 )
7377
7478 doseStore = DoseStore (
@@ -636,41 +640,75 @@ extension LoopDataManager {
636640 /// Executes an analysis of the current data, and recommends an adjustment to the current
637641 /// temporary basal rate.
638642 func loop( ) {
639- self . dataAccessQueue. async {
640- self . logger. default ( " Loop running " )
641- NotificationCenter . default. post ( name: . LoopRunning, object: self )
643+ let updatePublisher = Deferred {
644+ Future < ( ) , Error > { promise in
645+ do {
646+ try self . update ( )
647+ promise ( . success( ( ) ) )
648+ } catch let error {
649+ promise ( . failure( error) )
650+ }
651+ }
652+ }
653+ . subscribe ( on: dataAccessQueue)
654+ . eraseToAnyPublisher ( )
642655
643- self . lastLoopError = nil
644- let startDate = Date ( )
656+ let enactBolusPublisher = Deferred {
657+ Future < Bool , Error > { promise in
658+ self . calculateAndEnactMicroBolusIfNeeded { enacted, error in
659+ if let error = error {
660+ promise ( . failure( error) )
661+ }
662+ promise ( . success( enacted) )
663+ }
664+ }
665+ }
666+ . subscribe ( on: dataAccessQueue)
667+ . eraseToAnyPublisher ( )
645668
646- do {
647- try self . update ( )
669+ let setBasalPublisher = Deferred {
670+ Future < ( ) , Error > { promise in
671+ guard self . settings. dosingEnabled else {
672+ return promise ( . success( ( ) ) )
673+ }
648674
649- if self . settings. dosingEnabled {
650- self . setRecommendedTempBasal { ( error) -> Void in
651- self . lastLoopError = error
652-
653- if let error = error {
654- self . logger. error ( error)
655- } else {
656- self . loopDidComplete ( date: Date ( ) , duration: - startDate. timeIntervalSinceNow)
657- }
658- self . logger. default ( " Loop ended " )
659- self . notify ( forChange: . tempBasal)
675+ self . setRecommendedTempBasal { error in
676+ if let error = error {
677+ promise ( . failure( error) )
660678 }
661-
662- // Delay the notification until we know the result of the temp basal
663- return
664- } else {
665- self . loopDidComplete ( date: Date ( ) , duration: - startDate. timeIntervalSinceNow)
679+ promise ( . success( ( ) ) )
666680 }
667- } catch let error {
668- self . lastLoopError = error
669- }
670681
671- self . logger. default ( " Loop ended " )
672- self . notify ( forChange: . tempBasal)
682+ }
673683 }
684+ . subscribe ( on: dataAccessQueue)
685+ . eraseToAnyPublisher ( )
686+
687+ logger. default ( " Loop running " )
688+ NotificationCenter . default. post ( name: . LoopRunning, object: self )
689+
690+ loopSubscription? . cancel ( )
691+
692+ let startDate = Date ( )
693+ loopSubscription = updatePublisher
694+ . flatMap { _ in setBasalPublisher }
695+ . flatMap { _ in enactBolusPublisher }
696+ . receive ( on: dataAccessQueue)
697+ . sink (
698+ receiveCompletion: { completion in
699+ switch completion {
700+ case . finished:
701+ self . lastLoopError = nil
702+ self . loopDidComplete ( date: Date ( ) , duration: - startDate. timeIntervalSinceNow)
703+ case let . failure( error) :
704+ self . lastLoopError = error
705+ self . logger. error ( error)
706+ }
707+ self . logger. default ( " Loop ended " )
708+ self . notify ( forChange: . tempBasal)
709+ } ,
710+ receiveValue: { _ in }
711+ )
674712 }
675713
676714 /// - Throws:
@@ -1030,6 +1068,120 @@ extension LoopDataManager {
10301068 self . logger. debug ( " Recommending bolus: \( String ( describing: recommendedBolus) ) " )
10311069 }
10321070
1071+ /// *This method should only be called from the `dataAccessQueue`*
1072+ private func calculateAndEnactMicroBolusIfNeeded( _ completion: @escaping ( _ enacted: Bool , _ error: Error ? ) -> Void ) {
1073+ dispatchPrecondition ( condition: . onQueue( dataAccessQueue) )
1074+
1075+ guard settings. dosingEnabled else {
1076+ logger. debug ( " Closed loop disabled. Cancel microbolus calculation. " )
1077+ completion ( false , nil )
1078+ return
1079+ }
1080+
1081+ let cob = carbsOnBoard? . quantity. doubleValue ( for: . gram( ) ) ?? 0
1082+ let cobChek = ( cob > 0 && settings. microbolusesEnabled) || ( cob == 0 && settings. microbolusesWithoutCarbsEnabled)
1083+
1084+ guard cobChek else {
1085+ logger. debug ( " Microboluses disabled. " )
1086+ completion ( false , nil )
1087+ return
1088+ }
1089+
1090+ let startDate = Date ( )
1091+
1092+ guard let recommendedBolus = recommendedBolus else {
1093+ logger. debug ( " No recommended bolus. Cancel microbolus calculation. " )
1094+ completion ( false , nil )
1095+ return
1096+ }
1097+
1098+ guard abs ( recommendedBolus. date. timeIntervalSinceNow) < TimeInterval ( minutes: 5 ) else {
1099+ completion ( false , LoopError . recommendationExpired ( date: recommendedBolus. date) )
1100+ return
1101+ }
1102+
1103+ guard let currentBasalRate = basalRateScheduleApplyingOverrideHistory? . value ( at: startDate) else {
1104+ logger. debug ( " Basal rates not configured. Cancel microbolus calculation. " )
1105+ completion ( false , nil )
1106+ return
1107+ }
1108+
1109+ let insulinReq = recommendedBolus. recommendation. amount
1110+ guard insulinReq > 0 else {
1111+ logger. debug ( " No microbolus needed. " )
1112+ completion ( false , nil )
1113+ return
1114+ }
1115+
1116+ guard let glucose = self . glucoseStore. latestGlucose, let predictedGlucose = predictedGlucose else {
1117+ logger. debug ( " Glucose data not found. " )
1118+ completion ( false , nil )
1119+ return
1120+ }
1121+
1122+ let controlDate = glucose. startDate. addingTimeInterval ( . minutes( 15 + 1 ) ) // extra 1 min to find
1123+ var controlGlucoseQuantity : HKQuantity ?
1124+
1125+ for prediction in predictedGlucose {
1126+ if prediction. startDate <= controlDate {
1127+ controlGlucoseQuantity = prediction. quantity
1128+ continue
1129+ }
1130+ break
1131+ }
1132+
1133+ let lowTrend = controlGlucoseQuantity. map { $0 < glucose. quantity } ?? true
1134+
1135+ let safetyCheck = !( lowTrend && settings. microbolusesSafeMode == . enabled)
1136+ guard safetyCheck else {
1137+ logger. debug ( " Control glucose is lower then current. Microbolus is not allowed. " )
1138+ completion ( false , nil )
1139+ return
1140+ }
1141+
1142+ let maxBasalMinutes : Double = {
1143+ switch ( cob > 0 , lowTrend, settings. microbolusesSafeMode == . disabled) {
1144+ case ( true , false , _) , ( true , true , true ) :
1145+ return settings. microbolusesSize
1146+ case ( false , false , _) , ( false , true , true ) :
1147+ return settings. microbolusesWithoutCarbsSize
1148+ default :
1149+ return 30
1150+ }
1151+ } ( )
1152+
1153+ let maxMicroBolus = currentBasalRate * maxBasalMinutes / 60
1154+
1155+ let volumeRounder = { ( _ units: Double ) in
1156+ self . delegate? . loopDataManager ( self , roundBolusVolume: units) ?? units
1157+ }
1158+
1159+ let microBolus = volumeRounder ( min ( insulinReq / 2 , maxMicroBolus) )
1160+ guard microBolus > 0 else {
1161+ logger. debug ( " No microbolus needed. " )
1162+ completion ( false , nil )
1163+ return
1164+ }
1165+ guard microBolus >= settings. microbolusesMinimumBolusSize else {
1166+ logger. debug ( " Microbolus will not be enacted due to it being lower than the configured minimum bolus size. ( \( String ( describing: microBolus) ) vs \( String ( describing: settings. microbolusesMinimumBolusSize) ) ) " )
1167+ completion ( false , nil )
1168+ return
1169+ }
1170+
1171+ let recommendation = ( amount: microBolus, date: startDate)
1172+ logger. debug ( " Enact microbolus: \( String ( describing: microBolus) ) " )
1173+
1174+ self . delegate? . loopDataManager ( self , didRecommendMicroBolus: recommendation) { [ weak self] error in
1175+ if let error = error {
1176+ self ? . logger. debug ( " Microbolus failed: \( error. localizedDescription) " )
1177+ completion ( false , error)
1178+ } else {
1179+ self ? . logger. debug ( " Microbolus enacted " )
1180+ completion ( true , nil )
1181+ }
1182+ }
1183+ }
1184+
10331185 /// *This method should only be called from the `dataAccessQueue`*
10341186 private func setRecommendedTempBasal( _ completion: @escaping ( _ error: Error ? ) -> Void ) {
10351187 dispatchPrecondition ( condition: . onQueue( dataAccessQueue) )
@@ -1302,6 +1454,14 @@ protocol LoopDataManagerDelegate: class {
13021454 /// - units: The recommended bolus in U
13031455 /// - Returns: a supported bolus volume in U. The volume returned should not be larger than the passed in rate.
13041456 func loopDataManager( _ manager: LoopDataManager , roundBolusVolume units: Double ) -> Double
1457+
1458+ /// Informs the delegate that a micro bolus is recommended
1459+ ///
1460+ /// - Parameters:
1461+ /// - manager: The manager
1462+ /// - bolus: The new recommended micro bolus
1463+ /// - completion: A closure called once on completion
1464+ func loopDataManager( _ manager: LoopDataManager , didRecommendMicroBolus bolus: ( amount: Double , date: Date ) , completion: @escaping ( _ error: Error ? ) -> Void ) -> Void
13051465}
13061466
13071467private extension TemporaryScheduleOverride {
0 commit comments