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? /// 待枚举 characteristic 的 service 计数(每个 service 完成 didDiscoverCharacteristicsFor 时递减)。 private var servicesPendingCharDiscover: Int = 0 /// 待枚举 included services 的 service 计数(递归发现 secondary service)。 private var servicesPendingIncludedDiscover: Int = 0 /// 已发起 characteristic 发现的 service uuidString 去重集合(避免同一 service 被重复发现)。 private var charDiscoverDispatched: Set = [] 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 self.servicesPendingCharDiscover = 0 self.servicesPendingIncludedDiscover = 0 self.charDiscoverDispatched.removeAll() 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 // 发现全部 service(与 expo 端 discoverAllServicesAndCharacteristics 行为对齐)。 // 某些固件传入指定 UUID 列表时返回空,需在 didDiscoverServices 自行过滤。 peripheral.discoverServices(nil) } 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 } let services = peripheral.services ?? [] BleLog.d("Discovered \(services.count) primary service(s): \(services.map { $0.uuid.uuidString })", "BLE") // CoreBluetooth `discoverServices(nil)` 只发现 primary service, // 固件可能把目标 service 挂成 included service(react-native-ble-plx 默认会递归发现)。 // 这里对每个 primary service 同时发起: // 1. 发现 included services(递归) // 2. 发现自己的 characteristics for s in services { scheduleServiceFullDiscovery(peripheral: peripheral, service: s) } } func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) { servicesPendingIncludedDiscover = max(0, servicesPendingIncludedDiscover - 1) if let error = error { BleLog.w("Included service discover error for \(service.uuid.uuidString): \(error.localizedDescription)", "BLE") } else { let included = service.includedServices ?? [] if !included.isEmpty { BleLog.d(" [Service \(service.uuid.uuidString)] includes \(included.count) sub-service(s): \(included.map { $0.uuid.uuidString })", "BLE") } for sub in included { scheduleServiceFullDiscovery(peripheral: peripheral, service: sub) } } evaluateConnectCompletionIfReady() } /// 对一个 service 同时发起 included services + characteristics 发现,去重避免循环。 private func scheduleServiceFullDiscovery(peripheral: CBPeripheral, service: CBService) { let key = service.uuid.uuidString.lowercased() if charDiscoverDispatched.contains(key) { return } charDiscoverDispatched.insert(key) servicesPendingIncludedDiscover += 1 peripheral.discoverIncludedServices(nil, for: service) servicesPendingCharDiscover += 1 peripheral.discoverCharacteristics(nil, for: service) } /// 所有 included service 与 characteristic 发现都结束后,判定连接成败。 private func evaluateConnectCompletionIfReady() { guard servicesPendingIncludedDiscover == 0, servicesPendingCharDiscover == 0 else { return } guard connectCompletion != nil, let peripheral = connectedPeripheral else { return } if writeCharacteristic == nil || readCharacteristic == nil { BleLog.e("Required characteristics not found. SDK service=\(BleUUIDs.service.uuidString), write=\(BleUUIDs.writeCharacteristic.uuidString), read=\(BleUUIDs.readCharacteristic.uuidString). 检查上方 [Service ...] Char 列表,把固件实际使用的 UUID 反馈给 SDK。", "BLE") let cb = connectCompletion connectCompletion = nil cb?(.failure(DuooomiBleError.serviceNotFound)) return } let cb = connectCompletion connectCompletion = nil cb?(.success(peripheral)) } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { servicesPendingCharDiscover = max(0, servicesPendingCharDiscover - 1) if let error = error { BleLog.w("Char discover error for service \(service.uuid.uuidString): \(error.localizedDescription)", "BLE") } else { // 诊断:打印此 service 下所有 characteristic 的属性(write/notify/read…) for c in service.characteristics ?? [] { let props = Self.describeProps(c.properties) BleLog.d(" [Service \(service.uuid.uuidString)] Char \(c.uuid.uuidString) [\(props)]", "BLE") } // 命中目标 service 的 characteristic 才登记 if Self.uuidEqual(service.uuid, BleUUIDs.service) { for char in service.characteristics ?? [] { if Self.uuidEqual(char.uuid, BleUUIDs.writeCharacteristic) { writeCharacteristic = char } else if Self.uuidEqual(char.uuid, BleUUIDs.readCharacteristic) { readCharacteristic = char peripheral.setNotifyValue(true, for: char) } } } } evaluateConnectCompletionIfReady() } private static func describeProps(_ p: CBCharacteristicProperties) -> String { var bits: [String] = [] if p.contains(.read) { bits.append("Read") } if p.contains(.write) { bits.append("Write") } if p.contains(.writeWithoutResponse) { bits.append("WriteNoResp") } if p.contains(.notify) { bits.append("Notify") } if p.contains(.indicate) { bits.append("Indicate") } return bits.joined(separator: ",") } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard error == nil, let data = characteristic.value else { return } delegate?.bleClient(self, didReceiveData: data) } // MARK: - UUID 归一化比较 /// CoreBluetooth 在 service.uuid 上对蓝牙标准 base UUID 范围内的 UUID 会返回 16-bit 短形式 /// (如 `"02C4"`),与 `CBUUID(string: "000002c4-0000-1000-8000-00805f9b34fb")` 直接 `==` /// 比较会因 `data` 长度不同而不相等。把短形式展开成完整 128-bit 字符串再比较。 static func uuidEqual(_ a: CBUUID, _ b: CBUUID) -> Bool { if a == b { return true } return Self.canonicalUUIDString(a) == Self.canonicalUUIDString(b) } private static func canonicalUUIDString(_ uuid: CBUUID) -> String { let s = uuid.uuidString.lowercased() switch s.count { case 4: return "0000\(s)-0000-1000-8000-00805f9b34fb" case 8: return "\(s)-0000-1000-8000-00805f9b34fb" default: return s } } }