PluginSource.swift 7.9 KB

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