PluginSource.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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: "DexcomSource.processQueue")
  10. private let glucoseStorage: GlucoseStorage!
  11. var glucoseManager: FetchGlucoseManager?
  12. var cgmManager: CGMManagerUI?
  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 = glucoseManager.cgmManager
  19. cgmManager?.delegateQueue = processQueue
  20. cgmManager?.cgmManagerDelegate = self
  21. }
  22. /// Function that fetches blood glucose data
  23. /// This function combines two data fetching mechanisms (`callBLEFetch` and `fetchIfNeeded`) into a single publisher.
  24. /// It returns the first non-empty result from either of the sources within a 5-minute timeout period.
  25. /// If no valid data is fetched within the timeout, it returns an empty array.
  26. ///
  27. /// - Parameter timer: An optional `DispatchTimer` (not used in the function but can be used to trigger fetch logic).
  28. /// - Returns: An `AnyPublisher` that emits an array of `BloodGlucose` values or an empty array if an error occurs or the timeout is reached.
  29. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  30. Publishers.Merge(
  31. callBLEFetch(),
  32. fetchIfNeeded()
  33. )
  34. .filter { !$0.isEmpty }
  35. .first()
  36. .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
  37. .replaceError(with: [])
  38. .eraseToAnyPublisher()
  39. }
  40. func callBLEFetch() -> AnyPublisher<[BloodGlucose], Never> {
  41. Future<[BloodGlucose], Error> { [weak self] promise in
  42. self?.promise = promise
  43. }
  44. .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
  45. .replaceError(with: [])
  46. .replaceEmpty(with: [])
  47. .eraseToAnyPublisher()
  48. }
  49. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  50. Future<[BloodGlucose], Error> { [weak self] promise in
  51. guard let self = self else { return }
  52. self.processQueue.async {
  53. guard let cgmManager = self.cgmManager else { return }
  54. cgmManager.fetchNewDataIfNeeded { result in
  55. promise(self.readCGMResult(readingResult: result))
  56. }
  57. }
  58. }
  59. .replaceError(with: [])
  60. .replaceEmpty(with: [])
  61. .eraseToAnyPublisher()
  62. }
  63. deinit {
  64. // dexcomManager.transmitter.stopScanning()
  65. }
  66. }
  67. extension PluginSource: CGMManagerDelegate {
  68. func deviceManager(
  69. _: LoopKit.DeviceManager,
  70. logEventForDeviceIdentifier deviceIdentifier: String?,
  71. type _: LoopKit.DeviceLogEntryType,
  72. message: String,
  73. completion _: ((Error?) -> Void)?
  74. ) {
  75. debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
  76. }
  77. func issueAlert(_: LoopKit.Alert) {}
  78. func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
  79. func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
  80. func lookupAllUnretracted(
  81. managerIdentifier _: String,
  82. completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
  83. ) {}
  84. func lookupAllUnacknowledgedUnretracted(
  85. managerIdentifier _: String,
  86. completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
  87. ) {}
  88. func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
  89. func cgmManagerWantsDeletion(_ manager: CGMManager) {
  90. dispatchPrecondition(condition: .onQueue(processQueue))
  91. debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
  92. // TODO:
  93. glucoseManager?.cgmGlucoseSourceType = .none
  94. }
  95. func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {
  96. dispatchPrecondition(condition: .onQueue(processQueue))
  97. promise?(readCGMResult(readingResult: readingResult))
  98. debug(.deviceManager, "CGM PLUGIN - Direct return done")
  99. }
  100. func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
  101. dispatchPrecondition(condition: .onQueue(processQueue))
  102. // TODO: Events in APS ?
  103. // currently only display in log the date of the event
  104. events.forEach { event in
  105. debug(.deviceManager, "events from CGM at \(event.date)")
  106. if event.type == .sensorStart {
  107. self.glucoseManager?.removeCalibrations()
  108. }
  109. }
  110. }
  111. func startDateToFilterNewData(for _: CGMManager) -> Date? {
  112. dispatchPrecondition(condition: .onQueue(processQueue))
  113. return glucoseStorage.lastGlucoseDate()
  114. }
  115. func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
  116. dispatchPrecondition(condition: .onQueue(processQueue))
  117. guard let fetchGlucoseManager = glucoseManager else {
  118. debug(
  119. .deviceManager,
  120. "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
  121. )
  122. return
  123. }
  124. // Adjust app-specific NS Upload setting value when CGM setting is changed
  125. fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
  126. fetchGlucoseManager.updateGlucoseSource(
  127. cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
  128. cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
  129. newManager: cgmManager as? CGMManagerUI
  130. )
  131. }
  132. func credentialStoragePrefix(for _: CGMManager) -> String {
  133. // return string unique to this instance of the CGMManager
  134. UUID().uuidString
  135. }
  136. func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
  137. debug(.deviceManager, "DEBUG DID UPDATE STATE")
  138. processQueue.async {
  139. if self.cgmHasValidSensorSession != status.hasValidSensorSession {
  140. self.cgmHasValidSensorSession = status.hasValidSensorSession
  141. }
  142. }
  143. }
  144. private func readCGMResult(readingResult: CGMReadingResult) -> Result<[BloodGlucose], Error> {
  145. debug(.deviceManager, "PLUGIN CGM - Process CGM Reading Result launched with \(readingResult)")
  146. if glucoseManager?.glucoseSource == nil {
  147. debug(
  148. .deviceManager,
  149. "No glucose source available."
  150. )
  151. }
  152. switch readingResult {
  153. case let .newData(values):
  154. var sensorActivatedAt: Date?
  155. var sensorStartDate: Date?
  156. var sensorTransmitterID: String?
  157. /// SAGE
  158. if let cgmTransmitterManager = cgmManager as? LibreTransmitterManagerV3 {
  159. let sensorInfo = cgmTransmitterManager.sensorInfoObservable
  160. sensorActivatedAt = sensorInfo.activatedAt
  161. sensorStartDate = sensorInfo.activatedAt
  162. sensorTransmitterID = sensorInfo.sensorSerial
  163. } else if let cgmTransmitterManager = cgmManager as? G5CGMManager {
  164. let latestReading = cgmTransmitterManager.latestReading
  165. sensorActivatedAt = latestReading?.activationDate
  166. sensorStartDate = latestReading?.sessionStartDate
  167. sensorTransmitterID = latestReading?.transmitterID
  168. } else if let cgmTransmitterManager = cgmManager as? G6CGMManager {
  169. let latestReading = cgmTransmitterManager.latestReading
  170. sensorActivatedAt = latestReading?.activationDate
  171. sensorStartDate = latestReading?.sessionStartDate
  172. sensorTransmitterID = latestReading?.transmitterID
  173. } else if let cgmTransmitterManager = cgmManager as? G7CGMManager {
  174. sensorActivatedAt = cgmTransmitterManager.sensorActivatedAt
  175. sensorStartDate = cgmTransmitterManager.sensorActivatedAt
  176. sensorTransmitterID = cgmTransmitterManager.sensorName
  177. }
  178. let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
  179. let quantity = newGlucoseSample.quantity
  180. let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
  181. return BloodGlucose(
  182. _id: UUID().uuidString,
  183. sgv: value,
  184. direction: .init(trendType: newGlucoseSample.trend),
  185. date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
  186. dateString: newGlucoseSample.date,
  187. unfiltered: Decimal(value),
  188. filtered: nil,
  189. noise: nil,
  190. glucose: value,
  191. type: "sgv",
  192. activationDate: sensorActivatedAt,
  193. sessionStartDate: sensorStartDate,
  194. transmitterID: sensorTransmitterID
  195. )
  196. }
  197. return .success(bloodGlucose)
  198. case .unreliableData:
  199. // loopManager.receivedUnreliableCGMReading()
  200. return .failure(GlucoseDataError.unreliableData)
  201. case .noData:
  202. return .failure(GlucoseDataError.noData)
  203. case let .error(error):
  204. return .failure(error)
  205. }
  206. }
  207. }
  208. extension PluginSource {
  209. func sourceInfo() -> [String: Any]? {
  210. [GlucoseSourceKey.description.rawValue: "Plugin CGM source"]
  211. }
  212. }