PodComms.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. //
  2. // PodComms.swift
  3. // OmniKit
  4. //
  5. // Created by Pete Schwamb on 10/7/17.
  6. // Copyright © 2017 Pete Schwamb. All rights reserved.
  7. //
  8. import Foundation
  9. import RileyLinkBLEKit
  10. import LoopKit
  11. import os.log
  12. fileprivate var diagnosePairingRssi = false
  13. protocol PodCommsDelegate: AnyObject {
  14. func podComms(_ podComms: PodComms, didChange podState: PodState)
  15. }
  16. class PodComms: CustomDebugStringConvertible {
  17. private let configuredDevices: Locked<Set<RileyLinkDevice>> = Locked(Set())
  18. weak var delegate: PodCommsDelegate?
  19. weak var messageLogger: MessageLogger?
  20. public let log = OSLog(category: "PodComms")
  21. private var startingPacketNumber = 0
  22. // Only valid to access on the session serial queue
  23. private var podState: PodState? {
  24. didSet {
  25. if let newValue = podState, newValue != oldValue {
  26. //log.debug("Notifying delegate of new podState: %{public}@", String(reflecting: newValue))
  27. delegate?.podComms(self, didChange: newValue)
  28. }
  29. }
  30. }
  31. init(podState: PodState?) {
  32. self.podState = podState
  33. self.delegate = nil
  34. self.messageLogger = nil
  35. }
  36. var insulinType: InsulinType? {
  37. get { podState?.insulinType }
  38. set {
  39. if let insulinType = newValue {
  40. podState?.insulinType = insulinType
  41. }
  42. }
  43. }
  44. /// Handles all the common work to send and verify the version response for the two pairing commands, AssignAddress and SetupPod.
  45. /// Has side effects of creating pod state, assigning startingPacketNumber, and updating pod state.
  46. ///
  47. /// - parameter address: Address being assigned to the pod
  48. /// - parameter transport: PodMessageTransport used to send messages
  49. /// - parameter message: Message to send; must be an AssignAddress or SetupPod
  50. ///
  51. /// - returns: The VersionResponse from the pod
  52. ///
  53. /// - Throws:
  54. /// - PodCommsError.noResponse
  55. /// - PodCommsError.podAckedInsteadOfReturningResponse
  56. /// - PodCommsError.unexpectedPacketType
  57. /// - PodCommsError.emptyResponse
  58. /// - PodCommsError.unexpectedResponse
  59. /// - PodCommsError.podChange
  60. /// - PodCommsError.activationTimeExceeded
  61. /// - PodCommsError.rssiTooLow
  62. /// - PodCommsError.rssiTooHigh
  63. /// - PodCommsError.diagnosticMessage
  64. /// - PodCommsError.podIncompatible
  65. /// - MessageError.invalidCrc
  66. /// - MessageError.invalidSequence
  67. /// - MessageError.invalidAddress
  68. /// - RileyLinkDeviceError
  69. private func sendPairMessage(address: UInt32, transport: PodMessageTransport, message: Message, insulinType: InsulinType) throws -> VersionResponse {
  70. defer {
  71. log.debug("sendPairMessage saving current transport packet #%d", transport.packetNumber)
  72. if self.podState != nil {
  73. self.podState!.messageTransportState = MessageTransportState(packetNumber: transport.packetNumber, messageNumber: transport.messageNumber)
  74. } else {
  75. self.startingPacketNumber = transport.packetNumber
  76. }
  77. }
  78. var didRetry = false
  79. var rssiRetries = 2
  80. while true {
  81. let response: Message
  82. do {
  83. response = try transport.sendMessage(message)
  84. } catch let error {
  85. if let podCommsError = error as? PodCommsError {
  86. switch podCommsError {
  87. // These errors can happen some times when the responses are not seen for a long
  88. // enough time. Automatically retrying using the already incremented packet # can
  89. // clear this condition without requiring any user interaction for a pairing failure.
  90. case .podAckedInsteadOfReturningResponse, .noResponse:
  91. if didRetry == false {
  92. didRetry = true
  93. log.debug("sendPairMessage to retry using updated packet #%d", transport.packetNumber)
  94. continue // the transport packet # is already advanced for the retry
  95. }
  96. default:
  97. break
  98. }
  99. }
  100. throw error
  101. }
  102. if let fault = response.fault {
  103. log.error("Pod Fault: %{public}@", String(describing: fault))
  104. if let podState = self.podState, podState.fault == nil {
  105. self.podState!.fault = fault
  106. }
  107. throw PodCommsError.podFault(fault: fault)
  108. }
  109. guard let config = response.messageBlocks[0] as? VersionResponse else {
  110. log.error("sendPairMessage unexpected response: %{public}@", String(describing: response))
  111. let responseType = response.messageBlocks[0].blockType
  112. throw PodCommsError.unexpectedResponse(response: responseType)
  113. }
  114. guard config.address == address else {
  115. log.error("sendPairMessage unexpected address return of %{public}@ instead of expected %{public}@",
  116. String(format: "04X", config.address), String(format: "%04X", address))
  117. throw PodCommsError.invalidAddress(address: config.address, expectedAddress: address)
  118. }
  119. // If we previously had podState, verify that we are still dealing with the same pod
  120. if let podState = self.podState, (podState.lot != config.lot || podState.tid != config.tid) {
  121. // Have a new pod, could be a pod change w/o deactivation (or we're picking up some other pairing pod!)
  122. log.error("Received pod response for [lot %u tid %u], expected [lot %u tid %u]", config.lot, config.tid, podState.lot, podState.tid)
  123. throw PodCommsError.podChange
  124. }
  125. // Check the pod RSSI
  126. let maxRssiAllowed = 59 // maximum RSSI limit allowed
  127. let minRssiAllowed = 30 // minimum RSSI limit allowed
  128. if let rssi = config.rssi, let gain = config.gain {
  129. let rssiStr = String(format: "RSSI: %u.\nReceiver Low Gain: %u", rssi, gain)
  130. log.default("%@", rssiStr)
  131. if diagnosePairingRssi {
  132. throw PodCommsError.diagnosticMessage(str: rssiStr)
  133. }
  134. rssiRetries -= 1
  135. if rssi < minRssiAllowed {
  136. log.default("RSSI value %d is less than minimum allowed value of %d, %d retries left", rssi, minRssiAllowed, rssiRetries)
  137. if rssiRetries > 0 {
  138. continue
  139. }
  140. throw PodCommsError.rssiTooLow
  141. }
  142. if rssi > maxRssiAllowed {
  143. log.default("RSSI value %d is more than maximum allowed value of %d, %d retries left", rssi, maxRssiAllowed, rssiRetries)
  144. if rssiRetries > 0 {
  145. continue
  146. }
  147. throw PodCommsError.rssiTooHigh
  148. }
  149. }
  150. if self.podState == nil {
  151. log.default("Creating PodState for address %{public}@ [lot %u tid %u], packet #%d, message #%d", String(format: "%04X", config.address), config.lot, config.tid, transport.packetNumber, transport.messageNumber)
  152. self.podState = PodState(
  153. address: config.address,
  154. piVersion: String(describing: config.piVersion),
  155. pmVersion: String(describing: config.pmVersion),
  156. lot: config.lot,
  157. tid: config.tid,
  158. packetNumber: transport.packetNumber,
  159. messageNumber: transport.messageNumber,
  160. insulinType: insulinType
  161. )
  162. // podState setupProgress state should be addressAssigned
  163. }
  164. // Now that we have podState, check for an activation timeout condition that can be noted in setupProgress
  165. guard config.podProgressStatus != .activationTimeExceeded else {
  166. // The 2 hour window for the initial pairing has expired
  167. self.podState?.setupProgress = .activationTimeout
  168. throw PodCommsError.activationTimeExceeded
  169. }
  170. // It's unlikely that Insulet will release an updated Eros pod using any different fundemental values,
  171. // so just verify that the fundemental pod constants returned match the expected constant values in the Pod struct.
  172. // To actually be able to handle different fundemental values in Loop things would need to be reworked to save
  173. // these values in some persistent PodState and then make sure that everything properly works using these values.
  174. var errorStrings: [String] = []
  175. if let pulseSize = config.pulseSize, pulseSize != Pod.pulseSize {
  176. errorStrings.append(String(format: "Pod reported pulse size of %.3fU different than expected %.3fU", pulseSize, Pod.pulseSize))
  177. }
  178. if let secondsPerBolusPulse = config.secondsPerBolusPulse, secondsPerBolusPulse != Pod.secondsPerBolusPulse {
  179. errorStrings.append(String(format: "Pod reported seconds per pulse rate of %.1f different than expected %.1f", secondsPerBolusPulse, Pod.secondsPerBolusPulse))
  180. }
  181. if let secondsPerPrimePulse = config.secondsPerPrimePulse, secondsPerPrimePulse != Pod.secondsPerPrimePulse {
  182. errorStrings.append(String(format: "Pod reported seconds per prime pulse rate of %.1f different than expected %.1f", secondsPerPrimePulse, Pod.secondsPerPrimePulse))
  183. }
  184. if let primeUnits = config.primeUnits, primeUnits != Pod.primeUnits {
  185. errorStrings.append(String(format: "Pod reported prime bolus of %.2fU different than expected %.2fU", primeUnits, Pod.primeUnits))
  186. }
  187. if let cannulaInsertionUnits = config.cannulaInsertionUnits, Pod.cannulaInsertionUnits != cannulaInsertionUnits {
  188. errorStrings.append(String(format: "Pod reported cannula insertion bolus of %.2fU different than expected %.2fU", cannulaInsertionUnits, Pod.cannulaInsertionUnits))
  189. }
  190. if let serviceDuration = config.serviceDuration {
  191. if serviceDuration < Pod.serviceDuration {
  192. errorStrings.append(String(format: "Pod reported service duration of %.0f hours shorter than expected %.0f", serviceDuration.hours, Pod.serviceDuration.hours))
  193. } else if serviceDuration > Pod.serviceDuration {
  194. log.info("Pod reported service duration of %.0f hours limited to expected %.0f", serviceDuration.hours, Pod.serviceDuration.hours)
  195. }
  196. }
  197. let errMess = errorStrings.joined(separator: ".\n")
  198. if errMess.isEmpty == false {
  199. log.error("%@", errMess)
  200. self.podState?.setupProgress = .podIncompatible
  201. throw PodCommsError.podIncompatible(str: errMess)
  202. }
  203. if config.podProgressStatus == .pairingCompleted && self.podState?.setupProgress.isPaired == false {
  204. log.info("Version Response %{public}@ indicates pairing is now complete", String(describing: config))
  205. self.podState?.setupProgress = .podPaired
  206. }
  207. return config
  208. }
  209. }
  210. private func assignAddress(address: UInt32, commandSession: CommandSession, insulinType: InsulinType) throws {
  211. commandSession.assertOnSessionQueue()
  212. let packetNumber, messageNumber: Int
  213. if let podState = self.podState {
  214. packetNumber = podState.messageTransportState.packetNumber
  215. messageNumber = podState.messageTransportState.messageNumber
  216. } else {
  217. packetNumber = self.startingPacketNumber
  218. messageNumber = 0
  219. }
  220. log.debug("Attempting pairing with address %{public}@ using packet #%d", String(format: "%04X", address), packetNumber)
  221. let messageTransportState = MessageTransportState(packetNumber: packetNumber, messageNumber: messageNumber)
  222. let transport = PodMessageTransport(session: commandSession, address: 0xffffffff, ackAddress: address, state: messageTransportState)
  223. transport.messageLogger = messageLogger
  224. // Create the Assign Address command message
  225. let assignAddress = AssignAddressCommand(address: address)
  226. let message = Message(address: 0xffffffff, messageBlocks: [assignAddress], sequenceNum: transport.messageNumber)
  227. _ = try sendPairMessage(address: address, transport: transport, message: message, insulinType: insulinType)
  228. }
  229. private func setupPod(podState: PodState, timeZone: TimeZone, commandSession: CommandSession, insulinType: InsulinType) throws {
  230. commandSession.assertOnSessionQueue()
  231. let transport = PodMessageTransport(session: commandSession, address: 0xffffffff, ackAddress: podState.address, state: podState.messageTransportState)
  232. transport.messageLogger = messageLogger
  233. let dateComponents = SetupPodCommand.dateComponents(date: Date(), timeZone: timeZone)
  234. let setupPod = SetupPodCommand(address: podState.address, dateComponents: dateComponents, lot: podState.lot, tid: podState.tid)
  235. let message = Message(address: 0xffffffff, messageBlocks: [setupPod], sequenceNum: transport.messageNumber)
  236. let versionResponse: VersionResponse
  237. do {
  238. versionResponse = try sendPairMessage(address: podState.address, transport: transport, message: message, insulinType: insulinType)
  239. } catch let error {
  240. if case PodCommsError.podAckedInsteadOfReturningResponse = error {
  241. log.default("SetupPod acked instead of returning response.")
  242. if self.podState?.setupProgress.isPaired == false {
  243. log.default("Moving pod to paired state.")
  244. self.podState?.setupProgress = .podPaired
  245. }
  246. return
  247. }
  248. log.error("SetupPod returns error %{public}@", String(describing: error))
  249. throw error
  250. }
  251. guard versionResponse.isSetupPodVersionResponse else {
  252. log.error("SetupPod unexpected VersionResponse type: %{public}@", String(describing: versionResponse))
  253. throw PodCommsError.invalidData
  254. }
  255. }
  256. func assignAddressAndSetupPod(
  257. address: UInt32,
  258. using deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void,
  259. timeZone: TimeZone,
  260. messageLogger: MessageLogger?,
  261. insulinType: InsulinType,
  262. _ block: @escaping (_ result: SessionRunResult) -> Void)
  263. {
  264. deviceSelector { (device) in
  265. guard let device = device else {
  266. block(.failure(PodCommsError.noRileyLinkAvailable))
  267. return
  268. }
  269. device.runSession(withName: "Pair Pod") { (commandSession) in
  270. do {
  271. self.configureDevice(device, with: commandSession)
  272. if self.podState == nil {
  273. try self.assignAddress(address: address, commandSession: commandSession, insulinType: insulinType)
  274. }
  275. guard self.podState != nil else {
  276. block(.failure(PodCommsError.noPodPaired))
  277. return
  278. }
  279. if self.podState!.setupProgress.isPaired == false {
  280. try self.setupPod(podState: self.podState!, timeZone: timeZone, commandSession: commandSession, insulinType: insulinType)
  281. }
  282. guard self.podState!.setupProgress.isPaired else {
  283. self.log.error("Unexpected podStatus setupProgress value of %{public}@", String(describing: self.podState!.setupProgress))
  284. throw PodCommsError.invalidData
  285. }
  286. self.startingPacketNumber = 0
  287. // Run a session now for any post-pairing commands
  288. let transport = PodMessageTransport(session: commandSession, address: self.podState!.address, state: self.podState!.messageTransportState)
  289. transport.messageLogger = self.messageLogger
  290. let podSession = PodCommsSession(podState: self.podState!, transport: transport, delegate: self)
  291. block(.success(session: podSession))
  292. } catch let error as PodCommsError {
  293. block(.failure(error))
  294. } catch {
  295. block(.failure(PodCommsError.commsError(error: error)))
  296. }
  297. }
  298. }
  299. }
  300. enum SessionRunResult {
  301. case success(session: PodCommsSession)
  302. case failure(PodCommsError)
  303. }
  304. func runSession(withName name: String, using deviceSelector: @escaping (_ completion: @escaping (_ device: RileyLinkDevice?) -> Void) -> Void, _ block: @escaping (_ result: SessionRunResult) -> Void) {
  305. deviceSelector { (device) in
  306. guard let device = device else {
  307. block(.failure(PodCommsError.noRileyLinkAvailable))
  308. return
  309. }
  310. device.runSession(withName: name) { (commandSession) in
  311. guard self.podState != nil else {
  312. block(.failure(PodCommsError.noPodPaired))
  313. return
  314. }
  315. self.configureDevice(device, with: commandSession)
  316. let transport = PodMessageTransport(session: commandSession, address: self.podState!.address, state: self.podState!.messageTransportState)
  317. transport.messageLogger = self.messageLogger
  318. let podSession = PodCommsSession(podState: self.podState!, transport: transport, delegate: self)
  319. block(.success(session: podSession))
  320. }
  321. }
  322. }
  323. // Must be called from within the RileyLinkDevice sessionQueue
  324. private func configureDevice(_ device: RileyLinkDevice, with session: CommandSession) {
  325. session.assertOnSessionQueue()
  326. guard !self.configuredDevices.value.contains(device) else {
  327. return
  328. }
  329. do {
  330. log.debug("configureRadio (omnipod)")
  331. _ = try session.configureRadio()
  332. } catch let error {
  333. log.error("configure Radio failed with error: %{public}@", String(describing: error))
  334. // Ignore the error and let the block run anyway
  335. return
  336. }
  337. NotificationCenter.default.post(name: .DeviceRadioConfigDidChange, object: device)
  338. NotificationCenter.default.addObserver(self, selector: #selector(deviceRadioConfigDidChange(_:)), name: .DeviceRadioConfigDidChange, object: device)
  339. NotificationCenter.default.addObserver(self, selector: #selector(deviceRadioConfigDidChange(_:)), name: .DeviceConnectionStateDidChange, object: device)
  340. log.debug("added device %{public}@ to configuredDevices", device.name ?? "unknown")
  341. _ = configuredDevices.mutate { (value) in
  342. value.insert(device)
  343. }
  344. }
  345. @objc private func deviceRadioConfigDidChange(_ note: Notification) {
  346. guard let device = note.object as? RileyLinkDevice else {
  347. return
  348. }
  349. log.debug("removing device %{public}@ from configuredDevices", device.name ?? "unknown")
  350. NotificationCenter.default.removeObserver(self, name: .DeviceRadioConfigDidChange, object: device)
  351. NotificationCenter.default.removeObserver(self, name: .DeviceConnectionStateDidChange, object: device)
  352. _ = configuredDevices.mutate { (value) in
  353. value.remove(device)
  354. }
  355. }
  356. // MARK: - CustomDebugStringConvertible
  357. var debugDescription: String {
  358. return [
  359. "## PodComms",
  360. "podState: \(String(reflecting: podState))",
  361. "configuredDevices: \(configuredDevices.value.map { $0.peripheralIdentifier.uuidString })",
  362. "delegate: \(String(describing: delegate != nil))",
  363. ""
  364. ].joined(separator: "\n")
  365. }
  366. }
  367. extension PodComms: PodCommsSessionDelegate {
  368. func podCommsSession(_ podCommsSession: PodCommsSession, didChange state: PodState) {
  369. podCommsSession.assertOnSessionQueue()
  370. self.podState = state
  371. }
  372. }
  373. private extension CommandSession {
  374. func configureRadio() throws {
  375. // SYNC1 |0xDF00|0x54|Sync Word, High Byte
  376. // SYNC0 |0xDF01|0xC3|Sync Word, Low Byte
  377. // PKTLEN |0xDF02|0x32|Packet Length
  378. // PKTCTRL1 |0xDF03|0x24|Packet Automation Control
  379. // PKTCTRL0 |0xDF04|0x00|Packet Automation Control
  380. // FSCTRL1 |0xDF07|0x06|Frequency Synthesizer Control
  381. // FREQ2 |0xDF09|0x12|Frequency Control Word, High Byte
  382. // FREQ1 |0xDF0A|0x14|Frequency Control Word, Middle Byte
  383. // FREQ0 |0xDF0B|0x5F|Frequency Control Word, Low Byte
  384. // MDMCFG4 |0xDF0C|0xCA|Modem configuration
  385. // MDMCFG3 |0xDF0D|0xBC|Modem Configuration
  386. // MDMCFG2 |0xDF0E|0x0A|Modem Configuration
  387. // MDMCFG1 |0xDF0F|0x13|Modem Configuration
  388. // MDMCFG0 |0xDF10|0x11|Modem Configuration
  389. // MCSM0 |0xDF14|0x18|Main Radio Control State Machine Configuration
  390. // FOCCFG |0xDF15|0x17|Frequency Offset Compensation Configuration
  391. // AGCCTRL1 |0xDF18|0x70|AGC Control
  392. // FSCAL3 |0xDF1C|0xE9|Frequency Synthesizer Calibration
  393. // FSCAL2 |0xDF1D|0x2A|Frequency Synthesizer Calibration
  394. // FSCAL1 |0xDF1E|0x00|Frequency Synthesizer Calibration
  395. // FSCAL0 |0xDF1F|0x1F|Frequency Synthesizer Calibration
  396. // TEST1 |0xDF24|0x31|Various Test Settings
  397. // TEST0 |0xDF25|0x09|Various Test Settings
  398. // PA_TABLE0 |0xDF2E|0x60|PA Power Setting 0
  399. // VERSION |0xDF37|0x04|Chip ID[7:0]
  400. try setSoftwareEncoding(.manchester)
  401. try setPreamble(0x6665)
  402. try setBaseFrequency(Measurement(value: 433.91, unit: .megahertz))
  403. try updateRegister(.pktctrl1, value: 0x20)
  404. try updateRegister(.pktctrl0, value: 0x00)
  405. try updateRegister(.fsctrl1, value: 0x06)
  406. try updateRegister(.mdmcfg4, value: 0xCA)
  407. try updateRegister(.mdmcfg3, value: 0xBC) // 0xBB for next lower bitrate
  408. try updateRegister(.mdmcfg2, value: 0x06)
  409. try updateRegister(.mdmcfg1, value: 0x70)
  410. try updateRegister(.mdmcfg0, value: 0x11)
  411. try updateRegister(.deviatn, value: 0x44)
  412. try updateRegister(.mcsm0, value: 0x18)
  413. try updateRegister(.foccfg, value: 0x17)
  414. try updateRegister(.fscal3, value: 0xE9)
  415. try updateRegister(.fscal2, value: 0x2A)
  416. try updateRegister(.fscal1, value: 0x00)
  417. try updateRegister(.fscal0, value: 0x1F)
  418. try updateRegister(.test1, value: 0x31)
  419. try updateRegister(.test0, value: 0x09)
  420. try updateRegister(.paTable0, value: 0x84)
  421. try updateRegister(.sync1, value: 0xA5)
  422. try updateRegister(.sync0, value: 0x5A)
  423. }
  424. // This is just a testing function for spoofing PDM packets, or other times when you need to generate a custom packet
  425. private func sendPacket() throws {
  426. let packetNumber = 19
  427. let messageNumber = 0x24 >> 2
  428. let address: UInt32 = 0x1f0b3554
  429. let cmd = GetStatusCommand(podInfoType: .normal)
  430. let message = Message(address: address, messageBlocks: [cmd], sequenceNum: messageNumber)
  431. var dataRemaining = message.encoded()
  432. let sendPacket = Packet(address: address, packetType: .pdm, sequenceNum: packetNumber, data: dataRemaining)
  433. dataRemaining = dataRemaining.subdata(in: sendPacket.data.count..<dataRemaining.count)
  434. let _ = try sendAndListen(sendPacket.encoded(), repeatCount: 0, timeout: .milliseconds(333), retryCount: 0, preambleExtension: .milliseconds(127))
  435. throw PodCommsError.emptyResponse
  436. }
  437. }