PeripheralManager+RileyLink.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  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. guard let dataCharacteristic = manager.peripheral.getCharacteristicWithUUID(.data)
  98. else {
  99. return
  100. }
  101. manager.peripheral.readValue(for: dataCharacteristic)
  102. }
  103. ]
  104. )
  105. }
  106. }
  107. fileprivate extension CBPeripheral {
  108. func getBatteryCharacteristic(_ uuid: BatteryServiceCharacteristicUUID) -> CBCharacteristic? {
  109. guard let service = services?.itemWithUUID(RileyLinkServiceUUID.battery.cbUUID) else {
  110. return nil
  111. }
  112. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  113. }
  114. func getOrangeCharacteristic(_ uuid: OrangeServiceCharacteristicUUID) -> CBCharacteristic? {
  115. guard let service = services?.itemWithUUID(RileyLinkServiceUUID.orange.cbUUID) else {
  116. return nil
  117. }
  118. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  119. }
  120. func getCharacteristicWithUUID(_ uuid: MainServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .main) -> CBCharacteristic? {
  121. guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else {
  122. return nil
  123. }
  124. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  125. }
  126. }
  127. extension CBCentralManager {
  128. func scanForPeripherals(withOptions options: [String: Any]? = nil) {
  129. scanForPeripherals(withServices: [RileyLinkServiceUUID.main.cbUUID], options: options)
  130. }
  131. }
  132. extension Command {
  133. /// Encodes a command's data by validating and prepending its length
  134. ///
  135. /// - Returns: Writable command data
  136. /// - Throws: RileyLinkDeviceError.writeSizeLimitExceeded if the command data is too long
  137. fileprivate func writableData() throws -> Data {
  138. var data = self.data
  139. guard data.count <= 220 else {
  140. throw RileyLinkDeviceError.writeSizeLimitExceeded(maxLength: 220)
  141. }
  142. data.insert(UInt8(clamping: data.count), at: 0)
  143. return data
  144. }
  145. }
  146. private let log = OSLog(category: "PeripheralManager+RileyLink")
  147. extension PeripheralManager {
  148. static let expectedMaxBLELatency: TimeInterval = 2
  149. func readBatteryLevel(completion: @escaping (Int?) -> Void) {
  150. perform { (manager) in
  151. guard let characteristic = self.peripheral.getBatteryCharacteristic(.battery_level) else {
  152. completion(nil)
  153. return
  154. }
  155. do {
  156. guard let data = try self.readValue(for: characteristic, timeout: PeripheralManager.expectedMaxBLELatency) else {
  157. completion(nil)
  158. return
  159. }
  160. completion(Int(data[0]))
  161. } catch {
  162. completion(nil)
  163. }
  164. }
  165. }
  166. func readDiagnosticLEDMode(completion: @escaping (RileyLinkLEDMode?) -> Void) {
  167. perform { (manager) in
  168. do {
  169. guard
  170. let characteristic = self.peripheral.getCharacteristicWithUUID(.ledMode),
  171. let data = try self.readValue(for: characteristic, timeout: PeripheralManager.expectedMaxBLELatency),
  172. let mode = RileyLinkLEDMode(rawValue: data[0]) else
  173. {
  174. completion(nil)
  175. return
  176. }
  177. completion(mode)
  178. } catch {
  179. completion(nil)
  180. }
  181. }
  182. }
  183. var timerTickEnabled: Bool {
  184. return peripheral.getCharacteristicWithUUID(.timerTick)?.isNotifying ?? false
  185. }
  186. func setTimerTickEnabled(_ enabled: Bool, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
  187. perform { (manager) in
  188. do {
  189. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.timerTick) else {
  190. throw PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.timerTick.cbUUID)
  191. }
  192. try manager.setNotifyValue(enabled, for: characteristic, timeout: timeout)
  193. completion?(nil)
  194. } catch let error as PeripheralManagerError {
  195. completion?(.peripheralManagerError(error))
  196. } catch {
  197. assertionFailure()
  198. }
  199. }
  200. }
  201. func setLEDMode(mode: RileyLinkLEDMode) {
  202. perform { (manager) in
  203. do {
  204. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.ledMode) else {
  205. throw PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.ledMode.cbUUID)
  206. }
  207. let value = Data([mode.rawValue])
  208. try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  209. } catch (let error) {
  210. assertionFailure(String(describing: error))
  211. }
  212. }
  213. }
  214. func startIdleListening(idleTimeout: TimeInterval, channel: UInt8, timeout: TimeInterval = expectedMaxBLELatency, completion: @escaping (_ error: RileyLinkDeviceError?) -> Void) {
  215. perform { (manager) in
  216. let command = GetPacket(listenChannel: channel, timeoutMS: UInt32(clamping: Int(idleTimeout.milliseconds)))
  217. do {
  218. try manager.writeCommandWithoutResponse(command, timeout: timeout)
  219. completion(nil)
  220. } catch let error as RileyLinkDeviceError {
  221. completion(error)
  222. } catch {
  223. assertionFailure()
  224. }
  225. }
  226. }
  227. func setCustomName(_ name: String, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
  228. guard let value = name.data(using: .utf8) else {
  229. completion?(.invalidInput(name))
  230. return
  231. }
  232. perform { (manager) in
  233. do {
  234. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.customName) else {
  235. throw PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.customName.cbUUID)
  236. }
  237. try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
  238. completion?(nil)
  239. } catch let error as PeripheralManagerError {
  240. completion?(.peripheralManagerError(error))
  241. } catch {
  242. assertionFailure()
  243. }
  244. }
  245. }
  246. }
  247. // MARK: - Synchronous commands
  248. extension PeripheralManager {
  249. enum ResponseType {
  250. case single
  251. case buffered
  252. }
  253. /// Invokes a command expecting a response
  254. ///
  255. /// - Parameters:
  256. /// - command: The command
  257. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  258. /// - responseType: The BLE response value framing method
  259. /// - Returns: The received response
  260. /// - Throws:
  261. /// - RileyLinkDeviceError.invalidResponse
  262. /// - RileyLinkDeviceError.peripheralManagerError
  263. /// - RileyLinkDeviceError.writeSizeLimitExceeded
  264. func writeCommand<C: Command>(_ command: C, timeout: TimeInterval, responseType: ResponseType) throws -> C.ResponseType {
  265. guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else {
  266. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.data.cbUUID))
  267. }
  268. let value = try command.writableData()
  269. switch responseType {
  270. case .single:
  271. log.debug("RL Send (single): %@", value.hexadecimalString)
  272. return try writeCommand(value,
  273. for: characteristic,
  274. timeout: timeout
  275. )
  276. case .buffered:
  277. log.debug("RL Send (buffered): %@", value.hexadecimalString)
  278. return try writeLegacyCommand(value,
  279. for: characteristic,
  280. timeout: timeout,
  281. endOfResponseMarker: 0x00
  282. )
  283. }
  284. }
  285. /// Invokes a command without waiting for its response
  286. ///
  287. /// - Parameters:
  288. /// - command: The command
  289. /// - timeout: The amount of time to wait for the peripheral to confirm the write before throwing a timeout error
  290. /// - Throws:
  291. /// - RileyLinkDeviceError.invalidResponse
  292. /// - RileyLinkDeviceError.peripheralManagerError
  293. /// - RileyLinkDeviceError.writeSizeLimitExceeded
  294. fileprivate func writeCommandWithoutResponse<C: Command>(_ command: C, timeout: TimeInterval) throws {
  295. guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else {
  296. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.data.cbUUID))
  297. }
  298. let value = try command.writableData()
  299. log.debug("RL Send (no response expected): %@", value.hexadecimalString)
  300. do {
  301. try writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
  302. } catch let error as PeripheralManagerError {
  303. throw RileyLinkDeviceError.peripheralManagerError(error)
  304. }
  305. }
  306. /// - Throws:
  307. /// - RileyLinkDeviceError.invalidResponse
  308. /// - RileyLinkDeviceError.peripheralManagerError
  309. func readRadioFirmwareVersion(timeout: TimeInterval, responseType: ResponseType) throws -> String {
  310. let response = try writeCommand(GetVersion(), timeout: timeout, responseType: responseType)
  311. return response.version
  312. }
  313. /// - Throws:
  314. /// - RileyLinkDeviceError.invalidResponse
  315. /// - RileyLinkDeviceError.peripheralManagerError
  316. func readBluetoothFirmwareVersion(timeout: TimeInterval) throws -> String {
  317. guard let characteristic = peripheral.getCharacteristicWithUUID(.firmwareVersion) else {
  318. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.unknownCharacteristic(MainServiceCharacteristicUUID.firmwareVersion.cbUUID))
  319. }
  320. do {
  321. guard let data = try readValue(for: characteristic, timeout: timeout) else {
  322. throw RileyLinkDeviceError.peripheralManagerError(PeripheralManagerError.emptyValue)
  323. }
  324. guard let version = String(bytes: data, encoding: .utf8) else {
  325. throw RileyLinkDeviceError.invalidResponse(data)
  326. }
  327. return version
  328. } catch let error as PeripheralManagerError {
  329. throw RileyLinkDeviceError.peripheralManagerError(error)
  330. }
  331. }
  332. }
  333. // MARK: - Lower-level helper operations
  334. extension PeripheralManager {
  335. func setOrangeNotifyOn() throws {
  336. perform { [self] (manager) in
  337. guard let characteristicNotif = peripheral.getOrangeCharacteristic(.orangeTX) else {
  338. return
  339. }
  340. do {
  341. try setNotifyValue(true, for: characteristicNotif, timeout: 2)
  342. } catch {
  343. log.error("setOrangeNotifyOn failed: %@", error.localizedDescription)
  344. }
  345. }
  346. }
  347. func orangeAction(_ command: OrangeLinkCommand) {
  348. if command != .off, command != .shakeOff {
  349. orangeWritePwd()
  350. }
  351. perform { [self] (manager) in
  352. do {
  353. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  354. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  355. }
  356. let value = Data([OrangeLinkRequestType.fctHeader.rawValue, command.rawValue])
  357. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  358. } catch (_) {
  359. log.debug("orangeAction failed")
  360. }
  361. }
  362. if command == .off, command == .shakeOff {
  363. orangeClose()
  364. }
  365. }
  366. func findDevice() {
  367. perform { [self] (manager) in
  368. do {
  369. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  370. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  371. }
  372. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x04])
  373. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  374. } catch (_) {
  375. log.debug("findDevice failed")
  376. }
  377. }
  378. }
  379. func setOrangeConfig(_ config: OrangeLinkConfigurationSetting, isOn: Bool) {
  380. perform { [self] (manager) in
  381. do {
  382. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  383. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  384. }
  385. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x02, config.rawValue, isOn ? 1 : 0])
  386. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  387. } catch (_) {
  388. log.debug("setOrangeConfig failed")
  389. }
  390. }
  391. }
  392. func orangeWritePwd() {
  393. perform { [self] (manager) in
  394. do {
  395. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  396. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  397. }
  398. let value = Data([0xAA])
  399. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  400. } catch (_) {
  401. log.debug("orangeWritePwd failed")
  402. }
  403. }
  404. }
  405. func orangeReadSet() {
  406. perform { [self] (manager) in
  407. do {
  408. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  409. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  410. }
  411. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x01])
  412. log.debug("orangeReadSet write: %@", value.hexadecimalString)
  413. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  414. } catch (_) {
  415. log.debug("orangeReadSet failed")
  416. }
  417. }
  418. }
  419. func orangeReadVDC() {
  420. perform { [self] (manager) in
  421. do {
  422. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  423. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  424. }
  425. let value = Data([OrangeLinkRequestType.cfgHeader.rawValue, 0x03])
  426. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  427. } catch (_) {
  428. log.debug("orangeReadVDC failed")
  429. }
  430. }
  431. }
  432. func orangeClose() {
  433. perform { [self] (manager) in
  434. do {
  435. guard let characteristic = peripheral.getOrangeCharacteristic(.orangeRX) else {
  436. throw PeripheralManagerError.unknownCharacteristic(OrangeServiceCharacteristicUUID.orangeRX.cbUUID)
  437. }
  438. let value = Data([0xcc])
  439. try writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  440. } catch (_) {
  441. log.debug("orangeClose failed")
  442. }
  443. }
  444. }
  445. /// Writes command data expecting a single response
  446. ///
  447. /// - Parameters:
  448. /// - data: The command data
  449. /// - characteristic: The peripheral characteristic to write
  450. /// - type: The type of characteristic write
  451. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  452. /// - Returns: The recieved response
  453. /// - Throws:
  454. /// - RileyLinkDeviceError.invalidResponse
  455. /// - RileyLinkDeviceError.peripheralManagerError
  456. private func writeCommand<R: Response>(_ data: Data,
  457. for characteristic: CBCharacteristic,
  458. type: CBCharacteristicWriteType = .withResponse,
  459. timeout: TimeInterval
  460. ) throws -> R
  461. {
  462. var capturedResponse: R?
  463. do {
  464. try runCommand(timeout: timeout) {
  465. if case .withResponse = type {
  466. addCondition(.write(characteristic: characteristic))
  467. }
  468. addCondition(.valueUpdate(characteristic: characteristic, matching: { value in
  469. guard let value = value, value.count > 0 else {
  470. log.debug("Empty response from RileyLink. Continuing to listen for command response.")
  471. return false
  472. }
  473. log.debug("RL Recv(single): %@", value.hexadecimalString)
  474. guard let code = ResponseCode(rawValue: value[0]) else {
  475. let unknownCode = value[0..<1].hexadecimalString
  476. log.error("Unknown response code from RileyLink: %{public}@. Continuing to listen for command response.", unknownCode)
  477. return false
  478. }
  479. switch code {
  480. case .commandInterrupted:
  481. // This is expected in cases where an "Idle" GetPacket command is running
  482. log.debug("Idle command interrupted. Continuing to listen for command response.")
  483. return false
  484. default:
  485. guard let response = R(data: value) else {
  486. log.error("Unable to parse response: %{public}@", value.hexadecimalString)
  487. // We don't recognize the contents. Keep listening.
  488. return false
  489. }
  490. log.debug("RileyLink response: %{public}@", String(describing: response))
  491. capturedResponse = response
  492. return true
  493. }
  494. }))
  495. peripheral.writeValue(data, for: characteristic, type: type)
  496. }
  497. } catch let error as PeripheralManagerError {
  498. // If the write succeeded, but we get no response, BLE comms are working but RL command channel is hung
  499. if case .timeout(let unmetConditions) = error,
  500. let firstUnmetCondition = unmetConditions.first,
  501. case .valueUpdate = firstUnmetCondition
  502. {
  503. throw RileyLinkDeviceError.commandsBlocked
  504. } else {
  505. throw RileyLinkDeviceError.peripheralManagerError(error)
  506. }
  507. }
  508. guard let response = capturedResponse else {
  509. throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data())
  510. }
  511. return response
  512. }
  513. /// Writes command data expecting a bufferred response
  514. ///
  515. /// - Parameters:
  516. /// - data: The command data
  517. /// - characteristic: The peripheral characteristic to write
  518. /// - type: The type of characteristic write
  519. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  520. /// - endOfResponseMarker: The marker delimiting the end of a response in the buffer
  521. /// - Returns: The received response. In the event of multiple responses in the buffer, the first parsable response is returned.
  522. /// - Throws:
  523. /// - RileyLinkDeviceError.invalidResponse
  524. /// - RileyLinkDeviceError.peripheralManagerError
  525. private func writeLegacyCommand<R: Response>(_ data: Data,
  526. for characteristic: CBCharacteristic,
  527. type: CBCharacteristicWriteType = .withResponse,
  528. timeout: TimeInterval,
  529. endOfResponseMarker: UInt8
  530. ) throws -> R
  531. {
  532. var capturedResponse: R?
  533. var buffer = ResponseBuffer<R>(endMarker: endOfResponseMarker)
  534. do {
  535. try runCommand(timeout: timeout) {
  536. if case .withResponse = type {
  537. addCondition(.write(characteristic: characteristic))
  538. }
  539. addCondition(.valueUpdate(characteristic: characteristic, matching: { value in
  540. guard let value = value else {
  541. return false
  542. }
  543. log.debug("RL Recv(buffered): %@", value.hexadecimalString)
  544. buffer.append(value)
  545. for response in buffer.responses {
  546. switch response.code {
  547. case .rxTimeout, .zeroData, .invalidParam, .unknownCommand:
  548. log.debug("RileyLink response: %{public}@", String(describing: response))
  549. capturedResponse = response
  550. return true
  551. case .commandInterrupted:
  552. // This is expected in cases where an "Idle" GetPacket command is running
  553. log.debug("RileyLink response: %{public}@", String(describing: response))
  554. case .success:
  555. capturedResponse = response
  556. return true
  557. }
  558. }
  559. return false
  560. }))
  561. peripheral.writeValue(data, for: characteristic, type: type)
  562. }
  563. } catch let error as PeripheralManagerError {
  564. throw RileyLinkDeviceError.peripheralManagerError(error)
  565. }
  566. guard let response = capturedResponse else {
  567. throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data())
  568. }
  569. return response
  570. }
  571. }