MinimedPumpManager.swift 66 KB

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