PluginSource.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import CGMBLEKit
  2. import Combine
  3. import Foundation
  4. import G7SensorKit
  5. import LibreTransmitter
  6. import LoopKit
  7. import LoopKitUI
  8. final class PluginSource: GlucoseSource {
  9. private let processQueue = DispatchQueue(label: "CGMPluginSource.processQueue")
  10. private let glucoseStorage: GlucoseStorage!
  11. var glucoseManager: FetchGlucoseManager?
  12. var cgmManager: CGMManagerUI?
  13. var cgmHasValidSensorSession: Bool = false
  14. init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
  15. self.glucoseStorage = glucoseStorage
  16. self.glucoseManager = glucoseManager
  17. cgmManager = glucoseManager.cgmManager
  18. cgmManager?.delegateQueue = processQueue
  19. cgmManager?.cgmManagerDelegate = self
  20. }
  21. /// Function that fetches blood glucose data
  22. /// This function combines two data fetching mechanisms (`callBLEFetch` and `fetchIfNeeded`) into a single publisher.
  23. /// It returns the first non-empty result from either of the sources within a 5-minute timeout period.
  24. /// If no valid data is fetched within the timeout, it returns an empty array.
  25. ///
  26. /// - Parameter timer: An optional `DispatchTimer` (not used in the function but can be used to trigger fetch logic).
  27. /// - Returns: An `AnyPublisher` that emits an array of `BloodGlucose` values or an empty array if an error occurs or the timeout is reached.
  28. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  29. fetchIfNeeded()
  30. .filter { !$0.isEmpty }
  31. .first()
  32. .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
  33. .replaceError(with: [])
  34. .eraseToAnyPublisher()
  35. }
  36. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  37. Future<[BloodGlucose], Error> { [weak self] promise in
  38. guard let self = self else { return }
  39. self.processQueue.async {
  40. guard let cgmManager = self.cgmManager else { return }
  41. cgmManager.fetchNewDataIfNeeded { _ in
  42. // Ignore values returned from fetchNewDataIfNeeded since
  43. // these come from share client and cause a race condition
  44. // that causes the promise to complete before a CGM value
  45. // has a chance to return. From looking at the code this should
  46. // only impact G6 since that is the only CGM manager that will
  47. // return data and only if share credentials are set
  48. promise(.success([]))
  49. }
  50. }
  51. }
  52. .replaceError(with: [])
  53. .replaceEmpty(with: [])
  54. .eraseToAnyPublisher()
  55. }
  56. deinit {
  57. // dexcomManager.transmitter.stopScanning()
  58. }
  59. }
  60. extension PluginSource: 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. processQueue.async { [weak self] in
  84. guard let self = self else { return }
  85. dispatchPrecondition(condition: .onQueue(self.processQueue))
  86. debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
  87. Task {
  88. await self.glucoseManager?.deleteGlucoseSource()
  89. }
  90. }
  91. }
  92. func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {
  93. processQueue.async { [weak self] in
  94. guard let self = self else { return }
  95. dispatchPrecondition(condition: .onQueue(self.processQueue))
  96. switch self.readCGMResult(readingResult: readingResult) {
  97. case let .success(glucose):
  98. self.glucoseManager?.newGlucoseFromCgmManager(newGlucose: glucose)
  99. case .failure:
  100. debug(.deviceManager, "CGM PLUGIN - unable to read CGM result")
  101. }
  102. debug(.deviceManager, "CGM PLUGIN - Direct return done")
  103. }
  104. }
  105. func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
  106. processQueue.async { [weak self] in
  107. guard let self = self else { return }
  108. dispatchPrecondition(condition: .onQueue(self.processQueue))
  109. // TODO: Events in APS ?
  110. // currently only display in log the date of the event
  111. events.forEach { event in
  112. debug(.deviceManager, "events from CGM at \(event.date)")
  113. if event.type == .sensorStart {
  114. self.glucoseManager?.removeCalibrations()
  115. }
  116. }
  117. }
  118. }
  119. func startDateToFilterNewData(for _: CGMManager) -> Date? {
  120. dispatchPrecondition(condition: .onQueue(processQueue))
  121. return glucoseStorage.lastGlucoseDate()
  122. }
  123. func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
  124. processQueue.async { [weak self] in
  125. guard let self = self else { return }
  126. dispatchPrecondition(condition: .onQueue(self.processQueue))
  127. guard let fetchGlucoseManager = self.glucoseManager else {
  128. debug(
  129. .deviceManager,
  130. "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
  131. )
  132. return
  133. }
  134. // Adjust app-specific NS Upload setting value when CGM setting is changed
  135. fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
  136. fetchGlucoseManager.updateGlucoseSource(
  137. cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
  138. cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
  139. newManager: cgmManager as? CGMManagerUI
  140. )
  141. }
  142. }
  143. func credentialStoragePrefix(for _: CGMManager) -> String {
  144. // return string unique to this instance of the CGMManager
  145. UUID().uuidString
  146. }
  147. func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
  148. debug(.deviceManager, "CGM Manager did update state to \(status)")
  149. processQueue.async { [weak self] in
  150. guard let self = self else { return }
  151. dispatchPrecondition(condition: .onQueue(self.processQueue))
  152. if self.cgmHasValidSensorSession != status.hasValidSensorSession {
  153. self.cgmHasValidSensorSession = status.hasValidSensorSession
  154. }
  155. }
  156. }
  157. private func readCGMResult(readingResult: CGMReadingResult) -> Result<[BloodGlucose], Error> {
  158. debug(.deviceManager, "PLUGIN CGM - Process CGM Reading Result launched with \(readingResult)")
  159. if glucoseManager?.glucoseSource == nil {
  160. debug(
  161. .deviceManager,
  162. "No glucose source available."
  163. )
  164. }
  165. switch readingResult {
  166. case let .newData(values):
  167. var sensorActivatedAt: Date?
  168. var sensorStartDate: Date?
  169. var sensorTransmitterID: String?
  170. /// SAGE
  171. if let cgmTransmitterManager = cgmManager as? LibreTransmitterManagerV3 {
  172. let sensorInfo = cgmTransmitterManager.sensorInfoObservable
  173. sensorActivatedAt = sensorInfo.activatedAt
  174. sensorStartDate = sensorInfo.activatedAt
  175. sensorTransmitterID = sensorInfo.sensorSerial
  176. } else if let cgmTransmitterManager = cgmManager as? G5CGMManager {
  177. let latestReading = cgmTransmitterManager.latestReading
  178. sensorActivatedAt = latestReading?.activationDate
  179. sensorStartDate = latestReading?.sessionStartDate
  180. sensorTransmitterID = latestReading?.transmitterID
  181. } else if let cgmTransmitterManager = cgmManager as? G6CGMManager {
  182. let latestReading = cgmTransmitterManager.latestReading
  183. sensorActivatedAt = latestReading?.activationDate
  184. sensorStartDate = latestReading?.sessionStartDate
  185. sensorTransmitterID = latestReading?.transmitterID
  186. } else if let cgmTransmitterManager = cgmManager as? G7CGMManager {
  187. sensorActivatedAt = cgmTransmitterManager.sensorActivatedAt
  188. sensorStartDate = cgmTransmitterManager.sensorActivatedAt
  189. sensorTransmitterID = cgmTransmitterManager.sensorName
  190. }
  191. let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
  192. let quantity = newGlucoseSample.quantity
  193. let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
  194. return BloodGlucose(
  195. id: UUID().uuidString,
  196. sgv: value,
  197. direction: .init(trendType: newGlucoseSample.trend),
  198. date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
  199. dateString: newGlucoseSample.date,
  200. unfiltered: Decimal(value),
  201. filtered: nil,
  202. noise: nil,
  203. glucose: value,
  204. type: "sgv",
  205. activationDate: sensorActivatedAt,
  206. sessionStartDate: sensorStartDate,
  207. transmitterID: sensorTransmitterID
  208. )
  209. }
  210. return .success(bloodGlucose)
  211. case .unreliableData:
  212. // loopManager.receivedUnreliableCGMReading()
  213. return .failure(GlucoseDataError.unreliableData)
  214. case .noData:
  215. return .failure(GlucoseDataError.noData)
  216. case let .error(error):
  217. return .failure(error)
  218. }
  219. }
  220. }
  221. extension PluginSource {
  222. func sourceInfo() -> [String: Any]? {
  223. [GlucoseSourceKey.description.rawValue: "Plugin CGM source"]
  224. }
  225. }