Skip to content

Commit fff37ef

Browse files
committed
Merge remote-tracking branch 'ivalkou/dev-ivan' into dev-spike-microbolus-dt
* ivalkou/dev-ivan: (37 commits) MB with COB and without COB work independently revert notification names Cartfile.resolved Notification names changed in LoopKit (LoopKit#1165) Add ability to define a lower limit before performing the bolus (LoopKit#4) Fix texts Safe Mode refactored nonlinear-carb-model Revert "Carb Absorption Model UI" Carb Absorption Model UI Microboluses Safe Mode Add 15 min control clucose chek Fix MB without COM Bump version Microbolus screen (LoopKit#3) build version changed disable Microboluses if closed loop disabled misprint fixed little refactor fix 0 bolus bug ... # Conflicts: # Cartfile # Loop.xcodeproj/project.pbxproj # Loop/Managers/CGMManager.swift
2 parents 0b0c85d + 959fe30 commit fff37ef

17 files changed

+626
-48
lines changed

Cartfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
github "LoopKit/LoopKit" "dev"
1+
github "dm61/LoopKit" "nonlinear-carb-model"
22
github "LoopKit/CGMBLEKit" "dev"
33
github "i-schuetz/SwiftCharts" == 0.6.5
44
github "LoopKit/dexcom-share-client-swift" "dev"
55
github "LoopKit/G4ShareSpy" "dev"
66
github "ps2/rileylink_ios" "dev"
77
github "LoopKit/Amplitude-iOS" "decreepify"
88
github "dthornley/spike-client-swift-195" "dev-workspace"
9+
github "ivalkou/NightscoutAPIClient" "master"

Common/Models/WatchContext.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ final class WatchContext: RawRepresentable {
3131
var lastNetTempBasalDose: Double?
3232
var lastNetTempBasalDate: Date?
3333
var recommendedBolusDose: Double?
34+
var doNotOpenBolusScreenWithMicroboluses: Bool?
3435

3536
var cob: Double?
3637
var iob: Double?
@@ -68,6 +69,7 @@ final class WatchContext: RawRepresentable {
6869
lastNetTempBasalDate = rawValue["bad"] as? Date
6970
recommendedBolusDose = rawValue["rbo"] as? Double
7071
cob = rawValue["cob"] as? Double
72+
doNotOpenBolusScreenWithMicroboluses = rawValue["mb"] as? Bool
7173

7274
cgmManagerState = rawValue["cgmManagerState"] as? CGMManager.RawStateValue
7375

@@ -100,6 +102,7 @@ final class WatchContext: RawRepresentable {
100102
raw["r"] = reservoir
101103
raw["rbo"] = recommendedBolusDose
102104
raw["rp"] = reservoirPercentage
105+
raw["mb"] = doNotOpenBolusScreenWithMicroboluses
103106

104107
raw["pg"] = predictedGlucose?.rawValue
105108

Loop.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
MAIN_APP_BUNDLE_IDENTIFIER = com.${DEVELOPMENT_TEAM}.loopkit
1212
MAIN_APP_DISPLAY_NAME = Loop
1313

14-
LOOP_MARKETING_VERSION = 1.10.1
14+
LOOP_MARKETING_VERSION = 1.10.1-microboluses
1515

1616
APPICON_NAME = AppIcon
1717

Loop.xcodeproj/project.pbxproj

Lines changed: 56 additions & 12 deletions
Large diffs are not rendered by default.

Loop/Info.plist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
<false/>
3838
<key>LSApplicationQueriesSchemes</key>
3939
<array>
40+
<string>spikeapp</string>
4041
<string>dexcomg6</string>
4142
<string>dexcomcgm</string>
4243
<string>dexcomshare</string>
@@ -92,6 +93,11 @@
9293
<string>UIInterfaceOrientationLandscapeRight</string>
9394
<string>UIInterfaceOrientationPortraitUpsideDown</string>
9495
</array>
96+
<key>NSAppTransportSecurity</key>
97+
<dict>
98+
<key>NSAllowsArbitraryLoads</key>
99+
<true/>
100+
</dict>
95101
<key>UISupportedInterfaceOrientations~ipad</key>
96102
<array>
97103
<string>UIInterfaceOrientationPortrait</string>

Loop/Managers/CGMManager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import G4ShareSpy
1111
import ShareClient
1212
import MockKit
1313
import SpikeClient
14+
import NightscoutAPIClient
1415

1516

1617
let allCGMManagers: [CGMManager.Type] = [
1718
G6CGMManager.self,
1819
G5CGMManager.self,
1920
G4CGMManager.self,
2021
ShareClientManager.self,
22+
NightscoutAPIManager.self,
2123
MockCGMManager.self,
2224
SpikeClientManager.self,
2325
]

Loop/Managers/DeviceDataManager.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,13 @@ extension DeviceDataManager: LoopDataManagerDelegate {
672672
}
673673
)
674674
}
675+
676+
func loopDataManager(_ manager: LoopDataManager, didRecommendMicroBolus bolus: (amount: Double, date: Date), completion: @escaping (_ error: Error?) -> Void) -> Void {
677+
enactBolus(
678+
units: bolus.amount,
679+
at: bolus.date,
680+
completion: completion)
681+
}
675682
}
676683

677684

Loop/Managers/LoopDataManager.swift

Lines changed: 189 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Foundation
1010
import HealthKit
1111
import LoopKit
1212
import LoopCore
13+
import Combine
1314

1415

1516
final 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

13071467
private extension TemporaryScheduleOverride {

Loop/Managers/WatchDataManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ final class WatchDataManager: NSObject {
155155
context.recommendedBolusDose = state.recommendedBolus?.recommendation.amount
156156
context.cob = state.carbsOnBoard?.quantity.doubleValue(for: HKUnit.gram())
157157
context.glucoseTrendRawValue = self.deviceManager.sensorState?.trendType?.rawValue
158+
context.doNotOpenBolusScreenWithMicroboluses = loopManager.settings.dosingEnabled
159+
&& loopManager.settings.microbolusesEnabled
160+
&& !loopManager.settings.microbolusesOpenBolusScreen
158161

159162
context.cgmManagerState = self.deviceManager.cgmManager?.rawValue
160163

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// MicrobolusViewController.swift
3+
// Loop
4+
//
5+
// Created by Ivan Valkou on 01.11.2019.
6+
// Copyright © 2019 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
final class MicrobolusViewController: UIHostingController<MicrobolusView> {
12+
init(viewModel: MicrobolusView.ViewModel) {
13+
super.init(rootView: MicrobolusView(viewModel: viewModel))
14+
}
15+
16+
@objc required dynamic init?(coder aDecoder: NSCoder) {
17+
fatalError("init(coder:) has not been implemented")
18+
}
19+
20+
var onDeinit: (() -> Void)?
21+
22+
deinit {
23+
onDeinit?()
24+
}
25+
}

0 commit comments

Comments
 (0)