RileyLinkDevice.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. //
  2. // RileyLinkDevice.swift
  3. // RileyLinkBLEKit
  4. //
  5. // Copyright © 2017 Pete Schwamb. All rights reserved.
  6. //
  7. import CoreBluetooth
  8. import os.log
  9. public enum RileyLinkHardwareType {
  10. case riley
  11. case orange
  12. case ema
  13. var monitorsBattery: Bool {
  14. if self == .riley {
  15. return false
  16. }
  17. return true
  18. }
  19. }
  20. /// TODO: Should we be tracking the most recent "pump" RSSI?
  21. public class RileyLinkDevice {
  22. let manager: PeripheralManager
  23. private let log = OSLog(category: "RileyLinkDevice")
  24. // Confined to `manager.queue`
  25. private var bleFirmwareVersion: BLEFirmwareVersion?
  26. // Confined to `manager.queue`
  27. private var radioFirmwareVersion: RadioFirmwareVersion?
  28. public var rlFirmwareDescription: String {
  29. let versions = [radioFirmwareVersion, bleFirmwareVersion].compactMap { (version: CustomStringConvertible?) -> String? in
  30. if let version = version {
  31. return String(describing: version)
  32. } else {
  33. return nil
  34. }
  35. }
  36. return versions.joined(separator: " / ")
  37. }
  38. private var version: String {
  39. switch hardwareType {
  40. case .riley, .ema, .none:
  41. return rlFirmwareDescription
  42. case .orange:
  43. return orangeLinkFirmwareHardwareVersion
  44. }
  45. }
  46. // Confined to `lock`
  47. private var idleListeningState: IdleListeningState = .disabled
  48. // Confined to `lock`
  49. private var lastIdle: Date?
  50. // Confined to `lock`
  51. // TODO: Tidy up this state/preference machine
  52. private var isIdleListeningPending = false
  53. // Confined to `lock`
  54. private var isTimerTickEnabled = true
  55. /// Serializes access to device state
  56. private var lock = os_unfair_lock()
  57. private var orangeLinkFirmwareHardwareVersion = "v1.x"
  58. private var orangeLinkHardwareVersionMajorMinor: [Int]?
  59. private var ledOn: Bool = false
  60. private var vibrationOn: Bool = false
  61. private var voltage: Float?
  62. private var batteryLevel: Int?
  63. private var hasPiezo: Bool {
  64. if let olHW = orangeLinkHardwareVersionMajorMinor, olHW[0] == 1, olHW[1] >= 1 {
  65. return true
  66. } else if let olHW = orangeLinkHardwareVersionMajorMinor, olHW[0] == 2, olHW[1] == 6 {
  67. return true
  68. }
  69. return false
  70. }
  71. public var hasOrangeLinkService: Bool {
  72. return self.manager.peripheral.services?.itemWithUUID(RileyLinkServiceUUID.orange.cbUUID) != nil
  73. }
  74. public var hardwareType: RileyLinkHardwareType? {
  75. guard let services = self.manager.peripheral.services else {
  76. return nil
  77. }
  78. guard let bleComponents = self.bleFirmwareVersion else {
  79. return nil
  80. }
  81. if services.itemWithUUID(RileyLinkServiceUUID.secureDFU.cbUUID) != nil {
  82. return .orange
  83. } else if bleComponents.components[0] == 3 {
  84. // this returns true for riley with ema firmware, but that is OK
  85. return .ema
  86. } else {
  87. // as long as riley ble remains at 2.x with ema at 3.x this will work
  88. return .riley
  89. }
  90. }
  91. /// The queue used to serialize sessions and observe when they've drained
  92. private let sessionQueue: OperationQueue = {
  93. let queue = OperationQueue()
  94. queue.name = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.sessionQueue"
  95. queue.maxConcurrentOperationCount = 1
  96. return queue
  97. }()
  98. private var sessionQueueOperationCountObserver: NSKeyValueObservation!
  99. init(peripheralManager: PeripheralManager) {
  100. self.manager = peripheralManager
  101. sessionQueue.underlyingQueue = peripheralManager.queue
  102. peripheralManager.delegate = self
  103. sessionQueueOperationCountObserver = sessionQueue.observe(\.operationCount, options: [.new]) { [weak self] (queue, change) in
  104. if let newValue = change.newValue, newValue == 0 {
  105. self?.log.debug("Session queue operation count is now empty")
  106. self?.assertIdleListening(forceRestart: true)
  107. }
  108. }
  109. }
  110. }
  111. // MARK: - Peripheral operations. Thread-safe.
  112. extension RileyLinkDevice {
  113. public var name: String? {
  114. return manager.peripheral.name
  115. }
  116. public var deviceURI: String {
  117. return "rileylink://\(name ?? peripheralIdentifier.uuidString)"
  118. }
  119. public var peripheralIdentifier: UUID {
  120. return manager.peripheral.identifier
  121. }
  122. public var peripheralState: CBPeripheralState {
  123. return manager.peripheral.state
  124. }
  125. public func readRSSI() {
  126. guard case .connected = manager.peripheral.state, case .poweredOn? = manager.central?.state else {
  127. return
  128. }
  129. manager.peripheral.readRSSI()
  130. }
  131. public func setCustomName(_ name: String) {
  132. manager.setCustomName(name)
  133. }
  134. public func updateBatteryLevel() {
  135. manager.readBatteryLevel { value in
  136. if let batteryLevel = value {
  137. self.batteryLevel = batteryLevel
  138. NotificationCenter.default.post(
  139. name: .DeviceBatteryLevelUpdated,
  140. object: self,
  141. userInfo: [RileyLinkDevice.batteryLevelKey: batteryLevel]
  142. )
  143. NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
  144. }
  145. }
  146. }
  147. public func orangeAction(_ command: OrangeLinkCommand) {
  148. log.debug("orangeAction: %@", "\(command)")
  149. manager.orangeAction(command)
  150. }
  151. public func setOrangeConfig(_ config: OrangeLinkConfigurationSetting, isOn: Bool) {
  152. log.debug("setOrangeConfig: %@, %@", "\(String(describing: config))", "\(isOn)")
  153. manager.setOrangeConfig(config, isOn: isOn)
  154. }
  155. public func orangeWritePwd() {
  156. log.debug("orangeWritePwd")
  157. manager.orangeWritePwd()
  158. }
  159. public func orangeClose() {
  160. log.debug("orangeClose")
  161. manager.orangeClose()
  162. }
  163. public func orangeReadSet() {
  164. log.debug("orangeReadSet")
  165. manager.orangeReadSet()
  166. }
  167. public func orangeReadVDC() {
  168. log.debug("orangeReadVDC")
  169. manager.orangeReadVDC()
  170. }
  171. public func findDevice() {
  172. log.debug("findDevice")
  173. manager.findDevice()
  174. }
  175. public func setDiagnosticeLEDModeForBLEChip(_ mode: RileyLinkLEDMode) {
  176. manager.setLEDMode(mode: mode)
  177. }
  178. public func readDiagnosticLEDModeForBLEChip(completion: @escaping (RileyLinkLEDMode?) -> Void) {
  179. manager.readDiagnosticLEDMode(completion: completion)
  180. }
  181. public func enableBLELEDs() {
  182. manager.setLEDMode(mode: .on)
  183. }
  184. /// Asserts that the caller is currently on the session queue
  185. public func assertOnSessionQueue() {
  186. dispatchPrecondition(condition: .onQueue(manager.queue))
  187. }
  188. /// Schedules a closure to execute on the session queue after a specified time
  189. ///
  190. /// - Parameters:
  191. /// - deadline: The time after which to execute
  192. /// - execute: The closure to execute
  193. public func sessionQueueAsyncAfter(deadline: DispatchTime, execute: @escaping () -> Void) {
  194. manager.queue.asyncAfter(deadline: deadline, execute: execute)
  195. }
  196. }
  197. extension RileyLinkDevice: Equatable, Hashable {
  198. public static func ==(lhs: RileyLinkDevice, rhs: RileyLinkDevice) -> Bool {
  199. return lhs === rhs
  200. }
  201. public func hash(into hasher: inout Hasher) {
  202. hasher.combine(peripheralIdentifier)
  203. }
  204. }
  205. // MARK: - Status management
  206. extension RileyLinkDevice {
  207. public struct Status {
  208. public let lastIdle: Date?
  209. public let name: String?
  210. public let version: String
  211. public let ledOn: Bool
  212. public let vibrationOn: Bool
  213. public let voltage: Float?
  214. public let battery: Int?
  215. public let hasPiezo: Bool
  216. }
  217. public func getStatus(_ completion: @escaping (_ status: Status) -> Void) {
  218. os_unfair_lock_lock(&lock)
  219. let lastIdle = self.lastIdle
  220. os_unfair_lock_unlock(&lock)
  221. self.manager.queue.async {
  222. completion(Status(
  223. lastIdle: lastIdle,
  224. name: self.name,
  225. version: self.version,
  226. ledOn: self.ledOn,
  227. vibrationOn: self.vibrationOn,
  228. voltage: self.voltage,
  229. battery: self.batteryLevel,
  230. hasPiezo: self.hasPiezo
  231. ))
  232. }
  233. }
  234. }
  235. // MARK: - Command session management
  236. // CommandSessions are a way to serialize access to the RileyLink command/response facility.
  237. // All commands that send data out on the RL data characteristic need to be in a command session.
  238. // Accessing other characteristics on the RileyLink can be done without a command session.
  239. extension RileyLinkDevice {
  240. public func runSession(withName name: String, _ block: @escaping (_ session: CommandSession) -> Void) {
  241. self.log.default("Scheduling session %{public}@", name)
  242. sessionQueue.addOperation(manager.configureAndRun({ [weak self] (manager) in
  243. self?.log.default("======================== %{public}@ ===========================", name)
  244. let bleFirmwareVersion = self?.bleFirmwareVersion
  245. let radioFirmwareVersion = self?.radioFirmwareVersion
  246. if bleFirmwareVersion == nil || radioFirmwareVersion == nil {
  247. self?.log.error("Running session with incomplete configuration: bleFirmwareVersion %{public}@, radioFirmwareVersion: %{public}@", String(describing: bleFirmwareVersion), String(describing: radioFirmwareVersion))
  248. }
  249. block(CommandSession(manager: manager, responseType: bleFirmwareVersion?.responseType ?? .buffered, firmwareVersion: radioFirmwareVersion ?? .unknown))
  250. self?.log.default("------------------------ %{public}@ ---------------------------", name)
  251. }))
  252. }
  253. }
  254. // MARK: - Idle management
  255. extension RileyLinkDevice {
  256. public enum IdleListeningState {
  257. case enabled(timeout: TimeInterval, channel: UInt8)
  258. case disabled
  259. }
  260. func setIdleListeningState(_ state: IdleListeningState) {
  261. os_unfair_lock_lock(&lock)
  262. let oldValue = idleListeningState
  263. idleListeningState = state
  264. os_unfair_lock_unlock(&lock)
  265. switch (oldValue, state) {
  266. case (.disabled, .enabled):
  267. assertIdleListening(forceRestart: true)
  268. case (.enabled, .enabled):
  269. assertIdleListening(forceRestart: false)
  270. default:
  271. break
  272. }
  273. }
  274. public func assertIdleListening(forceRestart: Bool = false) {
  275. os_unfair_lock_lock(&lock)
  276. guard case .enabled(timeout: let timeout, channel: let channel) = self.idleListeningState else {
  277. os_unfair_lock_unlock(&lock)
  278. return
  279. }
  280. guard case .connected = self.manager.peripheral.state, case .poweredOn? = self.manager.central?.state else {
  281. os_unfair_lock_unlock(&lock)
  282. return
  283. }
  284. guard forceRestart || (self.lastIdle ?? .distantPast).timeIntervalSinceNow < -timeout else {
  285. os_unfair_lock_unlock(&lock)
  286. return
  287. }
  288. guard !self.isIdleListeningPending else {
  289. os_unfair_lock_unlock(&lock)
  290. return
  291. }
  292. self.isIdleListeningPending = true
  293. os_unfair_lock_unlock(&lock)
  294. self.manager.startIdleListening(idleTimeout: timeout, channel: channel) { (error) in
  295. os_unfair_lock_lock(&self.lock)
  296. self.isIdleListeningPending = false
  297. if let error = error {
  298. self.log.error("Unable to start idle listening: %@", String(describing: error))
  299. os_unfair_lock_unlock(&self.lock)
  300. } else {
  301. self.lastIdle = Date()
  302. self.log.debug("Started idle listening")
  303. os_unfair_lock_unlock(&self.lock)
  304. NotificationCenter.default.post(name: .DeviceDidStartIdle, object: self)
  305. }
  306. }
  307. }
  308. }
  309. // MARK: - Timer tick management
  310. extension RileyLinkDevice {
  311. func setTimerTickEnabled(_ enabled: Bool) {
  312. os_unfair_lock_lock(&lock)
  313. self.isTimerTickEnabled = enabled
  314. os_unfair_lock_unlock(&lock)
  315. self.assertTimerTick()
  316. }
  317. func assertTimerTick() {
  318. os_unfair_lock_lock(&self.lock)
  319. let isTimerTickEnabled = self.isTimerTickEnabled
  320. os_unfair_lock_unlock(&self.lock)
  321. if isTimerTickEnabled != self.manager.timerTickEnabled {
  322. self.manager.setTimerTickEnabled(isTimerTickEnabled)
  323. }
  324. }
  325. }
  326. // MARK: - CBCentralManagerDelegate Proxying
  327. extension RileyLinkDevice {
  328. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  329. if case .poweredOn = central.state {
  330. assertIdleListening(forceRestart: false)
  331. assertTimerTick()
  332. }
  333. manager.centralManagerDidUpdateState(central)
  334. }
  335. func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
  336. log.default("didConnect %{public}@", peripheral)
  337. if case .connected = peripheral.state {
  338. assertIdleListening(forceRestart: false)
  339. assertTimerTick()
  340. }
  341. manager.centralManager(central, didConnect: peripheral)
  342. NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
  343. }
  344. func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
  345. log.default("didDisconnectPeripheral %{public}@", peripheral)
  346. NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
  347. }
  348. func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  349. log.default("didFailToConnect %{public}@", peripheral)
  350. NotificationCenter.default.post(name: .DeviceConnectionStateDidChange, object: self)
  351. }
  352. }
  353. extension RileyLinkDevice: PeripheralManagerDelegate {
  354. func peripheralManager(_ manager: PeripheralManager, didUpdateNotificationStateFor characteristic: CBCharacteristic) {
  355. log.debug("Did didUpdateNotificationStateFor %@", characteristic)
  356. }
  357. // If PeripheralManager receives a response on the data queue, without an outstanding request,
  358. // it will pass the update to this method, which is called on the central's queue.
  359. // This is how idle listen responses are handled
  360. func peripheralManager(_ manager: PeripheralManager, didUpdateValueFor characteristic: CBCharacteristic) {
  361. let characteristicService: CBService? = characteristic.service
  362. guard let cbService = characteristicService, let service = RileyLinkServiceUUID(rawValue: cbService.uuid.uuidString) else {
  363. log.debug("Update from characteristic on unknown service: %@", String(describing: characteristic.service))
  364. return
  365. }
  366. switch service {
  367. case .main:
  368. guard let mainCharacteristic = MainServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) else {
  369. log.debug("Update from unknown characteristic %@ on main service.", characteristic.uuid.uuidString)
  370. return
  371. }
  372. handleCharacteristicUpdate(mainCharacteristic, value: characteristic.value)
  373. case .orange:
  374. guard let orangeCharacteristic = OrangeServiceCharacteristicUUID(rawValue: characteristic.uuid.uuidString) else {
  375. log.debug("Update from unknown characteristic %@ on orange service.", characteristic.uuid.uuidString)
  376. return
  377. }
  378. handleCharacteristicUpdate(orangeCharacteristic, value: characteristic.value)
  379. default:
  380. return
  381. }
  382. }
  383. private func handleCharacteristicUpdate(_ characteristic: MainServiceCharacteristicUUID, value: Data?) {
  384. switch characteristic {
  385. case .data:
  386. guard let value = value, value.count > 0 else {
  387. return
  388. }
  389. self.manager.queue.async {
  390. if let responseType = self.bleFirmwareVersion?.responseType {
  391. let response: PacketResponse?
  392. switch responseType {
  393. case .buffered:
  394. var buffer = ResponseBuffer<PacketResponse>(endMarker: 0x00)
  395. buffer.append(value)
  396. response = buffer.responses.last
  397. case .single:
  398. response = PacketResponse(data: value)
  399. }
  400. if let response = response {
  401. switch response.code {
  402. case .commandInterrupted:
  403. self.log.debug("Received commandInterrupted during idle; assuming device is still listening.")
  404. return
  405. case .rxTimeout, .zeroData, .invalidParam, .unknownCommand:
  406. self.log.debug("Idle error received: %@", String(describing: response.code))
  407. case .success:
  408. if let packet = response.packet {
  409. self.log.debug("Idle packet received: %@", value.hexadecimalString)
  410. NotificationCenter.default.post(
  411. name: .DevicePacketReceived,
  412. object: self,
  413. userInfo: [RileyLinkDevice.notificationPacketKey: packet]
  414. )
  415. }
  416. }
  417. } else {
  418. self.log.error("Unknown idle response: %@", value.hexadecimalString)
  419. }
  420. } else {
  421. self.log.error("Skipping parsing characteristic value update due to missing BLE firmware version")
  422. }
  423. self.assertIdleListening(forceRestart: true)
  424. }
  425. case .responseCount:
  426. // PeripheralManager.Configuration.valueUpdateMacros is responsible for handling this response.
  427. break
  428. case .timerTick:
  429. NotificationCenter.default.post(name: .DeviceTimerDidTick, object: self)
  430. assertIdleListening(forceRestart: false)
  431. case .customName, .firmwareVersion, .ledMode:
  432. break
  433. }
  434. }
  435. private func handleCharacteristicUpdate(_ characteristic: OrangeServiceCharacteristicUUID, value: Data?) {
  436. switch characteristic {
  437. case .orangeRX, .orangeTX:
  438. guard let data = value, !data.isEmpty else { return }
  439. if data.first == 0xbb {
  440. guard data.count > 6 else { return }
  441. if data[1] == 0x09, data[2] == 0xaa {
  442. orangeLinkFirmwareHardwareVersion = "FW\(data[3]).\(data[4])/HW\(data[5]).\(data[6])"
  443. orangeLinkHardwareVersionMajorMinor = [Int(data[5]), Int(data[6])]
  444. NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
  445. }
  446. } else if data.first == OrangeLinkRequestType.cfgHeader.rawValue {
  447. guard data.count > 2 else { return }
  448. if data[1] == 0x01 {
  449. guard data.count > 5 else { return }
  450. ledOn = (data[3] != 0)
  451. vibrationOn = (data[4] != 0)
  452. NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
  453. } else if data[1] == 0x03 {
  454. guard data.count > 4 else { return }
  455. let int = UInt16(bigEndian: Data(data[3...4]).withUnsafeBytes { $0.load(as: UInt16.self) })
  456. voltage = Float(int) / 1000
  457. NotificationCenter.default.post(name: .DeviceStatusUpdated, object: self)
  458. }
  459. }
  460. }
  461. }
  462. func peripheralManager(_ manager: PeripheralManager, didReadRSSI RSSI: NSNumber, error: Error?) {
  463. NotificationCenter.default.post(
  464. name: .DeviceRSSIDidChange,
  465. object: self,
  466. userInfo: [RileyLinkDevice.notificationRSSIKey: RSSI]
  467. )
  468. }
  469. func peripheralManagerDidUpdateName(_ manager: PeripheralManager) {
  470. NotificationCenter.default.post(
  471. name: .DeviceNameDidChange,
  472. object: self,
  473. userInfo: nil
  474. )
  475. }
  476. func completeConfiguration(for manager: PeripheralManager) throws {
  477. // Read bluetooth version to determine compatibility
  478. log.default("Reading firmware versions for PeripheralManager configuration")
  479. let bleVersionString = try manager.readBluetoothFirmwareVersion(timeout: 1)
  480. bleFirmwareVersion = BLEFirmwareVersion(versionString: bleVersionString)
  481. let radioVersionString = try manager.readRadioFirmwareVersion(timeout: 1, responseType: bleFirmwareVersion?.responseType ?? .buffered)
  482. radioFirmwareVersion = RadioFirmwareVersion(versionString: radioVersionString)
  483. try manager.setOrangeNotifyOn()
  484. }
  485. }
  486. extension RileyLinkDevice: CustomDebugStringConvertible {
  487. public var debugDescription: String {
  488. os_unfair_lock_lock(&lock)
  489. let lastIdle = self.lastIdle
  490. let isIdleListeningPending = self.isIdleListeningPending
  491. let isTimerTickEnabled = self.isTimerTickEnabled
  492. os_unfair_lock_unlock(&lock)
  493. return [
  494. "## RileyLinkDevice",
  495. "* name: \(name ?? "")",
  496. "* lastIdle: \(lastIdle ?? .distantPast)",
  497. "* isIdleListeningPending: \(isIdleListeningPending)",
  498. "* isTimerTickEnabled: \(isTimerTickEnabled)",
  499. "* isTimerTickNotifying: \(manager.timerTickEnabled)",
  500. "* radioFirmware: \(String(describing: radioFirmwareVersion))",
  501. "* bleFirmware: \(String(describing: bleFirmwareVersion))",
  502. "* peripheralManager: \(manager)",
  503. "* sessionQueue.operationCount: \(sessionQueue.operationCount)"
  504. ].joined(separator: "\n")
  505. }
  506. }
  507. extension RileyLinkDevice {
  508. public static let notificationPacketKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationPacket"
  509. public static let notificationRSSIKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.NotificationRSSI"
  510. public static let batteryLevelKey = "com.rileylink.RileyLinkBLEKit.RileyLinkDevice.BatteryLevel"
  511. }
  512. extension Notification.Name {
  513. public static let DeviceConnectionStateDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.ConnectionStateDidChange")
  514. public static let DeviceDidStartIdle = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DidStartIdle")
  515. public static let DeviceNameDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.NameDidChange")
  516. public static let DevicePacketReceived = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.PacketReceived")
  517. public static let DeviceRSSIDidChange = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.RSSIDidChange")
  518. public static let DeviceTimerDidTick = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.TimerTickDidChange")
  519. public static let DeviceStatusUpdated = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.DeviceStatusUpdated")
  520. public static let DeviceBatteryLevelUpdated = Notification.Name(rawValue: "com.rileylink.RileyLinkBLEKit.BatteryLevelUpdated")
  521. }