import CoreBluetooth import Foundation /// BLE 客户端内部回调协议 protocol BleClientDelegate: AnyObject { func bleClient(_ client: BleClient, didDiscoverDevice device: DiscoveredDevice) func bleClient(_ client: BleClient, didReceiveData data: Data) } final class BleClient: NSObject { weak var delegate: BleClientDelegate? var onConnectionStateChange: ((ConnectionState) -> Void)? var onDisconnected: (() -> Void)? private let queue = DispatchQueue(label: "com.duooomi.ble.client", qos: .userInitiated) private var centralManager: CBCentralManager! private var connectedPeripheral: CBPeripheral? private var writeCharacteristic: CBCharacteristic? private var readCharacteristic: CBCharacteristic? // Connect/Disconnect callbacks private var connectCompletion: ((Result) -> Void)? private var disconnectCompletion: ((Result) -> Void)? private var connectTimeoutWork: DispatchWorkItem? var bluetoothState: CBManagerState { centralManager.state } var maximumWriteLength: Int { connectedPeripheral?.maximumWriteValueLength(for: .withoutResponse) ?? 182 } override init() { super.init() centralManager = CBCentralManager(delegate: self, queue: queue) } // MARK: - Scan func scan(serviceUUIDs: [CBUUID] = [BleUUIDs.service]) { guard centralManager.state == .poweredOn else { BleLog.w("Bluetooth not powered on", "BLE") return } centralManager.scanForPeripherals( withServices: serviceUUIDs, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] ) } func stopScan() { centralManager.stopScan() } // MARK: - Connect func connect(deviceId: String, timeout: TimeInterval = 30, completion: @escaping (Result) -> Void) { guard centralManager.state == .poweredOn else { completion(.failure(DuooomiBleError.bluetoothNotPoweredOn("Bluetooth not powered on"))) return } let peripherals = centralManager.retrievePeripherals(withIdentifiers: [UUID(uuidString: deviceId)!]) guard let peripheral = peripherals.first else { completion(.failure(DuooomiBleError.connectionFailed("Device not found: \(deviceId)"))) return } self.connectCompletion = completion peripheral.delegate = self centralManager.connect(peripheral, options: nil) // Timeout let work = DispatchWorkItem { [weak self] in guard let self = self, self.connectCompletion != nil else { return } self.centralManager.cancelPeripheralConnection(peripheral) let cb = self.connectCompletion self.connectCompletion = nil cb?(.failure(DuooomiBleError.connectionFailed("Connection timeout"))) } connectTimeoutWork = work queue.asyncAfter(deadline: .now() + timeout, execute: work) } // MARK: - Disconnect func disconnect(completion: @escaping (Result) -> Void) { guard let peripheral = connectedPeripheral else { completion(.success(())) return } disconnectCompletion = completion centralManager.cancelPeripheralConnection(peripheral) } // MARK: - Write func writeWithoutResponse(_ data: Data) throws { guard let peripheral = connectedPeripheral, let char = writeCharacteristic else { throw DuooomiBleError.notConnected } peripheral.writeValue(data, for: char, type: .withoutResponse) } // MARK: - Retrieve Connected func retrieveConnectedDevices() -> [DiscoveredDevice] { centralManager.retrieveConnectedPeripherals(withServices: [BleUUIDs.service]).map { DiscoveredDevice(id: $0.identifier.uuidString, name: $0.name, rssi: 0) } } } // MARK: - CBCentralManagerDelegate extension BleClient: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { BleLog.i("Bluetooth state: \(central.state.rawValue)", "BLE") } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { let device = DiscoveredDevice( id: peripheral.identifier.uuidString, name: peripheral.name, rssi: RSSI.intValue ) delegate?.bleClient(self, didDiscoverDevice: device) } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { connectTimeoutWork?.cancel() connectTimeoutWork = nil connectedPeripheral = peripheral peripheral.delegate = self peripheral.discoverServices([BleUUIDs.service]) } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { connectTimeoutWork?.cancel() connectTimeoutWork = nil let cb = connectCompletion connectCompletion = nil cb?(.failure(error ?? DuooomiBleError.connectionFailed("Unknown"))) } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { connectedPeripheral = nil writeCharacteristic = nil readCharacteristic = nil if let cb = disconnectCompletion { disconnectCompletion = nil if let error = error { cb(.failure(error)) } else { cb(.success(())) } } DispatchQueue.main.async { [weak self] in self?.onDisconnected?() } } } // MARK: - CBPeripheralDelegate extension BleClient: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { let cb = connectCompletion connectCompletion = nil cb?(.failure(error)) return } guard let service = peripheral.services?.first(where: { $0.uuid == BleUUIDs.service }) else { let cb = connectCompletion connectCompletion = nil cb?(.failure(DuooomiBleError.serviceNotFound)) return } peripheral.discoverCharacteristics( [BleUUIDs.writeCharacteristic, BleUUIDs.readCharacteristic], for: service ) } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { let cb = connectCompletion connectCompletion = nil cb?(.failure(error)) return } guard let characteristics = service.characteristics else { let cb = connectCompletion connectCompletion = nil cb?(.failure(DuooomiBleError.characteristicNotFound)) return } for char in characteristics { if char.uuid == BleUUIDs.writeCharacteristic { writeCharacteristic = char } else if char.uuid == BleUUIDs.readCharacteristic { readCharacteristic = char peripheral.setNotifyValue(true, for: char) } } guard writeCharacteristic != nil, readCharacteristic != nil else { let cb = connectCompletion connectCompletion = nil cb?(.failure(DuooomiBleError.characteristicNotFound)) return } let cb = connectCompletion connectCompletion = nil cb?(.success(peripheral)) } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard error == nil, let data = characteristic.value else { return } delegate?.bleClient(self, didReceiveData: data) } }