PodCommsSession.swift 47 KB

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