PumpOpsSession.swift 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  1. //
  2. // PumpOpsSynchronous.swift
  3. // RileyLink
  4. //
  5. // Created by Pete Schwamb on 3/12/16.
  6. // Copyright © 2016 Pete Schwamb. All rights reserved.
  7. //
  8. import Foundation
  9. import LoopKit
  10. import RileyLinkBLEKit
  11. protocol PumpOpsSessionDelegate: AnyObject {
  12. func pumpOpsSession(_ session: PumpOpsSession, didChange state: PumpState)
  13. func pumpOpsSessionDidChangeRadioConfig(_ session: PumpOpsSession)
  14. }
  15. public class PumpOpsSession {
  16. private(set) public var pump: PumpState {
  17. didSet {
  18. delegate.pumpOpsSession(self, didChange: pump)
  19. }
  20. }
  21. public let settings: PumpSettings
  22. private let session: PumpMessageSender
  23. private unowned let delegate: PumpOpsSessionDelegate
  24. internal init(settings: PumpSettings, pumpState: PumpState, session: PumpMessageSender, delegate: PumpOpsSessionDelegate) {
  25. self.settings = settings
  26. self.pump = pumpState
  27. self.session = session
  28. self.delegate = delegate
  29. }
  30. }
  31. // MARK: - Wakeup and power
  32. extension PumpOpsSession {
  33. private static let minimumTimeBetweenWakeAttempts = TimeInterval(minutes: 1)
  34. /// Attempts to send initial short wakeup message that kicks off the wakeup process.
  35. ///
  36. /// If successful, still does not fully wake up the pump - only alerts it such that the longer wakeup message can be sent next.
  37. ///
  38. /// - Throws:
  39. /// - PumpCommandError.command containing:
  40. /// - PumpOpsError.couldNotDecode
  41. /// - PumpOpsError.crosstalk
  42. /// - PumpOpsError.deviceError
  43. /// - PumpOpsError.noResponse
  44. /// - PumpOpsError.unexpectedResponse
  45. /// - PumpOpsError.unknownResponse
  46. private func sendWakeUpBurst() throws {
  47. // Skip waking up if we recently tried
  48. guard pump.lastWakeAttempt == nil || pump.lastWakeAttempt!.timeIntervalSinceNow <= -PumpOpsSession.minimumTimeBetweenWakeAttempts
  49. else {
  50. return
  51. }
  52. pump.lastWakeAttempt = Date()
  53. let shortPowerMessage = PumpMessage(settings: settings, type: .powerOn)
  54. if pump.pumpModel == nil || !pump.pumpModel!.hasMySentry {
  55. // Older pumps have a longer sleep cycle between wakeups, so send an initial burst
  56. do {
  57. let _: PumpAckMessageBody = try session.getResponse(to: shortPowerMessage, repeatCount: 255, timeout: .milliseconds(1), retryCount: 0)
  58. }
  59. catch { }
  60. }
  61. do {
  62. let _: PumpAckMessageBody = try session.getResponse(to: shortPowerMessage, repeatCount: 255, timeout: .seconds(12), retryCount: 0)
  63. } catch let error as PumpOpsError {
  64. throw PumpCommandError.command(error)
  65. }
  66. }
  67. private func isPumpResponding() -> Bool {
  68. do {
  69. let _: GetPumpModelCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel, retryCount: 1)
  70. return true
  71. } catch {
  72. return false
  73. }
  74. }
  75. /// - Throws:
  76. /// - PumpCommandError.command
  77. /// - PumpCommandError.arguments
  78. /// - PumpOpsError.couldNotDecode
  79. /// - PumpOpsError.crosstalk
  80. /// - PumpOpsError.deviceError
  81. /// - PumpOpsError.noResponse
  82. /// - PumpOpsError.unexpectedResponse
  83. /// - PumpOpsError.unknownResponse
  84. private func wakeup(_ duration: TimeInterval = TimeInterval(minutes: 1)) throws {
  85. guard !pump.isAwake else {
  86. return
  87. }
  88. // Send a short message to the pump to see if its radio is still powered on
  89. if isPumpResponding() {
  90. // TODO: Convert logging
  91. NSLog("Pump responding despite our wake timer having expired. Extending timer")
  92. // By my observations, the pump stays awake > 1 minute past last comms. Usually
  93. // About 1.5 minutes, but we'll make it a minute to be safe.
  94. pump.awakeUntil = Date(timeIntervalSinceNow: TimeInterval(minutes: 1))
  95. return
  96. }
  97. // Command
  98. try sendWakeUpBurst()
  99. // Arguments
  100. do {
  101. let longPowerMessage = PumpMessage(settings: settings, type: .powerOn, body: PowerOnCarelinkMessageBody(duration: duration))
  102. let _: PumpAckMessageBody = try session.getResponse(to: longPowerMessage)
  103. } catch let error as PumpOpsError {
  104. throw PumpCommandError.arguments(error)
  105. } catch {
  106. assertionFailure()
  107. }
  108. // TODO: Convert logging
  109. NSLog("Power on for %.0f minutes", duration.minutes)
  110. pump.awakeUntil = Date(timeIntervalSinceNow: duration)
  111. }
  112. }
  113. // MARK: - Single reads
  114. extension PumpOpsSession {
  115. /// Retrieves the pump model from either the state or from the cache
  116. ///
  117. /// - Parameter usingCache: Whether the pump state should be checked first for a known pump model
  118. /// - Returns: The pump model
  119. /// - Throws:
  120. /// - PumpCommandError.command
  121. /// - PumpCommandError.arguments
  122. /// - PumpOpsError.couldNotDecode
  123. /// - PumpOpsError.crosstalk
  124. /// - PumpOpsError.deviceError
  125. /// - PumpOpsError.noResponse
  126. /// - PumpOpsError.unexpectedResponse
  127. /// - PumpOpsError.unknownResponse
  128. public func getPumpModel(usingCache: Bool = true) throws -> PumpModel {
  129. if usingCache, let pumpModel = pump.pumpModel {
  130. return pumpModel
  131. }
  132. try wakeup()
  133. let body: GetPumpModelCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .getPumpModel), responseType: .getPumpModel)
  134. guard let pumpModel = PumpModel(rawValue: body.model) else {
  135. throw PumpOpsError.unknownPumpModel(body.model)
  136. }
  137. pump.pumpModel = pumpModel
  138. return pumpModel
  139. }
  140. /// Retrieves the pump firmware version
  141. ///
  142. /// - Returns: The pump firmware version as string
  143. /// - Throws:
  144. /// - PumpCommandError.command
  145. /// - PumpCommandError.arguments
  146. /// - PumpOpsError.couldNotDecode
  147. /// - PumpOpsError.crosstalk
  148. /// - PumpOpsError.deviceError
  149. /// - PumpOpsError.noResponse
  150. /// - PumpOpsError.unexpectedResponse
  151. /// - PumpOpsError.unknownResponse
  152. public func getPumpFirmwareVersion() throws -> String {
  153. try wakeup()
  154. let body: GetPumpFirmwareVersionMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readFirmwareVersion), responseType: .readFirmwareVersion)
  155. return body.version
  156. }
  157. /// - Throws:
  158. /// - PumpCommandError.command
  159. /// - PumpCommandError.arguments
  160. /// - PumpOpsError.couldNotDecode
  161. /// - PumpOpsError.crosstalk
  162. /// - PumpOpsError.deviceError
  163. /// - PumpOpsError.noResponse
  164. /// - PumpOpsError.unexpectedResponse
  165. /// - PumpOpsError.unknownResponse
  166. public func getBatteryStatus() throws -> GetBatteryCarelinkMessageBody {
  167. try wakeup()
  168. return try session.getResponse(to: PumpMessage(settings: settings, type: .getBattery), responseType: .getBattery)
  169. }
  170. /// - Throws:
  171. /// - PumpCommandError.command
  172. /// - PumpCommandError.arguments
  173. /// - PumpOpsError.couldNotDecode
  174. /// - PumpOpsError.crosstalk
  175. /// - PumpOpsError.deviceError
  176. /// - PumpOpsError.noResponse
  177. /// - PumpOpsError.unexpectedResponse
  178. /// - PumpOpsError.unknownResponse
  179. internal func getPumpStatus() throws -> ReadPumpStatusMessageBody {
  180. try wakeup()
  181. return try session.getResponse(to: PumpMessage(settings: settings, type: .readPumpStatus), responseType: .readPumpStatus)
  182. }
  183. /// - Throws:
  184. /// - PumpCommandError.command
  185. /// - PumpCommandError.arguments
  186. /// - PumpOpsError.couldNotDecode
  187. /// - PumpOpsError.crosstalk
  188. /// - PumpOpsError.deviceError
  189. /// - PumpOpsError.noResponse
  190. /// - PumpOpsError.unexpectedResponse
  191. /// - PumpOpsError.unknownResponse
  192. public func getSettings() throws -> ReadSettingsCarelinkMessageBody {
  193. try wakeup()
  194. return try session.getResponse(to: PumpMessage(settings: settings, type: .readSettings), responseType: .readSettings)
  195. }
  196. /// Reads the pump's time, returning a set of DateComponents in the pump's presumed time zone.
  197. ///
  198. /// - Returns: The pump's time components including timeZone
  199. /// - Throws:
  200. /// - PumpCommandError.command
  201. /// - PumpCommandError.arguments
  202. /// - PumpOpsError.couldNotDecode
  203. /// - PumpOpsError.crosstalk
  204. /// - PumpOpsError.deviceError
  205. /// - PumpOpsError.noResponse
  206. /// - PumpOpsError.unexpectedResponse
  207. /// - PumpOpsError.unknownResponse
  208. public func getTime() throws -> DateComponents {
  209. try wakeup()
  210. let response: ReadTimeCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTime), responseType: .readTime)
  211. var components = response.dateComponents
  212. components.timeZone = pump.timeZone
  213. return components
  214. }
  215. /// Reads Basal Schedule from the pump
  216. ///
  217. /// - Returns: The pump's standard basal schedule
  218. /// - Throws:
  219. /// - PumpCommandError.command
  220. /// - PumpCommandError.arguments
  221. /// - PumpOpsError.couldNotDecode
  222. /// - PumpOpsError.crosstalk
  223. /// - PumpOpsError.deviceError
  224. /// - PumpOpsError.noResponse
  225. /// - PumpOpsError.unexpectedResponse
  226. /// - PumpOpsError.unknownResponse
  227. public func getBasalSchedule(for profile: BasalProfile = .standard) throws -> BasalSchedule? {
  228. try wakeup()
  229. var isFinished = false
  230. var message = PumpMessage(settings: settings, type: profile.readMessageType)
  231. var scheduleData = Data()
  232. while (!isFinished) {
  233. let body: DataFrameMessageBody = try session.getResponse(to: message, responseType: profile.readMessageType)
  234. scheduleData.append(body.contents)
  235. isFinished = body.isLastFrame
  236. message = PumpMessage(settings: settings, type: .pumpAck)
  237. }
  238. return BasalSchedule(rawValue: scheduleData)
  239. }
  240. /// - Throws:
  241. /// - PumpOpsError.couldNotDecode
  242. /// - PumpOpsError.crosstalk
  243. /// - PumpOpsError.deviceError
  244. /// - PumpOpsError.noResponse
  245. /// - PumpOpsError.unexpectedResponse
  246. /// - PumpOpsError.unknownResponse
  247. public func getOtherDevicesIDs() throws -> ReadOtherDevicesIDsMessageBody {
  248. try wakeup()
  249. return try session.getResponse(to: PumpMessage(settings: settings, type: .readOtherDevicesIDs), responseType: .readOtherDevicesIDs)
  250. }
  251. /// - Throws:
  252. /// - PumpOpsError.couldNotDecode
  253. /// - PumpOpsError.crosstalk
  254. /// - PumpOpsError.deviceError
  255. /// - PumpOpsError.noResponse
  256. /// - PumpOpsError.unexpectedResponse
  257. /// - PumpOpsError.unknownResponse
  258. public func getOtherDevicesEnabled() throws -> Bool {
  259. try wakeup()
  260. let response: ReadOtherDevicesStatusMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readOtherDevicesStatus), responseType: .readOtherDevicesStatus)
  261. return response.isEnabled
  262. }
  263. /// - Throws:
  264. /// - PumpOpsError.couldNotDecode
  265. /// - PumpOpsError.crosstalk
  266. /// - PumpOpsError.deviceError
  267. /// - PumpOpsError.noResponse
  268. /// - PumpOpsError.unexpectedResponse
  269. /// - PumpOpsError.unknownResponse
  270. public func getRemoteControlIDs() throws -> ReadRemoteControlIDsMessageBody {
  271. try wakeup()
  272. return try session.getResponse(to: PumpMessage(settings: settings, type: .readRemoteControlIDs), responseType: .readRemoteControlIDs)
  273. }
  274. }
  275. // MARK: - Aggregate reads
  276. public struct PumpStatus: Equatable {
  277. // Date components read from the pump, along with PumpState.timeZone
  278. public let clock: DateComponents
  279. public let batteryVolts: Measurement<UnitElectricPotentialDifference>
  280. public let batteryStatus: BatteryStatus
  281. public let suspended: Bool
  282. public let bolusing: Bool
  283. public let reservoir: Double
  284. public let model: PumpModel
  285. public let pumpID: String
  286. }
  287. extension PumpOpsSession {
  288. /// Reads the current insulin reservoir volume and the pump's date
  289. ///
  290. /// - Returns:
  291. /// - The reservoir volume, in units of insulin
  292. /// - DateCompoments representing the pump's clock
  293. /// - Throws:
  294. /// - PumpCommandError.command
  295. /// - PumpCommandError.arguments
  296. /// - PumpOpsError.couldNotDecode
  297. /// - PumpOpsError.crosstalk
  298. /// - PumpOpsError.deviceError
  299. /// - PumpOpsError.noResponse
  300. /// - PumpOpsError.unknownResponse
  301. public func getRemainingInsulin() throws -> (units: Double, clock: DateComponents) {
  302. let pumpModel = try getPumpModel()
  303. let pumpClock = try getTime()
  304. let reservoir: ReadRemainingInsulinMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readRemainingInsulin), responseType: .readRemainingInsulin)
  305. return (
  306. units: reservoir.getUnitsRemaining(insulinBitPackingScale: pumpModel.insulinBitPackingScale),
  307. clock: pumpClock
  308. )
  309. }
  310. /// Reads clock, reservoir, battery, bolusing, and suspended state from pump
  311. ///
  312. /// - Returns: The pump status
  313. /// - Throws:
  314. /// - PumpCommandError
  315. /// - PumpOpsError
  316. public func getCurrentPumpStatus() throws -> PumpStatus {
  317. let pumpModel = try getPumpModel()
  318. let battResp = try getBatteryStatus()
  319. let status = try getPumpStatus()
  320. let (reservoir, clock) = try getRemainingInsulin()
  321. return PumpStatus(
  322. clock: clock,
  323. batteryVolts: Measurement(value: battResp.volts, unit: UnitElectricPotentialDifference.volts),
  324. batteryStatus: battResp.status,
  325. suspended: status.suspended,
  326. bolusing: status.bolusing,
  327. reservoir: reservoir,
  328. model: pumpModel,
  329. pumpID: settings.pumpID
  330. )
  331. }
  332. }
  333. // MARK: - Command messages
  334. extension PumpOpsSession {
  335. /// - Throws: `PumpCommandError` specifying the failure sequence
  336. private func runCommandWithArguments<T: MessageBody>(_ message: PumpMessage, responseType: MessageType = .pumpAck) throws -> T {
  337. do {
  338. try wakeup()
  339. let shortMessage = PumpMessage(packetType: message.packetType, address: message.address.hexadecimalString, messageType: message.messageType, messageBody: CarelinkShortMessageBody())
  340. let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
  341. } catch let error as PumpOpsError {
  342. throw PumpCommandError.command(error)
  343. }
  344. do {
  345. return try session.getResponse(to: message, responseType: responseType)
  346. } catch let error as PumpOpsError {
  347. throw PumpCommandError.arguments(error)
  348. }
  349. }
  350. /// - Throws: `PumpCommandError` specifying the failure sequence
  351. public func pressButton(_ type: ButtonPressCarelinkMessageBody.ButtonType) throws {
  352. let message = PumpMessage(settings: settings, type: .buttonPress, body: ButtonPressCarelinkMessageBody(buttonType: type))
  353. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  354. }
  355. /// - Throws: `PumpCommandError` specifying the failure sequence
  356. public func setSuspendResumeState(_ state: SuspendResumeMessageBody.SuspendResumeState) throws {
  357. let message = PumpMessage(settings: settings, type: .suspendResume, body: SuspendResumeMessageBody(state: state))
  358. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  359. }
  360. /// - Throws: PumpCommandError
  361. public func selectBasalProfile(_ profile: BasalProfile) throws {
  362. let message = PumpMessage(settings: settings, type: .selectBasalProfile, body: SelectBasalProfileMessageBody(newProfile: profile))
  363. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  364. }
  365. /// - Throws: PumpCommandError
  366. public func setMaxBasalRate(unitsPerHour: Double) throws {
  367. guard let body = ChangeMaxBasalRateMessageBody(maxBasalUnitsPerHour: unitsPerHour) else {
  368. throw PumpCommandError.command(PumpOpsError.pumpError(PumpErrorCode.maxSettingExceeded))
  369. }
  370. let message = PumpMessage(settings: settings, type: .setMaxBasalRate, body: body)
  371. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  372. }
  373. /// - Throws: PumpCommandError
  374. public func setMaxBolus(units: Double) throws {
  375. guard let body = ChangeMaxBolusMessageBody(pumpModel: try getPumpModel(), maxBolusUnits: units) else {
  376. throw PumpCommandError.command(PumpOpsError.pumpError(PumpErrorCode.maxSettingExceeded))
  377. }
  378. let message = PumpMessage(settings: settings, type: .setMaxBolus, body: body)
  379. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  380. }
  381. /// Changes the current temporary basal rate
  382. ///
  383. /// - Parameters:
  384. /// - unitsPerHour: The new basal rate, in Units per hour
  385. /// - duration: The duration of the rate
  386. /// - Returns: A result containing the pump message body describing the new basal rate or an error
  387. public func setTempBasal(_ unitsPerHour: Double, duration: TimeInterval) -> Result<ReadTempBasalCarelinkMessageBody,PumpCommandError> {
  388. var lastError: PumpCommandError?
  389. let message = PumpMessage(settings: settings, type: .changeTempBasal, body: ChangeTempBasalCarelinkMessageBody(unitsPerHour: unitsPerHour, duration: duration))
  390. for attempt in 1..<4 {
  391. do {
  392. do {
  393. try wakeup()
  394. let shortMessage = PumpMessage(packetType: message.packetType, address: message.address.hexadecimalString, messageType: message.messageType, messageBody: CarelinkShortMessageBody())
  395. let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
  396. } catch let error as PumpOpsError {
  397. throw PumpCommandError.command(error)
  398. }
  399. do {
  400. let _: PumpAckMessageBody = try session.getResponse(to: message, retryCount: 0)
  401. } catch PumpOpsError.pumpError(let errorCode) {
  402. lastError = .arguments(.pumpError(errorCode))
  403. break // Stop because we have a pump error response
  404. } catch PumpOpsError.unknownPumpErrorCode(let errorCode) {
  405. lastError = .arguments(.unknownPumpErrorCode(errorCode))
  406. break // Stop because we have a pump error response
  407. } catch {
  408. // The pump does not ACK a successful temp basal. We'll check manually below if it was successful.
  409. }
  410. let response: ReadTempBasalCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal)
  411. if response.timeRemaining == duration && response.rateType == .absolute {
  412. return .success(response)
  413. } else {
  414. return .failure(PumpCommandError.arguments(PumpOpsError.rfCommsFailure("Could not verify TempBasal on attempt \(attempt). ")))
  415. }
  416. } catch let error as PumpCommandError {
  417. lastError = error
  418. } catch let error as PumpOpsError {
  419. lastError = .command(error)
  420. } catch {
  421. lastError = .command(.noResponse(during: "Set temp basal"))
  422. }
  423. }
  424. return .failure(lastError!)
  425. }
  426. public func readTempBasal() throws -> Double {
  427. try wakeup()
  428. let response: ReadTempBasalCarelinkMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readTempBasal), responseType: .readTempBasal)
  429. return response.rate
  430. }
  431. /// Changes the pump's clock to the specified date components in the system time zone
  432. ///
  433. /// - Parameter generator: A closure which returns the desired date components. An exeception is raised if the date components are not valid.
  434. /// - Throws: PumpCommandError
  435. public func setTime(_ generator: () -> DateComponents) throws {
  436. try wakeup()
  437. do {
  438. let shortMessage = PumpMessage(settings: settings, type: .changeTime)
  439. let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
  440. } catch let error as PumpOpsError {
  441. throw PumpCommandError.command(error)
  442. }
  443. do {
  444. let components = generator()
  445. let message = PumpMessage(settings: settings, type: .changeTime, body: ChangeTimeCarelinkMessageBody(dateComponents: components)!)
  446. let _: PumpAckMessageBody = try session.getResponse(to: message)
  447. self.pump.timeZone = components.timeZone?.fixed ?? .currentFixed
  448. } catch let error as PumpOpsError {
  449. throw PumpCommandError.arguments(error)
  450. }
  451. }
  452. public func setTimeToNow(in timeZone: TimeZone? = nil) throws {
  453. let timeZone = timeZone ?? pump.timeZone
  454. try setTime { () -> DateComponents in
  455. var calendar = Calendar(identifier: .gregorian)
  456. calendar.timeZone = timeZone
  457. var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: Date())
  458. components.timeZone = timeZone
  459. return components
  460. }
  461. }
  462. /// Sets a bolus
  463. ///
  464. /// *Note: Use at your own risk!*
  465. ///
  466. /// - Parameters:
  467. /// - units: The number of units to deliver
  468. /// - cancelExistingTemp: If true, additional pump commands will be issued to clear any running temp basal. Defaults to false.
  469. /// - Throws: SetBolusError describing the certainty of the underlying error
  470. public func setNormalBolus(units: Double, cancelExistingTemp: Bool = false) throws {
  471. let pumpModel: PumpModel
  472. try wakeup()
  473. pumpModel = try getPumpModel()
  474. let status = try getPumpStatus()
  475. if status.bolusing {
  476. throw PumpOpsError.bolusInProgress
  477. }
  478. if status.suspended {
  479. throw PumpOpsError.pumpSuspended
  480. }
  481. if cancelExistingTemp {
  482. _ = setTempBasal(0, duration: 0)
  483. }
  484. let message = PumpMessage(settings: settings, type: .bolus, body: BolusCarelinkMessageBody(units: units, insulinBitPackingScale: pumpModel.insulinBitPackingScale))
  485. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  486. return
  487. }
  488. /// - Throws: PumpCommandError
  489. public func setRemoteControlEnabled(_ enabled: Bool) throws {
  490. let message = PumpMessage(settings: settings, type: .setRemoteControlEnabled, body: SetRemoteControlEnabledMessageBody(enabled: enabled))
  491. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  492. }
  493. /// - Throws: PumpCommandError
  494. public func setRemoteControlID(_ id: Data, atIndex index: Int) throws {
  495. guard let body = ChangeRemoteControlIDMessageBody(id: id, index: index) else {
  496. throw PumpCommandError.command(PumpOpsError.pumpError(PumpErrorCode.maxSettingExceeded))
  497. }
  498. let message = PumpMessage(settings: settings, type: .setRemoteControlID, body: body)
  499. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  500. }
  501. /// - Throws: PumpCommandError
  502. public func removeRemoteControlID(atIndex index: Int) throws {
  503. guard let body = ChangeRemoteControlIDMessageBody(id: nil, index: index) else {
  504. throw PumpCommandError.command(PumpOpsError.pumpError(PumpErrorCode.maxSettingExceeded))
  505. }
  506. let message = PumpMessage(settings: settings, type: .setRemoteControlID, body: body)
  507. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  508. }
  509. /// - Throws: `PumpCommandError` specifying the failure sequence
  510. public func setBasalSchedule(_ basalSchedule: BasalSchedule, for profile: BasalProfile) throws {
  511. let frames = DataFrameMessageBody.dataFramesFromContents(basalSchedule.rawValue)
  512. guard let firstFrame = frames.first else {
  513. return
  514. }
  515. let type: MessageType
  516. switch profile {
  517. case .standard:
  518. type = .setBasalProfileStandard
  519. case .profileA:
  520. type = .setBasalProfileA
  521. case .profileB:
  522. type = .setBasalProfileB
  523. }
  524. let message = PumpMessage(settings: settings, type: type, body: firstFrame)
  525. let _: PumpAckMessageBody = try runCommandWithArguments(message)
  526. for nextFrame in frames.dropFirst() {
  527. let message = PumpMessage(settings: settings, type: type, body: nextFrame)
  528. do {
  529. let _: PumpAckMessageBody = try session.getResponse(to: message)
  530. } catch let error as PumpOpsError {
  531. throw PumpCommandError.arguments(error)
  532. }
  533. }
  534. }
  535. public func getStatistics() throws -> RileyLinkStatistics {
  536. return try session.getRileyLinkStatistics()
  537. }
  538. public func discoverCommands(in range: CountableClosedRange<UInt8>, _ updateHandler: (_ messages: [String]) -> Void) {
  539. let codes = range.compactMap { MessageType(rawValue: $0) }
  540. for code in codes {
  541. var messages = [String]()
  542. do {
  543. messages.append(contentsOf: [
  544. "## Command \(code)",
  545. ])
  546. try wakeup()
  547. // Try the short command message, without any arguments.
  548. let shortMessage = PumpMessage(settings: settings, type: code)
  549. let _: PumpAckMessageBody = try session.getResponse(to: shortMessage)
  550. messages.append(contentsOf: [
  551. "Succeeded",
  552. ""
  553. ])
  554. // Check history?
  555. } catch PumpOpsError.unexpectedResponse(let response, from: _) {
  556. messages.append(contentsOf: [
  557. "Unexpected response:",
  558. response.txData.hexadecimalString,
  559. ])
  560. } catch PumpOpsError.unknownResponse(rx: let response, during: _) {
  561. messages.append(contentsOf: [
  562. "Unknown response:",
  563. response.hexadecimalString,
  564. ])
  565. } catch let error {
  566. messages.append(contentsOf: [
  567. String(describing: error),
  568. "",
  569. ])
  570. }
  571. updateHandler(messages)
  572. Thread.sleep(until: Date(timeIntervalSinceNow: 2))
  573. }
  574. }
  575. }
  576. // MARK: - MySentry (Watchdog) pairing
  577. extension PumpOpsSession {
  578. /// Pairs the pump with a virtual "watchdog" device to enable it to broadcast periodic status packets. Only pump models x23 and up are supported.
  579. ///
  580. /// - Parameter watchdogID: A 3-byte address for the watchdog device.
  581. /// - Throws:
  582. /// - PumpOpsError.couldNotDecode
  583. /// - PumpOpsError.crosstalk
  584. /// - PumpOpsError.deviceError
  585. /// - PumpOpsError.noResponse
  586. /// - PumpOpsError.unexpectedResponse
  587. /// - PumpOpsError.unknownResponse
  588. public func changeWatchdogMarriageProfile(_ watchdogID: Data) throws {
  589. let commandTimeout = TimeInterval(seconds: 30)
  590. // Wait for the pump to start polling
  591. guard let encodedData = try session.listenForPacket(onChannel: 0, timeout: commandTimeout)?.data else {
  592. throw PumpOpsError.noResponse(during: "Watchdog listening")
  593. }
  594. guard let packet = MinimedPacket(encodedData: encodedData) else {
  595. throw PumpOpsError.couldNotDecode(rx: encodedData, during: "Watchdog listening")
  596. }
  597. guard let findMessage = PumpMessage(rxData: packet.data) else {
  598. // Unknown packet type or message type
  599. throw PumpOpsError.unknownResponse(rx: packet.data, during: "Watchdog listening")
  600. }
  601. guard findMessage.address.hexadecimalString == settings.pumpID && findMessage.packetType == .mySentry,
  602. let findMessageBody = findMessage.messageBody as? FindDeviceMessageBody, let findMessageResponseBody = MySentryAckMessageBody(sequence: findMessageBody.sequence, watchdogID: watchdogID, responseMessageTypes: [findMessage.messageType])
  603. else {
  604. throw PumpOpsError.unknownResponse(rx: packet.data, during: "Watchdog listening")
  605. }
  606. // Identify as a MySentry device
  607. let findMessageResponse = PumpMessage(packetType: .mySentry, address: settings.pumpID, messageType: .pumpAck, messageBody: findMessageResponseBody)
  608. let linkMessage = try session.sendAndListen(findMessageResponse, timeout: commandTimeout)
  609. guard let
  610. linkMessageBody = linkMessage.messageBody as? DeviceLinkMessageBody,
  611. let linkMessageResponseBody = MySentryAckMessageBody(sequence: linkMessageBody.sequence, watchdogID: watchdogID, responseMessageTypes: [linkMessage.messageType])
  612. else {
  613. throw PumpOpsError.unexpectedResponse(linkMessage, from: findMessageResponse)
  614. }
  615. // Acknowledge the pump linked with us
  616. let linkMessageResponse = PumpMessage(packetType: .mySentry, address: settings.pumpID, messageType: .pumpAck, messageBody: linkMessageResponseBody)
  617. try session.send(linkMessageResponse)
  618. }
  619. }
  620. // MARK: - Tuning
  621. private extension PumpRegion {
  622. var scanFrequencies: [Measurement<UnitFrequency>] {
  623. let scanFrequencies: [Double]
  624. switch self {
  625. case .worldWide:
  626. scanFrequencies = [868.25, 868.30, 868.35, 868.40, 868.45, 868.50, 868.55, 868.60, 868.65]
  627. case .northAmerica, .canada:
  628. scanFrequencies = [916.45, 916.50, 916.55, 916.60, 916.65, 916.70, 916.75, 916.80]
  629. }
  630. return scanFrequencies.map {
  631. return Measurement<UnitFrequency>(value: $0, unit: .megahertz)
  632. }
  633. }
  634. }
  635. enum RXFilterMode: UInt8 {
  636. case wide = 0x50 // 300KHz
  637. case narrow = 0x90 // 150KHz
  638. }
  639. public struct FrequencyTrial {
  640. public var tries: Int = 0
  641. public var successes: Int = 0
  642. public var avgRSSI: Double = -99
  643. public var frequency: Measurement<UnitFrequency>
  644. init(frequency: Measurement<UnitFrequency>) {
  645. self.frequency = frequency
  646. }
  647. }
  648. public struct FrequencyScanResults {
  649. public var trials: [FrequencyTrial]
  650. public var bestFrequency: Measurement<UnitFrequency>
  651. }
  652. extension PumpOpsSession {
  653. /// - Throws:
  654. /// - PumpOpsError.deviceError
  655. /// - PumpOpsError.noResponse
  656. /// - PumpOpsError.rfCommsFailure
  657. public func tuneRadio(attempts: Int = 3) throws -> FrequencyScanResults {
  658. let region = self.settings.pumpRegion
  659. do {
  660. let results = try scanForPump(in: region.scanFrequencies, fallback: pump.lastValidFrequency, tries: attempts)
  661. pump.lastValidFrequency = results.bestFrequency
  662. pump.lastTuned = Date()
  663. delegate.pumpOpsSessionDidChangeRadioConfig(self)
  664. return results
  665. } catch let error as PumpOpsError {
  666. throw error
  667. } catch let error as LocalizedError {
  668. throw PumpOpsError.deviceError(error)
  669. }
  670. }
  671. /// - Throws: PumpOpsError.deviceError
  672. private func setRXFilterMode(_ mode: RXFilterMode) throws {
  673. let drate_e = UInt8(0x9) // exponent of symbol rate (16kbps)
  674. let chanbw = mode.rawValue
  675. do {
  676. try session.updateRegister(.mdmcfg4, value: chanbw | drate_e)
  677. } catch let error as LocalizedError {
  678. throw PumpOpsError.deviceError(error)
  679. }
  680. }
  681. /// - Throws: PumpOpsError.deviceError
  682. func setCCLEDMode(_ mode: RileyLinkLEDMode) throws {
  683. throw PumpOpsError.noResponse(during: "Tests")
  684. }
  685. /// - Throws:
  686. /// - PumpOpsError.deviceError
  687. /// - RileyLinkDeviceError
  688. func configureRadio(for region: PumpRegion, frequency: Measurement<UnitFrequency>?) throws {
  689. try session.resetRadioConfig()
  690. switch region {
  691. case .worldWide:
  692. //try session.updateRegister(.mdmcfg4, value: 0x59)
  693. try setRXFilterMode(.wide)
  694. //try session.updateRegister(.mdmcfg3, value: 0x66)
  695. //try session.updateRegister(.mdmcfg2, value: 0x33)
  696. try session.updateRegister(.mdmcfg1, value: 0x62)
  697. try session.updateRegister(.mdmcfg0, value: 0x1A)
  698. try session.updateRegister(.deviatn, value: 0x13)
  699. case .northAmerica, .canada:
  700. //try session.updateRegister(.mdmcfg4, value: 0x99)
  701. try setRXFilterMode(.narrow)
  702. //try session.updateRegister(.mdmcfg3, value: 0x66)
  703. //try session.updateRegister(.mdmcfg2, value: 0x33)
  704. try session.updateRegister(.mdmcfg1, value: 0x61)
  705. try session.updateRegister(.mdmcfg0, value: 0x7E)
  706. try session.updateRegister(.deviatn, value: 0x15)
  707. }
  708. if let frequency = frequency {
  709. try session.setBaseFrequency(frequency)
  710. }
  711. }
  712. /// - Throws:
  713. /// - PumpOpsError.deviceError
  714. /// - PumpOpsError.noResponse
  715. /// - PumpOpsError.rfCommsFailure
  716. /// - LocalizedError
  717. private func scanForPump(in frequencies: [Measurement<UnitFrequency>], fallback: Measurement<UnitFrequency>?, tries: Int = 3) throws -> FrequencyScanResults {
  718. var trials = [FrequencyTrial]()
  719. let middleFreq = frequencies[frequencies.count / 2]
  720. do {
  721. // Needed to put the pump in listen mode
  722. try session.setBaseFrequency(middleFreq)
  723. try wakeup()
  724. } catch {
  725. // Continue anyway; the pump likely heard us, even if we didn't hear it.
  726. }
  727. for freq in frequencies {
  728. var trial = FrequencyTrial(frequency: freq)
  729. try session.setBaseFrequency(freq)
  730. var sumRSSI = 0
  731. for _ in 1...tries {
  732. // Ignore failures here
  733. let rfPacket = try? session.sendAndListenForPacket(PumpMessage(settings: settings, type: .getPumpModel), timeout: .milliseconds(130))
  734. if let rfPacket = rfPacket,
  735. let pkt = MinimedPacket(encodedData: rfPacket.data),
  736. let response = PumpMessage(rxData: pkt.data), response.messageType == .getPumpModel
  737. {
  738. sumRSSI += rfPacket.rssi
  739. trial.successes += 1
  740. }
  741. trial.tries += 1
  742. }
  743. // Mark each failure as a -99 rssi, so we can use highest rssi as best freq
  744. sumRSSI += -99 * (trial.tries - trial.successes)
  745. trial.avgRSSI = Double(sumRSSI) / Double(trial.tries)
  746. trials.append(trial)
  747. }
  748. let sortedTrials = trials.sorted(by: { (a, b) -> Bool in
  749. return a.avgRSSI > b.avgRSSI
  750. })
  751. guard sortedTrials.first!.successes > 0 else {
  752. try session.setBaseFrequency(fallback ?? middleFreq)
  753. throw PumpOpsError.rfCommsFailure("No pump responses during scan")
  754. }
  755. let results = FrequencyScanResults(
  756. trials: trials,
  757. bestFrequency: sortedTrials.first!.frequency
  758. )
  759. try session.setBaseFrequency(results.bestFrequency)
  760. return results
  761. }
  762. }
  763. // MARK: - Pump history
  764. extension PumpOpsSession {
  765. /// Fetches history entries which occurred on or after the specified date.
  766. ///
  767. /// It is possible for Minimed Pumps to non-atomically append multiple history entries with the same timestamp, for example, `BolusWizardEstimatePumpEvent` may appear and be read before `BolusNormalPumpEvent` is written. Therefore, the `startDate` parameter is used as part of an inclusive range, leaving the client to manage the possibility of duplicates.
  768. ///
  769. /// History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events.
  770. ///
  771. /// - Parameter startDate: The earliest date of events to retrieve
  772. /// - Returns:
  773. /// - An array of fetched history entries, in ascending order of insertion
  774. /// - The pump model
  775. /// - Throws:
  776. /// - PumpCommandError.command
  777. /// - PumpCommandError.arguments
  778. /// - PumpOpsError.couldNotDecode
  779. /// - PumpOpsError.crosstalk
  780. /// - PumpOpsError.deviceError
  781. /// - PumpOpsError.noResponse
  782. /// - PumpOpsError.unknownResponse
  783. /// - HistoryPageError.invalidCRC
  784. /// - HistoryPageError.unknownEventType
  785. public func getHistoryEvents(since startDate: Date) throws -> ([TimestampedHistoryEvent], PumpModel) {
  786. try wakeup()
  787. let pumpModel = try getPumpModel()
  788. var events = [TimestampedHistoryEvent]()
  789. pages: for pageNum in 0..<16 {
  790. // TODO: Convert logging
  791. NSLog("Fetching page %d", pageNum)
  792. let pageData: Data
  793. do {
  794. pageData = try getHistoryPage(pageNum)
  795. } catch PumpCommandError.arguments(let error) {
  796. if case PumpOpsError.pumpError(.pageDoesNotExist) = error {
  797. return (events, pumpModel)
  798. }
  799. throw PumpCommandError.arguments(error)
  800. }
  801. var idx = 0
  802. let chunkSize = 256
  803. while idx < pageData.count {
  804. let top = min(idx + chunkSize, pageData.count)
  805. let range = Range(uncheckedBounds: (lower: idx, upper: top))
  806. // TODO: Convert logging
  807. NSLog(String(format: "HistoryPage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString)
  808. idx = top
  809. }
  810. let page = try HistoryPage(pageData: pageData, pumpModel: pumpModel)
  811. let (timestampedEvents, hasMoreEvents, _) = page.timestampedEvents(after: startDate, timeZone: pump.timeZone, model: pumpModel)
  812. events = timestampedEvents + events
  813. if !hasMoreEvents {
  814. break
  815. }
  816. }
  817. return (events, pumpModel)
  818. }
  819. private func getHistoryPage(_ pageNum: Int) throws -> Data {
  820. var frameData = Data()
  821. let msg = PumpMessage(settings: settings, type: .getHistoryPage, body: GetHistoryPageCarelinkMessageBody(pageNum: pageNum))
  822. var curResp: GetHistoryPageCarelinkMessageBody = try runCommandWithArguments(msg, responseType: .getHistoryPage)
  823. var expectedFrameNum = 1
  824. while(expectedFrameNum == curResp.frameNumber) {
  825. frameData.append(curResp.frame)
  826. expectedFrameNum += 1
  827. let msg = PumpMessage(settings: settings, type: .pumpAck)
  828. if !curResp.lastFrame {
  829. curResp = try session.getResponse(to: msg, responseType: .getHistoryPage)
  830. } else {
  831. try session.send(msg)
  832. break
  833. }
  834. }
  835. guard frameData.count == 1024 else {
  836. throw PumpOpsError.rfCommsFailure("Short history page: \(frameData.count) bytes. Expected 1024")
  837. }
  838. return frameData
  839. }
  840. }
  841. // MARK: - Glucose history
  842. extension PumpOpsSession {
  843. private func logGlucoseHistory(pageData: Data, pageNum: Int) {
  844. var idx = 0
  845. let chunkSize = 256
  846. while idx < pageData.count {
  847. let top = min(idx + chunkSize, pageData.count)
  848. let range = Range(uncheckedBounds: (lower: idx, upper: top))
  849. // TODO: Convert logging
  850. NSLog(String(format: "GlucosePage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString)
  851. idx = top
  852. }
  853. }
  854. /// Fetches glucose history entries which occurred on or after the specified date.
  855. ///
  856. /// History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events.
  857. ///
  858. /// - Parameter startDate: The earliest date of events to retrieve
  859. /// - Returns: An array of fetched history entries, in ascending order of insertion
  860. /// - Throws:
  861. public func getGlucoseHistoryEvents(since startDate: Date) throws -> [TimestampedGlucoseEvent] {
  862. try wakeup()
  863. var events = [TimestampedGlucoseEvent]()
  864. let currentGlucosePage: ReadCurrentGlucosePageMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readCurrentGlucosePage), responseType: .readCurrentGlucosePage)
  865. let startPage = Int(currentGlucosePage.pageNum)
  866. //max lookback of 15 pages or when page is 0
  867. let endPage = max(startPage - 15, 0)
  868. pages: for pageNum in stride(from: startPage, to: endPage - 1, by: -1) {
  869. // TODO: Convert logging
  870. NSLog("Fetching page %d", pageNum)
  871. var pageData: Data
  872. var page: GlucosePage
  873. do {
  874. pageData = try getGlucosePage(UInt32(pageNum))
  875. // logGlucoseHistory(pageData: pageData, pageNum: pageNum)
  876. page = try GlucosePage(pageData: pageData)
  877. if page.needsTimestamp && pageNum == startPage {
  878. // TODO: Convert logging
  879. NSLog(String(format: "GlucosePage %02d needs a new sensor timestamp, writing...", pageNum))
  880. let _ = try writeGlucoseHistoryTimestamp()
  881. //fetch page again with new sensor timestamp
  882. pageData = try getGlucosePage(UInt32(pageNum))
  883. logGlucoseHistory(pageData: pageData, pageNum: pageNum)
  884. page = try GlucosePage(pageData: pageData)
  885. }
  886. } catch PumpOpsError.pumpError {
  887. break pages
  888. }
  889. for event in page.events.reversed() {
  890. var timestamp = event.timestamp
  891. timestamp.timeZone = pump.timeZone
  892. if event is UnknownGlucoseEvent {
  893. continue pages
  894. }
  895. if let date = timestamp.date {
  896. if date < startDate && event is SensorTimestampGlucoseEvent {
  897. // TODO: Convert logging
  898. NSLog("Found reference event at (%@) to be before startDate(%@)", date as NSDate, startDate as NSDate)
  899. break pages
  900. } else {
  901. events.insert(TimestampedGlucoseEvent(glucoseEvent: event, date: date), at: 0)
  902. }
  903. }
  904. }
  905. }
  906. return events
  907. }
  908. private func getGlucosePage(_ pageNum: UInt32) throws -> Data {
  909. var frameData = Data()
  910. let msg = PumpMessage(settings: settings, type: .getGlucosePage, body: GetGlucosePageMessageBody(pageNum: pageNum))
  911. var curResp: GetGlucosePageMessageBody = try runCommandWithArguments(msg, responseType: .getGlucosePage)
  912. var expectedFrameNum = 1
  913. while(expectedFrameNum == curResp.frameNumber) {
  914. frameData.append(curResp.frame)
  915. expectedFrameNum += 1
  916. let msg = PumpMessage(settings: settings, type: .pumpAck)
  917. if !curResp.lastFrame {
  918. curResp = try session.getResponse(to: msg, responseType: .getGlucosePage)
  919. } else {
  920. try session.send(msg)
  921. break
  922. }
  923. }
  924. guard frameData.count == 1024 else {
  925. throw PumpOpsError.rfCommsFailure("Short glucose history page: \(frameData.count) bytes. Expected 1024")
  926. }
  927. return frameData
  928. }
  929. public func writeGlucoseHistoryTimestamp() throws -> Void {
  930. try wakeup()
  931. let shortWriteTimestamp = PumpMessage(settings: settings, type: .writeGlucoseHistoryTimestamp)
  932. let _: PumpAckMessageBody = try session.getResponse(to: shortWriteTimestamp, timeout: .seconds(12))
  933. }
  934. }