BluetoothDevice.swift 11 KB

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