BluetoothDevice.swift 11 KB

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