/** * Duooomi BLE SDK - Brownfield API Layer * * 对外暴露的蓝牙 SDK 接口,通过 expo-brownfield 的消息机制与原生 App 通信。 * 原生 App 发送 action 消息 → SDK 执行蓝牙操作 → 通过事件/SharedState 返回结果。 */ import Brownfield, { addMessageListener, sendMessage, setSharedStateValue, } from 'expo-brownfield' import { Platform, PermissionsAndroid, Linking, Alert } from 'react-native' import { ScanMode } from 'react-native-ble-plx' import { BleClient } from '../core/BleClient' import { type BleDevice, type BleError, ConnectionState } from '../core/types' import { BLE_UUIDS, COMMAND_TYPES, EVENT_TYPES } from '../protocol/Constants' import { type BindingResponse, type DeleteFileResponse, type DeviceInfo, type PrepareTransferResponse, type UnBindResponse, type VersionInfo, } from '../protocol/types' import { BleProtocolService } from '../services/BleProtocolService' import { DeviceInfoService } from '../services/DeviceInfoService' import { FileTransferService } from '../services/FileTransferService' // ====== SharedState Keys ====== const STATE_KEYS = { BT_STATE: 'btState', // 'idle' | 'scanning' | 'connecting' | 'connected' | 'disconnected' CONNECTED_DEVICE: 'connectedDevice', // { id, name } | null DEVICE_INFO: 'deviceInfo', // DeviceInfo | null VERSION: 'version', // string IS_ACTIVATED: 'isActivated', // boolean TRANSFER_PROGRESS: 'transferProgress', // number 0-100 ERROR: 'error', // string | null DISCOVERED_DEVICES: 'discoveredDevices', // Array<{ id, name, rssi }> } as const class BleSDK { private bleClient = BleClient.getInstance() private deviceInfoService = DeviceInfoService.getInstance() private fileTransferService = FileTransferService.getInstance() private protocolService = BleProtocolService.getInstance() private connectedDeviceId: string | null = null private pendingOperations = new Map< string, { resolve: (value: any) => void reject: (reason?: any) => void timeoutId: ReturnType } >() // 批量设备发现缓冲 private pendingDevices: Array<{ id: string; name: string | null; rssi: number | null }> = [] private flushTimer: ReturnType | null = null initialize() { this.setupBleListeners() this.setupMessageHandler() this.updateState(STATE_KEYS.BT_STATE, 'idle') console.log('[BleSDK] Initialized') } // ====== 设置 BLE 事件监听 ====== private setupBleListeners() { this.bleClient.addListener('connectionStateChange', ({ deviceId, state }: { deviceId: string; state: ConnectionState }) => { const stateMap: Record = { [ConnectionState.CONNECTING]: 'connecting', [ConnectionState.CONNECTED]: 'connected', [ConnectionState.DISCONNECTING]: 'disconnecting', [ConnectionState.DISCONNECTED]: 'disconnected', } this.updateState(STATE_KEYS.BT_STATE, stateMap[state] || 'idle') if (state === ConnectionState.DISCONNECTED) { this.connectedDeviceId = null this.updateState(STATE_KEYS.CONNECTED_DEVICE, null) } sendMessage({ event: 'connectionStateChange', deviceId, state }) }) this.bleClient.addListener('scanError', (error: BleError) => { this.updateState(STATE_KEYS.ERROR, error.message) sendMessage({ event: 'error', type: 'scanError', message: error.message }) }) // 设备信息响应 this.deviceInfoService.addListener(EVENT_TYPES.DEVICE_INFO.name, (info: DeviceInfo) => { this.updateState(STATE_KEYS.DEVICE_INFO, info) this.resolvePending('deviceInfo', info) sendMessage({ event: 'deviceInfo', data: info }) }) this.deviceInfoService.addListener(EVENT_TYPES.VERSION_INFO.name, (info: VersionInfo) => { this.updateState(STATE_KEYS.VERSION, info.version) this.resolvePending('versionInfo', info) sendMessage({ event: 'versionInfo', data: info }) }) this.deviceInfoService.addListener(EVENT_TYPES.BIND_DEVICE.name, (resp: BindingResponse) => { this.updateState(STATE_KEYS.IS_ACTIVATED, resp.success === 1) if (resp.success === 1) { this.resolvePending('bindDevice', resp) } else { this.rejectPending('bindDevice', 'Device already bound to another user') } sendMessage({ event: 'bindDevice', data: resp }) }) this.deviceInfoService.addListener(EVENT_TYPES.UNBIND_DEVICE.name, (resp: UnBindResponse) => { if (resp.success === 1) { this.updateState(STATE_KEYS.IS_ACTIVATED, false) this.resolvePending('unbindDevice', resp) } else { this.rejectPending('unbindDevice', 'Unbind failed') } sendMessage({ event: 'unbindDevice', data: resp }) }) this.deviceInfoService.addListener(EVENT_TYPES.DELETE_FILE.name, (resp: DeleteFileResponse) => { if (resp.success === 0) { this.resolvePending('deleteFile', resp) } else { this.rejectPending('deleteFile', resp.success === 2 ? 'File not found' : 'Delete failed') } sendMessage({ event: 'deleteFile', data: resp }) }) this.deviceInfoService.addListener(EVENT_TYPES.PREPARE_TRANSFER.name, (resp: PrepareTransferResponse) => { const opId = `prepareTransfer_${resp.key}` if (resp.status === 'ready') { this.resolvePending(opId, resp) } else { this.rejectPending(opId, resp.status === 'no_space' ? 'No space on device' : 'File already exists') } sendMessage({ event: 'prepareTransfer', data: resp }) }) } // ====== 接收原生 App 的消息指令 ====== private setupMessageHandler() { addMessageListener((event: Record) => { const { action, ...params } = event console.log(`[BleSDK] Received action: ${action}`, params) switch (action) { case 'scan': this.startScan() break case 'stopScan': this.stopScan() break case 'connect': this.connect(params.deviceId) break case 'disconnect': this.disconnect() break case 'getDeviceInfo': this.getDeviceInfo() break case 'getVersion': this.getVersion() break case 'bind': this.bind(params.userId) break case 'unbind': this.unbind(params.userId) break case 'deleteFile': this.deleteFile(params.key) break case 'transferFile': this.transferFile(params.fileUri, params.commandType) break default: sendMessage({ event: 'error', type: 'unknownAction', message: `Unknown action: ${action}` }) } }) } // ====== 蓝牙操作实现 ====== private async startScan() { try { const hasPerms = await this.requestPermissions() if (!hasPerms) { sendMessage({ event: 'error', type: 'permission', message: 'Bluetooth permission denied' }) return } this.updateState(STATE_KEYS.BT_STATE, 'scanning') this.updateState(STATE_KEYS.DISCOVERED_DEVICES, []) this.pendingDevices = [] await this.bleClient.startScan( [BLE_UUIDS.SERVICE], { scanMode: ScanMode.Balanced, allowDuplicates: false }, (result) => { const device = result.device as BleDevice const hasService = device.serviceUUIDs?.some( (uuid) => uuid?.toLowerCase() === BLE_UUIDS.SERVICE.toLowerCase() ) if (!hasService || !device?.id) return this.queueDevice({ id: device.id, name: device.name || device.localName || null, rssi: result.rssi, }) } ) sendMessage({ event: 'scanStarted' }) } catch (e: any) { this.updateState(STATE_KEYS.BT_STATE, 'idle') this.updateState(STATE_KEYS.ERROR, e.message) sendMessage({ event: 'error', type: 'scan', message: e.message }) } } private stopScan() { this.bleClient.stopScan() this.flushDevices() this.updateState(STATE_KEYS.BT_STATE, this.connectedDeviceId ? 'connected' : 'idle') sendMessage({ event: 'scanStopped' }) } private async connect(deviceId: string) { if (!deviceId) { sendMessage({ event: 'error', type: 'connect', message: 'deviceId is required' }) return } this.stopScan() try { this.updateState(STATE_KEYS.BT_STATE, 'connecting') const device = await this.bleClient.connect(deviceId) this.connectedDeviceId = deviceId await this.protocolService.initialize(deviceId) this.updateState(STATE_KEYS.CONNECTED_DEVICE, { id: device.id, name: device.name || device.localName || null, }) this.updateState(STATE_KEYS.BT_STATE, 'connected') sendMessage({ event: 'connected', deviceId: device.id, name: device.name }) // 自动获取设备信息 this.getDeviceInfo().catch(() => {}) } catch (e: any) { this.updateState(STATE_KEYS.BT_STATE, 'idle') this.updateState(STATE_KEYS.ERROR, e.message) sendMessage({ event: 'error', type: 'connect', message: e.message }) } } private async disconnect() { try { await this.bleClient.disconnect() this.protocolService.disconnect() this.connectedDeviceId = null this.updateState(STATE_KEYS.CONNECTED_DEVICE, null) this.updateState(STATE_KEYS.DEVICE_INFO, null) this.updateState(STATE_KEYS.VERSION, '') this.updateState(STATE_KEYS.IS_ACTIVATED, false) this.updateState(STATE_KEYS.BT_STATE, 'disconnected') sendMessage({ event: 'disconnected' }) } catch (e: any) { sendMessage({ event: 'error', type: 'disconnect', message: e.message }) } } private async getDeviceInfo() { if (!this.connectedDeviceId) return try { const promise = this.createWaitable('deviceInfo') await this.deviceInfoService.getDeviceInfo(this.connectedDeviceId) return await promise } catch (e: any) { sendMessage({ event: 'error', type: 'deviceInfo', message: e.message }) } } private async getVersion() { if (!this.connectedDeviceId) return try { const promise = this.createWaitable('versionInfo') await this.deviceInfoService.getDeviceVersion(this.connectedDeviceId) return await promise } catch (e: any) { sendMessage({ event: 'error', type: 'version', message: e.message }) } } private async bind(userId: string) { if (!this.connectedDeviceId) return try { const promise = this.createWaitable('bindDevice') await this.deviceInfoService.bindDevice(this.connectedDeviceId, userId) return await promise } catch (e: any) { sendMessage({ event: 'error', type: 'bind', message: e.message }) } } private async unbind(userId: string) { if (!this.connectedDeviceId) return try { const promise = this.createWaitable('unbindDevice') await this.deviceInfoService.unbindDevice(this.connectedDeviceId, userId) return await promise } catch (e: any) { sendMessage({ event: 'error', type: 'unbind', message: e.message }) } } private async deleteFile(key: string) { if (!this.connectedDeviceId) return try { const promise = this.createWaitable('deleteFile') await this.deviceInfoService.deleteFile(this.connectedDeviceId, key) return await promise } catch (e: any) { sendMessage({ event: 'error', type: 'deleteFile', message: e.message }) } } private async transferFile(fileUri: string, commandType?: number) { if (!this.connectedDeviceId) return try { this.updateState(STATE_KEYS.TRANSFER_PROGRESS, 0) const type = commandType ?? COMMAND_TYPES.TRANSFER_ANI_VIDEO await this.fileTransferService.transferFile( this.connectedDeviceId, fileUri, type, (progress) => { const pct = Math.round(progress * 100) this.updateState(STATE_KEYS.TRANSFER_PROGRESS, pct) sendMessage({ event: 'transferProgress', progress: pct }) } ) sendMessage({ event: 'transferComplete' }) } catch (e: any) { this.updateState(STATE_KEYS.ERROR, e.message) sendMessage({ event: 'error', type: 'transfer', message: e.message }) } } // ====== 工具方法 ====== private updateState(key: string, value: any) { try { setSharedStateValue(key, value) } catch (e) { console.warn(`[BleSDK] Failed to set shared state ${key}:`, e) } } private queueDevice(device: { id: string; name: string | null; rssi: number | null }) { if (this.pendingDevices.some((d) => d.id === device.id)) return this.pendingDevices.push(device) if (this.flushTimer) clearTimeout(this.flushTimer) this.flushTimer = setTimeout(() => this.flushDevices(), 500) } private flushDevices() { if (this.pendingDevices.length === 0) return const devices = [...this.pendingDevices] this.pendingDevices = [] this.updateState(STATE_KEYS.DISCOVERED_DEVICES, devices) sendMessage({ event: 'devicesFound', devices }) } private createWaitable(opId: string, timeout = 10000): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.pendingOperations.delete(opId) reject(new Error(`Operation ${opId} timeout`)) }, timeout) this.pendingOperations.set(opId, { resolve, reject, timeoutId }) }) } private resolvePending(opId: string, value: any) { const pending = this.pendingOperations.get(opId) if (pending) { clearTimeout(pending.timeoutId) this.pendingOperations.delete(opId) pending.resolve(value) } } private rejectPending(opId: string, error: string) { const pending = this.pendingOperations.get(opId) if (pending) { clearTimeout(pending.timeoutId) this.pendingOperations.delete(opId) pending.reject(new Error(error)) } } private async requestPermissions(): Promise { if (Platform.OS !== 'android') return true try { const sdk = Number(Platform.Version) || 0 const perms: string[] = [] if (sdk >= 31) { perms.push(PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN) perms.push(PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT) } else { perms.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION) } const results = await PermissionsAndroid.requestMultiple(perms as any) return Object.values(results).every((r) => r === PermissionsAndroid.RESULTS.GRANTED) } catch { return false } } } export const bleSDK = new BleSDK()