PodCommsSession.swift 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. //
  2. // PodCommsSession.swift
  3. // OmniKit
  4. //
  5. // Created by Pete Schwamb on 10/13/17.
  6. // Copyright © 2017 Pete Schwamb. All rights reserved.
  7. //
  8. import Foundation
  9. import RileyLinkBLEKit
  10. import LoopKit
  11. import os.log
  12. public enum PodCommsError: Error {
  13. case noPodPaired
  14. case invalidData
  15. case noResponse
  16. case emptyResponse
  17. case podAckedInsteadOfReturningResponse
  18. case unexpectedPacketType(packetType: PacketType)
  19. case unexpectedResponse(response: MessageBlockType)
  20. case unknownResponseType(rawType: UInt8)
  21. case invalidAddress(address: UInt32, expectedAddress: UInt32)
  22. case noRileyLinkAvailable
  23. case unfinalizedBolus
  24. case unfinalizedTempBasal
  25. case nonceResyncFailed
  26. case podSuspended
  27. case podFault(fault: DetailedStatus)
  28. case commsError(error: Error)
  29. case rejectedMessage(errorCode: UInt8)
  30. case podChange
  31. case activationTimeExceeded
  32. case rssiTooLow
  33. case rssiTooHigh
  34. case diagnosticMessage(str: String)
  35. case podIncompatible(str: String)
  36. }
  37. extension PodCommsError: LocalizedError {
  38. public var errorDescription: String? {
  39. switch self {
  40. case .noPodPaired:
  41. return LocalizedString("No pod paired", comment: "Error message shown when no pod is paired")
  42. case .invalidData:
  43. return nil
  44. case .noResponse:
  45. return LocalizedString("No response from pod", comment: "Error message shown when no response from pod was received")
  46. case .emptyResponse:
  47. return LocalizedString("Empty response from pod", comment: "Error message shown when empty response from pod was received")
  48. case .podAckedInsteadOfReturningResponse:
  49. return LocalizedString("Pod sent ack instead of response", comment: "Error message shown when pod sends ack instead of response")
  50. case .unexpectedPacketType:
  51. return nil
  52. case .unexpectedResponse:
  53. return LocalizedString("Unexpected response from pod", comment: "Error message shown when empty response from pod was received")
  54. case .unknownResponseType:
  55. return nil
  56. case .invalidAddress(address: let address, expectedAddress: let expectedAddress):
  57. return String(format: LocalizedString("Invalid address 0x%x. Expected 0x%x", comment: "Error message for when unexpected address is received (1: received address) (2: expected address)"), address, expectedAddress)
  58. case .noRileyLinkAvailable:
  59. return LocalizedString("No RileyLink available", comment: "Error message shown when no response from pod was received")
  60. case .unfinalizedBolus:
  61. return LocalizedString("Bolus in progress", comment: "Error message shown when operation could not be completed due to existing bolus in progress")
  62. case .unfinalizedTempBasal:
  63. return LocalizedString("Temp basal in progress", comment: "Error message shown when temp basal could not be set due to existing temp basal in progress")
  64. case .nonceResyncFailed:
  65. return nil
  66. case .podSuspended:
  67. return LocalizedString("Pod is suspended", comment: "Error message action could not be performed because pod is suspended")
  68. case .podFault(let fault):
  69. let faultDescription = String(describing: fault.faultEventCode)
  70. return String(format: LocalizedString("Pod Fault: %1$@", comment: "Format string for pod fault code"), faultDescription)
  71. case .commsError(let error):
  72. return error.localizedDescription
  73. case .rejectedMessage(let errorCode):
  74. return String(format: LocalizedString("Command error %1$u", comment: "Format string for invalid message error code (1: error code number)"), errorCode)
  75. case .podChange:
  76. return LocalizedString("Unexpected pod change", comment: "Format string for unexpected pod change")
  77. case .activationTimeExceeded:
  78. return LocalizedString("Activation time exceeded", comment: "Format string for activation time exceeded")
  79. case .rssiTooLow: // occurs when RileyLink is too far from pod for reliable pairing, but can sometimes occur at other distances & positions
  80. return LocalizedString("Poor signal strength", comment: "Format string for poor pod signal strength")
  81. case .rssiTooHigh: // only occurs when RileyLink is too close to the pod for reliable pairing
  82. return LocalizedString("Signal strength too high", comment: "Format string for pod signal strength too high")
  83. case .diagnosticMessage(let str):
  84. return str
  85. case .podIncompatible(let str):
  86. return str
  87. }
  88. }
  89. // public var failureReason: String? {
  90. // return nil
  91. // }
  92. public var recoverySuggestion: String? {
  93. switch self {
  94. case .noPodPaired:
  95. return nil
  96. case .invalidData:
  97. return nil
  98. case .noResponse:
  99. return LocalizedString("Please try repositioning the pod or the RileyLink and try again", comment: "Recovery suggestion when no response is received from pod")
  100. case .emptyResponse:
  101. return nil
  102. case .podAckedInsteadOfReturningResponse:
  103. return LocalizedString("Try again", comment: "Recovery suggestion when ack received instead of response")
  104. case .unexpectedPacketType:
  105. return nil
  106. case .unexpectedResponse:
  107. return nil
  108. case .unknownResponseType:
  109. return nil
  110. case .invalidAddress:
  111. return LocalizedString("Crosstalk possible. Please move to a new location and try again", comment: "Recovery suggestion when unexpected address received")
  112. case .noRileyLinkAvailable:
  113. return LocalizedString("Make sure your RileyLink is nearby and powered on", comment: "Recovery suggestion when no RileyLink is available")
  114. case .unfinalizedBolus:
  115. return LocalizedString("Wait for existing bolus to finish, or cancel bolus", comment: "Recovery suggestion when operation could not be completed due to existing bolus in progress")
  116. case .unfinalizedTempBasal:
  117. return LocalizedString("Wait for existing temp basal to finish, or suspend to cancel", comment: "Recovery suggestion when operation could not be completed due to existing temp basal in progress")
  118. case .nonceResyncFailed:
  119. return nil
  120. case .podSuspended:
  121. return LocalizedString("Resume delivery", comment: "Recovery suggestion when pod is suspended")
  122. case .podFault:
  123. return nil
  124. case .commsError:
  125. return nil
  126. case .rejectedMessage:
  127. return nil
  128. case .podChange:
  129. return LocalizedString("Please bring only original pod in range or deactivate original pod", comment: "Recovery suggestion on unexpected pod change")
  130. case .activationTimeExceeded:
  131. return nil
  132. case .rssiTooLow:
  133. return LocalizedString("Please reposition the RileyLink relative to the pod", comment: "Recovery suggestion when pairing signal strength is too low")
  134. case .rssiTooHigh:
  135. return LocalizedString("Please reposition the RileyLink further from the pod", comment: "Recovery suggestion when pairing signal strength is too high")
  136. case .diagnosticMessage:
  137. return nil
  138. case .podIncompatible:
  139. return nil
  140. }
  141. }
  142. public var isFaulted: Bool {
  143. switch self {
  144. case .podFault, .activationTimeExceeded, .podIncompatible:
  145. return true
  146. default:
  147. return false
  148. }
  149. }
  150. }
  151. public protocol PodCommsSessionDelegate: AnyObject {
  152. func podCommsSession(_ podCommsSession: PodCommsSession, didChange state: PodState)
  153. }
  154. public class PodCommsSession {
  155. private let useCancelNoneForStatus: Bool = false // whether to always use a cancel none to get status
  156. public let log = OSLog(category: "PodCommsSession")
  157. private var podState: PodState {
  158. didSet {
  159. assertOnSessionQueue()
  160. delegate.podCommsSession(self, didChange: podState)
  161. }
  162. }
  163. private unowned let delegate: PodCommsSessionDelegate
  164. private var transport: MessageTransport
  165. init(podState: PodState, transport: MessageTransport, delegate: PodCommsSessionDelegate) {
  166. self.podState = podState
  167. self.transport = transport
  168. self.delegate = delegate
  169. self.transport.delegate = self
  170. }
  171. // Handles updating PodState on first pod fault seen
  172. private func handlePodFault(fault: DetailedStatus) {
  173. if self.podState.fault == nil {
  174. self.podState.fault = fault // save the first fault returned
  175. handleCancelDosing(deliveryType: .all, bolusNotDelivered: fault.bolusNotDelivered)
  176. podState.updateFromDetailedStatusResponse(fault)
  177. }
  178. log.error("Pod Fault: %@", String(describing: fault))
  179. }
  180. // Will throw either PodCommsError.podFault or PodCommsError.activationTimeExceeded
  181. private func throwPodFault(fault: DetailedStatus) throws {
  182. handlePodFault(fault: fault)
  183. if fault.podProgressStatus == .activationTimeExceeded {
  184. // avoids a confusing "No fault" error when activation time is exceeded
  185. throw PodCommsError.activationTimeExceeded
  186. }
  187. throw PodCommsError.podFault(fault: fault)
  188. }
  189. /// Performs a message exchange, handling nonce resync, pod faults
  190. ///
  191. /// - Parameters:
  192. /// - messageBlocks: The message blocks to send
  193. /// - confirmationBeepType: If specified, type of confirmation beep to append as a separate beep message block
  194. /// - expectFollowOnMessage: If true, the pod will expect another message within 4 minutes, or will alarm with an 0x33 (51) fault.
  195. /// - Returns: The received message response
  196. /// - Throws:
  197. /// - PodCommsError.noResponse
  198. /// - PodCommsError.podFault
  199. /// - PodCommsError.unexpectedResponse
  200. /// - PodCommsError.rejectedMessage
  201. /// - PodCommsError.nonceResyncFailed
  202. /// - MessageError
  203. /// - RileyLinkDeviceError
  204. func send<T: MessageBlock>(_ messageBlocks: [MessageBlock], confirmationBeepType: BeepConfigType? = nil, expectFollowOnMessage: Bool = false) throws -> T {
  205. var triesRemaining = 2 // Retries only happen for nonce resync
  206. var blocksToSend = messageBlocks
  207. // If a confirmation beep type was specified & pod isn't faulted, append a beep config message block to emit the requested beep type
  208. if let confirmationBeepType = confirmationBeepType, podState.isFaulted == false {
  209. let confirmationBeepBlock = BeepConfigCommand(beepConfigType: confirmationBeepType, basalCompletionBeep: true, tempBasalCompletionBeep: false, bolusCompletionBeep: true)
  210. blocksToSend += [confirmationBeepBlock]
  211. }
  212. if blocksToSend.contains(where: { $0 as? NonceResyncableMessageBlock != nil }) {
  213. podState.advanceToNextNonce()
  214. }
  215. let messageNumber = transport.messageNumber
  216. var sentNonce: UInt32?
  217. while (triesRemaining > 0) {
  218. triesRemaining -= 1
  219. for command in blocksToSend {
  220. if let nonceBlock = command as? NonceResyncableMessageBlock {
  221. sentNonce = nonceBlock.nonce
  222. break // N.B. all nonce commands in single message should have the same value
  223. }
  224. }
  225. let message = Message(address: podState.address, messageBlocks: blocksToSend, sequenceNum: messageNumber, expectFollowOnMessage: expectFollowOnMessage)
  226. self.podState.lastCommsOK = false // mark last comms as not OK until we get the expected response
  227. let response = try transport.sendMessage(message)
  228. // Simulate fault
  229. //let podInfoResponse = try PodInfoResponse(encodedData: Data(hexadecimalString: "0216020d0000000000ab6a038403ff03860000285708030d0000")!)
  230. //let response = Message(address: podState.address, messageBlocks: [podInfoResponse], sequenceNum: message.sequenceNum)
  231. if let responseMessageBlock = response.messageBlocks[0] as? T {
  232. log.info("POD Response: %@", String(describing: responseMessageBlock))
  233. self.podState.lastCommsOK = true // message successfully sent and expected response received
  234. return responseMessageBlock
  235. }
  236. if let fault = response.fault {
  237. try throwPodFault(fault: fault) // always throws
  238. }
  239. let responseType = response.messageBlocks[0].blockType
  240. guard let errorResponse = response.messageBlocks[0] as? ErrorResponse else {
  241. log.error("Unexpected response: %{public}@", String(describing: response.messageBlocks[0]))
  242. throw PodCommsError.unexpectedResponse(response: responseType)
  243. }
  244. switch errorResponse.errorResponseType {
  245. case .badNonce(let nonceResyncKey):
  246. guard let sentNonce = sentNonce else {
  247. log.error("Unexpected bad nonce response: %{public}@", String(describing: response.messageBlocks[0]))
  248. throw PodCommsError.unexpectedResponse(response: responseType)
  249. }
  250. podState.resyncNonce(syncWord: nonceResyncKey, sentNonce: sentNonce, messageSequenceNum: message.sequenceNum)
  251. log.info("resyncNonce(syncWord: 0x%02x, sentNonce: 0x%04x, messageSequenceNum: %d) -> 0x%04x", nonceResyncKey, sentNonce, message.sequenceNum, podState.currentNonce)
  252. blocksToSend = blocksToSend.map({ (block) -> MessageBlock in
  253. if var resyncableBlock = block as? NonceResyncableMessageBlock {
  254. log.info("Replaced old nonce 0x%04x with resync nonce 0x%04x", resyncableBlock.nonce, podState.currentNonce)
  255. resyncableBlock.nonce = podState.currentNonce
  256. return resyncableBlock
  257. }
  258. return block
  259. })
  260. podState.advanceToNextNonce()
  261. break
  262. case .nonretryableError(let errorCode, let faultEventCode, let podProgress):
  263. log.error("Command error: code %u, %{public}@, pod progress %{public}@", errorCode, String(describing: faultEventCode), String(describing: podProgress))
  264. throw PodCommsError.rejectedMessage(errorCode: errorCode)
  265. }
  266. }
  267. throw PodCommsError.nonceResyncFailed
  268. }
  269. // Returns time at which prime is expected to finish.
  270. public func prime() throws -> TimeInterval {
  271. let primeDuration: TimeInterval = .seconds(Pod.primeUnits / Pod.primeDeliveryRate) + 3 // as per PDM
  272. // If priming has never been attempted on this pod, handle the pre-prime setup tasks.
  273. // A FaultConfig can only be done before the prime bolus or the pod will generate an 049 fault.
  274. if podState.setupProgress.primingNeverAttempted {
  275. // This FaultConfig command will set Tab5[$16] to 0 during pairing, which disables $6x faults
  276. let _: StatusResponse = try send([FaultConfigCommand(nonce: podState.currentNonce, tab5Sub16: 0, tab5Sub17: 0)])
  277. // Set up the finish pod setup reminder alert which beeps every 5 minutes for 1 hour
  278. let finishSetupReminder = PodAlert.finishSetupReminder
  279. try configureAlerts([finishSetupReminder])
  280. } else {
  281. // Not the first time through, check to see if prime bolus was successfully started
  282. let status: StatusResponse = try send([GetStatusCommand()])
  283. podState.updateFromStatusResponse(status)
  284. if status.podProgressStatus == .priming || status.podProgressStatus == .primingCompleted {
  285. podState.setupProgress = .priming
  286. return podState.primeFinishTime?.timeIntervalSinceNow ?? primeDuration
  287. }
  288. }
  289. // Mark Pod.primeUnits (2.6U) bolus delivery with Pod.primeDeliveryRate (1) between pulses for prime
  290. let primeFinishTime = Date() + primeDuration
  291. podState.primeFinishTime = primeFinishTime
  292. podState.setupProgress = .startingPrime
  293. let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerPrimePulse)
  294. let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: Pod.primeUnits, timeBetweenPulses: timeBetweenPulses)
  295. let scheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, deliverySchedule: bolusSchedule)
  296. let bolusExtraCommand = BolusExtraCommand(units: Pod.primeUnits, timeBetweenPulses: timeBetweenPulses)
  297. let status: StatusResponse = try send([scheduleCommand, bolusExtraCommand])
  298. podState.updateFromStatusResponse(status)
  299. podState.setupProgress = .priming
  300. return primeFinishTime.timeIntervalSinceNow
  301. }
  302. public func programInitialBasalSchedule(_ basalSchedule: BasalSchedule, scheduleOffset: TimeInterval) throws {
  303. if podState.setupProgress == .settingInitialBasalSchedule {
  304. // We started basal schedule programming, but didn't get confirmation somehow, so check status
  305. let status: StatusResponse = try send([GetStatusCommand()])
  306. podState.updateFromStatusResponse(status)
  307. if status.podProgressStatus == .basalInitialized {
  308. podState.setupProgress = .initialBasalScheduleSet
  309. podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: Date(), scheduledCertainty: .certain, insulinType: podState.insulinType))
  310. return
  311. }
  312. }
  313. podState.setupProgress = .settingInitialBasalSchedule
  314. // Set basal schedule
  315. let _ = try setBasalSchedule(schedule: basalSchedule, scheduleOffset: scheduleOffset)
  316. podState.setupProgress = .initialBasalScheduleSet
  317. podState.finalizedDoses.append(UnfinalizedDose(resumeStartTime: Date(), scheduledCertainty: .certain, insulinType: podState.insulinType))
  318. }
  319. @discardableResult
  320. private func configureAlerts(_ alerts: [PodAlert], confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse {
  321. let configurations = alerts.map { $0.configuration }
  322. let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: configurations)
  323. let status: StatusResponse = try send([configureAlerts], confirmationBeepType: confirmationBeepType)
  324. for alert in alerts {
  325. podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
  326. }
  327. podState.updateFromStatusResponse(status)
  328. return status
  329. }
  330. // emits the specified beep type and sets the completion beep flags, doesn't throw
  331. public func beepConfig(beepConfigType: BeepConfigType, basalCompletionBeep: Bool, tempBasalCompletionBeep: Bool, bolusCompletionBeep: Bool) -> Result<StatusResponse, Error> {
  332. if let fault = self.podState.fault {
  333. log.info("Skip beep config with faulted pod")
  334. return .failure(PodCommsError.podFault(fault: fault))
  335. }
  336. let beepConfigCommand = BeepConfigCommand(beepConfigType: beepConfigType, basalCompletionBeep: basalCompletionBeep, tempBasalCompletionBeep: tempBasalCompletionBeep, bolusCompletionBeep: bolusCompletionBeep)
  337. do {
  338. let statusResponse: StatusResponse = try send([beepConfigCommand])
  339. podState.updateFromStatusResponse(statusResponse)
  340. return .success(statusResponse)
  341. } catch let error {
  342. return .failure(error)
  343. }
  344. }
  345. private func markSetupProgressCompleted(statusResponse: StatusResponse) {
  346. if (podState.setupProgress != .completed) {
  347. podState.setupProgress = .completed
  348. podState.setupUnitsDelivered = statusResponse.insulin // stash the current insulin delivered value as the baseline
  349. log.info("Total setup units delivered: %@", String(describing: statusResponse.insulin))
  350. }
  351. }
  352. public func insertCannula() throws -> TimeInterval {
  353. let cannulaInsertionUnits = Pod.cannulaInsertionUnits + Pod.cannulaInsertionUnitsExtra
  354. let insertionWait: TimeInterval = .seconds(cannulaInsertionUnits / Pod.primeDeliveryRate)
  355. guard let activatedAt = podState.activatedAt else {
  356. throw PodCommsError.noPodPaired
  357. }
  358. if podState.setupProgress == .startingInsertCannula || podState.setupProgress == .cannulaInserting {
  359. // We started cannula insertion, but didn't get confirmation somehow, so check status
  360. let status: StatusResponse = try send([GetStatusCommand()])
  361. if status.podProgressStatus == .insertingCannula {
  362. podState.setupProgress = .cannulaInserting
  363. podState.updateFromStatusResponse(status)
  364. return insertionWait // Not sure when it started, wait full time to be sure
  365. }
  366. if status.podProgressStatus.readyForDelivery {
  367. markSetupProgressCompleted(statusResponse: status)
  368. podState.updateFromStatusResponse(status)
  369. return TimeInterval(0) // Already done; no need to wait
  370. }
  371. podState.updateFromStatusResponse(status)
  372. } else {
  373. // Configure all the non-optional Pod Alarms
  374. let expirationTime = activatedAt + Pod.nominalPodLife
  375. let timeUntilExpirationAdvisory = expirationTime.timeIntervalSinceNow
  376. let expirationAdvisoryAlarm = PodAlert.expirationAdvisoryAlarm(alarmTime: timeUntilExpirationAdvisory, duration: Pod.expirationAdvisoryWindow)
  377. let endOfServiceTime = activatedAt + Pod.serviceDuration
  378. let shutdownImminentAlarm = PodAlert.shutdownImminentAlarm((endOfServiceTime - Pod.endOfServiceImminentWindow).timeIntervalSinceNow)
  379. try configureAlerts([expirationAdvisoryAlarm, shutdownImminentAlarm])
  380. }
  381. // Mark cannulaInsertionUnits (0.5U) bolus delivery with Pod.secondsPerPrimePulse (1) between pulses for cannula insertion
  382. let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerPrimePulse)
  383. let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses)
  384. let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, deliverySchedule: bolusSchedule)
  385. podState.setupProgress = .startingInsertCannula
  386. let bolusExtraCommand = BolusExtraCommand(units: cannulaInsertionUnits, timeBetweenPulses: timeBetweenPulses)
  387. let status2: StatusResponse = try send([bolusScheduleCommand, bolusExtraCommand])
  388. podState.updateFromStatusResponse(status2)
  389. podState.setupProgress = .cannulaInserting
  390. return insertionWait
  391. }
  392. public func checkInsertionCompleted() throws {
  393. if podState.setupProgress == .cannulaInserting {
  394. let response: StatusResponse = try send([GetStatusCommand()])
  395. if response.podProgressStatus.readyForDelivery {
  396. markSetupProgressCompleted(statusResponse: response)
  397. }
  398. podState.updateFromStatusResponse(response)
  399. }
  400. }
  401. // Throws SetBolusError
  402. public enum DeliveryCommandResult {
  403. case success(statusResponse: StatusResponse)
  404. case certainFailure(error: PodCommsError)
  405. case uncertainFailure(error: PodCommsError)
  406. }
  407. public enum CancelDeliveryResult {
  408. case success(statusResponse: StatusResponse, canceledDose: UnfinalizedDose?)
  409. case certainFailure(error: PodCommsError)
  410. case uncertainFailure(error: PodCommsError)
  411. }
  412. public func bolus(units: Double, automatic: Bool, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) -> DeliveryCommandResult {
  413. let timeBetweenPulses = TimeInterval(seconds: Pod.secondsPerBolusPulse)
  414. let bolusSchedule = SetInsulinScheduleCommand.DeliverySchedule.bolus(units: units, timeBetweenPulses: timeBetweenPulses)
  415. let bolusScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, deliverySchedule: bolusSchedule)
  416. guard podState.unfinalizedBolus == nil else {
  417. return DeliveryCommandResult.certainFailure(error: .unfinalizedBolus)
  418. }
  419. // Between bluetooth and the radio and firmware, about 1.2s on average passes before we start tracking
  420. let commsOffset = TimeInterval(seconds: -1.5)
  421. let bolusExtraCommand = BolusExtraCommand(units: units, timeBetweenPulses: timeBetweenPulses, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep, programReminderInterval: programReminderInterval)
  422. do {
  423. let statusResponse: StatusResponse = try send([bolusScheduleCommand, bolusExtraCommand])
  424. podState.unfinalizedBolus = UnfinalizedDose(bolusAmount: units, startTime: Date().addingTimeInterval(commsOffset), scheduledCertainty: .certain, insulinType: podState.insulinType, automatic: automatic)
  425. podState.updateFromStatusResponse(statusResponse)
  426. return DeliveryCommandResult.success(statusResponse: statusResponse)
  427. } catch PodCommsError.nonceResyncFailed {
  428. return DeliveryCommandResult.certainFailure(error: PodCommsError.nonceResyncFailed)
  429. } catch PodCommsError.rejectedMessage(let errorCode) {
  430. return DeliveryCommandResult.certainFailure(error: PodCommsError.rejectedMessage(errorCode: errorCode))
  431. } catch let error {
  432. self.log.debug("Uncertain result bolusing")
  433. // Attempt to verify bolus
  434. let podCommsError = error as? PodCommsError ?? PodCommsError.commsError(error: error)
  435. guard let status = try? getStatus() else {
  436. self.log.debug("Status check failed; could not resolve bolus uncertainty")
  437. podState.unfinalizedBolus = UnfinalizedDose(bolusAmount: units, startTime: Date(), scheduledCertainty: .uncertain, insulinType: podState.insulinType, automatic: automatic)
  438. return DeliveryCommandResult.uncertainFailure(error: podCommsError)
  439. }
  440. if status.deliveryStatus.bolusing {
  441. self.log.debug("getStatus resolved bolus uncertainty (succeeded)")
  442. podState.unfinalizedBolus = UnfinalizedDose(bolusAmount: units, startTime: Date().addingTimeInterval(commsOffset), scheduledCertainty: .certain, insulinType: podState.insulinType, automatic: automatic)
  443. return DeliveryCommandResult.success(statusResponse: status)
  444. } else {
  445. self.log.debug("getStatus resolved bolus uncertainty (failed)")
  446. return DeliveryCommandResult.certainFailure(error: podCommsError)
  447. }
  448. }
  449. }
  450. public func setTempBasal(rate: Double, duration: TimeInterval, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) -> DeliveryCommandResult {
  451. let tempBasalCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, tempBasalRate: rate, duration: duration)
  452. let tempBasalExtraCommand = TempBasalExtraCommand(rate: rate, duration: duration, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep, programReminderInterval: programReminderInterval)
  453. guard podState.unfinalizedBolus?.isFinished != false else {
  454. return DeliveryCommandResult.certainFailure(error: .unfinalizedBolus)
  455. }
  456. do {
  457. let status: StatusResponse = try send([tempBasalCommand, tempBasalExtraCommand])
  458. podState.unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: rate, startTime: Date(), duration: duration, scheduledCertainty: .certain, insulinType: podState.insulinType)
  459. podState.updateFromStatusResponse(status)
  460. return DeliveryCommandResult.success(statusResponse: status)
  461. } catch PodCommsError.nonceResyncFailed {
  462. return DeliveryCommandResult.certainFailure(error: PodCommsError.nonceResyncFailed)
  463. } catch PodCommsError.rejectedMessage(let errorCode) {
  464. return DeliveryCommandResult.certainFailure(error: PodCommsError.rejectedMessage(errorCode: errorCode))
  465. } catch let error {
  466. podState.unfinalizedTempBasal = UnfinalizedDose(tempBasalRate: rate, startTime: Date(), duration: duration, scheduledCertainty: .uncertain, insulinType: podState.insulinType)
  467. return DeliveryCommandResult.uncertainFailure(error: error as? PodCommsError ?? PodCommsError.commsError(error: error))
  468. }
  469. }
  470. @discardableResult
  471. private func handleCancelDosing(deliveryType: CancelDeliveryCommand.DeliveryType, bolusNotDelivered: Double) -> UnfinalizedDose? {
  472. var canceledDose: UnfinalizedDose? = nil
  473. let now = Date()
  474. if deliveryType.contains(.basal) {
  475. podState.unfinalizedSuspend = UnfinalizedDose(suspendStartTime: now, scheduledCertainty: .certain)
  476. podState.suspendState = .suspended(now)
  477. }
  478. if let unfinalizedTempBasal = podState.unfinalizedTempBasal,
  479. let finishTime = unfinalizedTempBasal.finishTime,
  480. deliveryType.contains(.tempBasal),
  481. finishTime > now
  482. {
  483. podState.unfinalizedTempBasal?.cancel(at: now)
  484. if !deliveryType.contains(.basal) {
  485. podState.suspendState = .resumed(now)
  486. }
  487. canceledDose = podState.unfinalizedTempBasal
  488. log.info("Interrupted temp basal: %@", String(describing: canceledDose))
  489. }
  490. if let unfinalizedBolus = podState.unfinalizedBolus,
  491. let finishTime = unfinalizedBolus.finishTime,
  492. deliveryType.contains(.bolus),
  493. finishTime > now
  494. {
  495. podState.unfinalizedBolus?.cancel(at: now, withRemaining: bolusNotDelivered)
  496. canceledDose = podState.unfinalizedBolus
  497. log.info("Interrupted bolus: %@", String(describing: canceledDose))
  498. }
  499. return canceledDose
  500. }
  501. // Suspends insulin delivery and sets appropriate podSuspendedReminder & suspendTimeExpired alerts.
  502. // A nil suspendReminder is an untimed suspend with no suspend reminders.
  503. // A suspendReminder of 0 is an untimed suspend which only uses podSuspendedReminder alert beeps.
  504. // A suspendReminder of 1-5 minutes will only use suspendTimeExpired alert beeps.
  505. // A suspendReminder of > 5 min will have periodic podSuspendedReminder beeps followed by suspendTimeExpired alerts.
  506. public func suspendDelivery(suspendReminder: TimeInterval? = nil, confirmationBeepType: BeepConfigType? = nil) -> CancelDeliveryResult {
  507. do {
  508. var alertConfigurations: [AlertConfiguration] = []
  509. var podSuspendedReminderAlert: PodAlert? = nil
  510. var suspendTimeExpiredAlert: PodAlert? = nil
  511. let suspendTime: TimeInterval = suspendReminder != nil ? suspendReminder! : 0
  512. let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: .all, beepType: .noBeep)
  513. var commandsToSend: [MessageBlock] = [cancelDeliveryCommand]
  514. // podSuspendedReminder provides a periodic pod suspended reminder beep until the specified suspend time.
  515. if suspendReminder != nil && (suspendTime == 0 || suspendTime > .minutes(5)) {
  516. // using reminder beeps for an untimed or long enough suspend time requiring pod suspended reminders
  517. podSuspendedReminderAlert = PodAlert.podSuspendedReminder(active: true, suspendTime: suspendTime)
  518. alertConfigurations += [podSuspendedReminderAlert!.configuration]
  519. }
  520. // suspendTimeExpired provides suspend time expired alert beeping after the expected suspend time has passed.
  521. if suspendTime > 0 {
  522. // a timed suspend using a suspend time expired alert
  523. suspendTimeExpiredAlert = PodAlert.suspendTimeExpired(suspendTime: suspendTime)
  524. alertConfigurations += [suspendTimeExpiredAlert!.configuration]
  525. }
  526. // append a ConfigureAlert command if we have any reminder alerts for this suspend
  527. if alertConfigurations.count != 0 {
  528. let configureAlerts = ConfigureAlertsCommand(nonce: podState.currentNonce, configurations: alertConfigurations)
  529. commandsToSend += [configureAlerts]
  530. }
  531. let status: StatusResponse = try send(commandsToSend, confirmationBeepType: confirmationBeepType)
  532. let canceledDose = handleCancelDosing(deliveryType: .all, bolusNotDelivered: status.bolusNotDelivered)
  533. podState.updateFromStatusResponse(status)
  534. if let alert = podSuspendedReminderAlert {
  535. podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
  536. }
  537. if let alert = suspendTimeExpiredAlert {
  538. podState.registerConfiguredAlert(slot: alert.configuration.slot, alert: alert)
  539. }
  540. return CancelDeliveryResult.success(statusResponse: status, canceledDose: canceledDose)
  541. } catch PodCommsError.nonceResyncFailed {
  542. return CancelDeliveryResult.certainFailure(error: PodCommsError.nonceResyncFailed)
  543. } catch PodCommsError.rejectedMessage(let errorCode) {
  544. return CancelDeliveryResult.certainFailure(error: PodCommsError.rejectedMessage(errorCode: errorCode))
  545. } catch let error {
  546. podState.unfinalizedSuspend = UnfinalizedDose(suspendStartTime: Date(), scheduledCertainty: .uncertain)
  547. return CancelDeliveryResult.uncertainFailure(error: error as? PodCommsError ?? PodCommsError.commsError(error: error))
  548. }
  549. }
  550. // Cancels any suspend related alerts, should be called when resuming after using suspendDelivery()
  551. @discardableResult
  552. public func cancelSuspendAlerts() throws -> StatusResponse {
  553. do {
  554. let podSuspendedReminder = PodAlert.podSuspendedReminder(active: false, suspendTime: 0)
  555. let suspendTimeExpired = PodAlert.suspendTimeExpired(suspendTime: 0) // A suspendTime of 0 deactivates this alert
  556. let status = try configureAlerts([podSuspendedReminder, suspendTimeExpired])
  557. return status
  558. } catch let error {
  559. throw error
  560. }
  561. }
  562. // Cancel beeping can be done implemented using beepType (for a single delivery type) or a separate confirmation beep message block (for cancel all).
  563. // N.B., Using the built-in cancel delivery command beepType method when cancelling all insulin delivery will emit 3 different sets of cancel beeps!!!
  564. public func cancelDelivery(deliveryType: CancelDeliveryCommand.DeliveryType, beepType: BeepType = .noBeep, confirmationBeepType: BeepConfigType? = nil) -> CancelDeliveryResult {
  565. do {
  566. let cancelDeliveryCommand = CancelDeliveryCommand(nonce: podState.currentNonce, deliveryType: deliveryType, beepType: beepType)
  567. let status: StatusResponse = try send([cancelDeliveryCommand], confirmationBeepType: confirmationBeepType)
  568. let canceledDose = handleCancelDosing(deliveryType: deliveryType, bolusNotDelivered: status.bolusNotDelivered)
  569. podState.updateFromStatusResponse(status)
  570. return CancelDeliveryResult.success(statusResponse: status, canceledDose: canceledDose)
  571. } catch PodCommsError.nonceResyncFailed {
  572. return CancelDeliveryResult.certainFailure(error: PodCommsError.nonceResyncFailed)
  573. } catch PodCommsError.rejectedMessage(let errorCode) {
  574. return CancelDeliveryResult.certainFailure(error: PodCommsError.rejectedMessage(errorCode: errorCode))
  575. } catch let error {
  576. podState.unfinalizedSuspend = UnfinalizedDose(suspendStartTime: Date(), scheduledCertainty: .uncertain)
  577. return CancelDeliveryResult.uncertainFailure(error: error as? PodCommsError ?? PodCommsError.commsError(error: error))
  578. }
  579. }
  580. public func testingCommands(confirmationBeepType: BeepConfigType? = nil) throws {
  581. try cancelNone(confirmationBeepType: confirmationBeepType) // reads status & verifies nonce by doing a cancel none
  582. }
  583. public func setTime(timeZone: TimeZone, basalSchedule: BasalSchedule, date: Date, acknowledgementBeep: Bool = false, completionBeep: Bool = false) throws -> StatusResponse {
  584. let result = cancelDelivery(deliveryType: .all)
  585. switch result {
  586. case .certainFailure(let error):
  587. throw error
  588. case .uncertainFailure(let error):
  589. throw error
  590. case .success:
  591. let scheduleOffset = timeZone.scheduleOffset(forDate: date)
  592. let status = try setBasalSchedule(schedule: basalSchedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep)
  593. return status
  594. }
  595. }
  596. public func setBasalSchedule(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse {
  597. let basalScheduleCommand = SetInsulinScheduleCommand(nonce: podState.currentNonce, basalSchedule: schedule, scheduleOffset: scheduleOffset)
  598. let basalExtraCommand = BasalScheduleExtraCommand.init(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep, programReminderInterval: programReminderInterval)
  599. do {
  600. let status: StatusResponse = try send([basalScheduleCommand, basalExtraCommand])
  601. let now = Date()
  602. podState.suspendState = .resumed(now)
  603. podState.unfinalizedResume = UnfinalizedDose(resumeStartTime: now, scheduledCertainty: .certain, insulinType: podState.insulinType)
  604. podState.updateFromStatusResponse(status)
  605. return status
  606. } catch PodCommsError.nonceResyncFailed {
  607. throw PodCommsError.nonceResyncFailed
  608. } catch PodCommsError.rejectedMessage(let errorCode) {
  609. throw PodCommsError.rejectedMessage(errorCode: errorCode)
  610. } catch let error {
  611. podState.unfinalizedResume = UnfinalizedDose(resumeStartTime: Date(), scheduledCertainty: .uncertain, insulinType: podState.insulinType)
  612. throw error
  613. }
  614. }
  615. public func resumeBasal(schedule: BasalSchedule, scheduleOffset: TimeInterval, acknowledgementBeep: Bool = false, completionBeep: Bool = false, programReminderInterval: TimeInterval = 0) throws -> StatusResponse {
  616. let status = try setBasalSchedule(schedule: schedule, scheduleOffset: scheduleOffset, acknowledgementBeep: acknowledgementBeep, completionBeep: completionBeep, programReminderInterval: programReminderInterval)
  617. podState.suspendState = .resumed(Date())
  618. return status
  619. }
  620. // use cancelDelivery with .none to get status as well as to validate & advance the nonce
  621. // Throws PodCommsError
  622. @discardableResult
  623. public func cancelNone(confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse {
  624. var statusResponse: StatusResponse
  625. let cancelResult: CancelDeliveryResult = cancelDelivery(deliveryType: .none, confirmationBeepType: confirmationBeepType)
  626. switch cancelResult {
  627. case .certainFailure(let error):
  628. throw error
  629. case .uncertainFailure(let error):
  630. throw error
  631. case .success(let response, _):
  632. statusResponse = response
  633. }
  634. podState.updateFromStatusResponse(statusResponse)
  635. return statusResponse
  636. }
  637. // Throws PodCommsError
  638. @discardableResult
  639. public func getStatus(confirmationBeepType: BeepConfigType? = nil) throws -> StatusResponse {
  640. if useCancelNoneForStatus {
  641. return try cancelNone(confirmationBeepType: confirmationBeepType) // functional replacement for getStatus()
  642. }
  643. let statusResponse: StatusResponse = try send([GetStatusCommand()], confirmationBeepType: confirmationBeepType)
  644. podState.updateFromStatusResponse(statusResponse)
  645. return statusResponse
  646. }
  647. @discardableResult
  648. public func getDetailedStatus(confirmationBeepType: BeepConfigType? = nil) throws -> DetailedStatus {
  649. let infoResponse: PodInfoResponse = try send([GetStatusCommand(podInfoType: .detailedStatus)], confirmationBeepType: confirmationBeepType)
  650. guard let detailedStatus = infoResponse.podInfo as? DetailedStatus else {
  651. throw PodCommsError.unexpectedResponse(response: .podInfoResponse)
  652. }
  653. if detailedStatus.isFaulted && self.podState.fault == nil {
  654. // just detected that the pod has faulted, handle setting the fault state but don't throw
  655. handlePodFault(fault: detailedStatus)
  656. } else {
  657. podState.updateFromDetailedStatusResponse(detailedStatus)
  658. }
  659. return detailedStatus
  660. }
  661. public func finalizeFinishedDoses() {
  662. podState.finalizeFinishedDoses()
  663. }
  664. @discardableResult
  665. public func readPodInfo(podInfoResponseSubType: PodInfoResponseSubType, confirmationBeepType: BeepConfigType? = nil) throws -> PodInfoResponse {
  666. let podInfoCommand = GetStatusCommand(podInfoType: podInfoResponseSubType)
  667. let podInfoResponse: PodInfoResponse = try send([podInfoCommand], confirmationBeepType: confirmationBeepType)
  668. return podInfoResponse
  669. }
  670. // Can be called a second time to deactivate a given pod
  671. public func deactivatePod() throws {
  672. // Don't try to cancel if the pod hasn't completed its setup as it will either receive no response
  673. // (pod progress state <= 2) or creates a $31 pod fault (pod progress states 3 through 7).
  674. if podState.setupProgress == .completed && podState.fault == nil && !podState.isSuspended {
  675. let result = cancelDelivery(deliveryType: .all)
  676. switch result {
  677. case .certainFailure(let error):
  678. throw error
  679. case .uncertainFailure(let error):
  680. throw error
  681. default:
  682. break
  683. }
  684. }
  685. // if faulted read the most recent pulse log entries
  686. if podState.fault != nil {
  687. // All the dosing cleanup from the fault should have already been
  688. // handled in handlePodFault() when podState.fault was initialized.
  689. do {
  690. // read the most recent pulse log entries for later analysis, but don't throw on error
  691. try readPodInfo(podInfoResponseSubType: .pulseLogRecent)
  692. } catch let error {
  693. log.error("Read pulse log failed: %@", String(describing: error))
  694. }
  695. }
  696. do {
  697. let deactivatePod = DeactivatePodCommand(nonce: podState.currentNonce)
  698. let _: StatusResponse = try send([deactivatePod])
  699. } catch let error as PodCommsError {
  700. switch error {
  701. case .podFault, .activationTimeExceeded, .unexpectedResponse:
  702. break
  703. default:
  704. throw error
  705. }
  706. }
  707. }
  708. public func acknowledgeAlerts(alerts: AlertSet, confirmationBeepType: BeepConfigType? = nil) throws -> [AlertSlot: PodAlert] {
  709. let cmd = AcknowledgeAlertCommand(nonce: podState.currentNonce, alerts: alerts)
  710. let status: StatusResponse = try send([cmd], confirmationBeepType: confirmationBeepType)
  711. podState.updateFromStatusResponse(status)
  712. return podState.activeAlerts
  713. }
  714. func dosesForStorage(_ storageHandler: ([UnfinalizedDose]) -> Bool) {
  715. assertOnSessionQueue()
  716. let dosesToStore = podState.dosesToStore
  717. if storageHandler(dosesToStore) {
  718. log.info("Stored doses: %@", String(describing: dosesToStore))
  719. self.podState.finalizedDoses.removeAll()
  720. }
  721. }
  722. public func assertOnSessionQueue() {
  723. transport.assertOnSessionQueue()
  724. }
  725. }
  726. extension PodCommsSession: MessageTransportDelegate {
  727. func messageTransport(_ messageTransport: MessageTransport, didUpdate state: MessageTransportState) {
  728. messageTransport.assertOnSessionQueue()
  729. podState.messageTransportState = state
  730. }
  731. }