Skip to content

Commit 98c3aeb

Browse files
authored
Merge pull request #410 from LoopKit/cgm-missing-files
Adding missing files from last cgm refactor pr
2 parents a27f6d2 + 21f3ae2 commit 98c3aeb

File tree

4 files changed

+487
-0
lines changed

4 files changed

+487
-0
lines changed

Loop/Managers/CGM/CGMManager.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// CGMManager.swift
3+
// Loop
4+
//
5+
// Copyright © 2017 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import HealthKit
9+
import LoopUI
10+
11+
12+
/// Describes the result of a CGM manager operation
13+
///
14+
/// - noData: No new data was available or retrieved
15+
/// - newData: New glucose data was received and stored
16+
/// - error: An error occurred while receiving or store data
17+
enum CGMResult {
18+
case noData
19+
case newData([(quantity: HKQuantity, date: Date, isDisplayOnly: Bool)])
20+
case error(Error)
21+
}
22+
23+
24+
protocol CGMManagerDelegate: class {
25+
/// Asks the delegate for a date with which to filter incoming glucose data
26+
///
27+
/// - Parameter manager: The manager instance
28+
/// - Returns: The date data occuring on or after which should be kept
29+
func startDateToFilterNewData(for manager: CGMManager) -> Date?
30+
31+
/// Informs the delegate that the device has updated with a new result
32+
///
33+
/// - Parameters:
34+
/// - manager: The manager instance
35+
/// - result: The result of the update
36+
func cgmManager(_ manager: CGMManager, didUpdateWith result: CGMResult) -> Void
37+
}
38+
39+
40+
protocol CGMManager: CustomDebugStringConvertible {
41+
weak var delegate: CGMManagerDelegate? { get set }
42+
43+
/// Whether the device is capable of waking the app
44+
var providesBLEHeartbeat: Bool { get }
45+
46+
var sensorState: SensorDisplayable? { get }
47+
48+
/// The representation of the device for use in HealthKit
49+
var device: HKDevice? { get }
50+
51+
/// Performs a manual fetch of glucose data from the device, if necessary
52+
///
53+
/// - Parameters:
54+
/// - deviceManager: The device manager instance to use for fetching
55+
/// - completion: A closure called when operation has completed
56+
func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) -> Void
57+
}
58+
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
//
2+
// DexCGMManager.swift
3+
// Loop
4+
//
5+
// Copyright © 2017 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import G4ShareSpy
9+
import HealthKit
10+
import LoopUI
11+
import ShareClient
12+
import xDripG5
13+
14+
15+
class DexCGMManager: CGMManager {
16+
var providesBLEHeartbeat: Bool = true
17+
18+
weak var delegate: CGMManagerDelegate? {
19+
didSet {
20+
shareManager?.delegate = delegate
21+
}
22+
}
23+
24+
func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) {
25+
guard let shareManager = shareManager else {
26+
completion(.noData)
27+
return
28+
}
29+
30+
shareManager.fetchNewDataIfNeeded(with: deviceManager, completion)
31+
}
32+
33+
var sensorState: SensorDisplayable? {
34+
return shareManager?.sensorState
35+
}
36+
37+
fileprivate var shareManager: ShareClientManager? = ShareClientManager()
38+
39+
var device: HKDevice? {
40+
return nil
41+
}
42+
43+
var debugDescription: String {
44+
return [
45+
"## DexCGMManager",
46+
"shareManager: \(String(reflecting: shareManager))",
47+
""
48+
].joined(separator: "\n")
49+
}
50+
}
51+
52+
53+
final class ShareClientManager: CGMManager {
54+
weak var delegate: CGMManagerDelegate?
55+
56+
var providesBLEHeartbeat = false
57+
58+
var sensorState: SensorDisplayable? {
59+
return latestBackfill
60+
}
61+
62+
private var latestBackfill: ShareGlucose?
63+
64+
func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) {
65+
guard let shareClient = deviceManager.remoteDataManager.shareService.client else {
66+
completion(.noData)
67+
return
68+
}
69+
70+
// If our last glucose was less than 4.5 minutes ago, don't fetch.
71+
if let latestGlucose = latestBackfill, latestGlucose.startDate.timeIntervalSinceNow > -TimeInterval(minutes: 4.5) {
72+
completion(.noData)
73+
return
74+
}
75+
76+
shareClient.fetchLast(6) { (error, glucose) in
77+
if let error = error {
78+
completion(.error(error))
79+
return
80+
}
81+
guard let glucose = glucose else {
82+
completion(.noData)
83+
return
84+
}
85+
86+
// Ignore glucose values that are up to a minute newer than our previous value, to account for possible time shifting in Share data
87+
let startDate = self.delegate?.startDateToFilterNewData(for: self)?.addingTimeInterval(TimeInterval(minutes: 1))
88+
let newGlucose = glucose.filterDateRange(startDate, nil).map {
89+
return (quantity: $0.quantity, date: $0.startDate, isDisplayOnly: false)
90+
}
91+
92+
self.latestBackfill = glucose.first
93+
94+
completion(.newData(newGlucose))
95+
}
96+
}
97+
98+
var device: HKDevice? = nil
99+
100+
var debugDescription: String {
101+
return [
102+
"## ShareClientManager",
103+
"latestBackfill: \(latestBackfill)",
104+
""
105+
].joined(separator: "\n")
106+
}
107+
}
108+
109+
110+
final class G5CGMManager: DexCGMManager, TransmitterDelegate {
111+
private let transmitter: Transmitter
112+
113+
init?(transmitterID: String?) {
114+
guard let transmitterID = transmitterID else {
115+
return nil
116+
}
117+
118+
self.transmitter = Transmitter(ID: transmitterID, passiveModeEnabled: true)
119+
120+
super.init()
121+
122+
self.transmitter.delegate = self
123+
}
124+
125+
override var sensorState: SensorDisplayable? {
126+
return latestReading ?? super.sensorState
127+
}
128+
129+
private var latestReading: Glucose? {
130+
didSet {
131+
// Once we have our first reading, disable backfill
132+
shareManager = nil
133+
}
134+
}
135+
136+
override var device: HKDevice? {
137+
return HKDevice(
138+
name: "xDripG5",
139+
manufacturer: "Dexcom",
140+
model: "G5 Mobile",
141+
hardwareVersion: nil,
142+
firmwareVersion: nil,
143+
softwareVersion: String(xDripG5VersionNumber),
144+
localIdentifier: nil,
145+
udiDeviceIdentifier: "00386270000002"
146+
)
147+
}
148+
149+
override var debugDescription: String {
150+
return [
151+
"## G5CGMManager",
152+
"latestReading: \(latestReading)",
153+
"transmitter: \(transmitter)",
154+
super.debugDescription,
155+
""
156+
].joined(separator: "\n")
157+
}
158+
159+
// MARK: - TransmitterDelegate
160+
161+
func transmitter(_ transmitter: Transmitter, didError error: Error) {
162+
delegate?.cgmManager(self, didUpdateWith: .error(error))
163+
}
164+
165+
func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) {
166+
guard glucose != latestReading, let quantity = glucose.glucose else {
167+
delegate?.cgmManager(self, didUpdateWith: .noData)
168+
return
169+
}
170+
latestReading = glucose
171+
172+
self.delegate?.cgmManager(self, didUpdateWith: .newData([
173+
(quantity: quantity, date: glucose.readDate, isDisplayOnly: glucose.isDisplayOnly)
174+
]))
175+
}
176+
177+
func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) {
178+
// This can be used for protocol discovery, but isn't necessary for normal operation
179+
}
180+
}
181+
182+
183+
final class G4CGMManager: DexCGMManager, ReceiverDelegate {
184+
private let receiver = Receiver()
185+
186+
override init() {
187+
super.init()
188+
189+
receiver.delegate = self
190+
}
191+
192+
override var sensorState: SensorDisplayable? {
193+
return latestReading ?? super.sensorState
194+
}
195+
196+
private var latestReading: GlucoseG4? {
197+
didSet {
198+
// Once we have our first reading, disable backfill
199+
shareManager = nil
200+
}
201+
}
202+
203+
override var device: HKDevice? {
204+
// "Dexcom G4 Platinum Transmitter (Retail) US" - see https://accessgudid.nlm.nih.gov/devices/search?query=dexcom+g4
205+
return HKDevice(
206+
name: "G4ShareSpy",
207+
manufacturer: "Dexcom",
208+
model: "G4 Share",
209+
hardwareVersion: nil,
210+
firmwareVersion: nil,
211+
softwareVersion: String(G4ShareSpyVersionNumber),
212+
localIdentifier: nil,
213+
udiDeviceIdentifier: "40386270000048"
214+
)
215+
}
216+
217+
override var debugDescription: String {
218+
return [
219+
"## G4CGMManager",
220+
"latestReading: \(latestReading)",
221+
"receiver: \(receiver)",
222+
super.debugDescription,
223+
""
224+
].joined(separator: "\n")
225+
}
226+
227+
// MARK: - ReceiverDelegate
228+
229+
func receiver(_ receiver: Receiver, didReadGlucoseHistory glucoseHistory: [GlucoseG4]) {
230+
guard let latest = glucoseHistory.sorted(by: { $0.sequence < $1.sequence }).last, latest != latestReading else {
231+
return
232+
}
233+
latestReading = latest
234+
235+
// In the event that some of the glucose history was already backfilled from Share, don't overwrite it.
236+
let includeAfter = delegate?.startDateToFilterNewData(for: self)?.addingTimeInterval(TimeInterval(minutes: 1))
237+
238+
let validGlucose = glucoseHistory.filter({
239+
$0.isStateValid
240+
}).filterDateRange(includeAfter, nil).map({
241+
(quantity: $0.quantity, date: $0.startDate, isDisplayOnly: $0.isDisplayOnly)
242+
})
243+
244+
self.delegate?.cgmManager(self, didUpdateWith: .newData(validGlucose))
245+
}
246+
247+
func receiver(_ receiver: Receiver, didError error: Error) {
248+
delegate?.cgmManager(self, didUpdateWith: .error(error))
249+
}
250+
251+
func receiver(_ receiver: Receiver, didLogBluetoothEvent event: String) {
252+
// Uncomment to debug communication
253+
// NSLog(["event": "\(event)", "collectedAt": NSDateFormatter.ISO8601StrictDateFormatter().stringFromDate(NSDate())])
254+
}
255+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// EnliteCGMManager.swift
3+
// Loop
4+
//
5+
// Created by Nate Racklyeft on 3/12/17.
6+
// Copyright © 2017 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import HealthKit
10+
import LoopUI
11+
import MinimedKit
12+
13+
14+
final class EnliteCGMManager: CGMManager {
15+
var providesBLEHeartbeat = false
16+
17+
weak var delegate: CGMManagerDelegate?
18+
19+
var sensorState: SensorDisplayable?
20+
21+
func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) {
22+
guard let device = deviceManager.rileyLinkManager.firstConnectedDevice?.ops
23+
else {
24+
completion(.noData)
25+
return
26+
}
27+
28+
let latestGlucoseDate = self.delegate?.startDateToFilterNewData(for: self) ?? Date(timeIntervalSinceNow: TimeInterval(hours: -24))
29+
30+
guard latestGlucoseDate.timeIntervalSinceNow <= TimeInterval(minutes: -4.5) else {
31+
completion(.noData)
32+
return
33+
}
34+
35+
device.getGlucoseHistoryEvents(since: latestGlucoseDate.addingTimeInterval(TimeInterval(minutes: 1))) { (result) in
36+
switch result {
37+
case .success(let events):
38+
if let latestSensorEvent = events.flatMap({ $0.glucoseEvent as? RelativeTimestampedGlucoseEvent }).last {
39+
self.sensorState = EnliteSensorDisplayable(latestSensorEvent)
40+
}
41+
42+
let unit = HKUnit.milligramsPerDeciliterUnit()
43+
let glucoseValues = events
44+
// TODO: Is the { $0.date > latestGlucoseDate } filter duplicative?
45+
.filter({ $0.glucoseEvent is SensorValueGlucoseEvent && $0.date > latestGlucoseDate })
46+
.map({ (e:TimestampedGlucoseEvent) -> (quantity: HKQuantity, date: Date, isDisplayOnly: Bool) in
47+
let glucoseEvent = e.glucoseEvent as! SensorValueGlucoseEvent
48+
let quantity = HKQuantity(unit: unit, doubleValue: Double(glucoseEvent.sgv))
49+
return (quantity: quantity, date: e.date, isDisplayOnly: false)
50+
})
51+
52+
completion(.newData(glucoseValues))
53+
case .failure(let error):
54+
completion(.error(error))
55+
}
56+
}
57+
}
58+
59+
var device: HKDevice? = nil
60+
61+
var debugDescription: String {
62+
return [
63+
"## EnliteCGMManager",
64+
"sensorState: \(sensorState)",
65+
""
66+
].joined(separator: "\n")
67+
}
68+
}
69+

0 commit comments

Comments
 (0)