TransmitterManager.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. //
  2. // TransmitterManager.swift
  3. // Loop
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import HealthKit
  8. import LoopKit
  9. import ShareClient
  10. import os.log
  11. public struct TransmitterManagerState: RawRepresentable, Equatable {
  12. public typealias RawValue = CGMManager.RawStateValue
  13. public static let version = 1
  14. public var transmitterID: String
  15. public var passiveModeEnabled: Bool = true
  16. public var shouldSyncToRemoteService: Bool
  17. public init(transmitterID: String, shouldSyncToRemoteService: Bool = false) {
  18. self.transmitterID = transmitterID
  19. self.shouldSyncToRemoteService = shouldSyncToRemoteService
  20. }
  21. public init?(rawValue: RawValue) {
  22. guard let transmitterID = rawValue["transmitterID"] as? String
  23. else {
  24. return nil
  25. }
  26. let shouldSyncToRemoteService = rawValue["shouldSyncToRemoteService"] as? Bool ?? false
  27. self.init(transmitterID: transmitterID, shouldSyncToRemoteService: shouldSyncToRemoteService)
  28. }
  29. public var rawValue: RawValue {
  30. return [
  31. "transmitterID": transmitterID,
  32. "shouldSyncToRemoteService": shouldSyncToRemoteService,
  33. ]
  34. }
  35. }
  36. public protocol TransmitterManagerObserver: AnyObject {
  37. func transmitterManagerDidUpdateLatestReading(_ manager: TransmitterManager)
  38. }
  39. public class TransmitterManager: TransmitterDelegate {
  40. private var state: TransmitterManagerState
  41. private let observers = WeakSynchronizedSet<TransmitterManagerObserver>()
  42. public var hasValidSensorSession: Bool {
  43. // TODO: we should decode and persist transmitter session state
  44. return !state.transmitterID.isEmpty
  45. }
  46. public var cgmManagerStatus: CGMManagerStatus {
  47. return CGMManagerStatus(hasValidSensorSession: hasValidSensorSession, device: device)
  48. }
  49. public required init(state: TransmitterManagerState) {
  50. self.state = state
  51. self.transmitter = Transmitter(id: state.transmitterID, passiveModeEnabled: state.passiveModeEnabled)
  52. self.shareManager = ShareClientManager()
  53. self.transmitter.delegate = self
  54. #if targetEnvironment(simulator)
  55. setupSimulatedSampleGenerator()
  56. #endif
  57. }
  58. #if targetEnvironment(simulator)
  59. var simulatedSampleGeneratorTimer: DispatchSourceTimer?
  60. private func setupSimulatedSampleGenerator() {
  61. let timer = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "com.loopkit.simulatedSampleGenerator"))
  62. timer.schedule(deadline: .now() + .seconds(10), repeating: .minutes(5))
  63. timer.setEventHandler(handler: { [weak self] in
  64. self?.generateSimulatedSample()
  65. })
  66. self.simulatedSampleGeneratorTimer = timer
  67. timer.resume()
  68. }
  69. private func generateSimulatedSample() {
  70. let timestamp = Date()
  71. let syncIdentifier = "\(self.state.transmitterID) \(timestamp)"
  72. let period = TimeInterval(hours: 3)
  73. func glucoseValueFunc(timestamp: Date, period: Double) -> Double {
  74. return 100 + 20 * cos(timestamp.timeIntervalSinceReferenceDate.remainder(dividingBy: period) / period * Double.pi * 2)
  75. }
  76. let glucoseValue = glucoseValueFunc(timestamp: timestamp, period: period)
  77. let prevGlucoseValue = glucoseValueFunc(timestamp: timestamp - period, period: period)
  78. let trendRateValue = glucoseValue - prevGlucoseValue
  79. let trend: GlucoseTrend? = {
  80. switch trendRateValue {
  81. case -0.01...0.01:
  82. return .flat
  83. case -2 ..< -0.01:
  84. return .down
  85. case -5 ..< -2:
  86. return .downDown
  87. case -Double.greatestFiniteMagnitude ..< -5:
  88. return .downDownDown
  89. case 0.01...2:
  90. return .up
  91. case 2...5:
  92. return .upUp
  93. case 5...Double.greatestFiniteMagnitude:
  94. return .upUpUp
  95. default:
  96. return nil
  97. }
  98. }()
  99. let quantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: glucoseValue)
  100. let trendRate = HKQuantity(unit: .milligramsPerDeciliterPerMinute, doubleValue: trendRateValue)
  101. let sample = NewGlucoseSample(date: timestamp, quantity: quantity, condition: nil, trend: trend, trendRate: trendRate, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: syncIdentifier)
  102. self.updateDelegate(with: .newData([sample]))
  103. }
  104. #endif
  105. required convenience public init?(rawState: CGMManager.RawStateValue) {
  106. guard let state = TransmitterManagerState(rawValue: rawState) else {
  107. return nil
  108. }
  109. self.init(state: state)
  110. }
  111. public var rawState: CGMManager.RawStateValue {
  112. return state.rawValue
  113. }
  114. func logDeviceCommunication(_ message: String, type: DeviceLogEntryType = .send) {
  115. }
  116. public var shouldSyncToRemoteService: Bool {
  117. get {
  118. return state.shouldSyncToRemoteService
  119. }
  120. set {
  121. self.state.shouldSyncToRemoteService = newValue
  122. notifyDelegateOfStateChange()
  123. }
  124. }
  125. public var cgmManagerDelegate: CGMManagerDelegate? {
  126. get {
  127. return shareManager.cgmManagerDelegate
  128. }
  129. set {
  130. shareManager.cgmManagerDelegate = newValue
  131. }
  132. }
  133. public var delegateQueue: DispatchQueue! {
  134. get {
  135. return shareManager.delegateQueue
  136. }
  137. set {
  138. shareManager.delegateQueue = newValue
  139. }
  140. }
  141. private(set) public var latestConnection: Date? {
  142. get {
  143. return lockedLatestConnection.value
  144. }
  145. set {
  146. lockedLatestConnection.value = newValue
  147. }
  148. }
  149. private let lockedLatestConnection: Locked<Date?> = Locked(nil)
  150. public let shareManager: ShareClientManager
  151. public let transmitter: Transmitter
  152. let log = OSLog(category: "TransmitterManager")
  153. public var providesBLEHeartbeat: Bool {
  154. return dataIsFresh
  155. }
  156. public var glucoseDisplay: GlucoseDisplayable? {
  157. let transmitterDate = latestReading?.readDate ?? .distantPast
  158. let shareDate = shareManager.latestBackfill?.startDate ?? .distantPast
  159. if transmitterDate >= shareDate {
  160. return latestReading
  161. } else {
  162. return shareManager.glucoseDisplay
  163. }
  164. }
  165. public var managedDataInterval: TimeInterval? {
  166. if transmitter.passiveModeEnabled {
  167. return .hours(3)
  168. }
  169. return shareManager.managedDataInterval
  170. }
  171. private(set) public var latestReading: Glucose? {
  172. get {
  173. return lockedLatestReading.value
  174. }
  175. set {
  176. lockedLatestReading.value = newValue
  177. }
  178. }
  179. private let lockedLatestReading: Locked<Glucose?> = Locked(nil)
  180. private var dataIsFresh: Bool {
  181. guard let latestGlucose = latestReading,
  182. latestGlucose.readDate > Date(timeIntervalSinceNow: .minutes(-4.5)) else {
  183. return false
  184. }
  185. return true
  186. }
  187. public func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) {
  188. // Ensure our transmitter connection is active
  189. transmitter.resumeScanning()
  190. // If our last glucose was less than 4.5 minutes ago, don't fetch.
  191. guard !dataIsFresh else {
  192. completion(.noData)
  193. return
  194. }
  195. if let latestReading = latestReading {
  196. log.default("Fetching new glucose from Share because last reading is %{public}.1f minutes old", latestReading.readDate.timeIntervalSinceNow.minutes)
  197. } else {
  198. log.default("Fetching new glucose from Share because we don't have a previous reading")
  199. }
  200. shareManager.fetchNewDataIfNeeded(completion)
  201. }
  202. public var device: HKDevice? {
  203. return nil
  204. }
  205. public var debugDescription: String {
  206. return [
  207. "## \(String(describing: type(of: self)))",
  208. "latestReading: \(String(describing: latestReading))",
  209. "latestConnection: \(String(describing: latestConnection))",
  210. "dataIsFresh: \(dataIsFresh)",
  211. "providesBLEHeartbeat: \(providesBLEHeartbeat)",
  212. shareManager.debugDescription,
  213. "observers.count: \(observers.cleanupDeallocatedElements().count)",
  214. String(reflecting: transmitter),
  215. ].joined(separator: "\n")
  216. }
  217. private func updateDelegate(with result: CGMReadingResult) {
  218. if let manager = self as? CGMManager {
  219. shareManager.delegate.notify { (delegate) in
  220. delegate?.cgmManager(manager, hasNew: result)
  221. }
  222. }
  223. notifyObserversOfLatestReading()
  224. }
  225. private func notifyDelegateOfStateChange() {
  226. if let manager = self as? CGMManager {
  227. shareManager.delegate.notify { (delegate) in
  228. delegate?.cgmManagerDidUpdateState(manager)
  229. }
  230. }
  231. }
  232. // MARK: - TransmitterDelegate
  233. public func transmitterDidConnect(_ transmitter: Transmitter) {
  234. log.default("%{public}@", #function)
  235. latestConnection = Date()
  236. logDeviceCommunication("Connected", type: .connection)
  237. }
  238. public func transmitter(_ transmitter: Transmitter, didError error: Error) {
  239. log.error("%{public}@: %{public}@", #function, String(describing: error))
  240. updateDelegate(with: .error(error))
  241. logDeviceCommunication("Error: \(error)", type: .error)
  242. }
  243. public func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) {
  244. guard glucose != latestReading else {
  245. updateDelegate(with: .noData)
  246. return
  247. }
  248. latestReading = glucose
  249. logDeviceCommunication("New reading: \(glucose.readDate)", type: .receive)
  250. guard glucose.state.hasReliableGlucose else {
  251. log.default("%{public}@: Unreliable glucose: %{public}@", #function, String(describing: glucose.state))
  252. updateDelegate(with: .error(CalibrationError.unreliableState(glucose.state)))
  253. return
  254. }
  255. guard let quantity = glucose.glucose else {
  256. updateDelegate(with: .noData)
  257. return
  258. }
  259. log.default("%{public}@: New glucose", #function)
  260. updateDelegate(with: .newData([
  261. NewGlucoseSample(
  262. date: glucose.readDate,
  263. quantity: quantity,
  264. condition: glucose.condition,
  265. trend: glucose.trendType,
  266. trendRate: glucose.trendRate,
  267. isDisplayOnly: glucose.isDisplayOnly,
  268. wasUserEntered: glucose.isDisplayOnly,
  269. syncIdentifier: glucose.syncIdentifier,
  270. device: device
  271. )
  272. ]))
  273. }
  274. public func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose]) {
  275. let samples = glucose.compactMap { (glucose) -> NewGlucoseSample? in
  276. guard glucose != latestReading, glucose.state.hasReliableGlucose, let quantity = glucose.glucose else {
  277. return nil
  278. }
  279. return NewGlucoseSample(
  280. date: glucose.readDate,
  281. quantity: quantity,
  282. condition: glucose.condition,
  283. trend: glucose.trendType,
  284. trendRate: glucose.trendRate,
  285. isDisplayOnly: glucose.isDisplayOnly,
  286. wasUserEntered: glucose.isDisplayOnly,
  287. syncIdentifier: glucose.syncIdentifier,
  288. device: device
  289. )
  290. }
  291. guard samples.count > 0 else {
  292. return
  293. }
  294. updateDelegate(with: .newData(samples))
  295. logDeviceCommunication("New backfill: \(String(describing: samples.first?.date))", type: .receive)
  296. }
  297. public func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) {
  298. log.error("Unknown sensor data: %{public}@", data.hexadecimalString)
  299. // This can be used for protocol discovery, but isn't necessary for normal operation
  300. logDeviceCommunication("Unknown sensor data: \(data.hexadecimalString)", type: .error)
  301. }
  302. }
  303. // MARK: - Observer management
  304. extension TransmitterManager {
  305. public func addObserver(_ observer: TransmitterManagerObserver, queue: DispatchQueue) {
  306. observers.insert(observer, queue: queue)
  307. }
  308. public func removeObserver(_ observer: TransmitterManagerObserver) {
  309. observers.removeElement(observer)
  310. }
  311. private func notifyObserversOfLatestReading() {
  312. observers.forEach { (observer) in
  313. observer.transmitterManagerDidUpdateLatestReading(self)
  314. }
  315. }
  316. }
  317. public class G5CGMManager: TransmitterManager, CGMManager {
  318. public let managerIdentifier: String = "DexG5Transmitter"
  319. public let localizedTitle = LocalizedString("Dexcom G5", comment: "CGM display title")
  320. public let isOnboarded = true // No distinction between created and onboarded
  321. public var appURL: URL? {
  322. return URL(string: "dexcomcgm://")
  323. }
  324. public override var device: HKDevice? {
  325. return HKDevice(
  326. name: "CGMBLEKit",
  327. manufacturer: "Dexcom",
  328. model: "G5 Mobile",
  329. hardwareVersion: nil,
  330. firmwareVersion: nil,
  331. softwareVersion: String(CGMBLEKitVersionNumber),
  332. localIdentifier: nil,
  333. udiDeviceIdentifier: "00386270000002"
  334. )
  335. }
  336. override func logDeviceCommunication(_ message: String, type: DeviceLogEntryType = .send) {
  337. self.cgmManagerDelegate?.deviceManager(self, logEventForDeviceIdentifier: transmitter.ID, type: type, message: message, completion: nil)
  338. }
  339. }
  340. public class G6CGMManager: TransmitterManager, CGMManager {
  341. public let managerIdentifier: String = "DexG6Transmitter"
  342. public let localizedTitle = LocalizedString("Dexcom G6", comment: "CGM display title")
  343. public let isOnboarded = true // No distinction between created and onboarded
  344. public var appURL: URL? {
  345. return nil
  346. }
  347. public override var device: HKDevice? {
  348. return HKDevice(
  349. name: "CGMBLEKit",
  350. manufacturer: "Dexcom",
  351. model: "G6",
  352. hardwareVersion: nil,
  353. firmwareVersion: nil,
  354. softwareVersion: String(CGMBLEKitVersionNumber),
  355. localIdentifier: nil,
  356. udiDeviceIdentifier: "00386270000385"
  357. )
  358. }
  359. override func logDeviceCommunication(_ message: String, type: DeviceLogEntryType = .send) {
  360. self.cgmManagerDelegate?.deviceManager(self, logEventForDeviceIdentifier: transmitter.ID, type: type, message: message, completion: nil)
  361. }
  362. }
  363. enum CalibrationError: Error {
  364. case unreliableState(CalibrationState)
  365. }
  366. extension CalibrationError: LocalizedError {
  367. var errorDescription: String? {
  368. switch self {
  369. case .unreliableState:
  370. return LocalizedString("Glucose data is unavailable", comment: "Error description for unreliable state")
  371. }
  372. }
  373. var failureReason: String? {
  374. switch self {
  375. case .unreliableState(let state):
  376. return state.localizedDescription
  377. }
  378. }
  379. }
  380. extension CalibrationState {
  381. public var localizedDescription: String {
  382. switch self {
  383. case .known(let state):
  384. switch state {
  385. case .needCalibration7, .needCalibration14, .needFirstInitialCalibration, .needSecondInitialCalibration, .calibrationError8, .calibrationError9, .calibrationError10, .calibrationError13:
  386. return LocalizedString("Sensor needs calibration", comment: "The description of sensor calibration state when sensor needs calibration.")
  387. case .ok:
  388. return LocalizedString("Sensor calibration is OK", comment: "The description of sensor calibration state when sensor calibration is ok.")
  389. case .stopped, .sensorFailure11, .sensorFailure12, .sessionFailure15, .sessionFailure16, .sessionFailure17:
  390. return LocalizedString("Sensor is stopped", comment: "The description of sensor calibration state when sensor sensor is stopped.")
  391. case .warmup, .questionMarks:
  392. return LocalizedString("Sensor is warming up", comment: "The description of sensor calibration state when sensor sensor is warming up.")
  393. }
  394. case .unknown(let rawValue):
  395. return String(format: LocalizedString("Sensor is in unknown state %1$d", comment: "The description of sensor calibration state when raw value is unknown. (1: missing data details)"), rawValue)
  396. }
  397. }
  398. }
  399. // MARK: - AlertResponder implementation
  400. extension G5CGMManager {
  401. public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) {
  402. completion(nil)
  403. }
  404. }
  405. // MARK: - AlertSoundVendor implementation
  406. extension G5CGMManager {
  407. public func getSoundBaseURL() -> URL? { return nil }
  408. public func getSounds() -> [Alert.Sound] { return [] }
  409. }
  410. // MARK: - AlertResponder implementation
  411. extension G6CGMManager {
  412. public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) {
  413. completion(nil)
  414. }
  415. }
  416. // MARK: - AlertSoundVendor implementation
  417. extension G6CGMManager {
  418. public func getSoundBaseURL() -> URL? { return nil }
  419. public func getSounds() -> [Alert.Sound] { return [] }
  420. }