Skip to content

Commit 1775f42

Browse files
authored
Tmecklem x22 enlite cgm (#378)
* Fetch cgm data from x22 pump * Add fetch glucose flag * Add SensorDisplayable wrapper for pump glucose history events * Restructure glucose history handling * Rename functions to Enlite to improve clarity Group Enlite functions and data together * Add pumpDataIsStale, move enlite fetch into timer tick Rename user default key * Keep stale glucose check to avoid fetching enlite pages too often
1 parent bb7cb32 commit 1775f42

File tree

6 files changed

+168
-27
lines changed

6 files changed

+168
-27
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
4FF4D0F91E17268800846527 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */; };
181181
4FF4D1001E18374700846527 /* WatchContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF4D0FF1E18374700846527 /* WatchContext.swift */; };
182182
4FF4D1011E18375000846527 /* WatchContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FF4D0FF1E18374700846527 /* WatchContext.swift */; };
183+
540DED971E14C75F002B2491 /* EnliteSensorDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */; };
183184
C10428971D17BAD400DD539A /* NightscoutUploadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */; };
184185
C12F21A71DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json in Resources */ = {isa = PBXBuildFile; fileRef = C12F21A61DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json */; };
185186
C15713821DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */; };
@@ -471,6 +472,7 @@
471472
4F75288E1DFE1DC600C322D6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
472473
4FC8C8001DEB93E400A1452E /* NSUserDefaults+StatusExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserDefaults+StatusExtension.swift"; sourceTree = "<group>"; };
473474
4FF4D0FF1E18374700846527 /* WatchContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchContext.swift; sourceTree = "<group>"; };
475+
540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnliteSensorDisplayable.swift; sourceTree = "<group>"; };
474476
C10428961D17BAD400DD539A /* NightscoutUploadKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NightscoutUploadKit.framework; path = Carthage/Build/iOS/NightscoutUploadKit.framework; sourceTree = "<group>"; };
475477
C12F21A61DFA79CB00748193 /* recommend_tamp_basal_very_low_end_in_range.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_tamp_basal_very_low_end_in_range.json; sourceTree = "<group>"; };
476478
C15713811DAC6983005BC4D2 /* MealBolusNightscoutTreatment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MealBolusNightscoutTreatment.swift; sourceTree = "<group>"; };
@@ -595,6 +597,7 @@
595597
438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */,
596598
43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */,
597599
4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */,
600+
540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */,
598601
);
599602
path = Models;
600603
sourceTree = "<group>";
@@ -1356,6 +1359,7 @@
13561359
4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */,
13571360
4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */,
13581361
4398973B1CD2FC2000223065 /* NSDateFormatter.swift in Sources */,
1362+
540DED971E14C75F002B2491 /* EnliteSensorDisplayable.swift in Sources */,
13591363
436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */,
13601364
43E2D8C61D204678004DA55F /* KeychainManager.swift in Sources */,
13611365
433EA4C21D9F39C900CD78FB /* PumpIDTableViewController.swift in Sources */,

Loop/Extensions/NSUserDefaults.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extension UserDefaults {
2626
case MaximumBasalRatePerHour = "com.loudnate.Naterade.MaximumBasalRatePerHour"
2727
case MaximumBolus = "com.loudnate.Naterade.MaximumBolus"
2828
case PreferredInsulinDataSource = "com.loudnate.Loop.PreferredInsulinDataSource"
29+
case FetchEnliteDataEnabled = "com.loopkit.Loop.FetchEnliteDataEnabled"
2930
case PumpID = "com.loudnate.Naterade.PumpID"
3031
case PumpModelNumber = "com.loudnate.Naterade.PumpModelNumber"
3132
case PumpRegion = "com.loopkit.Loop.PumpRegion"
@@ -215,6 +216,15 @@ extension UserDefaults {
215216
}
216217
}
217218

219+
var fetchEnliteDataEnabled: Bool {
220+
get {
221+
return bool(forKey: Key.FetchEnliteDataEnabled.rawValue)
222+
}
223+
set {
224+
set(newValue, forKey: Key.FetchEnliteDataEnabled.rawValue)
225+
}
226+
}
227+
218228
var retrospectiveCorrectionEnabled: Bool {
219229
get {
220230
return bool(forKey: Key.RetrospectiveCorrectionEnabled.rawValue)

Loop/Managers/DeviceDataManager.swift

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,17 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
6363
}
6464
}
6565

66+
var fetchEnliteDataEnabled: Bool {
67+
get {
68+
return UserDefaults.standard.fetchEnliteDataEnabled
69+
}
70+
set {
71+
UserDefaults.standard.fetchEnliteDataEnabled = newValue
72+
}
73+
}
74+
6675
var sensorInfo: SensorDisplayable? {
67-
return latestGlucoseG5 ?? latestGlucoseG4 ?? latestGlucoseFromShare ?? latestPumpStatusFromMySentry
76+
return latestGlucoseG5 ?? latestGlucoseG4 ?? latestGlucoseFromShare ?? latestPumpStatusFromMySentry ?? latestEnliteData
6877
}
6978

7079
var latestPumpStatus: RileyLinkKit.PumpStatus?
@@ -135,6 +144,10 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
135144

136145
@objc private func receivedRileyLinkTimerTickNotification(_ note: Notification) {
137146
backfillGlucoseFromShareIfNeeded() {
147+
if UserDefaults.standard.fetchEnliteDataEnabled {
148+
self.assertCurrentEnliteData()
149+
}
150+
138151
self.assertCurrentPumpData()
139152
}
140153
}
@@ -365,42 +378,43 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
365378
}
366379
}
367380

381+
private func pumpDataIsStale() -> Bool {
382+
// How long should we wait before we poll for new pump data?
383+
let pumpStatusAgeTolerance = rileyLinkManager.idleListeningEnabled ? TimeInterval(minutes: 11) : TimeInterval(minutes: 4)
384+
385+
return doseStore.lastReservoirValue == nil
386+
|| doseStore.lastReservoirValue!.startDate.timeIntervalSinceNow <= -pumpStatusAgeTolerance
387+
}
388+
368389
/**
369390
Ensures pump data is current by either waking and polling, or ensuring we're listening to sentry packets.
370391
*/
371392
private func assertCurrentPumpData() {
372-
guard let device = rileyLinkManager.firstConnectedDevice else {
393+
guard let device = rileyLinkManager.firstConnectedDevice, pumpDataIsStale() else {
373394
return
374395
}
375396

376397
device.assertIdleListening()
377398

378-
// How long should we wait before we poll for new pump data?
379-
let pumpStatusAgeTolerance = rileyLinkManager.idleListeningEnabled ? TimeInterval(minutes: 11) : TimeInterval(minutes: 4)
380-
381-
// If we don't yet have pump status, or it's old, poll for it.
382-
if doseStore.lastReservoirValue == nil ||
383-
doseStore.lastReservoirValue!.startDate.timeIntervalSinceNow <= -pumpStatusAgeTolerance {
384-
readPumpData { (result) in
385-
let nsPumpStatus: NightscoutUploadKit.PumpStatus?
386-
switch result {
387-
case .success(let (status, date)):
388-
self.observeBatteryDuring {
389-
self.latestPumpStatus = status
390-
}
399+
readPumpData { (result) in
400+
let nsPumpStatus: NightscoutUploadKit.PumpStatus?
401+
switch result {
402+
case .success(let (status, date)):
403+
self.observeBatteryDuring {
404+
self.latestPumpStatus = status
405+
}
391406

392-
self.updateReservoirVolume(status.reservoir, at: date, withTimeLeft: nil)
393-
let battery = BatteryStatus(voltage: status.batteryVolts, status: BatteryIndicator(batteryStatus: status.batteryStatus))
407+
self.updateReservoirVolume(status.reservoir, at: date, withTimeLeft: nil)
408+
let battery = BatteryStatus(voltage: status.batteryVolts, status: BatteryIndicator(batteryStatus: status.batteryStatus))
394409

395410

396-
nsPumpStatus = NightscoutUploadKit.PumpStatus(clock: date, pumpID: status.pumpID, iob: nil, battery: battery, suspended: status.suspended, bolusing: status.bolusing, reservoir: status.reservoir)
397-
case .failure(let error):
398-
self.troubleshootPumpComms(using: device)
399-
self.nightscoutDataManager.uploadLoopStatus(loopError: error)
400-
nsPumpStatus = nil
401-
}
402-
self.nightscoutDataManager.uploadDeviceStatus(nsPumpStatus)
411+
nsPumpStatus = NightscoutUploadKit.PumpStatus(clock: date, pumpID: status.pumpID, iob: nil, battery: battery, suspended: status.suspended, bolusing: status.bolusing, reservoir: status.reservoir)
412+
case .failure(let error):
413+
self.troubleshootPumpComms(using: device)
414+
self.nightscoutDataManager.uploadLoopStatus(loopError: error)
415+
nsPumpStatus = nil
403416
}
417+
self.nightscoutDataManager.uploadDeviceStatus(nsPumpStatus)
404418
}
405419
}
406420

@@ -484,6 +498,65 @@ final class DeviceDataManager: CarbStoreDelegate, CarbStoreSyncDelegate, DoseSto
484498
}
485499
}
486500

501+
// MARK: - Enlite
502+
503+
fileprivate var latestEnliteData: EnliteSensorDisplayable?
504+
505+
private func updateEnliteSensorStatus(_ events: [TimestampedGlucoseEvent]) {
506+
let sensorEvents = events.filter({ $0.glucoseEvent is RelativeTimestampedGlucoseEvent })
507+
508+
if let latestSensorEvent = sensorEvents.last?.glucoseEvent as? RelativeTimestampedGlucoseEvent {
509+
self.latestEnliteData = EnliteSensorDisplayable(latestSensorEvent)
510+
}
511+
}
512+
513+
private func assertCurrentEnliteData() {
514+
guard let device = rileyLinkManager.firstConnectedDevice, pumpDataIsStale() else {
515+
return
516+
}
517+
518+
device.assertIdleListening()
519+
520+
let fetchGlucoseSince = glucoseStore?.latestGlucose?.startDate.addingTimeInterval(TimeInterval(minutes: 1)) ?? Date(timeIntervalSinceNow: TimeInterval(hours: -24))
521+
522+
guard fetchGlucoseSince.timeIntervalSinceNow <= TimeInterval(minutes: -4.5) else {
523+
return
524+
}
525+
526+
device.ops?.getGlucoseHistoryEvents(since: fetchGlucoseSince, completion: { (result) in
527+
switch result {
528+
case .success(let glucoseEvents):
529+
530+
defer {
531+
_ = self.remoteDataManager.nightscoutUploader?.processGlucoseEvents(glucoseEvents, source: device.deviceURI)
532+
}
533+
534+
self.updateEnliteSensorStatus(glucoseEvents)
535+
536+
let glucoseValues = glucoseEvents
537+
.filter({ $0.glucoseEvent is SensorValueGlucoseEvent && $0.date > fetchGlucoseSince })
538+
.map({ (e:TimestampedGlucoseEvent) -> (quantity: HKQuantity, date: Date, isDisplayOnly: Bool) in
539+
let glucoseEvent = e.glucoseEvent as! SensorValueGlucoseEvent
540+
let quantity = HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucoseEvent.sgv))
541+
return (quantity: quantity, date: e.date, isDisplayOnly: false)
542+
})
543+
544+
self.glucoseStore?.addGlucoseValues(glucoseValues, device: nil, resultHandler: { (success, _, error) in
545+
if let error = error {
546+
self.logger.addError(error, fromSource: "GlucoseStore")
547+
}
548+
549+
if success {
550+
NotificationCenter.default.post(name: .GlucoseUpdated, object: self)
551+
}
552+
})
553+
554+
case .failure(let error):
555+
self.logger.addError(error, fromSource: "PumpOps")
556+
}
557+
})
558+
}
559+
487560
// MARK: - G5 Transmitter
488561
/// The G5 transmitter is a reliable heartbeat by which we can assert the loop state.
489562

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// EnliteSensorDisplayable.swift
3+
// Loop
4+
//
5+
// Created by Timothy Mecklem on 12/28/16.
6+
// Copyright © 2016 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import LoopUI
11+
import MinimedKit
12+
13+
struct EnliteSensorDisplayable: SensorDisplayable {
14+
public let isStateValid: Bool
15+
public let trendType: LoopUI.GlucoseTrend? = nil
16+
public let isLocal = true
17+
18+
public init?(_ e: RelativeTimestampedGlucoseEvent) {
19+
isStateValid = e is SensorValueGlucoseEvent
20+
}
21+
}

Loop/View Controllers/SettingsTableViewController.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
103103
}
104104

105105
fileprivate enum CGMRow: Int {
106-
case receiverEnabled = 0
106+
case fetchEnlite = 0
107+
case receiverEnabled
107108
case transmitterEnabled
108109
case transmitterID // optional, only displayed if transmitterEnabled
109110

110-
static let count = 3
111+
static let count = 4
111112
}
112113

113114
fileprivate enum ConfigurationRow: Int {
@@ -210,6 +211,17 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
210211
}
211212
cell = configCell
212213
case .cgm:
214+
if case .fetchEnlite = CGMRow(rawValue: indexPath.row)! {
215+
let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell
216+
217+
switchCell.`switch`?.isOn = dataManager.fetchEnliteDataEnabled
218+
switchCell.titleLabel.text = NSLocalizedString("Fetch Enlite Data", comment: "The title text for the fetch enlite data enabled switch cell")
219+
220+
switchCell.`switch`?.addTarget(self, action: #selector(fetchEnliteEnabledChanged(_:)), for: .valueChanged)
221+
222+
return switchCell
223+
}
224+
213225
if case .receiverEnabled = CGMRow(rawValue: indexPath.row)! {
214226
let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell
215227

@@ -235,6 +247,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
235247

236248
let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath)
237249
switch CGMRow(rawValue: indexPath.row)! {
250+
case .fetchEnlite:
251+
break
238252
case .transmitterEnabled:
239253
break
240254
case .transmitterID:
@@ -418,6 +432,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
418432
case .cgm:
419433
let row = CGMRow(rawValue: indexPath.row)!
420434
switch row {
435+
case .fetchEnlite:
436+
break
421437
case .transmitterEnabled:
422438
break
423439
case .transmitterID:
@@ -660,6 +676,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
660676
if dataManager.transmitterEnabled == false {
661677
dataManager.transmitterEnabled = true
662678
disableReceiver()
679+
disableEnlite()
663680
tableView.insertRows(at: [IndexPath(row: CGMRow.transmitterID.rawValue, section:Section.cgm.rawValue)], with: .top)
664681
}
665682
}
@@ -678,6 +695,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
678695

679696
if sender.isOn {
680697
disableTransmitter()
698+
disableEnlite()
681699
}
682700
}
683701

@@ -687,6 +705,21 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
687705
switchCell.`switch`?.setOn(false, animated: true)
688706
}
689707

708+
func fetchEnliteEnabledChanged(_ sender: UISwitch) {
709+
dataManager.fetchEnliteDataEnabled = sender.isOn
710+
711+
if sender.isOn {
712+
disableTransmitter()
713+
disableReceiver();
714+
}
715+
}
716+
717+
private func disableEnlite() {
718+
dataManager.fetchEnliteDataEnabled = false
719+
let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.fetchEnlite.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell
720+
switchCell.`switch`?.setOn(false, animated: true)
721+
}
722+
690723
// MARK: - DailyValueScheduleTableViewControllerDelegate
691724

692725
func dailyValueScheduleTableViewControllerWillFinishUpdating(_ controller: DailyValueScheduleTableViewController) {

LoopUI/Models/SensorDisplayable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public protocol SensorDisplayable {
1919
/// Enumerates the trend of the sensor values
2020
var trendType: GlucoseTrend? { get }
2121

22-
/// Returns wheter the data is from a locally-connected device
22+
/// Returns whether the data is from a locally-connected device
2323
var isLocal: Bool { get }
2424
}

0 commit comments

Comments
 (0)