| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- import Algorithms
- import Combine
- import Foundation
- import LoopKit
- import LoopKitUI
- import MinimedKit
- import MockKit
- import OmniBLE
- import OmniKit
- import SwiftDate
- import Swinject
- import UserNotifications
- protocol DeviceDataManager: GlucoseSource {
- var pumpManager: PumpManagerUI? { get set }
- var bluetoothManager: BluetoothStateManager { get }
- var loopInProgress: Bool { get set }
- var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
- var recommendsLoop: PassthroughSubject<Void, Never> { get }
- var bolusTrigger: PassthroughSubject<Bool, Never> { get }
- var manualTempBasal: PassthroughSubject<Bool, Never> { get }
- var errorSubject: PassthroughSubject<Error, Never> { get }
- var pumpName: CurrentValueSubject<String, Never> { get }
- var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
- func heartbeat(date: Date)
- func createBolusProgressReporter() -> DoseProgressReporter?
- var alertStore: [Alert] { get }
- }
- private let staticPumpManagers: [PumpManagerUI.Type] = [
- MinimedPumpManager.self,
- OmnipodPumpManager.self,
- OmniBLEPumpManager.self,
- MockPumpManager.self
- ]
- private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [
- MinimedPumpManager.managerIdentifier: MinimedPumpManager.self,
- OmnipodPumpManager.managerIdentifier: OmnipodPumpManager.self,
- OmniBLEPumpManager.managerIdentifier: OmniBLEPumpManager.self,
- MockPumpManager.managerIdentifier: MockPumpManager.self
- ]
- // private let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = staticPumpManagers.reduce(into: [:]) { map, Type in
- // map[Type.managerIdentifier] = Type
- // }
- private let accessLock = NSRecursiveLock(label: "BaseDeviceDataManager.accessLock")
- final class BaseDeviceDataManager: DeviceDataManager, Injectable {
- private let processQueue = DispatchQueue.markedQueue(label: "BaseDeviceDataManager.processQueue")
- @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
- @Injected() private var storage: FileStorage!
- @Injected() private var broadcaster: Broadcaster!
- @Injected() private var glucoseStorage: GlucoseStorage!
- @Injected() private var settingsManager: SettingsManager!
- @Injected() private var bluetoothProvider: BluetoothStateManager!
- @Persisted(key: "BaseDeviceDataManager.lastEventDate") var lastEventDate: Date? = nil
- @SyncAccess(lock: accessLock) @Persisted(key: "BaseDeviceDataManager.lastHeartBeatTime") var lastHeartBeatTime: Date =
- .distantPast
- let recommendsLoop = PassthroughSubject<Void, Never>()
- let bolusTrigger = PassthroughSubject<Bool, Never>()
- let errorSubject = PassthroughSubject<Error, Never>()
- let pumpNewStatus = PassthroughSubject<Void, Never>()
- let manualTempBasal = PassthroughSubject<Bool, Never>()
- var alertStore: [Alert]
- private let router = FreeAPSApp.resolver.resolve(Router.self)!
- @SyncAccess private var pumpUpdateCancellable: AnyCancellable?
- private var pumpUpdatePromise: Future<Bool, Never>.Promise?
- @SyncAccess var loopInProgress: Bool = false
- var pumpManager: PumpManagerUI? {
- didSet {
- pumpManager?.pumpManagerDelegate = self
- pumpManager?.delegateQueue = processQueue
- UserDefaults.standard.pumpManagerRawValue = pumpManager?.rawValue
- if let pumpManager = pumpManager {
- pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
- pumpName.send(pumpManager.localizedTitle)
- if let omnipod = pumpManager as? OmnipodPumpManager {
- guard let endTime = omnipod.state.podState?.expiresAt else {
- pumpExpiresAtDate.send(nil)
- return
- }
- pumpExpiresAtDate.send(endTime)
- }
- if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
- guard let endTime = omnipodBLE.state.podState?.expiresAt else {
- pumpExpiresAtDate.send(nil)
- return
- }
- pumpExpiresAtDate.send(endTime)
- }
- } else {
- pumpDisplayState.value = nil
- pumpExpiresAtDate.send(nil)
- pumpName.send("")
- }
- }
- }
- var bluetoothManager: BluetoothStateManager { bluetoothProvider }
- var hasBLEHeartbeat: Bool {
- (pumpManager as? MockPumpManager) == nil
- }
- let pumpDisplayState = CurrentValueSubject<PumpDisplayState?, Never>(nil)
- let pumpExpiresAtDate = CurrentValueSubject<Date?, Never>(nil)
- let pumpName = CurrentValueSubject<String, Never>("Pump")
- init(resolver: Resolver) {
- alertStore = []
- injectServices(resolver)
- setupPumpManager()
- UIDevice.current.isBatteryMonitoringEnabled = true
- }
- func setupPumpManager() {
- pumpManager = UserDefaults.standard.pumpManagerRawValue.flatMap { pumpManagerFromRawValue($0) }
- }
- func createBolusProgressReporter() -> DoseProgressReporter? {
- pumpManager?.createBolusProgressReporter(reportingOn: processQueue)
- }
- func heartbeat(date: Date) {
- guard pumpUpdateCancellable == nil else {
- warning(.deviceManager, "Pump updating already in progress. Skip updating.")
- return
- }
- guard !loopInProgress else {
- warning(.deviceManager, "Loop in progress. Skip updating.")
- return
- }
- func update(_: Future<Bool, Never>.Promise?) {}
- processQueue.safeSync {
- lastHeartBeatTime = date
- updatePumpData()
- }
- }
- private func updatePumpData() {
- guard let pumpManager = pumpManager else {
- debug(.deviceManager, "Pump is not set, skip updating")
- updateUpdateFinished(false)
- return
- }
- debug(.deviceManager, "Start updating the pump data")
- pumpUpdateCancellable = Future<Bool, Never> { [unowned self] promise in
- pumpUpdatePromise = promise
- debug(.deviceManager, "Waiting for pump update and loop recommendation")
- processQueue.safeSync {
- pumpManager.ensureCurrentPumpData { _ in
- debug(.deviceManager, "Pump data updated.")
- }
- }
- }
- .timeout(20, scheduler: processQueue)
- .replaceError(with: false)
- .replaceEmpty(with: false)
- .sink(receiveValue: updateUpdateFinished)
- }
- private func updateUpdateFinished(_ recommendsLoop: Bool) {
- pumpUpdateCancellable = nil
- pumpUpdatePromise = nil
- if !recommendsLoop {
- warning(.deviceManager, "Loop recommendation time out or got error. Trying to loop right now.")
- }
- guard !loopInProgress else {
- warning(.deviceManager, "Loop already in progress. Skip recommendation.")
- return
- }
- self.recommendsLoop.send()
- }
- private func pumpManagerFromRawValue(_ rawValue: [String: Any]) -> PumpManagerUI? {
- guard let rawState = rawValue["state"] as? PumpManager.RawStateValue,
- let Manager = pumpManagerTypeFromRawValue(rawValue)
- else {
- return nil
- }
- return Manager.init(rawState: rawState) as? PumpManagerUI
- }
- private func pumpManagerTypeFromRawValue(_ rawValue: [String: Any]) -> PumpManager.Type? {
- guard let managerIdentifier = rawValue["managerIdentifier"] as? String else {
- return nil
- }
- return staticPumpManagersByIdentifier[managerIdentifier]
- }
- // MARK: - GlucoseSource
- @Persisted(key: "BaseDeviceDataManager.lastFetchGlucoseDate") private var lastFetchGlucoseDate: Date = .distantPast
- func fetch() -> AnyPublisher<[BloodGlucose], Never> {
- guard let medtronic = pumpManager as? MinimedPumpManager else {
- warning(.deviceManager, "Fetch minilink glucose failed: Pump is not Medtronic")
- return Just([]).eraseToAnyPublisher()
- }
- guard lastFetchGlucoseDate.addingTimeInterval(5.minutes.timeInterval) < Date() else {
- return Just([]).eraseToAnyPublisher()
- }
- medtronic.cgmManagerDelegate = self
- return Future<[BloodGlucose], Error> { promise in
- self.processQueue.async {
- medtronic.fetchNewDataIfNeeded { result in
- switch result {
- case .noData:
- debug(.deviceManager, "Minilink glucose is empty")
- promise(.success([]))
- case .unreliableData:
- debug(.deviceManager, "Unreliable data received")
- promise(.success([]))
- case let .newData(glucose):
- let directions: [BloodGlucose.Direction?] = [nil]
- + glucose.windows(ofCount: 2).map { window -> BloodGlucose.Direction? in
- let pair = Array(window)
- guard pair.count == 2 else { return nil }
- let firstValue = Int(pair[0].quantity.doubleValue(for: .milligramsPerDeciliter))
- let secondValue = Int(pair[1].quantity.doubleValue(for: .milligramsPerDeciliter))
- return .init(trend: secondValue - firstValue)
- }
- let results = glucose.enumerated().map { index, sample -> BloodGlucose in
- let value = Int(sample.quantity.doubleValue(for: .milligramsPerDeciliter))
- return BloodGlucose(
- _id: sample.syncIdentifier,
- sgv: value,
- direction: directions[index],
- date: Decimal(Int(sample.date.timeIntervalSince1970 * 1000)),
- dateString: sample.date,
- unfiltered: nil,
- filtered: nil,
- noise: nil,
- glucose: value,
- type: "sgv"
- )
- }
- if let lastDate = results.last?.dateString {
- self.lastFetchGlucoseDate = lastDate
- }
- promise(.success(results))
- case let .error(error):
- warning(.deviceManager, "Fetch minilink glucose failed", error: error)
- promise(.failure(error))
- }
- }
- }
- }
- .timeout(60 * 3, scheduler: processQueue, options: nil, customError: nil)
- .replaceError(with: [])
- .replaceEmpty(with: [])
- .eraseToAnyPublisher()
- }
- }
- extension BaseDeviceDataManager: PumpManagerDelegate {
- func pumpManagerPumpWasReplaced(_: PumpManager) {
- debug(.deviceManager, "pumpManagerPumpWasReplaced")
- }
- var detectedSystemTimeOffset: TimeInterval {
- // trustedTimeChecker.detectedSystemTimeOffset
- 0
- }
- func pumpManager(_: PumpManager, didAdjustPumpClockBy adjustment: TimeInterval) {
- debug(.deviceManager, "didAdjustPumpClockBy \(adjustment)")
- }
- func pumpManagerDidUpdateState(_ pumpManager: PumpManager) {
- UserDefaults.standard.pumpManagerRawValue = pumpManager.rawValue
- if self.pumpManager == nil, let newPumpManager = pumpManager as? PumpManagerUI {
- self.pumpManager = newPumpManager
- }
- pumpName.send(pumpManager.localizedTitle)
- }
- func pumpManagerBLEHeartbeatDidFire(_: PumpManager) {
- debug(.deviceManager, "Pump Heartbeat: do nothing. Pump connection is OK")
- }
- func pumpManagerMustProvideBLEHeartbeat(_: PumpManager) -> Bool {
- true
- }
- func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus _: PumpManagerStatus) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- debug(.deviceManager, "New pump status Bolus: \(status.bolusState)")
- debug(.deviceManager, "New pump status Basal: \(String(describing: status.basalDeliveryState))")
- if case .inProgress = status.bolusState {
- bolusTrigger.send(true)
- } else {
- bolusTrigger.send(false)
- }
- let batteryPercent = Int((status.pumpBatteryChargeRemaining ?? 1) * 100)
- let battery = Battery(
- percent: batteryPercent,
- voltage: nil,
- string: batteryPercent >= 10 ? .normal : .low,
- display: pumpManager.status.pumpBatteryChargeRemaining != nil
- )
- storage.save(battery, as: OpenAPS.Monitor.battery)
- broadcaster.notify(PumpBatteryObserver.self, on: processQueue) {
- $0.pumpBatteryDidChange(battery)
- }
- if let omnipod = pumpManager as? OmnipodPumpManager {
- let reservoirVal = omnipod.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
- // TODO: find the value Pod.maximumReservoirReading
- let reservoir = Decimal(reservoirVal) > 50.0 ? 0xDEAD_BEEF : reservoirVal
- storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir)
- broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
- $0.pumpReservoirDidChange(Decimal(reservoir))
- }
- if let tempBasal = omnipod.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(),
- !tempBasal.automatic
- {
- // the manual basal temp is launch - block every thing
- debug(.deviceManager, "manual temp basal")
- manualTempBasal.send(true)
- } else {
- // no more manual Temp Basal !
- manualTempBasal.send(false)
- }
- guard let endTime = omnipod.state.podState?.expiresAt else {
- pumpExpiresAtDate.send(nil)
- return
- }
- pumpExpiresAtDate.send(endTime)
- }
- if let omnipodBLE = pumpManager as? OmniBLEPumpManager {
- let reservoirVal = omnipodBLE.state.podState?.lastInsulinMeasurements?.reservoirLevel ?? 0xDEAD_BEEF
- // TODO: find the value Pod.maximumReservoirReading
- let reservoir = Decimal(reservoirVal) > 50.0 ? 0xDEAD_BEEF : reservoirVal
- storage.save(Decimal(reservoir), as: OpenAPS.Monitor.reservoir)
- broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
- $0.pumpReservoirDidChange(Decimal(reservoir))
- }
- // manual temp basal on
- if let tempBasal = omnipodBLE.state.podState?.unfinalizedTempBasal, !tempBasal.isFinished(),
- !tempBasal.automatic
- {
- // the manual basal temp is launch - block every thing
- debug(.deviceManager, "manual temp basal")
- manualTempBasal.send(true)
- } else {
- // no more manual Temp Basal !
- manualTempBasal.send(false)
- }
- guard let endTime = omnipodBLE.state.podState?.expiresAt else {
- pumpExpiresAtDate.send(nil)
- return
- }
- pumpExpiresAtDate.send(endTime)
- }
- }
- func pumpManagerWillDeactivate(_: PumpManager) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- pumpManager = nil
- }
- func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
- func pumpManager(_: PumpManager, didError error: PumpManagerError) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- debug(.deviceManager, "error: \(error.localizedDescription), reason: \(String(describing: error.failureReason))")
- errorSubject.send(error)
- }
- func pumpManager(
- _: PumpManager,
- hasNewPumpEvents events: [NewPumpEvent],
- lastReconciliation _: Date?,
- completion: @escaping (_ error: Error?) -> Void
- ) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- debug(.deviceManager, "New pump events:\n\(events.map(\.title).joined(separator: "\n"))")
- // filter buggy TBRs > maxBasal from MDT
- let events = events.filter {
- // type is optional...
- guard let type = $0.type, type == .tempBasal else { return true }
- return $0.dose?.unitsPerHour ?? 0 <= Double(settingsManager.pumpSettings.maxBasal)
- }
- pumpHistoryStorage.storePumpEvents(events)
- lastEventDate = events.last?.date
- completion(nil)
- }
- func pumpManager(
- _: PumpManager,
- didReadReservoirValue units: Double,
- at date: Date,
- completion: @escaping (Result<
- (newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool),
- Error
- >) -> Void
- ) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- debug(.deviceManager, "Reservoir Value \(units), at: \(date)")
- storage.save(Decimal(units), as: OpenAPS.Monitor.reservoir)
- broadcaster.notify(PumpReservoirObserver.self, on: processQueue) {
- $0.pumpReservoirDidChange(Decimal(units))
- }
- completion(.success((
- newValue: Reservoir(startDate: Date(), unitVolume: units),
- lastValue: nil,
- areStoredValuesContinuous: true
- )))
- }
- func pumpManagerRecommendsLoop(_: PumpManager) {
- dispatchPrecondition(condition: .onQueue(processQueue))
- debug(.deviceManager, "Pump recommends loop")
- guard let promise = pumpUpdatePromise else {
- warning(.deviceManager, "We do not waiting for loop recommendation at this time.")
- return
- }
- promise(.success(true))
- }
- func startDateToFilterNewPumpEvents(for _: PumpManager) -> Date {
- lastEventDate?.addingTimeInterval(-15.minutes.timeInterval) ?? Date().addingTimeInterval(-2.hours.timeInterval)
- }
- }
- // MARK: - DeviceManagerDelegate
- extension BaseDeviceDataManager: DeviceManagerDelegate {
- func issueAlert(_ alert: Alert) {
- if !alertStore.contains(where: { $0.identifier.alertIdentifier == alert.identifier.alertIdentifier }) {
- alertStore.append(alert)
- let typeMessage: MessageType
- let alertUp = alert.identifier.alertIdentifier.uppercased()
- if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
- typeMessage = .errorPump
- } else {
- typeMessage = .warning
- }
- DispatchQueue.main.async {
- let messageCont = MessageContent(content: alert.foregroundContent!.body, type: typeMessage)
- self.router.alertMessage.send(messageCont)
- // validation
- self.pumpManager?.acknowledgeAlert(alertIdentifier: alert.identifier.alertIdentifier) { error in
- if let error = error {
- debug(.deviceManager, "acknowledge not succeeded with error \(error.localizedDescription)")
- }
- }
- }
- broadcaster.notify(pumpNotificationObserver.self, on: processQueue) {
- $0.pumpNotification(alert: alert)
- }
- }
- }
- func retractAlert(identifier: Alert.Identifier) {
- if let idx = alertStore.firstIndex(where: { $0.identifier.alertIdentifier == identifier.alertIdentifier }) {
- alertStore.remove(at: idx)
- broadcaster.notify(pumpNotificationObserver.self, on: processQueue) {
- $0.pumpRemoveNotification()
- }
- }
- }
- func doesIssuedAlertExist(identifier _: Alert.Identifier, completion _: @escaping (Result<Bool, Error>) -> Void) {
- debug(.deviceManager, "doesIssueAlertExist")
- }
- func lookupAllUnretracted(managerIdentifier _: String, completion _: @escaping (Result<[PersistedAlert], Error>) -> Void) {
- debug(.deviceManager, "lookupAllUnretracted")
- }
- func lookupAllUnacknowledgedUnretracted(
- managerIdentifier _: String,
- completion _: @escaping (Result<[PersistedAlert], Error>) -> Void
- ) {}
- func recordRetractedAlert(_: Alert, at _: Date) {}
- // func scheduleNotification(
- // for _: DeviceManager,
- // identifier: String,
- // content: UNNotificationContent,
- // trigger: UNNotificationTrigger?
- // ) {
- // let request = UNNotificationRequest(
- // identifier: identifier,
- // content: content,
- // trigger: trigger
- // )
- //
- // DispatchQueue.main.async {
- // UNUserNotificationCenter.current().add(request)
- // }
- // }
- //
- // func clearNotification(for _: DeviceManager, identifier: String) {
- // DispatchQueue.main.async {
- // UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
- // }
- // }
- func removeNotificationRequests(for _: DeviceManager, identifiers: [String]) {
- DispatchQueue.main.async {
- UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
- }
- }
- func deviceManager(
- _: DeviceManager,
- logEventForDeviceIdentifier _: String?,
- type _: DeviceLogEntryType,
- message: String,
- completion _: ((Error?) -> Void)?
- ) {
- debug(.deviceManager, "Device message: \(message)")
- }
- }
- extension BaseDeviceDataManager: CGMManagerDelegate {
- func startDateToFilterNewData(for _: CGMManager) -> Date? {
- glucoseStorage.syncDate().addingTimeInterval(-10.minutes.timeInterval) // additional time to calculate directions
- }
- func cgmManager(_: CGMManager, hasNew _: CGMReadingResult) {}
- func cgmManagerWantsDeletion(_: CGMManager) {}
- func cgmManagerDidUpdateState(_: CGMManager) {}
- func credentialStoragePrefix(for _: CGMManager) -> String { "BaseDeviceDataManager" }
- func cgmManager(_: CGMManager, didUpdate _: CGMManagerStatus) {}
- }
- // MARK: - AlertPresenter
- // extension BaseDeviceDataManager: AlertPresenter {
- // func issueAlert(_: Alert) {}
- // func retractAlert(identifier _: Alert.Identifier) {}
- // }
- // MARK: Others
- protocol PumpReservoirObserver {
- func pumpReservoirDidChange(_ reservoir: Decimal)
- }
- protocol PumpBatteryObserver {
- func pumpBatteryDidChange(_ battery: Battery)
- }
|