Skip to content

Commit 172243e

Browse files
authored
Prep for supporting insulin delivery in HealthKit (#564)
1 parent a520f9e commit 172243e

12 files changed

+175
-92
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
437CCAE01D285C7B0075D2C3 /* ServiceAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CCADF1D285C7B0075D2C3 /* ServiceAuthentication.swift */; };
7474
437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEE31CDE5C0A003C8C80 /* UIImage.swift */; };
7575
437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */; };
76+
4381D2261F3C0FDD004ACCF9 /* RileyLinkDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4381D2251F3C0FDD004ACCF9 /* RileyLinkDevice.swift */; };
7677
43846AD51D8FA67800799272 /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43846AD41D8FA67800799272 /* Base.lproj */; };
7778
43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43846ADA1D91057000799272 /* ContextUpdatable.swift */; };
7879
438849EA1D297CB6003B3F23 /* NightscoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849E91D297CB6003B3F23 /* NightscoutService.swift */; };
@@ -248,7 +249,6 @@
248249
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 */; };
249250
C17824A51E1AD4D100D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; };
250251
C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; };
251-
C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884621D51A7A400405663 /* BatteryIndicator.swift */; };
252252
C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; };
253253
C1C6591C1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json in Resources */ = {isa = PBXBuildFile; fileRef = C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */; };
254254
C1C73F0D1DE3D0270022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F0F1DE3D0270022FC89 /* InfoPlist.strings */; };
@@ -445,6 +445,7 @@
445445
437CEEE31CDE5C0A003C8C80 /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
446446
437D9BA11D7B5203007245E8 /* Loop.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Loop.xcconfig; sourceTree = "<group>"; };
447447
437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionTableViewController.swift; sourceTree = "<group>"; };
448+
4381D2251F3C0FDD004ACCF9 /* RileyLinkDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkDevice.swift; sourceTree = "<group>"; };
448449
43846AD41D8FA67800799272 /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base.lproj; sourceTree = "<group>"; };
449450
43846ADA1D91057000799272 /* ContextUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextUpdatable.swift; sourceTree = "<group>"; };
450451
438849E91D297CB6003B3F23 /* NightscoutService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutService.swift; sourceTree = "<group>"; };
@@ -588,7 +589,6 @@
588589
C178249F1E19CF9800D9D25C /* GlucoseThresholdTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseThresholdTableViewController.swift; sourceTree = "<group>"; };
589590
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 = "<group>"; };
590591
C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusRecommendation.swift; sourceTree = "<group>"; };
591-
C17884621D51A7A400405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = "<group>"; };
592592
C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = "<group>"; };
593593
C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_dropping_then_rising.json; sourceTree = "<group>"; };
594594
C1C73F0E1DE3D0270022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -877,7 +877,6 @@
877877
43E344A01B9E144300C85C07 /* Extensions */ = {
878878
isa = PBXGroup;
879879
children = (
880-
C17884621D51A7A400405663 /* BatteryIndicator.swift */,
881880
C17824991E1999FA00D9D25C /* CaseCountable.swift */,
882881
4F6663931E905FD2009E74FC /* ChartColorPalette+Loop.swift */,
883882
4346D1F51C78501000ABAFE3 /* ChartPoint+Loop.swift */,
@@ -891,6 +890,7 @@
891890
4398973A1CD2FC2000223065 /* NSDateFormatter.swift */,
892891
43E344A31B9E1B1C00C85C07 /* NSUserDefaults.swift */,
893892
43441A9F1EDB4D390087958C /* OSLog.swift */,
893+
4381D2251F3C0FDD004ACCF9 /* RileyLinkDevice.swift */,
894894
43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */,
895895
43F41C361D3BF32400C11ED6 /* UIAlertController.swift */,
896896
43BFF0BB1E45C80600FF19A9 /* UIColor+Loop.swift */,
@@ -1510,6 +1510,7 @@
15101510
434F54571D287FDB002A9274 /* NibLoadable.swift in Sources */,
15111511
43441A9C1EDB34810087958C /* StatusExtensionContext+LoopKit.swift in Sources */,
15121512
4FF4D1001E18374700846527 /* WatchContext.swift in Sources */,
1513+
4381D2261F3C0FDD004ACCF9 /* RileyLinkDevice.swift in Sources */,
15131514
4315D28A1CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift in Sources */,
15141515
43C418B51CE0575200405B6A /* ShareGlucose+GlucoseKit.swift in Sources */,
15151516
4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */,
@@ -1549,7 +1550,6 @@
15491550
437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */,
15501551
43DBF0591C93F73800B3C386 /* CarbEntryTableViewController.swift in Sources */,
15511552
43E93FB71E469A5100EAB8DB /* HKUnit.swift in Sources */,
1552-
C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */,
15531553
43BFF0BC1E45C80600FF19A9 /* UIColor+Loop.swift in Sources */,
15541554
43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */,
15551555
4F08DE9D1E81D0E9006741EA /* StatusChartsManager+LoopKit.swift in Sources */,

Loop/Extensions/BatteryIndicator.swift

Lines changed: 0 additions & 26 deletions
This file was deleted.

Loop/Extensions/DoseStore.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,17 @@ extension LoopDataManager {
8181
}
8282
case let alarm as PumpAlarmPumpEvent:
8383
eventType = .alarm
84+
8485
if case .noDelivery = alarm.alarmType {
85-
// TODO: Interpret a no delivery alarm as a suspend
86+
dose = DoseEntry(suspendDate: event.date)
8687
}
8788
break
88-
case is ClearAlarmPumpEvent:
89+
case let alarm as ClearAlarmPumpEvent:
8990
eventType = .alarmClear
90-
// TODO: Interpret a clear no delivery as a Resume
91+
92+
if case .noDelivery = alarm.alarmType {
93+
dose = DoseEntry(resumeDate: event.date)
94+
}
9195
break
9296
default:
9397
break
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// RileyLinkDevice.swift
3+
// Loop
4+
//
5+
// Copyright © 2017 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import HealthKit
9+
import MinimedKit
10+
import RileyLinkKit
11+
12+
13+
extension RileyLinkDevice {
14+
var device: HKDevice? {
15+
return HKDevice(
16+
name: name,
17+
manufacturer: "Medtronic",
18+
model: pumpState?.pumpModel?.rawValue,
19+
hardwareVersion: nil,
20+
firmwareVersion: firmwareVersion,
21+
softwareVersion: String(RileyLinkKitVersionNumber),
22+
localIdentifier: pumpState?.pumpID,
23+
udiDeviceIdentifier: nil
24+
)
25+
}
26+
}

Loop/Managers/DeviceDataManager.swift

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ final class DeviceDataManager {
3636

3737
private var nightscoutDataManager: NightscoutDataManager!
3838

39-
var latestPumpStatus: RileyLinkKit.PumpStatus?
39+
fileprivate var latestPumpStatus: RileyLinkKit.PumpStatus?
4040

4141
private(set) var lastError: (date: Date, error: Error)?
4242

@@ -61,7 +61,7 @@ final class DeviceDataManager {
6161
}
6262

6363
// Battery monitor
64-
func observeBatteryDuring(_ block: () -> Void) {
64+
fileprivate func observeBatteryDuring(_ block: () -> Void) {
6565
let oldVal = pumpBatteryChargeRemaining
6666
block()
6767
if let newVal = pumpBatteryChargeRemaining {
@@ -79,6 +79,17 @@ final class DeviceDataManager {
7979

8080
@objc private func receivedRileyLinkManagerNotification(_ note: Notification) {
8181
NotificationCenter.default.post(name: note.name, object: self, userInfo: note.userInfo)
82+
83+
switch note.name {
84+
case Notification.Name.DeviceConnectionStateDidChange,
85+
Notification.Name.DeviceNameDidChange:
86+
// Update the HKDevice to include the name or connection status change
87+
if let device = rileyLinkManager.firstConnectedDevice?.device {
88+
loopManager.doseStore.setDevice(device)
89+
}
90+
default:
91+
break
92+
}
8293
}
8394

8495
/**
@@ -136,9 +147,45 @@ final class DeviceDataManager {
136147
}
137148
}
138149

150+
fileprivate func updateTimerTickPreference() {
151+
/// Controls the management of the RileyLink timer tick, which is a reliably-changing BLE
152+
/// characteristic which can cause the app to wake. For most users, the G5 Transmitter and
153+
/// G4 Receiver are reliable as hearbeats, but users who find their resources extremely constrained
154+
/// due to greedy apps or older devices may choose to always enable the timer by always setting `true`
155+
rileyLinkManager.timerTickEnabled = pumpDataIsStale() || !(cgmManager?.providesBLEHeartbeat == true)
156+
}
157+
158+
/**
159+
Attempts to fix an extended communication failure between a RileyLink device and the pump
160+
161+
- parameter device: The RileyLink device
162+
*/
163+
private func troubleshootPumpComms(using device: RileyLinkDevice) {
164+
// Ensuring timer tick is enabled will allow more tries to bring the pump data up-to-date.
165+
updateTimerTickPreference()
166+
167+
// How long we should wait before we re-tune the RileyLink
168+
let tuneTolerance = TimeInterval(minutes: 14)
169+
170+
if device.lastTuned == nil || device.lastTuned!.timeIntervalSinceNow <= -tuneTolerance {
171+
device.tunePump { (result) in
172+
switch result {
173+
case .success(let scanResult):
174+
self.logger.addError("Device \(device.name ?? "") auto-tuned to \(scanResult.bestFrequency) MHz", fromSource: "RileyLink")
175+
case .failure(let error):
176+
self.logger.addError("Device \(device.name ?? "") auto-tune failed with error: \(error)", fromSource: "RileyLink")
177+
self.rileyLinkManager.deprioritizeDevice(device: device)
178+
self.setLastError(error: error)
179+
}
180+
}
181+
} else {
182+
rileyLinkManager.deprioritizeDevice(device: device)
183+
}
184+
}
185+
139186
// MARK: Pump data
140187

141-
var latestPumpStatusFromMySentry: MySentryPumpStatusMessageBody?
188+
fileprivate var latestPumpStatusFromMySentry: MySentryPumpStatusMessageBody?
142189

143190
/**
144191
Handles receiving a MySentry status message, which are only posted by MM x23 pumps.
@@ -266,6 +313,9 @@ final class DeviceDataManager {
266313
}
267314
}
268315
}
316+
317+
// New reservoir data means we may want to adjust our timer tick requirements
318+
self.updateTimerTickPreference()
269319
}
270320
}
271321

@@ -303,7 +353,7 @@ final class DeviceDataManager {
303353

304354
private func pumpDataIsStale() -> Bool {
305355
// How long should we wait before we poll for new pump data?
306-
let pumpStatusAgeTolerance = rileyLinkManager.idleListeningEnabled ? TimeInterval(minutes: 11) : TimeInterval(minutes: 4)
356+
let pumpStatusAgeTolerance = rileyLinkManager.idleListeningEnabled ? TimeInterval(minutes: 9) : TimeInterval(minutes: 4)
307357

308358
return loopManager.doseStore.lastReservoirValue == nil
309359
|| loopManager.doseStore.lastReservoirValue!.startDate.timeIntervalSinceNow <= -pumpStatusAgeTolerance
@@ -421,31 +471,6 @@ final class DeviceDataManager {
421471
}
422472
}
423473

424-
/**
425-
Attempts to fix an extended communication failure between a RileyLink device and the pump
426-
427-
- parameter device: The RileyLink device
428-
*/
429-
private func troubleshootPumpComms(using device: RileyLinkDevice) {
430-
// How long we should wait before we re-tune the RileyLink
431-
let tuneTolerance = TimeInterval(minutes: 14)
432-
433-
if device.lastTuned == nil || device.lastTuned!.timeIntervalSinceNow <= -tuneTolerance {
434-
device.tunePump { (result) in
435-
switch result {
436-
case .success(let scanResult):
437-
self.logger.addError("Device \(device.name ?? "") auto-tuned to \(scanResult.bestFrequency) MHz", fromSource: "RileyLink")
438-
case .failure(let error):
439-
self.logger.addError("Device \(device.name ?? "") auto-tune failed with error: \(error)", fromSource: "RileyLink")
440-
self.rileyLinkManager.deprioritizeDevice(device: device)
441-
self.setLastError(error: error)
442-
}
443-
}
444-
} else {
445-
rileyLinkManager.deprioritizeDevice(device: device)
446-
}
447-
}
448-
449474
// MARK: - CGM
450475

451476
var cgm: CGM? = UserDefaults.standard.cgm {
@@ -465,11 +490,7 @@ final class DeviceDataManager {
465490
cgmManager?.delegate = self
466491
loopManager.glucoseStore.managedDataInterval = cgmManager?.managedDataInterval
467492

468-
/// Controls the management of the RileyLink timer tick, which is a reliably-changing BLE
469-
/// characteristic which can cause the app to wake. For most users, the G5 Transmitter and
470-
/// G4 Receiver are reliable as hearbeats, but users who find their resources extremely constrained
471-
/// due to greedy apps or older devices may choose to always enable the timer by always setting `true`
472-
rileyLinkManager.timerTickEnabled = !(cgmManager?.providesBLEHeartbeat == true)
493+
updateTimerTickPreference()
473494
}
474495

475496
var sensorInfo: SensorDisplayable? {
@@ -545,6 +566,16 @@ final class DeviceDataManager {
545566
rileyLinkManager.idleListeningEnabled = false
546567
}
547568

569+
// Update the HKDevice to include the model change
570+
if let device = rileyLinkManager.firstConnectedDevice?.device {
571+
loopManager.doseStore.setDevice(device)
572+
}
573+
574+
// Update the preference for basal profile start events
575+
if let recordsBasalProfileStartEvents = pumpState?.pumpModel?.recordsBasalProfileStartEvents {
576+
loopManager.doseStore.pumpRecordsBasalProfileStartEvents = recordsBasalProfileStartEvents
577+
}
578+
548579
UserDefaults.standard.pumpModelNumber = pumpState?.pumpModel?.rawValue
549580
case "pumpRegion"?:
550581
UserDefaults.standard.pumpRegion = pumpState?.pumpRegion
@@ -630,6 +661,10 @@ final class DeviceDataManager {
630661

631662
loopManager.carbStore.syncDelegate = remoteDataManager.nightscoutService.uploader
632663
loopManager.doseStore.delegate = self
664+
// Proliferate PumpModel preferences to DoseStore
665+
if let pumpModel = pumpState?.pumpModel {
666+
loopManager.doseStore.pumpRecordsBasalProfileStartEvents = pumpModel.recordsBasalProfileStartEvents
667+
}
633668

634669
setupCGM()
635670
}

Loop/Managers/LoopDataManager.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,40 @@ final class LoopDataManager {
212212
settings.glucoseTargetRangeSchedule?.timeZone = timeZone
213213
}
214214

215+
/// All the HealthKit types to be read by stores
216+
var readTypes: Set<HKSampleType> {
217+
return glucoseStore.readTypes.union(
218+
carbStore.readTypes).union(
219+
doseStore.readTypes)
220+
}
221+
222+
/// All the HealthKit types we to be shared by stores
223+
var shareTypes: Set<HKSampleType> {
224+
return glucoseStore.shareTypes.union(
225+
carbStore.shareTypes).union(
226+
doseStore.shareTypes)
227+
}
228+
229+
/// True if any stores require HealthKit authorization
230+
var authorizationRequired: Bool {
231+
return glucoseStore.authorizationRequired ||
232+
carbStore.authorizationRequired ||
233+
doseStore.authorizationRequired
234+
}
235+
236+
/// True if the user has explicitly denied access to any stores' HealthKit types
237+
var sharingDenied: Bool {
238+
return glucoseStore.sharingDenied ||
239+
carbStore.sharingDenied ||
240+
doseStore.sharingDenied
241+
}
242+
243+
func authorize(_ completion: @escaping () -> Void) {
244+
carbStore.healthStore.requestAuthorization(toShare: shareTypes, read: readTypes) { (success, error) in
245+
completion()
246+
}
247+
}
248+
215249
// MARK: - Intake
216250

217251
/// Adds and stores glucose data

Loop/View Controllers/BolusViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex
193193
localizedReason: String(format: NSLocalizedString("Authenticate to Bolus %@ Units", comment: "The message displayed during a device authentication prompt for bolus specification"), amountString),
194194
reply: { (success, error) in
195195
if success {
196-
self.setBolusAndClose(bolus)
196+
DispatchQueue.main.async {
197+
self.setBolusAndClose(bolus)
198+
}
197199
}
198200
})
199201
} else {
@@ -244,8 +246,6 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex
244246
}
245247
}
246248

247-
248-
249249
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
250250
super.prepare(for: segue, sender: sender)
251251

Loop/View Controllers/InsulinModelSettingsViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class InsulinModelSettingsViewController: ChartsTableViewController, Identifiabl
102102
override func viewDidLoad() {
103103
super.viewDidLoad()
104104

105+
tableView.rowHeight = UITableViewAutomaticDimension
105106
tableView.estimatedRowHeight = 91
106107
}
107108

Loop/View Controllers/PredictionTableViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas
2222
override func viewDidLoad() {
2323
super.viewDidLoad()
2424

25+
tableView.rowHeight = UITableViewAutomaticDimension
2526
tableView.cellLayoutMarginsFollowReadableWidth = true
2627

2728
charts.glucoseDisplayRange = (

Loop/View Controllers/PumpIDTableViewController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ final class PumpIDTableViewController: TextFieldTableViewController {
5252

5353
override func viewDidLoad() {
5454
super.viewDidLoad()
55+
56+
tableView.rowHeight = UITableViewAutomaticDimension
5557
}
5658

5759
// MARK: - Table view data source

0 commit comments

Comments
 (0)