RileyLinkDevice.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. //
  2. // RileyLinkDevice.swift
  3. // RileyLinkBLEKit
  4. //
  5. // Copyright © 2017 Pete Schwamb. All rights reserved.
  6. //
  7. import CoreBluetooth
  8. import os.log
  9. /// TODO: Should we be tracking the most recent "pump" RSSI?
  10. public class RileyLinkDevice {
  11. let manager: PeripheralManager
  12. private let log = OSLog(category: "RileyLinkDevice")
  13. // Confined to `manager.queue`
  14. private var bleFirmwareVersion: BLEFirmwareVersion?
  15. // Confined to `manager.queue`
  16. private var radioFirmwareVersion: RadioFirmwareVersion?
  17. // Confined to `lock`
  18. private var idleListeningState: IdleListeningState = .disabled
  19. // Confined to `lock`
  20. private var lastIdle: Date?
  21. // Confined to `lock`
  22. // TODO: Tidy up this state/preference machine
  23. private var isIdleListeningPending = false
  24. // Confined to `lock`
  25. private var isTimerTickEnabled = true
  26. /// Serializes access to device state
  27. private var lock = os_unfair_lock()
  28. /// The queue used to serialize sessions and observe when they've drained
  29. private let sessionQueue: OperationQueue = {
  30. let queue = OperationQueue()
  31. queue.name = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.sessionQueue"
  32. queue.maxConcurrentOperationCount = 1
  33. return queue
  34. }()
  35. private var sessionQueueOperationCountObserver: NSKeyValueObservation!
  36. init(peripheralManager: PeripheralManager) {
  37. self.manager = peripheralManager
  38. sessionQueue.underlyingQueue = peripheralManager.queue
  39. peripheralManager.delegate = self
  40. sessionQueueOperationCountObserver = sessionQueue.observe(\.operationCount, options: [.new]) { [weak self] (queue, change) in
  41. if let newValue = change.newValue, newValue == 0 {
  42. self?.log.debug("Session queue operation count is now empty")
  43. self?.assertIdleListening(forceRestart: true)
  44. }
  45. }
  46. }
  47. }
  48. // MARK: - Peripheral operations. Thread-safe.
  49. extension RileyLinkDevice {
  50. public var name: String? {
  51. return manager.peripheral.name
  52. }
  53. public var deviceURI: String {
  54. return "rileylink://\(name ?? peripheralIdentifier.uuidString)"
  55. }
  56. public var peripheralIdentifier: UUID {
  57. return manager.peripheral.identifier
  58. }
  59. public var peripheralState: CBPeripheralState {
  60. return manager.peripheral.state
  61. }
  62. public func readRSSI() {
  63. guard case .connected = manager.peripheral.state, case .poweredOn? = manager.central?.state else {
  64. return
  65. }
  66. manager.peripheral.readRSSI()
  67. }
  68. public func setCustomName(_ name: String) {
  69. manager.setCustomName(name)
  70. }
  71. public func enableBLELEDs() {
  72. manager.setLEDMode(mode: .on)
  73. }
  74. /// Asserts that the caller is currently on the session queue
  75. public func assertOnSessionQueue() {
  76. dispatchPrecondition(condition: .onQueue(manager.queue))
  77. }
  78. /// Schedules a closure to execute on the session queue after a specified time
  79. ///
  80. /// - Parameters:
  81. /// - deadline: The time after which to execute
  82. /// - execute: The closure to execute
  83. public func sessionQueueAsyncAfter(deadline: DispatchTime, execute: @escaping () -> Void) {
  84. manager.queue.asyncAfter(deadline: deadline, execute: execute)
  85. }
  86. }
  87. extension RileyLinkDevice: Equatable, Hashable {
  88. public static func ==(lhs: RileyLinkDevice, rhs: RileyLinkDevice) -> Bool {
  89. return lhs === rhs
  90. }
  91. public func hash(into hasher: inout Hasher) {
  92. hasher.combine(peripheralIdentifier)
  93. }
  94. }
  95. // MARK: - Status management
  96. extension RileyLinkDevice {
  97. public struct Status {
  98. public let lastIdle: Date?
  99. public let name: String?
  100. public let bleFirmwareVersion: BLEFirmwareVersion?
  101. public let radioFirmwareVersion: RadioFirmwareVersion?
  102. }
  103. public func getStatus(_ completion: @escaping (_ status: Status) -> Void) {
  104. os_unfair_lock_lock(&lock)
  105. let lastIdle = self.lastIdle
  106. os_unfair_lock_unlock(&lock)
  107. self.manager.queue.async {
  108. completion(Status(
  109. lastIdle: lastIdle,
  110. name: self.name,
  111. bleFirmwareVersion: self.bleFirmwareVersion,
  112. radioFirmwareVersion: self.radioFirmwareVersion
  113. ))
  114. }
  115. }
  116. }
  117. // MARK: - Command session management
  118. extension RileyLinkDevice {
  119. public func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void) {
  120. sessionQueue.addOperation(manager.configureAndRun({ [weak self] (manager) in
  121. self?.log.default("======================== %{public}@ ===========================", name)
  122. let bleFirmwareVersion = self?.bleFirmwareVersion
  123. let radioFirmwareVersion = self?.radioFirmwareVersion
  124. if bleFirmwareVersion == nil || radioFirmwareVersion == nil {
  125. self?.log.error("Running session with incomplete configuration: bleFirmwareVersion %{public}@, radioFirmwareVersion: %{public}@", String(describing: bleFirmwareVersion), String(describing: radioFirmwareVersion))
  126. }
  127. block(CommandSession(manager: manager, responseType: bleFirmwareVersion?.responseType ?? .buffered, firmwareVersion: radioFirmwareVersion ?? .unknown))
  128. self?.log.default("------------------------ %{public}@ ---------------------------", name)
  129. }))
  130. }
  131. }
  132. // MARK: - Idle management
  133. extension RileyLinkDevice {
  134. public enum IdleListeningState {
  135. case enabled(timeout: TimeInterval, channel: UInt8)
  136. case disabled
  137. }
  138. func setIdleListeningState(_ state: IdleListeningState) {
  139. os_unfair_lock_lock(&lock)
  140. let oldValue = idleListeningState
  141. idleListeningState = state
  142. os_unfair_lock_unlock(&lock)
  143. switch (oldValue, state) {
  144. case (.disabled, .enabled):
  145. assertIdleListening(forceRestart: true)
  146. case (.enabled, .enabled):
  147. assertIdleListening(forceRestart: false)
  148. default:
  149. break
  150. }
  151. }
  152. public func assertIdleListening(forceRestart: Bool = false) {
  153. os_unfair_lock_lock(&lock)
  154. guard case .enabled(timeout: let timeout, channel: let channel) = self.idleListeningState else {
  155. os_unfair_lock_unlock(&lock)
  156. return
  157. }
  158. guard case .connected = self.manager.peripheral.state, case .poweredOn? = self.manager.central?.state else {
  159. os_unfair_lock_unlock(&lock)
  160. return
  161. }
  162. guard forceRestart || (self.lastIdle ?? .distantPast).timeIntervalSinceNow < -timeout else {
  163. os_unfair_lock_unlock(&lock)
  164. return
  165. }
  166. guard !self.isIdleListeningPending else {
  167. os_unfair_lock_unlock(&lock)
  168. return
  169. }
  170. self.isIdleListeningPending = true
  171. os_unfair_lock_unlock(&lock)
  172. self.log.debug("Enqueuing idle listening")
  173. self.manager.startIdleListening(idleTimeout: timeout, channel: channel) { (error) in
  174. os_unfair_lock_lock(&self.lock)
  175. self.isIdleListeningPending = false
  176. if let error = error {
  177. self.log.error("Unable to start idle listening: %@", String(describing: error))
  178. os_unfair_lock_unlock(&self.lock)
  179. } else {
  180. self.lastIdle = Date()
  181. os_unfair_lock_unlock(&self.lock)
  182. NotificationCenter.default.post(name: .DeviceDidStartIdle, object: self)
  183. }
  184. }
  185. }
  186. }
  187. // MARK: - Timer tick management
  188. extension RileyLinkDevice {
  189. func setTimerTickEnabled(_ enabled: Bool) {
  190. os_unfair_lock_lock(&lock)
  191. self.isTimerTickEnabled = enabled
  192. os_unfair_lock_unlock(&lock)
  193. self.assertTimerTick()
  194. }
  195. func assertTimerTick() {
  196. os_unfair_lock_lock(&self.lock)
  197. let isTimerTickEnabled = self.isTimerTickEnabled
  198. os_unfair_lock_unlock(&self.lock)
  199. if isTimerTickEnabled != self.manager.timerTickEnabled {
  200. self.manager.setTimerTickEnabled(isTimerTickEnabled)
  201. }
  202. }
  203. }
  204. // MARK: - CBCentralManagerDelegate Proxying
  205. extension RileyLinkDevice {
  206. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  207. if case .poweredOn = central.state {
  208. assertIdleListening(forceRestart: false)
  209. assertTimerTick()
  210. }
  211. manager.centralManagerDidUpdateState(central)
  212. }
  213. func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  214. log.debug("didConnect %@", peripheral)
  215. if case .connected = peripheral.state {
  216. assertIdleListening(forceRestart: false)
  217. assertTimerTick()
  218. }
  219. manager.centralManager(central, didConnect: peripheral)
  220. NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
  221. }
  222. func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
  223. log.debug("didDisconnectPeripheral %@", peripheral)
  224. NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
  225. }
  226. func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  227. log.debug("didFailToConnect %@", peripheral)
  228. NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
  229. }
  230. }
  231. extension RileyLinkDevice: PeripheralManagerDelegate {
  232. // This is called from the central's queue
  233. func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) {
  234. log.debug("Did UpdateValueFor %@", characteristic)
  235. switch MainServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) {
  236. case .data?:
  237. guard let value = characteristic.value, value.count > 0 else {
  238. return
  239. }
  240. self.manager.queue.async {
  241. if let responseType = self.bleFirmwareVersion?.responseType {
  242. let response: PacketResponse?
  243. switch responseType {
  244. case .buffered:
  245. var buffer = ResponseBuffer<PacketResponse>(endMarker: 0x00)
  246. buffer.append(value)
  247. response = buffer.responses.last
  248. case .single:
  249. response = PacketResponse(data: value)
  250. }
  251. if let response = response {
  252. switch response.code {
  253. case .rxTimeout, .commandInterrupted, .zeroData, .invalidParam, .unknownCommand:
  254. self.log.debug("Idle error received: %@", String(describing: response.code))
  255. case .success:
  256. if let packet = response.packet {
  257. self.log.debug("Idle packet received: %@", value.hexadecimalString)
  258. NotificationCenter.default.post(
  259. name: .DevicePacketReceived,
  260. object: self,
  261. userInfo: [RileyLinkDevice.notificationPacketKey: packet]
  262. )
  263. }
  264. }
  265. } else {
  266. self.log.error("Unknown idle response: %@", value.hexadecimalString)
  267. }
  268. } else {
  269. self.log.error("Skipping parsing characteristic value update due to missing BLE firmware version")
  270. }
  271. self.assertIdleListening(forceRestart: true)
  272. }
  273. case .responseCount?:
  274. // PeripheralManager.Configuration.valueUpdateMacros is responsible for handling this response.
  275. break
  276. case .timerTick?:
  277. NotificationCenter.default.post(name: .DeviceTimerDidTick, object: self)
  278. assertIdleListening(forceRestart: false)
  279. case .customName?, .firmwareVersion?, .ledMode?, .none:
  280. break
  281. }
  282. }
  283. func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) {
  284. NotificationCenter.default.post(
  285. name: .DeviceRSSIDidChange,
  286. object: self,
  287. userInfo: [RileyLinkDevice.notificationRSSIKey: RSSI]
  288. )
  289. }
  290. func peripheralManagerDidUpdateName(_ manager: PeripheralManager) {
  291. NotificationCenter.default.post(
  292. name: .DeviceNameDidChange,
  293. object: self,
  294. userInfo: nil
  295. )
  296. }
  297. func completeConfiguration(for manager: PeripheralManager) throws {
  298. // Read bluetooth version to determine compatibility
  299. log.default("Reading firmware versions for PeripheralManager configuration")
  300. let bleVersionString = try manager.readBluetoothFirmwareVersion(timeout: 1)
  301. bleFirmwareVersion = BLEFirmwareVersion(versionString: bleVersionString)
  302. let radioVersionString = try manager.readRadioFirmwareVersion(timeout: 1, responseType: bleFirmwareVersion?.responseType ?? .buffered)
  303. radioFirmwareVersion = RadioFirmwareVersion(versionString: radioVersionString)
  304. }
  305. }
  306. extension RileyLinkDevice: CustomDebugStringConvertible {
  307. public var debugDescription: String {
  308. os_unfair_lock_lock(&lock)
  309. let lastIdle = self.lastIdle
  310. let isIdleListeningPending = self.isIdleListeningPending
  311. let isTimerTickEnabled = self.isTimerTickEnabled
  312. os_unfair_lock_unlock(&lock)
  313. return [
  314. "## RileyLinkDevice",
  315. "* name: \(name ?? "")",
  316. "* lastIdle: \(lastIdle ?? .distantPast)",
  317. "* isIdleListeningPending: \(isIdleListeningPending)",
  318. "* isTimerTickEnabled: \(isTimerTickEnabled)",
  319. "* isTimerTickNotifying: \(manager.timerTickEnabled)",
  320. "* radioFirmware: \(String(describing: radioFirmwareVersion))",
  321. "* bleFirmware: \(String(describing: bleFirmwareVersion))",
  322. "* peripheralManager: \(manager)",
  323. "* sessionQueue.operationCount: \(sessionQueue.operationCount)"
  324. ].joined(separator: "\n")
  325. }
  326. }
  327. extension RileyLinkDevice {
  328. public static let notificationPacketKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationPacket"
  329. public static let notificationRSSIKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationRSSI"
  330. }
  331. extension Notification.Name {
  332. public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.ConnectionStateDidChange")
  333. public static let DeviceDidStartIdle = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DidStartIdle")
  334. public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.NameDidChange")
  335. public static let DevicePacketReceived = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.PacketReceived")
  336. public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.RSSIDidChange")
  337. public static let DeviceTimerDidTick = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.TimerTickDidChange")
  338. }