DexcomSourceG6.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import CGMBLEKit
  2. import Combine
  3. import Foundation
  4. import LoopKit
  5. import LoopKitUI
  6. import ShareClient
  7. final class DexcomSourceG6: GlucoseSource {
  8. private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
  9. private let glucoseStorage: GlucoseStorage!
  10. var glucoseManager: FetchGlucoseManager?
  11. var cgmManager: CGMManagerUI?
  12. var cgmType: CGMType = .dexcomG6
  13. var cgmHasValidSensorSession: Bool = false
  14. private var promise: Future<[BloodGlucose], Error>.Promise?
  15. init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
  16. self.glucoseStorage = glucoseStorage
  17. self.glucoseManager = glucoseManager
  18. cgmManager = G6CGMManager
  19. .init(state: TransmitterManagerState(
  20. transmitterID: UserDefaults.standard
  21. .dexcomTransmitterID ?? "000000",
  22. shouldSyncToRemoteService: glucoseManager.settingsManager.settings.uploadGlucose
  23. ))
  24. cgmManager?.delegateQueue = processQueue
  25. cgmManager?.cgmManagerDelegate = self
  26. }
  27. var transmitterID: String {
  28. guard let cgmG6Manager = cgmManager as? G6CGMManager else { return "000000" }
  29. return cgmG6Manager.transmitter.ID
  30. }
  31. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  32. Future<[BloodGlucose], Error> { [weak self] promise in
  33. self?.promise = promise
  34. }
  35. .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
  36. .replaceError(with: [])
  37. .replaceEmpty(with: [])
  38. .eraseToAnyPublisher()
  39. }
  40. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  41. Future<[BloodGlucose], Error> { _ in
  42. self.processQueue.async {
  43. guard let cgmManager = self.cgmManager else { return }
  44. cgmManager.fetchNewDataIfNeeded { result in
  45. self.processCGMReadingResult(cgmManager, readingResult: result) {
  46. // nothing to do
  47. }
  48. }
  49. }
  50. }
  51. .timeout(60, scheduler: processQueue, options: nil, customError: nil)
  52. .replaceError(with: [])
  53. .replaceEmpty(with: [])
  54. .eraseToAnyPublisher()
  55. }
  56. deinit {
  57. // dexcomManager.transmitter.stopScanning()
  58. }
  59. }
  60. extension DexcomSourceG6: CGMManagerDelegate {
  61. func deviceManager(
  62. _: LoopKit.DeviceManager,
  63. logEventForDeviceIdentifier deviceIdentifier: String?,
  64. type _: LoopKit.DeviceLogEntryType,
  65. message: String,
  66. completion _: ((Error?) -> Void)?
  67. ) {
  68. debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
  69. }
  70. func issueAlert(_: LoopKit.Alert) {}
  71. func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
  72. func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
  73. func lookupAllUnretracted(
  74. managerIdentifier _: String,
  75. completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
  76. ) {}
  77. func lookupAllUnacknowledgedUnretracted(
  78. managerIdentifier _: String,
  79. completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
  80. ) {}
  81. func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
  82. func cgmManagerWantsDeletion(_ manager: CGMManager) {
  83. dispatchPrecondition(condition: .onQueue(processQueue))
  84. debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
  85. glucoseManager?.cgmGlucoseSourceType = nil
  86. }
  87. func cgmManager(_ manager: CGMManager, hasNew readingResult: CGMReadingResult) {
  88. dispatchPrecondition(condition: .onQueue(processQueue))
  89. processCGMReadingResult(manager, readingResult: readingResult) {
  90. debug(.deviceManager, "DEXCOM - Direct return done")
  91. }
  92. }
  93. func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
  94. dispatchPrecondition(condition: .onQueue(processQueue))
  95. // TODO: Events in APS ?
  96. // currently only display in log the date of the event
  97. events.forEach { debug(.deviceManager, "events from CGM at \($0.date)") }
  98. }
  99. func startDateToFilterNewData(for _: CGMManager) -> Date? {
  100. dispatchPrecondition(condition: .onQueue(processQueue))
  101. return glucoseStorage.lastGlucoseDate()
  102. // return glucoseStore.latestGlucose?.startDate
  103. }
  104. func cgmManagerDidUpdateState(_ manager: CGMManager) {
  105. dispatchPrecondition(condition: .onQueue(processQueue))
  106. guard let g6Manager = manager as? TransmitterManager else {
  107. return
  108. }
  109. glucoseManager?.settingsManager.settings.uploadGlucose = g6Manager.shouldSyncToRemoteService
  110. UserDefaults.standard.dexcomTransmitterID = g6Manager.rawState["transmitterID"] as? String
  111. }
  112. func credentialStoragePrefix(for _: CGMManager) -> String {
  113. // return string unique to this instance of the CGMManager
  114. UUID().uuidString
  115. }
  116. func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
  117. processQueue.async {
  118. if self.cgmHasValidSensorSession != status.hasValidSensorSession {
  119. self.cgmHasValidSensorSession = status.hasValidSensorSession
  120. }
  121. }
  122. }
  123. private func processCGMReadingResult(
  124. _: CGMManager,
  125. readingResult: CGMReadingResult,
  126. completion: @escaping () -> Void
  127. ) {
  128. debug(.deviceManager, "DEXCOM - Process CGM Reading Result launched with \(readingResult)")
  129. switch readingResult {
  130. case let .newData(values):
  131. if let cgmG6Manager = cgmManager as? G6CGMManager,
  132. let activationDate = cgmG6Manager.latestReading?.activationDate,
  133. let sessionStartDate = cgmG6Manager.latestReading?.sessionStartDate
  134. {
  135. let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
  136. let quantity = newGlucoseSample.quantity
  137. let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
  138. return BloodGlucose(
  139. _id: UUID().uuidString,
  140. sgv: value,
  141. direction: .init(trendType: newGlucoseSample.trend),
  142. date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
  143. dateString: newGlucoseSample.date,
  144. unfiltered: Decimal(value),
  145. filtered: nil,
  146. noise: nil,
  147. glucose: value,
  148. type: "sgv",
  149. activationDate: activationDate,
  150. sessionStartDate: sessionStartDate,
  151. transmitterID: self.transmitterID
  152. )
  153. }
  154. promise?(.success(bloodGlucose))
  155. completion()
  156. } else {
  157. // Handle the case where activationDate or sessionStartDate is nil
  158. completion()
  159. }
  160. case .unreliableData:
  161. // loopManager.receivedUnreliableCGMReading()
  162. promise?(.failure(GlucoseDataError.unreliableData))
  163. completion()
  164. case .noData:
  165. promise?(.failure(GlucoseDataError.noData))
  166. completion()
  167. case let .error(error):
  168. promise?(.failure(error))
  169. completion()
  170. }
  171. }
  172. }
  173. extension DexcomSourceG6 {
  174. func sourceInfo() -> [String: Any]? {
  175. [GlucoseSourceKey.description.rawValue: "Dexcom tramsmitter ID: \(transmitterID)"]
  176. }
  177. }