PluginSource.swift 12 KB

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