MinimedPumpManager.swift 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350
  1. //
  2. // MinimedPumpManager.swift
  3. // Loop
  4. //
  5. // Copyright © 2018 LoopKit Authors. All rights reserved.
  6. //
  7. import HealthKit
  8. import LoopKit
  9. import RileyLinkKit
  10. import RileyLinkBLEKit
  11. import os.log
  12. public protocol MinimedPumpManagerStateObserver: AnyObject {
  13. func didUpdatePumpManagerState(_ state: MinimedPumpManagerState)
  14. }
  15. public class MinimedPumpManager: RileyLinkPumpManager {
  16. public init(state: MinimedPumpManagerState, rileyLinkDeviceProvider: RileyLinkDeviceProvider, rileyLinkConnectionManager: RileyLinkConnectionManager? = nil, pumpOps: PumpOps? = nil) {
  17. self.lockedState = Locked(state)
  18. self.hkDevice = HKDevice(
  19. name: type(of: self).managerIdentifier,
  20. manufacturer: "Medtronic",
  21. model: state.pumpModel.rawValue,
  22. hardwareVersion: nil,
  23. firmwareVersion: state.pumpFirmwareVersion,
  24. softwareVersion: String(MinimedKitVersionNumber),
  25. localIdentifier: state.pumpID,
  26. udiDeviceIdentifier: nil
  27. )
  28. super.init(rileyLinkDeviceProvider: rileyLinkDeviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
  29. // Pump communication
  30. let idleListeningEnabled = state.pumpModel.hasMySentry && state.useMySentry
  31. self.pumpOps = pumpOps ?? PumpOps(pumpSettings: state.pumpSettings, pumpState: state.pumpState, delegate: self)
  32. self.rileyLinkDeviceProvider.idleListeningState = idleListeningEnabled ? MinimedPumpManagerState.idleListeningEnabledDefaults : .disabled
  33. }
  34. public required convenience init?(rawState: PumpManager.RawStateValue) {
  35. guard let state = MinimedPumpManagerState(rawValue: rawState),
  36. let connectionManagerState = state.rileyLinkConnectionManagerState else
  37. {
  38. return nil
  39. }
  40. let rileyLinkConnectionManager = RileyLinkConnectionManager(state: connectionManagerState)
  41. self.init(state: state, rileyLinkDeviceProvider: rileyLinkConnectionManager.deviceProvider, rileyLinkConnectionManager: rileyLinkConnectionManager)
  42. rileyLinkConnectionManager.delegate = self
  43. }
  44. public private(set) var pumpOps: PumpOps!
  45. // MARK: - PumpManager
  46. public let stateObservers = WeakSynchronizedSet<MinimedPumpManagerStateObserver>()
  47. public var state: MinimedPumpManagerState {
  48. return lockedState.value
  49. }
  50. private let lockedState: Locked<MinimedPumpManagerState>
  51. private func setState(_ changes: (_ state: inout MinimedPumpManagerState) -> Void) -> Void {
  52. return setStateWithResult(changes)
  53. }
  54. private func mutateState(_ changes: (_ state: inout MinimedPumpManagerState) -> Void) -> MinimedPumpManagerState {
  55. return setStateWithResult({ (state) -> MinimedPumpManagerState in
  56. changes(&state)
  57. return state
  58. })
  59. }
  60. private func setStateWithResult<ReturnType>(_ changes: (_ state: inout MinimedPumpManagerState) -> ReturnType) -> ReturnType {
  61. var oldValue: MinimedPumpManagerState!
  62. var returnValue: ReturnType!
  63. let newValue = lockedState.mutate { (state) in
  64. oldValue = state
  65. returnValue = changes(&state)
  66. }
  67. guard oldValue != newValue else {
  68. return returnValue
  69. }
  70. let recents = self.recents
  71. let oldStatus = status(for: oldValue, recents: recents)
  72. let newStatus = status(for: newValue, recents: recents)
  73. // PumpManagerStatus may have changed
  74. if oldStatus != newStatus
  75. {
  76. notifyStatusObservers(oldStatus: oldStatus)
  77. }
  78. pumpDelegate.notify { (delegate) in
  79. delegate?.pumpManagerDidUpdateState(self)
  80. }
  81. stateObservers.forEach { (observer) in
  82. observer.didUpdatePumpManagerState(newValue)
  83. }
  84. return returnValue
  85. }
  86. /// Temporal state of the manager
  87. private var recents: MinimedPumpManagerRecents {
  88. get {
  89. return lockedRecents.value
  90. }
  91. set {
  92. let oldValue = recents
  93. let oldStatus = status
  94. lockedRecents.value = newValue
  95. // Battery percentage may have changed
  96. if oldValue.latestPumpStatusFromMySentry != newValue.latestPumpStatusFromMySentry ||
  97. oldValue.latestPumpStatus != newValue.latestPumpStatus
  98. {
  99. let oldBatteryPercentage = state.batteryPercentage
  100. let newBatteryPercentage: Double?
  101. // Persist the updated battery level
  102. if let status = newValue.latestPumpStatusFromMySentry {
  103. newBatteryPercentage = Double(status.batteryRemainingPercent) / 100
  104. } else if let status = newValue.latestPumpStatus {
  105. newBatteryPercentage = batteryChemistry.chargeRemaining(at: status.batteryVolts)
  106. } else {
  107. newBatteryPercentage = nil
  108. }
  109. if oldBatteryPercentage != newBatteryPercentage {
  110. setState { (state) in
  111. state.batteryPercentage = newBatteryPercentage
  112. }
  113. }
  114. }
  115. if oldStatus != status {
  116. notifyStatusObservers(oldStatus: oldStatus)
  117. }
  118. }
  119. }
  120. private let lockedRecents = Locked(MinimedPumpManagerRecents())
  121. private let statusObservers = WeakSynchronizedSet<PumpManagerStatusObserver>()
  122. private func notifyStatusObservers(oldStatus: PumpManagerStatus) {
  123. let status = self.status
  124. pumpDelegate.notify { (delegate) in
  125. delegate?.pumpManager(self, didUpdate: status, oldStatus: oldStatus)
  126. }
  127. statusObservers.forEach { (observer) in
  128. observer.pumpManager(self, didUpdate: status, oldStatus: oldStatus)
  129. }
  130. }
  131. private let cgmDelegate = WeakSynchronizedDelegate<CGMManagerDelegate>()
  132. private let pumpDelegate = WeakSynchronizedDelegate<PumpManagerDelegate>()
  133. public let log = OSLog(category: "MinimedPumpManager")
  134. // MARK: - CGMManager
  135. private let hkDevice: HKDevice
  136. // MARK: - RileyLink Updates
  137. override public var rileyLinkConnectionManagerState: RileyLinkConnectionManagerState? {
  138. get {
  139. return state.rileyLinkConnectionManagerState
  140. }
  141. set {
  142. setState { (state) in
  143. state.rileyLinkConnectionManagerState = newValue
  144. }
  145. }
  146. }
  147. override public func device(_ device: RileyLinkDevice, didReceivePacket packet: RFPacket) {
  148. device.assertOnSessionQueue()
  149. guard let data = MinimedPacket(encodedData: packet.data)?.data,
  150. let message = PumpMessage(rxData: data),
  151. message.address.hexadecimalString == state.pumpID,
  152. case .mySentry = message.packetType
  153. else {
  154. return
  155. }
  156. switch message.messageBody {
  157. case let body as MySentryPumpStatusMessageBody:
  158. self.updatePumpStatus(body, from: device)
  159. case is MySentryAlertMessageBody, is MySentryAlertClearedMessageBody:
  160. break
  161. case let body:
  162. self.log.error("Unknown MySentry Message: %d: %{public}@", message.messageType.rawValue, body.txData.hexadecimalString)
  163. }
  164. }
  165. override public func deviceTimerDidTick(_ device: RileyLinkDevice) {
  166. pumpDelegate.notify { (delegate) in
  167. delegate?.pumpManagerBLEHeartbeatDidFire(self)
  168. }
  169. }
  170. public var rileyLinkBatteryAlertLevel: Int? {
  171. get {
  172. return state.rileyLinkBatteryAlertLevel
  173. }
  174. set {
  175. setState { state in
  176. state.rileyLinkBatteryAlertLevel = newValue
  177. }
  178. }
  179. }
  180. public override func device(_ device: RileyLinkDevice, didUpdateBattery level: Int) {
  181. let repeatInterval: TimeInterval = .hours(1)
  182. if let alertLevel = state.rileyLinkBatteryAlertLevel,
  183. level <= alertLevel,
  184. state.lastRileyLinkBatteryAlertDate.addingTimeInterval(repeatInterval) < Date()
  185. {
  186. self.setState { state in
  187. state.lastRileyLinkBatteryAlertDate = Date()
  188. }
  189. // HACK Alert. This is temporary for the 2.2.5 release. Dev and newer releases will use the new Loop Alert facility
  190. let notification = UNMutableNotificationContent()
  191. notification.body = String(format: LocalizedString("\"%1$@\" has a low battery", comment: "Format string for low battery alert body for RileyLink. (1: device name)"), device.name ?? "unnamed")
  192. notification.title = LocalizedString("Low RileyLink Battery", comment: "Title for RileyLink low battery alert")
  193. notification.sound = .default
  194. notification.categoryIdentifier = LoopNotificationCategory.loopNotRunning.rawValue
  195. notification.threadIdentifier = LoopNotificationCategory.loopNotRunning.rawValue
  196. let request = UNNotificationRequest(
  197. identifier: "batteryalert.rileylink",
  198. content: notification,
  199. trigger: nil)
  200. UNUserNotificationCenter.current().add(request)
  201. }
  202. }
  203. // MARK: - CustomDebugStringConvertible
  204. override public var debugDescription: String {
  205. return [
  206. "## MinimedPumpManager",
  207. "isPumpDataStale: \(isPumpDataStale)",
  208. "pumpOps: \(String(reflecting: pumpOps))",
  209. "recents: \(String(reflecting: recents))",
  210. "state: \(String(reflecting: state))",
  211. "status: \(String(describing: status))",
  212. "stateObservers.count: \(stateObservers.cleanupDeallocatedElements().count)",
  213. "statusObservers.count: \(statusObservers.cleanupDeallocatedElements().count)",
  214. super.debugDescription,
  215. ].joined(separator: "\n")
  216. }
  217. }
  218. extension MinimedPumpManager {
  219. /**
  220. Attempts to fix an extended communication failure between a RileyLink device and the pump
  221. - parameter device: The RileyLink device
  222. */
  223. private func troubleshootPumpComms(using device: RileyLinkDevice) {
  224. device.assertOnSessionQueue()
  225. // Ensuring timer tick is enabled will allow more tries to bring the pump data up-to-date.
  226. updateBLEHeartbeatPreference()
  227. // How long we should wait before we re-tune the RileyLink
  228. let tuneTolerance = TimeInterval(minutes: 14)
  229. let lastTuned = state.lastTuned ?? .distantPast
  230. if lastTuned.timeIntervalSinceNow <= -tuneTolerance {
  231. pumpOps.runSession(withName: "Tune pump", using: device) { (session) in
  232. do {
  233. let scanResult = try session.tuneRadio(attempts: 1)
  234. self.log.default("Device %{public}@ auto-tuned to %{public}@ MHz", device.name ?? "", String(describing: scanResult.bestFrequency))
  235. } catch let error {
  236. self.log.error("Device %{public}@ auto-tune failed with error: %{public}@", device.name ?? "", String(describing: error))
  237. self.rileyLinkDeviceProvider.deprioritize(device, completion: nil)
  238. if let error = error as? LocalizedError {
  239. self.pumpDelegate.notify { (delegate) in
  240. delegate?.pumpManager(self, didError: PumpManagerError.communication(MinimedPumpManagerError.tuneFailed(error)))
  241. }
  242. }
  243. }
  244. }
  245. } else {
  246. rileyLinkDeviceProvider.deprioritize(device, completion: nil)
  247. }
  248. }
  249. /// - Throws: `PumpCommandError` specifying the failure sequence
  250. private func runSuspendResumeOnSession(suspendResumeState: SuspendResumeMessageBody.SuspendResumeState, session: PumpOpsSession, insulinType: InsulinType) throws {
  251. defer { self.recents.suspendEngageState = .stable }
  252. self.recents.suspendEngageState = suspendResumeState == .suspend ? .engaging : .disengaging
  253. try session.setSuspendResumeState(suspendResumeState)
  254. setState { (state) in
  255. let date = Date()
  256. switch suspendResumeState {
  257. case .suspend:
  258. state.suspendState = .suspended(date)
  259. case .resume:
  260. state.suspendState = .resumed(date)
  261. }
  262. if suspendResumeState == .suspend {
  263. let pumpModel = state.pumpModel
  264. state.unfinalizedBolus?.cancel(at: Date(), pumpModel: pumpModel)
  265. if let bolus = state.unfinalizedBolus {
  266. state.pendingDoses.append(bolus)
  267. }
  268. state.unfinalizedBolus = nil
  269. state.pendingDoses.append(UnfinalizedDose(suspendStartTime: Date()))
  270. } else {
  271. state.pendingDoses.append(UnfinalizedDose(resumeStartTime: Date(), insulinType: insulinType))
  272. }
  273. }
  274. }
  275. private func setSuspendResumeState(state: SuspendResumeMessageBody.SuspendResumeState, insulinType: InsulinType, completion: @escaping (MinimedPumpManagerError?) -> Void) {
  276. rileyLinkDeviceProvider.getDevices { (devices) in
  277. guard let device = devices.firstConnected else {
  278. completion(MinimedPumpManagerError.noRileyLink)
  279. return
  280. }
  281. let sessionName: String = {
  282. switch state {
  283. case .suspend:
  284. return "Suspend Delivery"
  285. case .resume:
  286. return "Resume Delivery"
  287. }
  288. }()
  289. self.pumpOps.runSession(withName: sessionName, using: device) { (session) in
  290. do {
  291. try self.runSuspendResumeOnSession(suspendResumeState: state, session: session, insulinType: insulinType)
  292. self.storePendingPumpEvents({ (error) in
  293. completion(error)
  294. })
  295. } catch let error {
  296. self.troubleshootPumpComms(using: device)
  297. completion(MinimedPumpManagerError.commsError(error as! PumpCommandError))
  298. }
  299. }
  300. }
  301. }
  302. /**
  303. Handles receiving a MySentry status message, which are only posted by MM x23 pumps.
  304. This message has two important pieces of info about the pump: reservoir volume and battery.
  305. Because the RileyLink must actively listen for these packets, they are not a reliable heartbeat. However, we can still use them to assert glucose data is current.
  306. - parameter status: The status message body
  307. - parameter device: The RileyLink that received the message
  308. */
  309. private func updatePumpStatus(_ status: MySentryPumpStatusMessageBody, from device: RileyLinkDevice) {
  310. device.assertOnSessionQueue()
  311. log.default("MySentry message received")
  312. var pumpDateComponents = status.pumpDateComponents
  313. var glucoseDateComponents = status.glucoseDateComponents
  314. let timeZone = state.timeZone
  315. pumpDateComponents.timeZone = timeZone
  316. glucoseDateComponents?.timeZone = timeZone
  317. checkRileyLinkBattery()
  318. // The pump sends the same message 3x, so ignore it if we've already seen it.
  319. guard status != recents.latestPumpStatusFromMySentry, let pumpDate = pumpDateComponents.date else {
  320. return
  321. }
  322. // Ignore status messages without some semblance of recency.
  323. guard abs(pumpDate.timeIntervalSinceNow) < .minutes(5) else {
  324. log.error("Ignored MySentry status due to date mismatch: %{public}@ in %{public}", String(describing: pumpDate), String(describing: timeZone))
  325. return
  326. }
  327. recents.latestPumpStatusFromMySentry = status
  328. switch status.glucose {
  329. case .active(glucose: let glucose):
  330. // Enlite data is included
  331. if let date = glucoseDateComponents?.date {
  332. let sample = NewGlucoseSample(
  333. date: date,
  334. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose)),
  335. isDisplayOnly: false,
  336. wasUserEntered: false,
  337. syncIdentifier: status.glucoseSyncIdentifier ?? UUID().uuidString,
  338. device: self.device
  339. )
  340. cgmDelegate.notify { (delegate) in
  341. delegate?.cgmManager(self, hasNew: .newData([sample]))
  342. }
  343. }
  344. case .off:
  345. // Enlite is disabled, so assert glucose from another source
  346. pumpDelegate.notify { (delegate) in
  347. delegate?.pumpManagerBLEHeartbeatDidFire(self)
  348. }
  349. default:
  350. // Anything else is an Enlite error
  351. // TODO: Provide info about status.glucose
  352. cgmDelegate.notify { (delegate) in
  353. delegate?.cgmManager(self, hasNew: .error(PumpManagerError.deviceState(nil)))
  354. }
  355. }
  356. // Sentry packets are sent in groups of 3, 5s apart. Wait 11s before allowing the loop data to continue to avoid conflicting comms.
  357. device.sessionQueueAsyncAfter(deadline: .now() + .seconds(11)) { [weak self] in
  358. self?.updateReservoirVolume(status.reservoirRemainingUnits, at: pumpDate, withTimeLeft: TimeInterval(minutes: Double(status.reservoirRemainingMinutes)))
  359. }
  360. }
  361. private func checkRileyLinkBattery() {
  362. rileyLinkDeviceProvider.getDevices { devices in
  363. for device in devices {
  364. device.updateBatteryLevel()
  365. }
  366. }
  367. }
  368. /**
  369. Store a new reservoir volume and notify observers of new pump data.
  370. - parameter units: The number of units remaining
  371. - parameter date: The date the reservoir was read
  372. - parameter timeLeft: The approximate time before the reservoir is empty
  373. */
  374. private func updateReservoirVolume(_ units: Double, at date: Date, withTimeLeft timeLeft: TimeInterval?) {
  375. // Must be called from the sessionQueue
  376. setState { (state) in
  377. state.lastReservoirReading = ReservoirReading(units: units, validAt: date)
  378. }
  379. pumpDelegate.notify { (delegate) in
  380. delegate?.pumpManager(self, didReadReservoirValue: units, at: date) { (result) in
  381. self.pumpManagerDelegateDidProcessReservoirValue(result)
  382. }
  383. }
  384. // New reservoir data means we may want to adjust our timer tick requirements
  385. updateBLEHeartbeatPreference()
  386. }
  387. /// Called on an unknown queue by the delegate
  388. private func pumpManagerDelegateDidProcessReservoirValue(_ result: Result<(newValue: ReservoirValue, lastValue: ReservoirValue?, areStoredValuesContinuous: Bool), Error>) {
  389. switch result {
  390. case .failure:
  391. break
  392. case .success(let (_, _, areStoredValuesContinuous)):
  393. // Run a loop as long as we have fresh, reliable pump data.
  394. if state.preferredInsulinDataSource == .pumpHistory || !areStoredValuesContinuous {
  395. fetchPumpHistory { (error) in // Can be centralQueue or sessionQueue
  396. self.pumpDelegate.notify { (delegate) in
  397. if let error = error as? PumpManagerError {
  398. delegate?.pumpManager(self, didError: error)
  399. }
  400. if error == nil || areStoredValuesContinuous {
  401. delegate?.pumpManagerRecommendsLoop(self)
  402. }
  403. }
  404. }
  405. } else {
  406. pumpDelegate.notify { (delegate) in
  407. delegate?.pumpManagerRecommendsLoop(self)
  408. }
  409. }
  410. }
  411. }
  412. static func reconcilePendingDosesWith(_ events: [NewPumpEvent], reconciliationMappings: [Data:ReconciledDoseMapping], pendingDoses: [UnfinalizedDose]) ->
  413. (remainingEvents: [NewPumpEvent], reconciliationMappings: [Data:ReconciledDoseMapping], pendingDoses: [UnfinalizedDose]) {
  414. var newReconciliationMapping = reconciliationMappings
  415. var reconcilableEvents = events.filter { !newReconciliationMapping.keys.contains($0.raw) }
  416. // Pending doses can be matched to history events if start time difference is smaller than this
  417. let matchingTimeWindow = TimeInterval(minutes: 1)
  418. func addReconciliationMapping(startTime: Date, uuid: UUID, eventRaw: Data, index: Int) -> Void {
  419. let mapping = ReconciledDoseMapping(startTime: startTime, uuid: uuid, eventRaw: eventRaw)
  420. newReconciliationMapping[eventRaw] = mapping
  421. }
  422. // Reconcile any pending doses
  423. let allPending = pendingDoses.map { (dose) -> UnfinalizedDose in
  424. if let index = reconcilableEvents.firstMatchingIndex(for: dose, within: matchingTimeWindow) {
  425. let historyEvent = reconcilableEvents[index]
  426. addReconciliationMapping(startTime: dose.startTime, uuid: dose.uuid, eventRaw: historyEvent.raw, index: index)
  427. var reconciledDose = dose
  428. reconciledDose.reconcile(with: historyEvent)
  429. reconcilableEvents.remove(at: index)
  430. return reconciledDose
  431. }
  432. return dose
  433. }
  434. // Remove reconciled events
  435. let remainingPumpEvents = events.filter { (event) -> Bool in
  436. return newReconciliationMapping[event.raw] == nil
  437. }
  438. return (remainingEvents: remainingPumpEvents, reconciliationMappings: newReconciliationMapping, pendingDoses: allPending)
  439. }
  440. private func reconcilePendingDosesWith(_ events: [NewPumpEvent]) -> [NewPumpEvent] {
  441. // Must be called from the sessionQueue
  442. return setStateWithResult { (state) -> [NewPumpEvent] in
  443. let allPending = (state.pendingDoses + [state.unfinalizedTempBasal, state.unfinalizedBolus]).compactMap({ $0 })
  444. let result = MinimedPumpManager.reconcilePendingDosesWith(events, reconciliationMappings: state.reconciliationMappings, pendingDoses: allPending)
  445. state.lastReconciliation = Date()
  446. // Pending doses and reconciliation mappings will not be kept past this threshold
  447. let expirationCutoff = Date().addingTimeInterval(.hours(-12))
  448. state.reconciliationMappings = result.reconciliationMappings.filter { (key, value) -> Bool in
  449. return value.startTime >= expirationCutoff
  450. }
  451. state.unfinalizedBolus = nil
  452. state.unfinalizedTempBasal = nil
  453. state.pendingDoses = result.pendingDoses.filter { (dose) -> Bool in
  454. if !dose.isFinished {
  455. switch dose.doseType {
  456. case .bolus:
  457. state.unfinalizedBolus = dose
  458. return false
  459. case .tempBasal:
  460. state.unfinalizedTempBasal = dose
  461. return false
  462. default:
  463. break
  464. }
  465. }
  466. return dose.startTime >= expirationCutoff
  467. }
  468. if var runningTempBasal = state.unfinalizedTempBasal {
  469. // Look for following temp basal cancel event
  470. if let tempBasalCancellation = result.remainingEvents.first(where: { (event) -> Bool in
  471. if let dose = event.dose,
  472. dose.type == .tempBasal,
  473. dose.startDate > runningTempBasal.startTime,
  474. dose.startDate < runningTempBasal.finishTime,
  475. dose.unitsPerHour == 0
  476. {
  477. return true
  478. }
  479. return false
  480. }) {
  481. runningTempBasal.finishTime = tempBasalCancellation.date
  482. state.unfinalizedTempBasal = runningTempBasal
  483. state.suspendState = .resumed(tempBasalCancellation.date)
  484. }
  485. }
  486. return result.remainingEvents
  487. }
  488. }
  489. /// Polls the pump for new history events and passes them to the loop manager
  490. ///
  491. /// - Parameters:
  492. /// - completion: A closure called once upon completion
  493. /// - error: An error describing why the fetch and/or store failed
  494. private func fetchPumpHistory(_ completion: @escaping (_ error: Error?) -> Void) {
  495. guard let insulinType = insulinType else {
  496. completion(PumpManagerError.configuration(nil))
  497. return
  498. }
  499. rileyLinkDeviceProvider.getDevices { (devices) in
  500. guard let device = devices.firstConnected else {
  501. completion(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink))
  502. return
  503. }
  504. self.pumpOps.runSession(withName: "Fetch Pump History", using: device) { (session) in
  505. do {
  506. guard let startDate = self.pumpDelegate.call({ (delegate) in
  507. return delegate?.startDateToFilterNewPumpEvents(for: self)
  508. }) else {
  509. preconditionFailure("pumpManagerDelegate cannot be nil")
  510. }
  511. // Include events up to a minute before startDate, since pump event time and pending event time might be off
  512. let (historyEvents, model) = try session.getHistoryEvents(since: startDate.addingTimeInterval(.minutes(-1)))
  513. // Reconcile history with pending doses
  514. let newPumpEvents = historyEvents.pumpEvents(from: model)
  515. // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType
  516. let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents).map { (event) -> NewPumpEvent in
  517. return NewPumpEvent(
  518. date: event.date,
  519. dose: event.dose?.annotated(with: insulinType),
  520. isMutable: event.isMutable,
  521. raw: event.raw,
  522. title: event.title,
  523. type: event.type)
  524. }
  525. self.pumpDelegate.notify({ (delegate) in
  526. guard let delegate = delegate else {
  527. preconditionFailure("pumpManagerDelegate cannot be nil")
  528. }
  529. let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent })
  530. delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastReconciliation: self.lastReconciliation, completion: { (error) in
  531. // Called on an unknown queue by the delegate
  532. if error == nil {
  533. self.recents.lastAddedPumpEvents = Date()
  534. self.setState({ (state) in
  535. // Remove any pending doses that have been reconciled and are finished
  536. if let bolus = state.unfinalizedBolus, bolus.isReconciledWithHistory, bolus.isFinished {
  537. state.unfinalizedBolus = nil
  538. }
  539. if let tempBasal = state.unfinalizedTempBasal, tempBasal.isReconciledWithHistory, tempBasal.isFinished {
  540. state.unfinalizedTempBasal = nil
  541. }
  542. state.pendingDoses.removeAll(where: { (dose) -> Bool in
  543. if dose.isReconciledWithHistory && dose.isFinished {
  544. print("Removing stored, finished, reconciled dose: \(dose)")
  545. }
  546. return dose.isReconciledWithHistory && dose.isFinished
  547. })
  548. })
  549. }
  550. completion(error)
  551. })
  552. })
  553. } catch let error {
  554. self.troubleshootPumpComms(using: device)
  555. completion(PumpManagerError.communication(error as? LocalizedError))
  556. }
  557. }
  558. }
  559. }
  560. private func storePendingPumpEvents(_ completion: @escaping (_ error: MinimedPumpManagerError?) -> Void) {
  561. // Must be called from the sessionQueue
  562. let events = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent })
  563. log.debug("Storing pending pump events: %{public}@", String(describing: events))
  564. self.pumpDelegate.notify({ (delegate) in
  565. guard let delegate = delegate else {
  566. preconditionFailure("pumpManagerDelegate cannot be nil")
  567. }
  568. delegate.pumpManager(self, hasNewPumpEvents: events, lastReconciliation: self.lastReconciliation, completion: { (error) in
  569. // Called on an unknown queue by the delegate
  570. if let error = error {
  571. self.log.error("Pump event storage failed: %{public}@", String(describing: error))
  572. completion(MinimedPumpManagerError.storageFailure)
  573. } else {
  574. completion(nil)
  575. }
  576. })
  577. })
  578. }
  579. // Safe to call from any thread
  580. private var isPumpDataStale: Bool {
  581. // How long should we wait before we poll for new pump data?
  582. let pumpStatusAgeTolerance = rileyLinkDeviceProvider.idleListeningEnabled ? TimeInterval(minutes: 6) : TimeInterval(minutes: 4)
  583. return isReservoirDataOlderThan(timeIntervalSinceNow: -pumpStatusAgeTolerance)
  584. }
  585. // Safe to call from any thread
  586. private func isReservoirDataOlderThan(timeIntervalSinceNow: TimeInterval) -> Bool {
  587. let state = self.state
  588. var lastReservoirDate = state.lastReservoirReading?.validAt ?? .distantPast
  589. // Look for reservoir data from MySentry that hasn't yet been written (due to 11-second imposed delay)
  590. if let sentryStatus = recents.latestPumpStatusFromMySentry {
  591. var components = sentryStatus.pumpDateComponents
  592. components.timeZone = state.timeZone
  593. lastReservoirDate = max(components.date ?? .distantPast, lastReservoirDate)
  594. }
  595. return lastReservoirDate.timeIntervalSinceNow <= timeIntervalSinceNow
  596. }
  597. private func updateBLEHeartbeatPreference() {
  598. // Must not be called on the delegate's queue
  599. rileyLinkDeviceProvider.timerTickEnabled = isPumpDataStale || pumpDelegate.call({ (delegate) -> Bool in
  600. return delegate?.pumpManagerMustProvideBLEHeartbeat(self) == true
  601. })
  602. }
  603. // MARK: - Configuration
  604. // MARK: Pump
  605. /// The user's preferred method of fetching insulin data from the pump
  606. public var preferredInsulinDataSource: InsulinDataSource {
  607. get {
  608. return state.preferredInsulinDataSource
  609. }
  610. set {
  611. setState { (state) in
  612. state.preferredInsulinDataSource = newValue
  613. }
  614. }
  615. }
  616. /// The pump battery chemistry, for voltage -> percentage calculation
  617. public var batteryChemistry: BatteryChemistryType {
  618. get {
  619. return state.batteryChemistry
  620. }
  621. set {
  622. setState { (state) in
  623. state.batteryChemistry = newValue
  624. }
  625. }
  626. }
  627. /// Whether to use MySentry packets on capable pumps:
  628. public var useMySentry: Bool {
  629. get {
  630. return state.useMySentry
  631. }
  632. set {
  633. let oldValue = state.useMySentry
  634. setState { (state) in
  635. state.useMySentry = newValue
  636. }
  637. if oldValue != newValue {
  638. let useIdleListening = state.pumpModel.hasMySentry && state.useMySentry
  639. self.rileyLinkDeviceProvider.idleListeningState = useIdleListening ? MinimedPumpManagerState.idleListeningEnabledDefaults : .disabled
  640. }
  641. }
  642. }
  643. }
  644. // MARK: - PumpManager
  645. extension MinimedPumpManager: PumpManager {
  646. public static let managerIdentifier: String = "Minimed500"
  647. public static let localizedTitle = LocalizedString("Minimed 500/700 Series", comment: "Generic title of the minimed pump manager")
  648. public var localizedTitle: String {
  649. return String(format: LocalizedString("Minimed %@", comment: "Pump title (1: model number)"), state.pumpModel.rawValue)
  650. }
  651. /*
  652. It takes a MM pump about 40s to deliver 1 Unit while bolusing
  653. See: http://www.healthline.com/diabetesmine/ask-dmine-speed-insulin-pumps#3
  654. */
  655. private static let deliveryUnitsPerMinute = 1.5
  656. public var supportedBasalRates: [Double] {
  657. return state.pumpModel.supportedBasalRates
  658. }
  659. public var supportedBolusVolumes: [Double] {
  660. return state.pumpModel.supportedBolusVolumes
  661. }
  662. public var maximumBasalScheduleEntryCount: Int {
  663. return state.pumpModel.maximumBasalScheduleEntryCount
  664. }
  665. public var minimumBasalScheduleEntryDuration: TimeInterval {
  666. return state.pumpModel.minimumBasalScheduleEntryDuration
  667. }
  668. public var pumpRecordsBasalProfileStartEvents: Bool {
  669. return state.pumpModel.recordsBasalProfileStartEvents
  670. }
  671. public var pumpReservoirCapacity: Double {
  672. return Double(state.pumpModel.reservoirCapacity)
  673. }
  674. public var lastReconciliation: Date? {
  675. return state.lastReconciliation
  676. }
  677. public var insulinType: InsulinType? {
  678. get {
  679. return state.insulinType
  680. }
  681. set {
  682. setState { (state) in
  683. state.insulinType = newValue
  684. }
  685. }
  686. }
  687. private func status(for state: MinimedPumpManagerState, recents: MinimedPumpManagerRecents) -> PumpManagerStatus {
  688. let basalDeliveryState: PumpManagerStatus.BasalDeliveryState
  689. switch recents.suspendEngageState {
  690. case .engaging:
  691. basalDeliveryState = .suspending
  692. case .disengaging:
  693. basalDeliveryState = .resuming
  694. case .stable:
  695. switch recents.tempBasalEngageState {
  696. case .engaging:
  697. basalDeliveryState = .initiatingTempBasal
  698. case .disengaging:
  699. basalDeliveryState = .cancelingTempBasal
  700. case .stable:
  701. switch self.state.suspendState {
  702. case .suspended(let date):
  703. basalDeliveryState = .suspended(date)
  704. case .resumed(let date):
  705. if let tempBasal = state.unfinalizedTempBasal, !tempBasal.isFinished {
  706. basalDeliveryState = .tempBasal(DoseEntry(tempBasal))
  707. } else {
  708. basalDeliveryState = .active(date)
  709. }
  710. }
  711. }
  712. }
  713. let bolusState: PumpManagerStatus.BolusState
  714. switch recents.bolusEngageState {
  715. case .engaging:
  716. bolusState = .initiating
  717. case .disengaging:
  718. bolusState = .canceling
  719. case .stable:
  720. if let bolus = state.unfinalizedBolus, !bolus.isFinished {
  721. bolusState = .inProgress(DoseEntry(bolus))
  722. } else {
  723. bolusState = .noBolus
  724. }
  725. }
  726. return PumpManagerStatus(
  727. timeZone: state.timeZone,
  728. device: hkDevice,
  729. pumpBatteryChargeRemaining: state.batteryPercentage,
  730. basalDeliveryState: basalDeliveryState,
  731. bolusState: bolusState,
  732. insulinType: state.insulinType
  733. )
  734. }
  735. public var status: PumpManagerStatus {
  736. // Acquire the locks just once
  737. let state = self.state
  738. let recents = self.recents
  739. return status(for: state, recents: recents)
  740. }
  741. public var rawState: PumpManager.RawStateValue {
  742. return state.rawValue
  743. }
  744. public var pumpManagerDelegate: PumpManagerDelegate? {
  745. get {
  746. return pumpDelegate.delegate
  747. }
  748. set {
  749. pumpDelegate.delegate = newValue
  750. }
  751. }
  752. public var delegateQueue: DispatchQueue! {
  753. get {
  754. return pumpDelegate.queue
  755. }
  756. set {
  757. pumpDelegate.queue = newValue
  758. cgmDelegate.queue = newValue
  759. }
  760. }
  761. // MARK: Methods
  762. public func suspendDelivery(completion: @escaping (Error?) -> Void) {
  763. guard let insulinType = insulinType else {
  764. completion(PumpManagerError.configuration(nil))
  765. return
  766. }
  767. setSuspendResumeState(state: .suspend, insulinType: insulinType, completion: completion)
  768. }
  769. public func resumeDelivery(completion: @escaping (Error?) -> Void) {
  770. guard let insulinType = insulinType else {
  771. completion(PumpManagerError.configuration(nil))
  772. return
  773. }
  774. setSuspendResumeState(state: .resume, insulinType: insulinType, completion: completion)
  775. }
  776. public func addStatusObserver(_ observer: PumpManagerStatusObserver, queue: DispatchQueue) {
  777. statusObservers.insert(observer, queue: queue)
  778. }
  779. public func removeStatusObserver(_ observer: PumpManagerStatusObserver) {
  780. statusObservers.removeElement(observer)
  781. }
  782. public func setMustProvideBLEHeartbeat(_ mustProvideBLEHeartbeat: Bool) {
  783. rileyLinkDeviceProvider.timerTickEnabled = isPumpDataStale || mustProvideBLEHeartbeat
  784. }
  785. /**
  786. Ensures pump data is current by either waking and polling, or ensuring we're listening to sentry packets.
  787. */
  788. public func ensureCurrentPumpData(completion: (() -> Void)?) {
  789. rileyLinkDeviceProvider.assertIdleListening(forcingRestart: true)
  790. guard isPumpDataStale else {
  791. completion?()
  792. return
  793. }
  794. log.default("Pump data is stale, fetching.")
  795. rileyLinkDeviceProvider.getDevices { (devices) in
  796. guard let device = devices.firstConnected else {
  797. let error = PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)
  798. self.log.error("No devices found while fetching pump data")
  799. self.pumpDelegate.notify({ (delegate) in
  800. delegate?.pumpManager(self, didError: error)
  801. completion?()
  802. })
  803. return
  804. }
  805. self.pumpOps.runSession(withName: "Get Pump Status", using: device) { (session) in
  806. do {
  807. defer { completion?() }
  808. let status = try session.getCurrentPumpStatus()
  809. guard var date = status.clock.date else {
  810. assertionFailure("Could not interpret a valid date from \(status.clock) in the system calendar")
  811. throw PumpManagerError.configuration(MinimedPumpManagerError.noDate)
  812. }
  813. // Check if the clock should be reset
  814. if abs(date.timeIntervalSinceNow) > .seconds(20) {
  815. self.log.error("Pump clock is more than 20 seconds off. Resetting.")
  816. self.pumpDelegate.notify({ (delegate) in
  817. delegate?.pumpManager(self, didAdjustPumpClockBy: date.timeIntervalSinceNow)
  818. })
  819. try session.setTimeToNow()
  820. guard let newDate = try session.getTime().date else {
  821. throw PumpManagerError.configuration(MinimedPumpManagerError.noDate)
  822. }
  823. date = newDate
  824. }
  825. self.setState({ (state) in
  826. if case .resumed = state.suspendState, status.suspended {
  827. state.suspendState = .suspended(Date())
  828. }
  829. })
  830. self.recents.latestPumpStatus = status
  831. self.updateReservoirVolume(status.reservoir, at: date, withTimeLeft: nil)
  832. } catch let error {
  833. self.log.error("Failed to fetch pump status: %{public}@", String(describing: error))
  834. self.pumpDelegate.notify({ (delegate) in
  835. delegate?.pumpManager(self, didError: PumpManagerError.communication(error as? LocalizedError))
  836. })
  837. self.troubleshootPumpComms(using: device)
  838. }
  839. }
  840. }
  841. }
  842. public func enactBolus(units: Double, automatic: Bool, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
  843. let enactUnits = roundToSupportedBolusVolume(units: units)
  844. guard enactUnits > 0 else {
  845. assertionFailure("Invalid zero unit bolus")
  846. return
  847. }
  848. guard let insulinType = insulinType else {
  849. completion(.failure(.configuration(nil)))
  850. return
  851. }
  852. pumpOps.runSession(withName: "Bolus", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
  853. guard let session = session else {
  854. completion(.failure(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
  855. return
  856. }
  857. if let unfinalizedBolus = self.state.unfinalizedBolus {
  858. guard unfinalizedBolus.isFinished else {
  859. completion(.failure(PumpManagerError.deviceState(MinimedPumpManagerError.bolusInProgress)))
  860. return
  861. }
  862. self.setState({ (state) in
  863. state.pendingDoses.append(unfinalizedBolus)
  864. state.unfinalizedBolus = nil
  865. })
  866. }
  867. self.recents.bolusEngageState = .engaging
  868. // If we don't have recent pump data, or the pump was recently rewound, read new pump data before bolusing.
  869. if self.isReservoirDataOlderThan(timeIntervalSinceNow: .minutes(-6)) {
  870. do {
  871. let reservoir = try session.getRemainingInsulin()
  872. self.pumpDelegate.notify({ (delegate) in
  873. delegate?.pumpManager(self, didReadReservoirValue: reservoir.units, at: reservoir.clock.date!) { _ in
  874. // Ignore result
  875. }
  876. })
  877. } catch let error {
  878. self.recents.bolusEngageState = .stable
  879. self.log.error("Failed to fetch pump status: %{public}@", String(describing: error))
  880. completion(.failure(PumpManagerError.communication(error as? LocalizedError)))
  881. return
  882. }
  883. }
  884. if case .suspended = self.state.suspendState {
  885. do {
  886. try self.runSuspendResumeOnSession(suspendResumeState: .resume, session: session, insulinType: insulinType)
  887. } catch let error {
  888. self.recents.bolusEngageState = .stable
  889. self.log.error("Failed to resume pump for bolus: %{public}@", String(describing: error))
  890. completion(.failure(PumpManagerError.communication(error as? LocalizedError)))
  891. return
  892. }
  893. }
  894. let deliveryTime = self.state.pumpModel.bolusDeliveryTime(units: enactUnits)
  895. do {
  896. try session.setNormalBolus(units: enactUnits)
  897. // Between bluetooth and the radio and firmware, about 2s on average passes before we start tracking
  898. let commsOffset = TimeInterval(seconds: -2)
  899. let doseStart = Date().addingTimeInterval(commsOffset)
  900. let dose = UnfinalizedDose(bolusAmount: enactUnits, startTime: doseStart, duration: deliveryTime, insulinType: insulinType, automatic: automatic)
  901. self.setState({ (state) in
  902. state.unfinalizedBolus = dose
  903. })
  904. self.recents.bolusEngageState = .stable
  905. self.storePendingPumpEvents({ (error) in
  906. completion(.success(DoseEntry(dose)))
  907. })
  908. } catch let error {
  909. self.log.error("Failed to bolus: %{public}@", String(describing: error))
  910. self.recents.bolusEngageState = .stable
  911. completion(.failure(PumpManagerError.communication(error as? LocalizedError)))
  912. }
  913. }
  914. }
  915. public func cancelBolus(completion: @escaping (PumpManagerResult<DoseEntry?>) -> Void) {
  916. guard let insulinType = insulinType else {
  917. completion(.failure(.configuration(nil)))
  918. return
  919. }
  920. self.recents.bolusEngageState = .disengaging
  921. setSuspendResumeState(state: .suspend, insulinType: insulinType) { (error) in
  922. self.recents.bolusEngageState = .stable
  923. if let error = error {
  924. completion(.failure(PumpManagerError.communication(error)))
  925. } else {
  926. completion(.success(nil))
  927. }
  928. }
  929. }
  930. public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerResult<DoseEntry>) -> Void) {
  931. guard let insulinType = insulinType else {
  932. completion(.failure(.configuration(nil)))
  933. return
  934. }
  935. pumpOps.runSession(withName: "Set Temp Basal", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
  936. guard let session = session else {
  937. completion(.failure(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
  938. return
  939. }
  940. self.recents.tempBasalEngageState = .engaging
  941. let result = session.setTempBasal(unitsPerHour, duration: duration)
  942. switch result {
  943. case .success(let response):
  944. let now = Date()
  945. let endDate = now.addingTimeInterval(response.timeRemaining)
  946. let startDate = endDate.addingTimeInterval(-duration)
  947. let dose = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: startDate, duration: duration, insulinType: insulinType)
  948. self.recents.tempBasalEngageState = .stable
  949. let isResumingScheduledBasal = duration < .ulpOfOne
  950. // If we were successful, then we know we aren't suspended
  951. self.setState({ (state) in
  952. if case .suspended = state.suspendState {
  953. state.suspendState = .resumed(startDate)
  954. } else if isResumingScheduledBasal {
  955. state.suspendState = .resumed(startDate)
  956. }
  957. let pumpModel = state.pumpModel
  958. state.unfinalizedTempBasal?.cancel(at: startDate, pumpModel: pumpModel)
  959. if let previousTempBasal = state.unfinalizedTempBasal {
  960. state.pendingDoses.append(previousTempBasal)
  961. }
  962. if isResumingScheduledBasal {
  963. state.unfinalizedTempBasal = nil
  964. } else {
  965. state.unfinalizedTempBasal = dose
  966. }
  967. })
  968. self.storePendingPumpEvents({ (error) in
  969. completion(.success(DoseEntry(dose)))
  970. })
  971. // Continue below
  972. case .failure(let error):
  973. completion(.failure(PumpManagerError.communication(error)))
  974. // If we got a command-refused error, we might be suspended or bolusing, so update the state accordingly
  975. if case .arguments(.pumpError(.commandRefused)) = error {
  976. do {
  977. let status = try session.getCurrentPumpStatus()
  978. self.setState({ (state) in
  979. if case .resumed = state.suspendState, status.suspended {
  980. state.suspendState = .suspended(Date())
  981. }
  982. })
  983. self.recents.latestPumpStatus = status
  984. } catch {
  985. self.log.error("Post-basal suspend state fetch failed: %{public}@", String(describing: error))
  986. }
  987. }
  988. self.recents.tempBasalEngageState = .stable
  989. return
  990. }
  991. do {
  992. // If we haven't fetched history in a while, our preferredInsulinDataSource is probably .reservoir, so
  993. // let's take advantage of the pump radio being on.
  994. if self.recents.lastAddedPumpEvents.timeIntervalSinceNow < .minutes(-4) {
  995. let clock = try session.getTime()
  996. // Check if the clock should be reset
  997. if let date = clock.date, abs(date.timeIntervalSinceNow) > .seconds(20) {
  998. self.log.error("Pump clock is more than 20 seconds off. Resetting.")
  999. self.pumpDelegate.notify({ (delegate) in
  1000. delegate?.pumpManager(self, didAdjustPumpClockBy: date.timeIntervalSinceNow)
  1001. })
  1002. try session.setTimeToNow()
  1003. }
  1004. self.fetchPumpHistory { (error) in
  1005. if let error = error {
  1006. self.log.error("Post-basal history fetch failed: %{public}@", String(describing: error))
  1007. }
  1008. }
  1009. }
  1010. } catch {
  1011. self.log.error("Post-basal time sync failed: %{public}@", String(describing: error))
  1012. }
  1013. }
  1014. }
  1015. public func createBolusProgressReporter(reportingOn dispatchQueue: DispatchQueue) -> DoseProgressReporter? {
  1016. if let bolus = self.state.unfinalizedBolus, !bolus.isFinished {
  1017. return MinimedDoseProgressEstimator(dose: DoseEntry(bolus), pumpModel: state.pumpModel, reportingQueue: dispatchQueue)
  1018. }
  1019. return nil
  1020. }
  1021. public func setMaximumTempBasalRate(_ rate: Double) { }
  1022. public func syncBasalRateSchedule(items scheduleItems: [RepeatingScheduleValue<Double>], completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) {
  1023. pumpOps.runSession(withName: "Save Basal Profile", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
  1024. guard let session = session else {
  1025. completion(.failure(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
  1026. return
  1027. }
  1028. do {
  1029. let newSchedule = BasalSchedule(repeatingScheduleValues: scheduleItems)
  1030. try session.setBasalSchedule(newSchedule, for: .standard)
  1031. completion(.success(BasalRateSchedule(dailyItems: scheduleItems, timeZone: session.pump.timeZone)!))
  1032. } catch let error {
  1033. self.log.error("Save basal profile failed: %{public}@", String(describing: error))
  1034. completion(.failure(error))
  1035. }
  1036. }
  1037. }
  1038. }
  1039. extension MinimedPumpManager: PumpOpsDelegate {
  1040. public func pumpOps(_ pumpOps: PumpOps, didChange state: PumpState) {
  1041. setState { (pumpManagerState) in
  1042. pumpManagerState.pumpState = state
  1043. }
  1044. }
  1045. }
  1046. extension MinimedPumpManager: CGMManager {
  1047. public var device: HKDevice? {
  1048. return hkDevice
  1049. }
  1050. public var cgmManagerDelegate: CGMManagerDelegate? {
  1051. get {
  1052. return cgmDelegate.delegate
  1053. }
  1054. set {
  1055. cgmDelegate.delegate = newValue
  1056. }
  1057. }
  1058. public var shouldSyncToRemoteService: Bool {
  1059. return true
  1060. }
  1061. public var providesBLEHeartbeat: Bool {
  1062. return false
  1063. }
  1064. public var managedDataInterval: TimeInterval? {
  1065. return nil
  1066. }
  1067. public var glucoseDisplay: GlucoseDisplayable? {
  1068. return recents.sensorState
  1069. }
  1070. public var cgmStatus: CGMManagerStatus {
  1071. return CGMManagerStatus(hasValidSensorSession: hasValidSensorSession)
  1072. }
  1073. public var hasValidSensorSession: Bool {
  1074. // No tracking of session available
  1075. return true
  1076. }
  1077. public func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) {
  1078. rileyLinkDeviceProvider.getDevices { (devices) in
  1079. guard let device = devices.firstConnected else {
  1080. completion(.error(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
  1081. return
  1082. }
  1083. let latestGlucoseDate = self.cgmDelegate.call({ (delegate) -> Date in
  1084. return delegate?.startDateToFilterNewData(for: self) ?? Date(timeIntervalSinceNow: TimeInterval(hours: -24))
  1085. })
  1086. guard latestGlucoseDate.timeIntervalSinceNow <= TimeInterval(minutes: -4.5) else {
  1087. completion(.noData)
  1088. return
  1089. }
  1090. self.pumpOps.runSession(withName: "Fetch Enlite History", using: device) { (session) in
  1091. do {
  1092. let events = try session.getGlucoseHistoryEvents(since: latestGlucoseDate.addingTimeInterval(.minutes(1)))
  1093. if let latestSensorEvent = events.compactMap({ $0.glucoseEvent as? RelativeTimestampedGlucoseEvent }).last {
  1094. self.recents.sensorState = EnliteSensorDisplayable(latestSensorEvent)
  1095. }
  1096. let unit = HKUnit.milligramsPerDeciliter
  1097. let glucoseValues: [NewGlucoseSample] = events
  1098. // TODO: Is the { $0.date > latestGlucoseDate } filter duplicative?
  1099. .filter({ $0.glucoseEvent is SensorValueGlucoseEvent && $0.date > latestGlucoseDate })
  1100. .map {
  1101. let glucoseEvent = $0.glucoseEvent as! SensorValueGlucoseEvent
  1102. let quantity = HKQuantity(unit: unit, doubleValue: Double(glucoseEvent.sgv))
  1103. return NewGlucoseSample(date: $0.date, quantity: quantity, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: glucoseEvent.glucoseSyncIdentifier ?? UUID().uuidString, device: self.device)
  1104. }
  1105. completion(.newData(glucoseValues))
  1106. } catch let error {
  1107. completion(.error(error))
  1108. }
  1109. }
  1110. }
  1111. }
  1112. }
  1113. // MARK: - AlertResponder implementation
  1114. extension MinimedPumpManager {
  1115. public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier) { }
  1116. }
  1117. // MARK: - AlertSoundVendor implementation
  1118. extension MinimedPumpManager {
  1119. public func getSoundBaseURL() -> URL? { return nil }
  1120. public func getSounds() -> [Alert.Sound] { return [] }
  1121. }