import { Platform } from 'react-native' import { BleManager, type Characteristic, type Device, type ScanOptions } from 'react-native-ble-plx' import { BLE_UUIDS } from '../protocol/Constants' import { type BleDevice, type BleError, ConnectionState, type ScanResult } from './types' export class BleClient { private static instance: BleClient private manager: BleManager | null = null private connectedDevice: Device | null = null // Simple event system private listeners: Map> = new Map() private constructor() { if (Platform.OS !== 'web') { this.manager = new BleManager() } } public static getInstance(): BleClient { if (!BleClient.instance) { BleClient.instance = new BleClient() } return BleClient.instance } public addListener(event: string, callback: Function) { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()) } this.listeners.get(event)!.add(callback) } public removeListener(event: string, callback: Function) { if (this.listeners.has(event)) { this.listeners.get(event)!.delete(callback) } } public removeAllListeners() { this.listeners.clear() console.debug('All BLE event listeners cleared') } private emit(event: string, ...args: any[]) { if (this.listeners.has(event)) { this.listeners.get(event)!.forEach((cb) => cb(...args)) } } public async startScan( serviceUUIDs: string[] | null = null, options: ScanOptions = {}, onDeviceFound: (result: ScanResult) => void, ): Promise { if (!this.manager) { console.warn('BLE not supported on web') return } const state = ((await this.manager.state()) || '').toString() if (!state.toLowerCase().includes('poweredon')) { throw new Error(`Bluetooth is not powered on. State: ${state}`) } this.manager.startDeviceScan(serviceUUIDs, options, (error, device) => { if (error) { this.emit('scanError', error) return } if (device) { onDeviceFound({ device, rssi: device.rssi, localName: device.name, }) } }) } public async getConnectedDevices(serviceUUIDs: string[]): Promise { if (!this.manager) return [] try { const devices = await this.manager.connectedDevices(serviceUUIDs) return devices as BleDevice[] } catch (e) { throw this.normalizeError(e) } } public stopScan() { if (!this.manager) return this.manager.stopDeviceScan() } public async connect(deviceId: string, timeout: number = 30000): Promise { if (!this.manager) throw new Error('BLE not supported on web') // 创建超时 Promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Connection timeout after ${timeout / 1000}s`)) }, timeout) }) try { this.emit('connectionStateChange', { deviceId, state: ConnectionState.CONNECTING }) // 使用 Promise.race 实现超时控制 const connectPromise = this.manager.connectToDevice(deviceId, { autoConnect: false }) let device = await Promise.race([connectPromise, timeoutPromise]) if (!device) { throw new Error('Failed to connect: device is null') } // 连上后再单独请求 MTU(容错处理) if (Platform.OS === 'android') { try { if (typeof (device as any).requestMTU === 'function') { const newDevice = await (device as any).requestMTU?.(BLE_UUIDS.REQUEST_MTU) if (newDevice && typeof newDevice.mtu === 'number') { device = newDevice } else { console.warn('requestMTU did not return a device with MTU info, but continuing...') } await new Promise((resolve) => setTimeout(resolve, 500)) } else { console.warn('requestMTU not supported on this platform/device, but continuing...') } } catch (mtuErr) { console.warn('requestMTU failed, continuing without MTU change', mtuErr) } } await device.discoverAllServicesAndCharacteristics() console.log('Connected to device with MTU = ', device.mtu) this.connectedDevice = await device.discoverAllServicesAndCharacteristics() this.emit('connectionStateChange', { deviceId, state: ConnectionState.CONNECTED }) // Handle disconnection monitoring device.onDisconnected((error: any, disconnectedDevice?: Device) => { this.connectedDevice = null const id = disconnectedDevice?.id || device.id || deviceId this.emit('connectionStateChange', { deviceId: id, state: ConnectionState.DISCONNECTED, }) this.emit('disconnected', disconnectedDevice || { id }) }) return this.connectedDevice } catch (error: any) { this.emit('connectionStateChange', { deviceId, state: ConnectionState.DISCONNECTED }) throw this.normalizeError(error) } } async disconnect(): Promise { try { if (this.connectedDevice) { console.log(`Disconnecting from ${this.connectedDevice.id}`) const deviceId = this.connectedDevice.id // ✅ 修复:先触发断开状态,然后清理 this.emit('connectionStateChange', { deviceId, state: ConnectionState.DISCONNECTING }) try { // ✅ 修复:安全断开,设置较短超时 await Promise.race([ this.connectedDevice.cancelConnection(), new Promise((_, reject) => setTimeout(() => reject(new Error('Disconnect timeout')), 5000)), ]) } catch (disconnectError: any) { // 忽略断开连接过程中的预期错误 const errorMsg = disconnectError?.message || String(disconnectError) console.debug('Disconnect operation completed with:', errorMsg) } // ✅ 修复:无论断开是否成功,都清理状态 this.connectedDevice = null this.emit('connectionStateChange', { deviceId, state: ConnectionState.DISCONNECTED }) console.log('Device disconnected successfully') } } catch (error: any) { const errorMsg = error?.message || String(error) console.error('Disconnect error:', errorMsg) // 即使出现错误,也要清理状态 if (this.connectedDevice) { const deviceId = this.connectedDevice.id this.connectedDevice = null this.emit('connectionStateChange', { deviceId, state: ConnectionState.DISCONNECTED }) } // 只在严重错误时抛出异常 if (!this.isDisconnectionError(error)) { throw this.normalizeError(error) } } } public async read(deviceId: string, serviceUUID: string, characteristicUUID: string): Promise { if (!this.manager) throw new Error('BLE not supported on web') try { const char = await this.manager.readCharacteristicForDevice(deviceId, serviceUUID, characteristicUUID) return char.value || '' // Base64 string } catch (e) { throw this.normalizeError(e) } } public async write( deviceId: string, serviceUUID: string, characteristicUUID: string, dataBase64: string, response: boolean = true, ): Promise { if (!this.manager) throw new Error('BLE not supported on web') let result: Characteristic | null try { if (response) { result = await this.manager.writeCharacteristicWithResponseForDevice( deviceId, serviceUUID, characteristicUUID, dataBase64, ) } else { result = await this.manager.writeCharacteristicWithoutResponseForDevice( deviceId, serviceUUID, characteristicUUID, dataBase64, ) } return result } catch (e) { throw this.normalizeError(e) } } public async monitor( deviceId: string, serviceUUID: string, characteristicUUID: string, listener: (error: BleError | null, value: string | null) => void, ) { if (!this.manager) { listener({ message: 'BLE not supported on web', errorCode: -1, reason: 'platform_not_supported' }, null) return { remove: () => {}, } } try { return this.manager.monitorCharacteristicForDevice(deviceId, serviceUUID, characteristicUUID, (error, char) => { if (error) { // ✅ 修复:安全处理错误,确保不传递 null/undefined 值 const normalizedError = this.normalizeError(error) // 特别处理断开连接错误,避免崩溃 if (this.isDisconnectionError(error)) { console.debug('Device disconnected during monitoring:', normalizedError.message) } listener(normalizedError, null) } else { listener(null, char?.value || null) } }) } catch (error: any) { // ✅ 修复:捕获监听器设置时的异常 const normalizedError = this.normalizeError(error) listener(normalizedError, null) return { remove: () => {}, } } } public async requestMtu(deviceId: string, mtu: number): Promise { if (!this.manager) return 23 try { const device = await this.manager.requestMTUForDevice(deviceId, mtu) return device.mtu } catch (e) { console.warn('MTU negotiation failed', e) // iOS doesn't allow explicit MTU request usually, so we might ignore or return default return 23 } } public getConnectedDevice(): Device | null { return this.connectedDevice } private normalizeError(e: any): BleError { console.log('Normalizing BLE error:', e) // ✅ 修复:安全处理错误对象,确保所有字段都有有效值 const errorCode = typeof e?.errorCode === 'number' ? e.errorCode : -1 const message = typeof e?.message === 'string' && e.message.trim() ? e.message : typeof e === 'string' && e.trim() ? e : 'Unknown BLE error' const reason = typeof e?.reason === 'string' && e.reason.trim() ? e.reason : 'unknown' return { errorCode, message, reason, } } private isDisconnectionError(error: any): boolean { const message = error?.message || '' const reason = error?.reason || '' return ( message.toLowerCase().includes('disconnect') || message.toLowerCase().includes('gatt_conn_terminate') || reason.toLowerCase().includes('disconnect') || error?.errorCode === 19 ) // GATT_CONN_TERMINATE_PEER_USER } }