| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- //
- // BluetoothDevice.swift
- // LoopFollow
- //
- // Created by Jonas Björkert on 2025-01-04.
- // Copyright © 2025 Jon Fawcett. All rights reserved.
- //
- import Foundation
- import CoreBluetooth
- import os
- import UIKit
- class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
- public weak var bluetoothDeviceDelegate: BluetoothDeviceDelegate?
- private(set) var deviceAddress:String
- private(set) var deviceName:String?
- private let CBUUID_Advertisement:String?
- private let servicesCBUUIDs:[CBUUID]?
- private let CBUUID_ReceiveCharacteristic:String
- private var centralManager: CBCentralManager?
- private var peripheral: CBPeripheral?
- private var timeStampLastStatusUpdate:Date
- private var receiveCharacteristic:CBCharacteristic?
- private let maxTimeToWaitForPeripheralResponse = 5.0
- private var connectTimeOutTimer: Timer?
- var lastHeartbeatTime: Date?
- init(address:String, name:String?, CBUUID_Advertisement:String?, servicesCBUUIDs:[CBUUID]?, CBUUID_ReceiveCharacteristic:String, bluetoothDeviceDelegate: BluetoothDeviceDelegate) {
- self.lastHeartbeatTime = nil
- self.deviceAddress = address
- self.deviceName = name
- self.servicesCBUUIDs = servicesCBUUIDs
- self.CBUUID_Advertisement = CBUUID_Advertisement
- self.CBUUID_ReceiveCharacteristic = CBUUID_ReceiveCharacteristic
- timeStampLastStatusUpdate = Date()
- self.bluetoothDeviceDelegate = bluetoothDeviceDelegate
- super.init()
- initialize()
- }
- deinit {
- disconnect()
- }
- func connect() {
- if let centralManager = centralManager, !retrievePeripherals(centralManager) {
- _ = startScanning()
- }
- }
- func disconnect() {
- if let peripheral = peripheral {
- if let centralManager = centralManager {
- centralManager.cancelPeripheralConnection(peripheral)
- }
- }
- }
- func disconnectAndForget() {
- disconnect()
- peripheral = nil
- deviceName = nil
- //deviceAddress = nil
- }
- func stopScanning() {
- self.centralManager?.stopScan()
- }
- func isScanning() -> Bool {
- if let centralManager = centralManager {
- return centralManager.isScanning
- }
- return false
- }
- func startScanning() -> BluetoothDevice.startScanningResult {
- LogManager.shared.log(category: .bluetooth, message: "Start Scanning", isDebug: true)
- var returnValue = BluetoothDevice.startScanningResult.unknown
- if let peripheral = peripheral {
- switch peripheral.state {
- case .connected:
- return .alreadyConnected
- case .connecting:
- if Date() > Date(timeInterval: maxTimeToWaitForPeripheralResponse, since: timeStampLastStatusUpdate) {
- disconnect()
- }
- return .connecting
- default:()
- }
- }
- var services:[CBUUID]?
- if let CBUUID_Advertisement = CBUUID_Advertisement {
- services = [CBUUID(string: CBUUID_Advertisement)]
- }
- if let centralManager = centralManager {
- if centralManager.isScanning {
- return .alreadyScanning
- }
- switch centralManager.state {
- case .poweredOn:
- centralManager.scanForPeripherals(withServices: services, options: nil)
- returnValue = .success
- case .poweredOff:
- return .poweredOff
- case .unknown:
- return .unknown
- case .unauthorized:
- return .unauthorized
- default:
- return returnValue
- }
- } else {
- returnValue = .other(reason:"centralManager is nil, can not start scanning")
- }
- return returnValue
- }
- func readValueForCharacteristic(for characteristic: CBCharacteristic) {
- peripheral?.readValue(for: characteristic)
- }
- func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic) {
- if let peripheral = peripheral {
- peripheral.setNotifyValue(enabled, for: characteristic)
- }
- }
- fileprivate func stopScanAndconnect(to peripheral: CBPeripheral) {
- LogManager.shared.log(category: .bluetooth, message: "Stop Scan And Connect", isDebug: true)
- self.centralManager?.stopScan()
- self.deviceAddress = peripheral.identifier.uuidString
- self.deviceName = peripheral.name
- peripheral.delegate = self
- self.peripheral = peripheral
- if peripheral.state == .disconnected {
- connectTimeOutTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(stopConnectAndRestartScanning), userInfo: nil, repeats: false)
- centralManager?.connect(peripheral, options: nil)
- } else {
- if let newCentralManager = centralManager {
- centralManager(newCentralManager, didConnect: peripheral)
- }
- }
- }
- @objc fileprivate func stopConnectAndRestartScanning() {
- disconnectAndForget()
- _ = startScanning()
- }
- public func cancelConnectionTimer() {
- if let connectTimeOutTimer = connectTimeOutTimer {
- connectTimeOutTimer.invalidate()
- self.connectTimeOutTimer = nil
- }
- }
- fileprivate func retrievePeripherals(_ central:CBCentralManager) -> Bool {
- if let uuid = UUID(uuidString: deviceAddress) {
- //trace(" uuid is not nil", log: log, category: ConstantsLog.categoryBlueToothTransmitter, type: .info)
- let peripheralArr = central.retrievePeripherals(withIdentifiers: [uuid])
- if peripheralArr.count > 0 {
- peripheral = peripheralArr[0]
- if let peripheral = peripheral {
- peripheral.delegate = self
- central.connect(peripheral, options: nil)
- return true
- }
- }
- }
- return false
- }
- func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
- print("[BLE] didDiscover")
- timeStampLastStatusUpdate = Date()
- if peripheral.identifier.uuidString == deviceAddress {
- stopScanAndconnect(to: peripheral)
- }
- }
- func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
- cancelConnectionTimer()
- timeStampLastStatusUpdate = Date()
- bluetoothDeviceDelegate?.didConnectTo(bluetoothDevice: self)
- peripheral.discoverServices(servicesCBUUIDs)
- }
- func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
- timeStampLastStatusUpdate = Date()
- let peripheralName = peripheral.name ?? "Unknown"
- let errorMessage = error?.localizedDescription ?? "No error details provided"
- LogManager.shared.log(category: .bluetooth, message: "Failed to connect to peripheral '\(peripheralName)' (UUID: \(peripheral.identifier.uuidString)). Error: \(errorMessage). Retrying...")
- centralManager?.connect(peripheral, options: nil)
- }
- func centralManagerDidUpdateState(_ central: CBCentralManager) {
- LogManager.shared.log(category: .bluetooth, message: "Central Manager Did Update State", isDebug: true)
- timeStampLastStatusUpdate = Date()
- if central.state == .poweredOn {
- _ = retrievePeripherals(central)
- }
- }
- func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
- timeStampLastStatusUpdate = Date()
- bluetoothDeviceDelegate?.didDisconnectFrom(bluetoothDevice: self)
- if let ownPeripheral = self.peripheral {
- centralManager?.connect(ownPeripheral, options: nil)
- }
- }
- func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
- timeStampLastStatusUpdate = Date()
- if let services = peripheral.services {
- for service in services {
- peripheral.discoverCharacteristics(nil, for: service)
- }
- } else {
- disconnect()
- }
- }
- func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
- timeStampLastStatusUpdate = Date()
- if let characteristics = service.characteristics {
- for characteristic in characteristics {
- if characteristic.uuid == CBUUID(string: CBUUID_ReceiveCharacteristic) {
- receiveCharacteristic = characteristic
- peripheral.setNotifyValue(true, for: characteristic)
- }
- }
- }
- }
- func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
- timeStampLastStatusUpdate = Date()
- }
- func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
- timeStampLastStatusUpdate = Date()
- }
- func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
- timeStampLastStatusUpdate = Date()
- }
- func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
- LogManager.shared.log(category: .bluetooth, message: "Restoring BLE after crash/kill")
- }
- private func initialize() {
- var cBCentralManagerOptionRestoreIdentifierKeyToUse: String?
- cBCentralManagerOptionRestoreIdentifierKeyToUse = "LoopFollow-" + deviceAddress
- centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true, CBCentralManagerOptionRestoreIdentifierKey: cBCentralManagerOptionRestoreIdentifierKeyToUse!])
- }
- enum startScanningResult: Equatable {
- case success
- case alreadyScanning
- case poweredOff
- case alreadyConnected
- case connecting
- case unknown
- case unauthorized
- case nfcScanNeeded
- case other(reason:String)
- func description() -> String {
- switch self {
- case .success:
- return "success"
- case .alreadyScanning:
- return "alreadyScanning"
- case .poweredOff:
- return "poweredOff"
- case .alreadyConnected:
- return "alreadyConnected"
- case .connecting:
- return "connecting"
- case .other(let reason):
- return "other reason : " + reason
- case .unknown:
- return "unknown"
- case .unauthorized:
- return "unauthorized"
- case .nfcScanNeeded:
- return "nfcScanNeeded"
- }
- }
- }
- func expectedHeartbeatInterval() -> TimeInterval? {
- return nil
- }
- }
|