PumpOpsSession.swift 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  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: class {
  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. public func enableCCLEDs() throws {
  683. do {
  684. try session.enableCCLEDs()
  685. } catch let error as LocalizedError {
  686. throw PumpOpsError.deviceError(error)
  687. }
  688. }
  689. /// - Throws:
  690. /// - PumpOpsError.deviceError
  691. /// - RileyLinkDeviceError
  692. func configureRadio(for region: PumpRegion, frequency: Measurement<UnitFrequency>?) throws {
  693. try session.resetRadioConfig()
  694. switch region {
  695. case .worldWide:
  696. //try session.updateRegister(.mdmcfg4, value: 0x59)
  697. try setRXFilterMode(.wide)
  698. //try session.updateRegister(.mdmcfg3, value: 0x66)
  699. //try session.updateRegister(.mdmcfg2, value: 0x33)
  700. try session.updateRegister(.mdmcfg1, value: 0x62)
  701. try session.updateRegister(.mdmcfg0, value: 0x1A)
  702. try session.updateRegister(.deviatn, value: 0x13)
  703. case .northAmerica, .canada:
  704. //try session.updateRegister(.mdmcfg4, value: 0x99)
  705. try setRXFilterMode(.narrow)
  706. //try session.updateRegister(.mdmcfg3, value: 0x66)
  707. //try session.updateRegister(.mdmcfg2, value: 0x33)
  708. try session.updateRegister(.mdmcfg1, value: 0x61)
  709. try session.updateRegister(.mdmcfg0, value: 0x7E)
  710. try session.updateRegister(.deviatn, value: 0x15)
  711. }
  712. if let frequency = frequency {
  713. try session.setBaseFrequency(frequency)
  714. }
  715. }
  716. /// - Throws:
  717. /// - PumpOpsError.deviceError
  718. /// - PumpOpsError.noResponse
  719. /// - PumpOpsError.rfCommsFailure
  720. /// - LocalizedError
  721. private func scanForPump(in frequencies: [Measurement<UnitFrequency>], fallback: Measurement<UnitFrequency>?, tries: Int = 3) throws -> FrequencyScanResults {
  722. var trials = [FrequencyTrial]()
  723. let middleFreq = frequencies[frequencies.count / 2]
  724. do {
  725. // Needed to put the pump in listen mode
  726. try session.setBaseFrequency(middleFreq)
  727. try wakeup()
  728. } catch {
  729. // Continue anyway; the pump likely heard us, even if we didn't hear it.
  730. }
  731. for freq in frequencies {
  732. var trial = FrequencyTrial(frequency: freq)
  733. try session.setBaseFrequency(freq)
  734. var sumRSSI = 0
  735. for _ in 1...tries {
  736. // Ignore failures here
  737. let rfPacket = try? session.sendAndListenForPacket(PumpMessage(settings: settings, type: .getPumpModel), timeout: .milliseconds(130))
  738. if let rfPacket = rfPacket,
  739. let pkt = MinimedPacket(encodedData: rfPacket.data),
  740. let response = PumpMessage(rxData: pkt.data), response.messageType == .getPumpModel
  741. {
  742. sumRSSI += rfPacket.rssi
  743. trial.successes += 1
  744. }
  745. trial.tries += 1
  746. }
  747. // Mark each failure as a -99 rssi, so we can use highest rssi as best freq
  748. sumRSSI += -99 * (trial.tries - trial.successes)
  749. trial.avgRSSI = Double(sumRSSI) / Double(trial.tries)
  750. trials.append(trial)
  751. }
  752. let sortedTrials = trials.sorted(by: { (a, b) -> Bool in
  753. return a.avgRSSI > b.avgRSSI
  754. })
  755. guard sortedTrials.first!.successes > 0 else {
  756. try session.setBaseFrequency(fallback ?? middleFreq)
  757. throw PumpOpsError.rfCommsFailure("No pump responses during scan")
  758. }
  759. let results = FrequencyScanResults(
  760. trials: trials,
  761. bestFrequency: sortedTrials.first!.frequency
  762. )
  763. try session.setBaseFrequency(results.bestFrequency)
  764. return results
  765. }
  766. }
  767. // MARK: - Pump history
  768. extension PumpOpsSession {
  769. /// Fetches history entries which occurred on or after the specified date.
  770. ///
  771. /// 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.
  772. ///
  773. /// History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events.
  774. ///
  775. /// - Parameter startDate: The earliest date of events to retrieve
  776. /// - Returns:
  777. /// - An array of fetched history entries, in ascending order of insertion
  778. /// - The pump model
  779. /// - Throws:
  780. /// - PumpCommandError.command
  781. /// - PumpCommandError.arguments
  782. /// - PumpOpsError.couldNotDecode
  783. /// - PumpOpsError.crosstalk
  784. /// - PumpOpsError.deviceError
  785. /// - PumpOpsError.noResponse
  786. /// - PumpOpsError.unknownResponse
  787. /// - HistoryPageError.invalidCRC
  788. /// - HistoryPageError.unknownEventType
  789. public func getHistoryEvents(since startDate: Date) throws -> ([TimestampedHistoryEvent], PumpModel) {
  790. try wakeup()
  791. let pumpModel = try getPumpModel()
  792. var events = [TimestampedHistoryEvent]()
  793. pages: for pageNum in 0..<16 {
  794. // TODO: Convert logging
  795. NSLog("Fetching page %d", pageNum)
  796. let pageData: Data
  797. do {
  798. pageData = try getHistoryPage(pageNum)
  799. } catch PumpCommandError.arguments(let error) {
  800. if case PumpOpsError.pumpError(.pageDoesNotExist) = error {
  801. return (events, pumpModel)
  802. }
  803. throw PumpCommandError.arguments(error)
  804. }
  805. var idx = 0
  806. let chunkSize = 256
  807. while idx < pageData.count {
  808. let top = min(idx + chunkSize, pageData.count)
  809. let range = Range(uncheckedBounds: (lower: idx, upper: top))
  810. // TODO: Convert logging
  811. NSLog(String(format: "HistoryPage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString)
  812. idx = top
  813. }
  814. let page = try HistoryPage(pageData: pageData, pumpModel: pumpModel)
  815. let (timestampedEvents, hasMoreEvents, _) = page.timestampedEvents(after: startDate, timeZone: pump.timeZone, model: pumpModel)
  816. events = timestampedEvents + events
  817. if !hasMoreEvents {
  818. break
  819. }
  820. }
  821. return (events, pumpModel)
  822. }
  823. private func getHistoryPage(_ pageNum: Int) throws -> Data {
  824. var frameData = Data()
  825. let msg = PumpMessage(settings: settings, type: .getHistoryPage, body: GetHistoryPageCarelinkMessageBody(pageNum: pageNum))
  826. var curResp: GetHistoryPageCarelinkMessageBody = try runCommandWithArguments(msg, responseType: .getHistoryPage)
  827. var expectedFrameNum = 1
  828. while(expectedFrameNum == curResp.frameNumber) {
  829. frameData.append(curResp.frame)
  830. expectedFrameNum += 1
  831. let msg = PumpMessage(settings: settings, type: .pumpAck)
  832. if !curResp.lastFrame {
  833. curResp = try session.getResponse(to: msg, responseType: .getHistoryPage)
  834. } else {
  835. try session.send(msg)
  836. break
  837. }
  838. }
  839. guard frameData.count == 1024 else {
  840. throw PumpOpsError.rfCommsFailure("Short history page: \(frameData.count) bytes. Expected 1024")
  841. }
  842. return frameData
  843. }
  844. }
  845. // MARK: - Glucose history
  846. extension PumpOpsSession {
  847. private func logGlucoseHistory(pageData: Data, pageNum: Int) {
  848. var idx = 0
  849. let chunkSize = 256
  850. while idx < pageData.count {
  851. let top = min(idx + chunkSize, pageData.count)
  852. let range = Range(uncheckedBounds: (lower: idx, upper: top))
  853. // TODO: Convert logging
  854. NSLog(String(format: "GlucosePage %02d - (bytes %03d-%03d): ", pageNum, idx, top-1) + pageData.subdata(in: range).hexadecimalString)
  855. idx = top
  856. }
  857. }
  858. /// Fetches glucose history entries which occurred on or after the specified date.
  859. ///
  860. /// History timestamps are reconciled with UTC based on the `timeZone` property of PumpState, as well as recorded clock change events.
  861. ///
  862. /// - Parameter startDate: The earliest date of events to retrieve
  863. /// - Returns: An array of fetched history entries, in ascending order of insertion
  864. /// - Throws:
  865. public func getGlucoseHistoryEvents(since startDate: Date) throws -> [TimestampedGlucoseEvent] {
  866. try wakeup()
  867. var events = [TimestampedGlucoseEvent]()
  868. let currentGlucosePage: ReadCurrentGlucosePageMessageBody = try session.getResponse(to: PumpMessage(settings: settings, type: .readCurrentGlucosePage), responseType: .readCurrentGlucosePage)
  869. let startPage = Int(currentGlucosePage.pageNum)
  870. //max lookback of 15 pages or when page is 0
  871. let endPage = max(startPage - 15, 0)
  872. pages: for pageNum in stride(from: startPage, to: endPage - 1, by: -1) {
  873. // TODO: Convert logging
  874. NSLog("Fetching page %d", pageNum)
  875. var pageData: Data
  876. var page: GlucosePage
  877. do {
  878. pageData = try getGlucosePage(UInt32(pageNum))
  879. // logGlucoseHistory(pageData: pageData, pageNum: pageNum)
  880. page = try GlucosePage(pageData: pageData)
  881. if page.needsTimestamp && pageNum == startPage {
  882. // TODO: Convert logging
  883. NSLog(String(format: "GlucosePage %02d needs a new sensor timestamp, writing...", pageNum))
  884. let _ = try writeGlucoseHistoryTimestamp()
  885. //fetch page again with new sensor timestamp
  886. pageData = try getGlucosePage(UInt32(pageNum))
  887. logGlucoseHistory(pageData: pageData, pageNum: pageNum)
  888. page = try GlucosePage(pageData: pageData)
  889. }
  890. } catch PumpOpsError.pumpError {
  891. break pages
  892. }
  893. for event in page.events.reversed() {
  894. var timestamp = event.timestamp
  895. timestamp.timeZone = pump.timeZone
  896. if event is UnknownGlucoseEvent {
  897. continue pages
  898. }
  899. if let date = timestamp.date {
  900. if date < startDate && event is SensorTimestampGlucoseEvent {
  901. // TODO: Convert logging
  902. NSLog("Found reference event at (%@) to be before startDate(%@)", date as NSDate, startDate as NSDate)
  903. break pages
  904. } else {
  905. events.insert(TimestampedGlucoseEvent(glucoseEvent: event, date: date), at: 0)
  906. }
  907. }
  908. }
  909. }
  910. return events
  911. }
  912. private func getGlucosePage(_ pageNum: UInt32) throws -> Data {
  913. var frameData = Data()
  914. let msg = PumpMessage(settings: settings, type: .getGlucosePage, body: GetGlucosePageMessageBody(pageNum: pageNum))
  915. var curResp: GetGlucosePageMessageBody = try runCommandWithArguments(msg, responseType: .getGlucosePage)
  916. var expectedFrameNum = 1
  917. while(expectedFrameNum == curResp.frameNumber) {
  918. frameData.append(curResp.frame)
  919. expectedFrameNum += 1
  920. let msg = PumpMessage(settings: settings, type: .pumpAck)
  921. if !curResp.lastFrame {
  922. curResp = try session.getResponse(to: msg, responseType: .getGlucosePage)
  923. } else {
  924. try session.send(msg)
  925. break
  926. }
  927. }
  928. guard frameData.count == 1024 else {
  929. throw PumpOpsError.rfCommsFailure("Short glucose history page: \(frameData.count) bytes. Expected 1024")
  930. }
  931. return frameData
  932. }
  933. public func writeGlucoseHistoryTimestamp() throws -> Void {
  934. try wakeup()
  935. let shortWriteTimestamp = PumpMessage(settings: settings, type: .writeGlucoseHistoryTimestamp)
  936. let _: PumpAckMessageBody = try session.getResponse(to: shortWriteTimestamp, timeout: .seconds(12))
  937. }
  938. }