PeripheralManager+RileyLink.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. //
  2. // PeripheralManager+RileyLink.swift
  3. // xDripG5
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import CoreBluetooth
  8. import os.log
  9. protocol CBUUIDRawValue: RawRepresentable {}
  10. extension CBUUIDRawValue where RawValue == String {
  11. var cbUUID: CBUUID {
  12. return CBUUID(string: rawValue.uppercased())
  13. }
  14. }
  15. enum RileyLinkServiceUUID: String, CBUUIDRawValue {
  16. case main
  17. = "0235733B-99C5-4197-B856-69219C2A3845"
  18. case battery = "180F"
  19. case orange = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
  20. case secureDFU = "FE59"
  21. }
  22. enum MainServiceCharacteristicUUID: String, CBUUIDRawValue {
  23. case data = "C842E849-5028-42E2-867C-016ADADA9155"
  24. case responseCount = "6E6C7910-B89E-43A5-A0FE-50C5E2B81F4A"
  25. case customName = "D93B2AF0-1E28-11E4-8C21-0800200C9A66"
  26. case timerTick = "6E6C7910-B89E-43A5-78AF-50C5E2B86F7E"
  27. case firmwareVersion = "30D99DC9-7C91-4295-A051-0A104D238CF2"
  28. case ledMode = "C6D84241-F1A7-4F9C-A25F-FCE16732F14E"
  29. }
  30. enum BatteryServiceCharacteristicUUID: String, CBUUIDRawValue {
  31. case battery_level = "2A19"
  32. }
  33. enum OrangeServiceCharacteristicUUID: String, CBUUIDRawValue {
  34. case orangeRX = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
  35. case orangeTX = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
  36. }
  37. enum SecureDFUCharacteristicUUID: String, CBUUIDRawValue {
  38. case control = "8EC90001-F315-4F60-9FB8-838830DAEA50"
  39. }
  40. public enum OrangeLinkCommand: UInt8 {
  41. case yellow = 0x1
  42. case red = 0x2
  43. case off = 0x3
  44. case shake = 0x4
  45. case shakeOff = 0x5
  46. case fw_hw = 0x9
  47. }
  48. public enum OrangeLinkRequestType: UInt8 {
  49. case fctStartLoop = 0xaa // Fct_StartLoop
  50. case fctHeader = 0xbb // Fct_PutReq
  51. case fctStopLoop = 0xcc // Fct_StopLoop
  52. case cfgHeader = 0xdd // Cfg_PutReq
  53. }
  54. public enum OrangeLinkConfigurationSetting: UInt8 {
  55. case connectionLED = 0x00
  56. case connectionVibrate = 0x01
  57. }
  58. public enum RileyLinkLEDMode: UInt8 {
  59. case off = 0x00
  60. case on = 0x01
  61. case auto = 0x02
  62. }
  63. extension PeripheralManager.Configuration {
  64. static var rileyLink: PeripheralManager.Configuration {
  65. return PeripheralManager.Configuration(
  66. serviceCharacteristics: [
  67. RileyLinkServiceUUID.main.cbUUID: [
  68. MainServiceCharacteristicUUID.data.cbUUID,
  69. MainServiceCharacteristicUUID.responseCount.cbUUID,
  70. MainServiceCharacteristicUUID.customName.cbUUID,
  71. MainServiceCharacteristicUUID.timerTick.cbUUID,
  72. MainServiceCharacteristicUUID.firmwareVersion.cbUUID,
  73. MainServiceCharacteristicUUID.ledMode.cbUUID
  74. ],
  75. RileyLinkServiceUUID.battery.cbUUID: [
  76. BatteryServiceCharacteristicUUID.battery_level.cbUUID
  77. ],
  78. RileyLinkServiceUUID.orange.cbUUID: [
  79. OrangeServiceCharacteristicUUID.orangeRX.cbUUID,
  80. OrangeServiceCharacteristicUUID.orangeTX.cbUUID,
  81. ],
  82. RileyLinkServiceUUID.secureDFU.cbUUID: [
  83. SecureDFUCharacteristicUUID.control.cbUUID,
  84. ]
  85. ],
  86. notifyingCharacteristics: [
  87. RileyLinkServiceUUID.main.cbUUID: [
  88. MainServiceCharacteristicUUID.responseCount.cbUUID
  89. ],
  90. RileyLinkServiceUUID.orange.cbUUID: [
  91. OrangeServiceCharacteristicUUID.orangeTX.cbUUID,
  92. ]
  93. ],
  94. valueUpdateMacros: [
  95. // When the responseCount changes, the data characteristic should be read.
  96. MainServiceCharacteristicUUID.responseCount.cbUUID: { (manager: PeripheralManager) in
  97. log.debug("responseCount valueUpdated")
  98. guard let dataCharacteristic = manager.peripheral.getCharacteristicWithUUID(.data)
  99. else {
  100. log.debug("could not get data characteristic")
  101. return
  102. }
  103. log.debug("Reading data characteristic")
  104. manager.peripheral.readValue(for: dataCharacteristic)
  105. }
  106. ]
  107. )
  108. }
  109. }
  110. fileprivate extension CBPeripheral {
  111. func getBatteryCharacteristic(_ uuid: BatteryServiceCharacteristicUUID) -> CBCharacteristic? {
  112. guard let service = services?.itemWithUUID(RileyLinkServiceUUID.battery.cbUUID) else {
  113. return nil
  114. }
  115. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  116. }
  117. func getOrangeCharacteristic(_ uuid: OrangeServiceCharacteristicUUID) -> CBCharacteristic? {
  118. guard let service = services?.itemWithUUID(RileyLinkServiceUUID.orange.cbUUID) else {
  119. return nil
  120. }
  121. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  122. }
  123. func getCharacteristicWithUUID(_ uuid: MainServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .main) -> CBCharacteristic? {
  124. guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else {
  125. return nil
  126. }
  127. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  128. }
  129. }
  130. extension CBCentralManager {
  131. func scanForPeripherals(withOptions options: [String: Any]? = nil) {
  132. scanForPeripherals(withServices: [RileyLinkServiceUUID.main.cbUUID], options: options)
  133. }
  134. }
  135. extension Command {
  136. /// Encodes a command's data by validating and prepending its length
  137. ///
  138. /// - Returns: Writable command data
  139. /// - Throws: RileyLinkDeviceError.writeSizeLimitExceeded if the command data is too long
  140. fileprivate func writableData() throws -> Data {
  141. var data = self.data
  142. guard data.count <= 220 else {
  143. throw RileyLinkDeviceError.writeSizeLimitExceeded(maxLength: 220)
  144. }
  145. data.insert(UInt8(clamping: data.count), at: 0)
  146. return data
  147. }
  148. }
  149. private let log = OSLog(category: "PeripheralManager+RileyLink")
  150. extension PeripheralManager {
  151. static let expectedMaxBLELatency: TimeInterval = 2
  152. func readBatteryLevel(completion: @escaping (Int?) -> Void) {
  153. perform { (manager) in
  154. guard let characteristic = self.peripheral.getBatteryCharacteristic(.battery_level) else {
  155. completion(nil)
  156. return
  157. }
  158. do {
  159. guard let data = try self.readValue(for: characteristic, timeout: PeripheralManager.expectedMaxBLELatency) else {
  160. completion(nil)
  161. return
  162. }
  163. completion(Int(data[0]))
  164. } catch {
  165. completion(nil)
  166. }
  167. }
  168. }
  169. func readDiagnosticLEDMode(completion: @escaping (RileyLinkLEDMode?) -> Void) {
  170. perform { (manager) in
  171. do {
  172. guard
  173. let characteristic = self.peripheral.getCharacteristicWithUUID(.ledMode),
  174. let data = try self.readValue(for: characteristic, timeout: PeripheralManager.expectedMaxBLELatency),
  175. let mode = RileyLinkLEDMode(rawValue: data[0]) else
  176. {
  177. completion(nil)
  178. return
  179. }
  180. completion(mode)
  181. } catch {
  182. completion(nil)
  183. }
  184. }
  185. }
  186. var timerTickEnabled: Bool {
  187. return peripheral.getCharacteristicWithUUID(.timerTick)?.isNotifying ?? false
  188. }
  189. func setTimerTickEnabled(_ enabled: Bool, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
  190. perform { (manager) in
  191. do {
  192. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.timerTick) else {
  193. throw PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.timerTick.cbUUID)
  194. }
  195. try manager.setNotifyValue(enabled, for: characteristic, timeout: timeout)
  196. completion?(nil)
  197. } catch let error as PeripheralManagerError {
  198. completion?(.peripheralManagerError(error))
  199. } catch {
  200. assertionFailure()
  201. }
  202. }
  203. }
  204. func setLEDMode(mode: RileyLinkLEDMode) {
  205. perform { (manager) in
  206. do {
  207. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.ledMode) else {
  208. throw PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.ledMode.cbUUID)
  209. }
  210. let value = Data([mode.rawValue])
  211. try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  212. } catch (let error) {
  213. assertionFailure(String(describing: error))
  214. }
  215. }
  216. }
  217. func startIdleListening(idleTimeout: TimeInterval, channel: UInt8, timeout: TimeInterval = expectedMaxBLELatency, completion: @escaping (_ error: RileyLinkDeviceError?) -> Void) {
  218. perform { (manager) in
  219. let command = GetPacket(listenChannel: channel, timeoutMS: UInt32(clamping: Int(idleTimeout.milliseconds)))
  220. do {
  221. try manager.writeCommandWithoutResponse(command, timeout: timeout)
  222. completion(nil)
  223. } catch let error as RileyLinkDeviceError {
  224. completion(error)
  225. } catch {
  226. assertionFailure()
  227. }
  228. }
  229. }
  230. func setCustomName(_ name: String, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
  231. guard let value = name.data(using: .utf8) else {
  232. completion?(.errorResponse(name))
  233. return
  234. }
  235. perform { (manager) in
  236. do {
  237. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.customName) else {
  238. throw PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.customName.cbUUID)
  239. }
  240. try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
  241. completion?(nil)
  242. } catch let error as PeripheralManagerError {
  243. completion?(.peripheralManagerError(error))
  244. } catch {
  245. assertionFailure()
  246. }
  247. }
  248. }
  249. }
  250. // MARK: - Synchronous commands
  251. extension PeripheralManager {
  252. enum ResponseType {
  253. case single
  254. case buffered
  255. }
  256. /// Invokes a command expecting a response
  257. ///
  258. /// - Parameters:
  259. /// - command: The command
  260. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  261. /// - responseType: The BLE response value framing method
  262. /// - Returns: The received response
  263. /// - Throws:
  264. /// - RileyLinkDeviceError.invalidResponse
  265. /// - RileyLinkDeviceError.peripheralManagerError
  266. /// - RileyLinkDeviceError.writeSizeLimitExceeded
  267. func writeCommand<C: Command>(_ command: C, timeout: TimeInterval, responseType: ResponseType) throws -> C.ResponseType {
  268. guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else {
  269. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.data.cbUUID))
  270. }
  271. let value = try command.writableData()
  272. switch responseType {
  273. case .single:
  274. log.debug("RL Send (single): %@", value.hexadecimalString)
  275. return try writeCommand(value,
  276. for: characteristic,
  277. timeout: timeout
  278. )
  279. case .buffered:
  280. log.debug("RL Send (buffered): %@", value.hexadecimalString)
  281. return try writeLegacyCommand(value,
  282. for: characteristic,
  283. timeout: timeout,
  284. endOfResponseMarker: 0x00
  285. )
  286. }
  287. }
  288. /// Invokes a command without waiting for its response
  289. ///
  290. /// - Parameters:
  291. /// - command: The command
  292. /// - timeout: The amount of time to wait for the peripheral to confirm the write before throwing a timeout error
  293. /// - Throws:
  294. /// - RileyLinkDeviceError.invalidResponse
  295. /// - RileyLinkDeviceError.peripheralManagerError
  296. /// - RileyLinkDeviceError.writeSizeLimitExceeded
  297. fileprivate func writeCommandWithoutResponse<C: Command>(_ command: C, timeout: TimeInterval) throws {
  298. guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else {
  299. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.data.cbUUID))
  300. }
  301. let value = try command.writableData()
  302. log.debug("RL Send (no response expected): %@", value.hexadecimalString)
  303. do {
  304. try writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
  305. } catch let error as PeripheralManagerError {
  306. throw RileyLinkDeviceError.peripheralManagerError(error)
  307. }
  308. }
  309. /// - Throws:
  310. /// - RileyLinkDeviceError.invalidResponse
  311. /// - RileyLinkDeviceError.peripheralManagerError
  312. func readRadioFirmwareVersion(timeout: TimeInterval, responseType: ResponseType) throws -> String {
  313. let response = try writeCommand(GetVersion(), timeout: timeout, responseType: responseType)
  314. return response.version
  315. }
  316. /// - Throws:
  317. /// - RileyLinkDeviceError.invalidResponse
  318. /// - RileyLinkDeviceError.peripheralManagerError
  319. func readBluetoothFirmwareVersion(timeout: TimeInterval) throws -> String {
  320. guard let characteristic = peripheral.getCharacteristicWithUUID(.firmwareVersion) else {
  321. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.firmwareVersion.cbUUID))
  322. }
  323. do {
  324. guard let data = try readValue(for: characteristic, timeout: timeout) else {
  325. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.emptyValue)
  326. }
  327. guard let version = String(bytes: data, encoding: .utf8) else {
  328. throw RileyLinkDeviceError.invalidResponse(data)
  329. }
  330. return version
  331. } catch let error as PeripheralManagerError {
  332. throw RileyLinkDeviceError.peripheralManagerError(error)
  333. }
  334. }
  335. }
  336. // MARK: - Lower-level helper operations
  337. extension PeripheralManager {
  338. func setOrangeNotifyOn() throws {
  339. perform { [self] (manager) in
  340. guard let characteristicNotif = peripheral.getOrangeCharacteristic(.orangeTX) else {
  341. return
  342. }
  343. do {
  344. try setNotifyValue(true, for: characteristicNotif, timeout: 2)
  345. } catch {
  346. log.error("setOrangeNotifyOn failed: %@", error.localizedDescription)
  347. }
  348. }
  349. }
  350. func orangeAction(_ command: OrangeLinkCommand) {
  351. if command != .off, command != .shakeOff {
  352. orangeWritePwd()
  353. }
  354. perform { [self] (manager) in
  355. do {
  356. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  357. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  358. }
  359. let value = Data([OrangeLinkRequestType.fctHeader.rawValue, command.rawValue])
  360. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  361. } catch (_) {
  362. log.debug("orangeAction failed")
  363. }
  364. }
  365. if command == .off, command == .shakeOff {
  366. orangeClose()
  367. }
  368. }
  369. func findDevice() {
  370. perform { [self] (manager) in
  371. do {
  372. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  373. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  374. }
  375. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x04])
  376. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  377. } catch (_) {
  378. log.debug("findDevice failed")
  379. }
  380. }
  381. }
  382. func setOrangeConfig(_ config: OrangeLinkConfigurationSetting, isOn: Bool) {
  383. perform { [self] (manager) in
  384. do {
  385. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  386. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  387. }
  388. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x02, config.rawValue, isOn ? 1 : 0])
  389. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  390. } catch (_) {
  391. log.debug("setOrangeConfig failed")
  392. }
  393. }
  394. }
  395. func orangeWritePwd() {
  396. perform { [self] (manager) in
  397. do {
  398. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  399. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  400. }
  401. let value = Data([0xAA])
  402. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  403. } catch (_) {
  404. log.debug("orangeWritePwd failed")
  405. }
  406. }
  407. }
  408. func orangeReadSet() {
  409. perform { [self] (manager) in
  410. do {
  411. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  412. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  413. }
  414. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x01])
  415. log.debug("orangeReadSet write: %@", value.hexadecimalString)
  416. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  417. } catch (_) {
  418. log.debug("orangeReadSet failed")
  419. }
  420. }
  421. }
  422. func orangeReadVDC() {
  423. perform { [self] (manager) in
  424. do {
  425. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  426. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  427. }
  428. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x03])
  429. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  430. } catch (_) {
  431. log.debug("orangeReadVDC failed")
  432. }
  433. }
  434. }
  435. func orangeClose() {
  436. perform { [self] (manager) in
  437. do {
  438. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  439. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  440. }
  441. let value = Data([0xcc])
  442. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  443. } catch (_) {
  444. log.debug("orangeClose failed")
  445. }
  446. }
  447. }
  448. /// Writes command data expecting a single response
  449. ///
  450. /// - Parameters:
  451. /// - data: The command data
  452. /// - characteristic: The peripheral characteristic to write
  453. /// - type: The type of characteristic write
  454. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  455. /// - Returns: The recieved response
  456. /// - Throws:
  457. /// - RileyLinkDeviceError.invalidResponse
  458. /// - RileyLinkDeviceError.peripheralManagerError
  459. private func writeCommand<R: Response>(_ data: Data,
  460. for characteristic: CBCharacteristic,
  461. type: CBCharacteristicWriteType = .withResponse,
  462. timeout: TimeInterval
  463. ) throws -> R
  464. {
  465. var capturedResponse: R?
  466. do {
  467. try runCommand(timeout: timeout) {
  468. if case .withResponse = type {
  469. addCondition(.write(characteristic: characteristic))
  470. }
  471. addCondition(.valueUpdate(characteristic: characteristic, matching: { value in
  472. guard let value = value, value.count > 0 else {
  473. log.debug("Empty response from RileyLink. Continuing to listen for command response.")
  474. return false
  475. }
  476. log.debug("RL Recv(single): %@", value.hexadecimalString)
  477. guard let code = ResponseCode(rawValue: value[0]) else {
  478. let unknownCode = value[0..<1].hexadecimalString
  479. log.error("Unknown response code from RileyLink: %{public}@. Continuing to listen for command response.", unknownCode)
  480. return false
  481. }
  482. switch code {
  483. case .commandInterrupted:
  484. // This is expected in cases where an "Idle" GetPacket command is running
  485. log.debug("Idle command interrupted. Continuing to listen for command response.")
  486. return false
  487. default:
  488. guard let response = R(data: value) else {
  489. log.error("Unable to parse response: %{public}@", value.hexadecimalString)
  490. // We don't recognize the contents. Keep listening.
  491. return false
  492. }
  493. log.debug("writeCommand response: %{public}@", String(describing: response))
  494. capturedResponse = response
  495. return true
  496. }
  497. }))
  498. peripheral.writeValue(data, for: characteristic, type: type)
  499. }
  500. } catch let error as PeripheralManagerError {
  501. // If the write succeeded, but we get no response, BLE comms are working but RL command channel is hung
  502. if case .timeout(let unmetConditions) = error,
  503. let firstUnmetCondition = unmetConditions.first,
  504. case .valueUpdate = firstUnmetCondition
  505. {
  506. throw RileyLinkDeviceError.commandsBlocked
  507. } else {
  508. throw RileyLinkDeviceError.peripheralManagerError(error)
  509. }
  510. }
  511. guard let response = capturedResponse else {
  512. throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data())
  513. }
  514. return response
  515. }
  516. /// Writes command data expecting a bufferred response
  517. ///
  518. /// - Parameters:
  519. /// - data: The command data
  520. /// - characteristic: The peripheral characteristic to write
  521. /// - type: The type of characteristic write
  522. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  523. /// - endOfResponseMarker: The marker delimiting the end of a response in the buffer
  524. /// - Returns: The received response. In the event of multiple responses in the buffer, the first parsable response is returned.
  525. /// - Throws:
  526. /// - RileyLinkDeviceError.invalidResponse
  527. /// - RileyLinkDeviceError.peripheralManagerError
  528. private func writeLegacyCommand<R: Response>(_ data: Data,
  529. for characteristic: CBCharacteristic,
  530. type: CBCharacteristicWriteType = .withResponse,
  531. timeout: TimeInterval,
  532. endOfResponseMarker: UInt8
  533. ) throws -> R
  534. {
  535. var capturedResponse: R?
  536. var buffer = ResponseBuffer<R>(endMarker: endOfResponseMarker)
  537. do {
  538. try runCommand(timeout: timeout) {
  539. if case .withResponse = type {
  540. addCondition(.write(characteristic: characteristic))
  541. }
  542. addCondition(.valueUpdate(characteristic: characteristic, matching: { value in
  543. guard let value = value else {
  544. return false
  545. }
  546. log.debug("RL Recv(buffered): %@", value.hexadecimalString)
  547. buffer.append(value)
  548. for response in buffer.responses {
  549. switch response.code {
  550. case .rxTimeout, .zeroData, .invalidParam, .unknownCommand:
  551. log.debug("writeLegacyCommand response: %{public}@", String(describing: response))
  552. capturedResponse = response
  553. return true
  554. case .commandInterrupted:
  555. // This is expected in cases where an "Idle" GetPacket command is running
  556. log.debug("writeLegacyCommand response (commandInterrupted): %{public}@", String(describing: response))
  557. case .success:
  558. capturedResponse = response
  559. return true
  560. }
  561. }
  562. return false
  563. }))
  564. peripheral.writeValue(data, for: characteristic, type: type)
  565. }
  566. } catch let error as PeripheralManagerError {
  567. throw RileyLinkDeviceError.peripheralManagerError(error)
  568. }
  569. guard let response = capturedResponse else {
  570. throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data())
  571. }
  572. return response
  573. }
  574. }