| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- import CGMBLEKit
- import Combine
- import Foundation
- import G7SensorKit
- import LibreTransmitter
- import LoopKit
- import LoopKitUI
- final class PluginSource: GlucoseSource {
- private let processQueue = DispatchQueue(label: "DexcomSource.processQueue")
- private let glucoseStorage: GlucoseStorage!
- var glucoseManager: FetchGlucoseManager?
- var cgmManager: CGMManagerUI?
- var cgmHasValidSensorSession: Bool = false
- private var promise: Future<[BloodGlucose], Error>.Promise?
- init(glucoseStorage: GlucoseStorage, glucoseManager: FetchGlucoseManager) {
- self.glucoseStorage = glucoseStorage
- self.glucoseManager = glucoseManager
- cgmManager = glucoseManager.cgmManager
- cgmManager?.delegateQueue = processQueue
- cgmManager?.cgmManagerDelegate = self
- }
- /// Function that fetches blood glucose data
- /// This function combines two data fetching mechanisms (`callBLEFetch` and `fetchIfNeeded`) into a single publisher.
- /// It returns the first non-empty result from either of the sources within a 5-minute timeout period.
- /// If no valid data is fetched within the timeout, it returns an empty array.
- ///
- /// - Parameter timer: An optional `DispatchTimer` (not used in the function but can be used to trigger fetch logic).
- /// - Returns: An `AnyPublisher` that emits an array of `BloodGlucose` values or an empty array if an error occurs or the timeout is reached.
- func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
- Publishers.Merge(
- callBLEFetch(),
- fetchIfNeeded()
- )
- .filter { !$0.isEmpty }
- .first()
- .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
- .replaceError(with: [])
- .eraseToAnyPublisher()
- }
- func callBLEFetch() -> AnyPublisher<[BloodGlucose], Never> {
- Future<[BloodGlucose], Error> { [weak self] promise in
- self?.promise = promise
- }
- .timeout(60 * 5, scheduler: processQueue, options: nil, customError: nil)
- .replaceError(with: [])
- .replaceEmpty(with: [])
- .eraseToAnyPublisher()
- }
- func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
- Future<[BloodGlucose], Error> { [weak self] promise in
- guard let self = self else { return }
- self.processQueue.async {
- guard let cgmManager = self.cgmManager else { return }
- cgmManager.fetchNewDataIfNeeded { result in
- promise(self.readCGMResult(readingResult: result))
- }
- }
- }
- .replaceError(with: [])
- .replaceEmpty(with: [])
- .eraseToAnyPublisher()
- }
- deinit {
- // dexcomManager.transmitter.stopScanning()
- }
- }
- extension PluginSource: CGMManagerDelegate {
- func deviceManager(
- _: LoopKit.DeviceManager,
- logEventForDeviceIdentifier deviceIdentifier: String?,
- type _: LoopKit.DeviceLogEntryType,
- message: String,
- completion _: ((Error?) -> Void)?
- ) {
- debug(.deviceManager, "device Manager for \(String(describing: deviceIdentifier)) : \(message)")
- }
- func issueAlert(_: LoopKit.Alert) {}
- func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
- func doesIssuedAlertExist(identifier _: LoopKit.Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {}
- func lookupAllUnretracted(
- managerIdentifier _: String,
- completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
- ) {}
- func lookupAllUnacknowledgedUnretracted(
- managerIdentifier _: String,
- completion _: @escaping (Result<[LoopKit.PersistedAlert], Error>) -> Void
- ) {}
- func recordRetractedAlert(_: LoopKit.Alert, at _: Date) {}
- func cgmManagerWantsDeletion(_ manager: CGMManager) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- debug(.deviceManager, " CGM Manager with identifier \(manager.pluginIdentifier) wants deletion")
- // TODO:
- glucoseManager?.cgmGlucoseSourceType = .none
- }
- func cgmManager(_: CGMManager, hasNew readingResult: CGMReadingResult) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- promise?(readCGMResult(readingResult: readingResult))
- debug(.deviceManager, "CGM PLUGIN - Direct return done")
- }
- func cgmManager(_: LoopKit.CGMManager, hasNew events: [LoopKit.PersistedCgmEvent]) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- // TODO: Events in APS ?
- // currently only display in log the date of the event
- events.forEach { event in
- debug(.deviceManager, "events from CGM at \(event.date)")
- if event.type == .sensorStart {
- self.glucoseManager?.removeCalibrations()
- }
- }
- }
- func startDateToFilterNewData(for _: CGMManager) -> Date? {
- dispatchPrecondition(condition: .onQueue(processQueue))
- return glucoseStorage.lastGlucoseDate()
- }
- func cgmManagerDidUpdateState(_ cgmManager: CGMManager) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- guard let fetchGlucoseManager = glucoseManager else {
- debug(
- .deviceManager,
- "Could not gracefully unwrap FetchGlucoseManager upon observing LoopKit's cgmManagerDidUpdateState"
- )
- return
- }
- // Adjust app-specific NS Upload setting value when CGM setting is changed
- fetchGlucoseManager.settingsManager.settings.uploadGlucose = cgmManager.shouldSyncToRemoteService
- fetchGlucoseManager.updateGlucoseSource(
- cgmGlucoseSourceType: fetchGlucoseManager.settingsManager.settings.cgm,
- cgmGlucosePluginId: fetchGlucoseManager.settingsManager.settings.cgmPluginIdentifier,
- newManager: cgmManager as? CGMManagerUI
- )
- }
- func credentialStoragePrefix(for _: CGMManager) -> String {
- // return string unique to this instance of the CGMManager
- UUID().uuidString
- }
- func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
- debug(.deviceManager, "DEBUG DID UPDATE STATE")
- processQueue.async {
- if self.cgmHasValidSensorSession != status.hasValidSensorSession {
- self.cgmHasValidSensorSession = status.hasValidSensorSession
- }
- }
- }
- private func readCGMResult(readingResult: CGMReadingResult) -> Result<[BloodGlucose], Error> {
- debug(.deviceManager, "PLUGIN CGM - Process CGM Reading Result launched with \(readingResult)")
- if glucoseManager?.glucoseSource == nil {
- debug(
- .deviceManager,
- "No glucose source available."
- )
- }
- switch readingResult {
- case let .newData(values):
- var sensorActivatedAt: Date?
- var sensorStartDate: Date?
- var sensorTransmitterID: String?
- /// SAGE
- if let cgmTransmitterManager = cgmManager as? LibreTransmitterManagerV3 {
- let sensorInfo = cgmTransmitterManager.sensorInfoObservable
- sensorActivatedAt = sensorInfo.activatedAt
- sensorStartDate = sensorInfo.activatedAt
- sensorTransmitterID = sensorInfo.sensorSerial
- } else if let cgmTransmitterManager = cgmManager as? G5CGMManager {
- let latestReading = cgmTransmitterManager.latestReading
- sensorActivatedAt = latestReading?.activationDate
- sensorStartDate = latestReading?.sessionStartDate
- sensorTransmitterID = latestReading?.transmitterID
- } else if let cgmTransmitterManager = cgmManager as? G6CGMManager {
- let latestReading = cgmTransmitterManager.latestReading
- sensorActivatedAt = latestReading?.activationDate
- sensorStartDate = latestReading?.sessionStartDate
- sensorTransmitterID = latestReading?.transmitterID
- } else if let cgmTransmitterManager = cgmManager as? G7CGMManager {
- sensorActivatedAt = cgmTransmitterManager.sensorActivatedAt
- sensorStartDate = cgmTransmitterManager.sensorActivatedAt
- sensorTransmitterID = cgmTransmitterManager.sensorName
- }
- let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
- let quantity = newGlucoseSample.quantity
- let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
- return BloodGlucose(
- _id: UUID().uuidString,
- sgv: value,
- direction: .init(trendType: newGlucoseSample.trend),
- date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
- dateString: newGlucoseSample.date,
- unfiltered: Decimal(value),
- filtered: nil,
- noise: nil,
- glucose: value,
- type: "sgv",
- activationDate: sensorActivatedAt,
- sessionStartDate: sensorStartDate,
- transmitterID: sensorTransmitterID
- )
- }
- return .success(bloodGlucose)
- case .unreliableData:
- // loopManager.receivedUnreliableCGMReading()
- return .failure(GlucoseDataError.unreliableData)
- case .noData:
- return .failure(GlucoseDataError.noData)
- case let .error(error):
- return .failure(error)
- }
- }
- }
- extension PluginSource {
- func sourceInfo() -> [String: Any]? {
- [GlucoseSourceKey.description.rawValue: "Plugin CGM source"]
- }
- }
|