BluetoothDevice.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. // LoopFollow
  2. // BluetoothDevice.swift
  3. import CoreBluetooth
  4. import Foundation
  5. import os
  6. class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
  7. weak var bluetoothDeviceDelegate: BluetoothDeviceDelegate?
  8. private(set) var deviceAddress: String
  9. private(set) var deviceName: String?
  10. private let CBUUID_Advertisement: String?
  11. private let servicesCBUUIDs: [CBUUID]?
  12. private let CBUUID_ReceiveCharacteristic: String
  13. private var centralManager: CBCentralManager?
  14. private var peripheral: CBPeripheral?
  15. private var timeStampLastStatusUpdate: Date
  16. private var receiveCharacteristic: CBCharacteristic?
  17. private let maxTimeToWaitForPeripheralResponse = 5.0
  18. private var connectTimeOutTimer: Timer?
  19. var lastHeartbeatTime: Date?
  20. init(address: String, name: String?, CBUUID_Advertisement: String?, servicesCBUUIDs: [CBUUID]?, CBUUID_ReceiveCharacteristic: String, bluetoothDeviceDelegate: BluetoothDeviceDelegate) {
  21. lastHeartbeatTime = nil
  22. deviceAddress = address
  23. deviceName = name
  24. self.servicesCBUUIDs = servicesCBUUIDs
  25. self.CBUUID_Advertisement = CBUUID_Advertisement
  26. self.CBUUID_ReceiveCharacteristic = CBUUID_ReceiveCharacteristic
  27. timeStampLastStatusUpdate = Date()
  28. self.bluetoothDeviceDelegate = bluetoothDeviceDelegate
  29. super.init()
  30. initialize()
  31. }
  32. deinit {
  33. disconnect()
  34. }
  35. func connect() {
  36. if let centralManager = centralManager, !retrievePeripherals(centralManager) {
  37. _ = startScanning()
  38. }
  39. }
  40. func disconnect() {
  41. if let peripheral = peripheral {
  42. if let centralManager = centralManager {
  43. centralManager.cancelPeripheralConnection(peripheral)
  44. }
  45. }
  46. }
  47. func disconnectAndForget() {
  48. disconnect()
  49. peripheral = nil
  50. deviceName = nil
  51. // deviceAddress = nil
  52. }
  53. func stopScanning() {
  54. centralManager?.stopScan()
  55. }
  56. func isScanning() -> Bool {
  57. if let centralManager = centralManager {
  58. return centralManager.isScanning
  59. }
  60. return false
  61. }
  62. func startScanning() -> BluetoothDevice.startScanningResult {
  63. LogManager.shared.log(category: .bluetooth, message: "Start Scanning", isDebug: true)
  64. var returnValue = BluetoothDevice.startScanningResult.unknown
  65. if let peripheral = peripheral {
  66. switch peripheral.state {
  67. case .connected:
  68. return .alreadyConnected
  69. case .connecting:
  70. if Date() > Date(timeInterval: maxTimeToWaitForPeripheralResponse, since: timeStampLastStatusUpdate) {
  71. disconnect()
  72. }
  73. return .connecting
  74. default: ()
  75. }
  76. }
  77. var services: [CBUUID]?
  78. if let CBUUID_Advertisement = CBUUID_Advertisement {
  79. services = [CBUUID(string: CBUUID_Advertisement)]
  80. }
  81. if let centralManager = centralManager {
  82. if centralManager.isScanning {
  83. return .alreadyScanning
  84. }
  85. switch centralManager.state {
  86. case .poweredOn:
  87. centralManager.scanForPeripherals(withServices: services, options: nil)
  88. returnValue = .success
  89. case .poweredOff:
  90. return .poweredOff
  91. case .unknown:
  92. return .unknown
  93. case .unauthorized:
  94. return .unauthorized
  95. default:
  96. return returnValue
  97. }
  98. } else {
  99. returnValue = .other(reason: "centralManager is nil, can not start scanning")
  100. }
  101. return returnValue
  102. }
  103. func readValueForCharacteristic(for characteristic: CBCharacteristic) {
  104. peripheral?.readValue(for: characteristic)
  105. }
  106. func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic) {
  107. if let peripheral = peripheral {
  108. peripheral.setNotifyValue(enabled, for: characteristic)
  109. }
  110. }
  111. fileprivate func stopScanAndconnect(to peripheral: CBPeripheral) {
  112. LogManager.shared.log(category: .bluetooth, message: "Stop Scan And Connect", isDebug: true)
  113. centralManager?.stopScan()
  114. deviceAddress = peripheral.identifier.uuidString
  115. deviceName = peripheral.name
  116. peripheral.delegate = self
  117. self.peripheral = peripheral
  118. if peripheral.state == .disconnected {
  119. connectTimeOutTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(stopConnectAndRestartScanning), userInfo: nil, repeats: false)
  120. centralManager?.connect(peripheral, options: nil)
  121. } else {
  122. if let newCentralManager = centralManager {
  123. centralManager(newCentralManager, didConnect: peripheral)
  124. }
  125. }
  126. }
  127. @objc fileprivate func stopConnectAndRestartScanning() {
  128. disconnectAndForget()
  129. _ = startScanning()
  130. }
  131. func cancelConnectionTimer() {
  132. if let connectTimeOutTimer = connectTimeOutTimer {
  133. connectTimeOutTimer.invalidate()
  134. self.connectTimeOutTimer = nil
  135. }
  136. }
  137. fileprivate func retrievePeripherals(_ central: CBCentralManager) -> Bool {
  138. if let uuid = UUID(uuidString: deviceAddress) {
  139. // trace(" uuid is not nil", log: log, category: ConstantsLog.categoryBlueToothTransmitter, type: .info)
  140. let peripheralArr = central.retrievePeripherals(withIdentifiers: [uuid])
  141. if peripheralArr.count > 0 {
  142. peripheral = peripheralArr[0]
  143. if let peripheral = peripheral {
  144. peripheral.delegate = self
  145. central.connect(peripheral, options: nil)
  146. return true
  147. }
  148. }
  149. }
  150. return false
  151. }
  152. func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData _: [String: Any], rssi _: NSNumber) {
  153. print("[BLE] didDiscover")
  154. timeStampLastStatusUpdate = Date()
  155. if peripheral.identifier.uuidString == deviceAddress {
  156. stopScanAndconnect(to: peripheral)
  157. }
  158. }
  159. func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
  160. cancelConnectionTimer()
  161. timeStampLastStatusUpdate = Date()
  162. bluetoothDeviceDelegate?.didConnectTo(bluetoothDevice: self)
  163. peripheral.discoverServices(servicesCBUUIDs)
  164. }
  165. func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
  166. timeStampLastStatusUpdate = Date()
  167. let peripheralName = peripheral.name ?? "Unknown"
  168. let errorMessage = error?.localizedDescription ?? "No error details provided"
  169. LogManager.shared.log(category: .bluetooth, message: "Failed to connect to peripheral '\(peripheralName)' (UUID: \(peripheral.identifier.uuidString)). Error: \(errorMessage). Retrying...")
  170. centralManager?.connect(peripheral, options: nil)
  171. }
  172. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  173. LogManager.shared.log(category: .bluetooth, message: "Central Manager Did Update State", isDebug: true)
  174. timeStampLastStatusUpdate = Date()
  175. if central.state == .poweredOn {
  176. _ = retrievePeripherals(central)
  177. }
  178. }
  179. func centralManager(_: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error _: Error?) {
  180. timeStampLastStatusUpdate = Date()
  181. bluetoothDeviceDelegate?.didDisconnectFrom(bluetoothDevice: self)
  182. cancelConnectionTimer()
  183. guard let ownPeripheral = peripheral else { return }
  184. DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
  185. guard let self = self,
  186. let manager = self.centralManager,
  187. manager.state == .poweredOn else { return }
  188. switch ownPeripheral.state {
  189. case .connected, .connecting:
  190. // Already (re)connecting; do nothing
  191. break
  192. default:
  193. manager.connect(ownPeripheral, options: nil)
  194. }
  195. }
  196. }
  197. func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
  198. timeStampLastStatusUpdate = Date()
  199. if let services = peripheral.services {
  200. for service in services {
  201. peripheral.discoverCharacteristics(nil, for: service)
  202. }
  203. } else {
  204. disconnect()
  205. }
  206. }
  207. func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?) {
  208. timeStampLastStatusUpdate = Date()
  209. if let characteristics = service.characteristics {
  210. for characteristic in characteristics {
  211. if characteristic.uuid == CBUUID(string: CBUUID_ReceiveCharacteristic) {
  212. receiveCharacteristic = characteristic
  213. peripheral.setNotifyValue(true, for: characteristic)
  214. }
  215. }
  216. }
  217. }
  218. func peripheral(_: CBPeripheral, didWriteValueFor _: CBCharacteristic, error _: Error?) {
  219. timeStampLastStatusUpdate = Date()
  220. }
  221. func peripheral(_: CBPeripheral, didUpdateNotificationStateFor _: CBCharacteristic, error _: Error?) {
  222. timeStampLastStatusUpdate = Date()
  223. }
  224. func peripheral(_: CBPeripheral, didUpdateValueFor _: CBCharacteristic, error _: Error?) {
  225. timeStampLastStatusUpdate = Date()
  226. }
  227. func centralManager(_: CBCentralManager, willRestoreState _: [String: Any]) {
  228. LogManager.shared.log(category: .bluetooth, message: "Restoring BLE after crash/kill")
  229. }
  230. private func initialize() {
  231. var cBCentralManagerOptionRestoreIdentifierKeyToUse: String?
  232. cBCentralManagerOptionRestoreIdentifierKeyToUse = "LoopFollow-" + deviceAddress
  233. centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true, CBCentralManagerOptionRestoreIdentifierKey: cBCentralManagerOptionRestoreIdentifierKeyToUse!])
  234. }
  235. enum startScanningResult: Equatable {
  236. case success
  237. case alreadyScanning
  238. case poweredOff
  239. case alreadyConnected
  240. case connecting
  241. case unknown
  242. case unauthorized
  243. case nfcScanNeeded
  244. case other(reason: String)
  245. func description() -> String {
  246. switch self {
  247. case .success:
  248. return "success"
  249. case .alreadyScanning:
  250. return "alreadyScanning"
  251. case .poweredOff:
  252. return "poweredOff"
  253. case .alreadyConnected:
  254. return "alreadyConnected"
  255. case .connecting:
  256. return "connecting"
  257. case let .other(reason):
  258. return "other reason : " + reason
  259. case .unknown:
  260. return "unknown"
  261. case .unauthorized:
  262. return "unauthorized"
  263. case .nfcScanNeeded:
  264. return "nfcScanNeeded"
  265. }
  266. }
  267. }
  268. func expectedHeartbeatInterval() -> TimeInterval? {
  269. return nil
  270. }
  271. }