PeripheralManager+RileyLink.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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)
  13. }
  14. }
  15. enum RileyLinkServiceUUID: String, CBUUIDRawValue {
  16. case main = "0235733B-99C5-4197-B856-69219C2A3845"
  17. }
  18. enum MainServiceCharacteristicUUID: String, CBUUIDRawValue {
  19. case data = "C842E849-5028-42E2-867C-016ADADA9155"
  20. case responseCount = "6E6C7910-B89E-43A5-A0FE-50C5E2B81F4A"
  21. case customName = "D93B2AF0-1E28-11E4-8C21-0800200C9A66"
  22. case timerTick = "6E6C7910-B89E-43A5-78AF-50C5E2B86F7E"
  23. case firmwareVersion = "30D99DC9-7C91-4295-A051-0A104D238CF2"
  24. case ledMode = "C6D84241-F1A7-4F9C-A25F-FCE16732F14E"
  25. }
  26. enum RileyLinkLEDMode: UInt8 {
  27. case off = 0x00
  28. case on = 0x01
  29. case auto = 0x02
  30. }
  31. extension PeripheralManager.Configuration {
  32. static var rileyLink: PeripheralManager.Configuration {
  33. return PeripheralManager.Configuration(
  34. serviceCharacteristics: [
  35. RileyLinkServiceUUID.main.cbUUID: [
  36. MainServiceCharacteristicUUID.data.cbUUID,
  37. MainServiceCharacteristicUUID.responseCount.cbUUID,
  38. MainServiceCharacteristicUUID.customName.cbUUID,
  39. MainServiceCharacteristicUUID.timerTick.cbUUID,
  40. MainServiceCharacteristicUUID.firmwareVersion.cbUUID,
  41. MainServiceCharacteristicUUID.ledMode.cbUUID
  42. ]
  43. ],
  44. notifyingCharacteristics: [
  45. RileyLinkServiceUUID.main.cbUUID: [
  46. MainServiceCharacteristicUUID.responseCount.cbUUID
  47. // TODO: Should timer tick default to on?
  48. ]
  49. ],
  50. valueUpdateMacros: [
  51. // When the responseCount changes, the data characteristic should be read.
  52. MainServiceCharacteristicUUID.responseCount.cbUUID: { (manager: PeripheralManager) in
  53. guard let dataCharacteristic = manager.peripheral.getCharacteristicWithUUID(.data)
  54. else {
  55. return
  56. }
  57. manager.peripheral.readValue(for: dataCharacteristic)
  58. }
  59. ]
  60. )
  61. }
  62. }
  63. fileprivate extension CBPeripheral {
  64. func getCharacteristicWithUUID(_ uuid: MainServiceCharacteristicUUID, serviceUUID: RileyLinkServiceUUID = .main) -> CBCharacteristic? {
  65. guard let service = services?.itemWithUUID(serviceUUID.cbUUID) else {
  66. return nil
  67. }
  68. return service.characteristics?.itemWithUUID(uuid.cbUUID)
  69. }
  70. }
  71. extension CBCentralManager {
  72. func scanForPeripherals(withOptions options: [String: Any]? = nil) {
  73. scanForPeripherals(withServices: [RileyLinkServiceUUID.main.cbUUID], options: options)
  74. }
  75. }
  76. extension Command {
  77. /// Encodes a command's data by validating and prepending its length
  78. ///
  79. /// - Returns: Writable command data
  80. /// - Throws: RileyLinkDeviceError.writeSizeLimitExceeded if the command data is too long
  81. fileprivate func writableData() throws -> Data {
  82. var data = self.data
  83. guard data.count <= 220 else {
  84. throw RileyLinkDeviceError.writeSizeLimitExceeded(maxLength: 220)
  85. }
  86. data.insert(UInt8(clamping: data.count), at: 0)
  87. return data
  88. }
  89. }
  90. private let log = OSLog(category: "PeripheralManager+RileyLink")
  91. extension PeripheralManager {
  92. static let expectedMaxBLELatency: TimeInterval = 2
  93. var timerTickEnabled: Bool {
  94. return peripheral.getCharacteristicWithUUID(.timerTick)?.isNotifying ?? false
  95. }
  96. func setTimerTickEnabled(_ enabled: Bool, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
  97. perform { (manager) in
  98. do {
  99. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.timerTick) else {
  100. throw PeripheralManagerError.unknownCharacteristic
  101. }
  102. try manager.setNotifyValue(enabled, for: characteristic, timeout: timeout)
  103. completion?(nil)
  104. } catch let error as PeripheralManagerError {
  105. completion?(.peripheralManagerError(error))
  106. } catch {
  107. assertionFailure()
  108. }
  109. }
  110. }
  111. func setLEDMode(mode: RileyLinkLEDMode) {
  112. perform { (manager) in
  113. do {
  114. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.ledMode) else {
  115. throw PeripheralManagerError.unknownCharacteristic
  116. }
  117. let value = Data([mode.rawValue])
  118. try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: PeripheralManager.expectedMaxBLELatency)
  119. } catch (let error) {
  120. assertionFailure(String(describing: error))
  121. }
  122. }
  123. }
  124. func startIdleListening(idleTimeout: TimeInterval, channel: UInt8, timeout: TimeInterval = expectedMaxBLELatency, completion: @escaping (_ error: RileyLinkDeviceError?) -> Void) {
  125. perform { (manager) in
  126. let command = GetPacket(listenChannel: channel, timeoutMS: UInt32(clamping: Int(idleTimeout.milliseconds)))
  127. do {
  128. try manager.writeCommandWithoutResponse(command, timeout: timeout)
  129. completion(nil)
  130. } catch let error as RileyLinkDeviceError {
  131. completion(error)
  132. } catch {
  133. assertionFailure()
  134. }
  135. }
  136. }
  137. func setCustomName(_ name: String, timeout: TimeInterval = expectedMaxBLELatency, completion: ((_ error: RileyLinkDeviceError?) -> Void)? = nil) {
  138. guard let value = name.data(using: .utf8) else {
  139. completion?(.invalidInput(name))
  140. return
  141. }
  142. perform { (manager) in
  143. do {
  144. guard let characteristic = manager.peripheral.getCharacteristicWithUUID(.customName) else {
  145. throw PeripheralManagerError.unknownCharacteristic
  146. }
  147. try manager.writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
  148. completion?(nil)
  149. } catch let error as PeripheralManagerError {
  150. completion?(.peripheralManagerError(error))
  151. } catch {
  152. assertionFailure()
  153. }
  154. }
  155. }
  156. }
  157. // MARK: - Synchronous commands
  158. extension PeripheralManager {
  159. enum ResponseType {
  160. case single
  161. case buffered
  162. }
  163. /// Invokes a command expecting a response
  164. ///
  165. /// - Parameters:
  166. /// - command: The command
  167. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  168. /// - responseType: The BLE response value framing method
  169. /// - Returns: The received response
  170. /// - Throws:
  171. /// - RileyLinkDeviceError.invalidResponse
  172. /// - RileyLinkDeviceError.peripheralManagerError
  173. /// - RileyLinkDeviceError.writeSizeLimitExceeded
  174. func writeCommand<C: Command>(_ command: C, timeout: TimeInterval, responseType: ResponseType) throws -> C.ResponseType {
  175. guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else {
  176. throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic)
  177. }
  178. let value = try command.writableData()
  179. switch responseType {
  180. case .single:
  181. log.debug("RL Send (single): %@", value.hexadecimalString)
  182. return try writeCommand(value,
  183. for: characteristic,
  184. timeout: timeout
  185. )
  186. case .buffered:
  187. log.debug("RL Send (buffered): %@", value.hexadecimalString)
  188. return try writeLegacyCommand(value,
  189. for: characteristic,
  190. timeout: timeout,
  191. endOfResponseMarker: 0x00
  192. )
  193. }
  194. }
  195. /// Invokes a command without waiting for its response
  196. ///
  197. /// - Parameters:
  198. /// - command: The command
  199. /// - timeout: The amount of time to wait for the peripheral to confirm the write before throwing a timeout error
  200. /// - Throws:
  201. /// - RileyLinkDeviceError.invalidResponse
  202. /// - RileyLinkDeviceError.peripheralManagerError
  203. /// - RileyLinkDeviceError.writeSizeLimitExceeded
  204. fileprivate func writeCommandWithoutResponse<C: Command>(_ command: C, timeout: TimeInterval) throws {
  205. guard let characteristic = peripheral.getCharacteristicWithUUID(.data) else {
  206. throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic)
  207. }
  208. let value = try command.writableData()
  209. log.debug("RL Send (no response expected): %@", value.hexadecimalString)
  210. do {
  211. try writeValue(value, for: characteristic, type: .withResponse, timeout: timeout)
  212. } catch let error as PeripheralManagerError {
  213. throw RileyLinkDeviceError.peripheralManagerError(error)
  214. }
  215. }
  216. /// - Throws:
  217. /// - RileyLinkDeviceError.invalidResponse
  218. /// - RileyLinkDeviceError.peripheralManagerError
  219. func readRadioFirmwareVersion(timeout: TimeInterval, responseType: ResponseType) throws -> String {
  220. let response = try writeCommand(GetVersion(), timeout: timeout, responseType: responseType)
  221. return response.version
  222. }
  223. /// - Throws:
  224. /// - RileyLinkDeviceError.invalidResponse
  225. /// - RileyLinkDeviceError.peripheralManagerError
  226. func readBluetoothFirmwareVersion(timeout: TimeInterval) throws -> String {
  227. guard let characteristic = peripheral.getCharacteristicWithUUID(.firmwareVersion) else {
  228. throw RileyLinkDeviceError.peripheralManagerError(.unknownCharacteristic)
  229. }
  230. do {
  231. guard let data = try readValue(for: characteristic, timeout: timeout) else {
  232. // TODO: This is an "unknown value" issue, not a timeout
  233. throw RileyLinkDeviceError.peripheralManagerError(.timeout)
  234. }
  235. guard let version = String(bytes: data, encoding: .utf8) else {
  236. throw RileyLinkDeviceError.invalidResponse(data)
  237. }
  238. return version
  239. } catch let error as PeripheralManagerError {
  240. throw RileyLinkDeviceError.peripheralManagerError(error)
  241. }
  242. }
  243. }
  244. // MARK: - Lower-level helper operations
  245. extension PeripheralManager {
  246. /// Writes command data expecting a single response
  247. ///
  248. /// - Parameters:
  249. /// - data: The command data
  250. /// - characteristic: The peripheral characteristic to write
  251. /// - type: The type of characteristic write
  252. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  253. /// - Returns: The recieved response
  254. /// - Throws:
  255. /// - RileyLinkDeviceError.invalidResponse
  256. /// - RileyLinkDeviceError.peripheralManagerError
  257. private func writeCommand<R: Response>(_ data: Data,
  258. for characteristic: CBCharacteristic,
  259. type: CBCharacteristicWriteType = .withResponse,
  260. timeout: TimeInterval
  261. ) throws -> R
  262. {
  263. var capturedResponse: R?
  264. do {
  265. try runCommand(timeout: timeout) {
  266. if case .withResponse = type {
  267. addCondition(.write(characteristic: characteristic))
  268. }
  269. addCondition(.valueUpdate(characteristic: characteristic, matching: { value in
  270. guard let value = value, value.count > 0 else {
  271. log.debug("Empty response from RileyLink. Continuing to listen for command response.")
  272. return false
  273. }
  274. log.debug("RL Recv(single): %@", value.hexadecimalString)
  275. guard let code = ResponseCode(rawValue: value[0]) else {
  276. let unknownCode = value[0..<1].hexadecimalString
  277. log.error("Unknown response code from RileyLink: %{public}@. Continuing to listen for command response.", unknownCode)
  278. return false
  279. }
  280. switch code {
  281. case .commandInterrupted:
  282. // This is expected in cases where an "Idle" GetPacket command is running
  283. log.debug("Idle command interrupted. Continuing to listen for command response.")
  284. return false
  285. default:
  286. guard let response = R(data: value) else {
  287. log.error("Unable to parse response.")
  288. // We don't recognize the contents. Keep listening.
  289. return false
  290. }
  291. log.debug("RileyLink response: %{public}@", String(describing: response))
  292. capturedResponse = response
  293. return true
  294. }
  295. }))
  296. peripheral.writeValue(data, for: characteristic, type: type)
  297. }
  298. } catch let error as PeripheralManagerError {
  299. throw RileyLinkDeviceError.peripheralManagerError(error)
  300. }
  301. guard let response = capturedResponse else {
  302. throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data())
  303. }
  304. return response
  305. }
  306. /// Writes command data expecting a bufferred response
  307. ///
  308. /// - Parameters:
  309. /// - data: The command data
  310. /// - characteristic: The peripheral characteristic to write
  311. /// - type: The type of characteristic write
  312. /// - timeout: The amount of time to wait for the peripheral to respond before throwing a timeout error
  313. /// - endOfResponseMarker: The marker delimiting the end of a response in the buffer
  314. /// - Returns: The received response. In the event of multiple responses in the buffer, the first parsable response is returned.
  315. /// - Throws:
  316. /// - RileyLinkDeviceError.invalidResponse
  317. /// - RileyLinkDeviceError.peripheralManagerError
  318. private func writeLegacyCommand<R: Response>(_ data: Data,
  319. for characteristic: CBCharacteristic,
  320. type: CBCharacteristicWriteType = .withResponse,
  321. timeout: TimeInterval,
  322. endOfResponseMarker: UInt8
  323. ) throws -> R
  324. {
  325. var capturedResponse: R?
  326. var buffer = ResponseBuffer<R>(endMarker: endOfResponseMarker)
  327. do {
  328. try runCommand(timeout: timeout) {
  329. if case .withResponse = type {
  330. addCondition(.write(characteristic: characteristic))
  331. }
  332. addCondition(.valueUpdate(characteristic: characteristic, matching: { value in
  333. guard let value = value else {
  334. return false
  335. }
  336. log.debug("RL Recv(buffered): %@", value.hexadecimalString)
  337. buffer.append(value)
  338. for response in buffer.responses {
  339. switch response.code {
  340. case .rxTimeout, .zeroData, .invalidParam, .unknownCommand:
  341. log.debug("RileyLink response: %{public}@", String(describing: response))
  342. capturedResponse = response
  343. return true
  344. case .commandInterrupted:
  345. // This is expected in cases where an "Idle" GetPacket command is running
  346. log.debug("RileyLink response: %{public}@", String(describing: response))
  347. case .success:
  348. capturedResponse = response
  349. return true
  350. }
  351. }
  352. return false
  353. }))
  354. peripheral.writeValue(data, for: characteristic, type: type)
  355. }
  356. } catch let error as PeripheralManagerError {
  357. throw RileyLinkDeviceError.peripheralManagerError(error)
  358. }
  359. guard let response = capturedResponse else {
  360. throw RileyLinkDeviceError.invalidResponse(characteristic.value ?? Data())
  361. }
  362. return response
  363. }
  364. }