import { Buffer } from 'buffer' import { type Subscription } from 'react-native-ble-plx' import { BleClient } from '../core/BleClient' import { type APP_COMMAND_TYPES, BLE_UUIDS, FRAME_CONSTANTS } from '../protocol/Constants' import { ProtocolManager } from '../protocol/ProtocolManager' import { type ProtocolFrame } from '../protocol/types' export class BleProtocolService { private static instance: BleProtocolService private client = BleClient.getInstance() private listeners: Map void>> = new Map() private subscription: Subscription | null = null // deviceId_type -> { total: number, frames: ArrayBuffer[] } private fragments: Map = new Map() private constructor() {} public static getInstance(): BleProtocolService { if (!BleProtocolService.instance) { BleProtocolService.instance = new BleProtocolService() } return BleProtocolService.instance } public addListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) { if (!this.listeners.has(type)) { this.listeners.set(type, new Set()) } this.listeners.get(type)!.add(callback) } public removeListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) { if (this.listeners.has(type)) { this.listeners.get(type)!.delete(callback) } } private emit(type: number, data: ArrayBuffer, deviceId: string) { if (this.listeners.has(type)) { this.listeners.get(type)!.forEach((cb) => cb(data, deviceId)) } } public async initialize(deviceId: string) { // Clean up previous subscription this.disconnect() // Clear fragments for this device this.clearFragments(deviceId) try { console.log('Initializing BLE protocol service for device:', deviceId) this.subscription = await this.client.monitor( deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.READ_CHARACTERISTIC, (error, value) => { if (error) { // ✅ 修复:增强错误处理,避免崩溃 const errorMsg = error.message || 'Unknown monitor error' // 忽略预期的断开连接错误 if (this.isExpectedDisconnectionError(error)) { console.debug('Device disconnected during monitoring:', errorMsg) return } // 忽略已知的原生崩溃错误 if (this.isKnownNativeCrashError(error)) { console.debug('Ignored known native monitor error:', errorMsg) return } console.warn('BLE monitor error:', errorMsg) return } if (value) { try { const buffer = Buffer.from(value, 'base64') const hexString = buffer .toString('hex') .match(/.{1,2}/g) ?.join(' ') .toUpperCase() || '' console.log(`[BleProtocol] Received ${buffer.byteLength} bytes:`, hexString) this.handleRawData(deviceId, buffer) } catch (parseError: any) { console.warn('Failed to parse received data:', parseError.message) } } }, ) console.log('BLE protocol service initialized successfully') } catch (error: any) { console.error('Failed to initialize protocol service:', error.message) throw error } } public disconnect() { if (this.subscription) { try { console.log('Cleaning up BLE protocol subscription...') this.subscription.remove() console.log('BLE protocol subscription removed successfully') } catch (e: any) { // ✅ 修复:安全处理订阅清理错误,避免崩溃 const errorMsg = e?.message || String(e) console.debug('Subscription cleanup completed with warning:', errorMsg) } finally { // ✅ 确保订阅引用被清理 this.subscription = null } } // ✅ 清理分片缓存 this.fragments.clear() console.log('BLE protocol service disconnected') } private handleRawData(deviceId: string, data: Buffer) { const frame = ProtocolManager.parseFrame(data.buffer) if (!frame) return if (frame.subpageTotal > 0) { this.handleFragment(deviceId, frame) } else { this.emit(frame.type, frame.data, deviceId) } } private handleFragment(deviceId: string, frame: ProtocolFrame) { const key = `${deviceId}_${frame.type}` if (!this.fragments.has(key)) { this.fragments.set(key, { total: frame.subpageTotal, frames: new Array(frame.subpageTotal).fill(null), }) } const session = this.fragments.get(key)! // Basic validation if (frame.curPage >= session.total) return session.frames[frame.curPage] = frame.data // Check if complete if (session.frames.every((f) => f !== null)) { const combinedLength = session.frames.reduce((acc, val) => acc + (val ? val.byteLength : 0), 0) const combined = new Uint8Array(combinedLength) let offset = 0 // Reassemble from High to Low pages for (let i = session.total - 1; i >= 0; i--) { const part = session.frames[i] if (part) { combined.set(new Uint8Array(part), offset) offset += part.byteLength } } this.fragments.delete(key) this.emit(frame.type, combined.buffer as ArrayBuffer, deviceId) } } private clearFragments(deviceId: string) { for (const key of this.fragments.keys()) { if (key.startsWith(deviceId)) { this.fragments.delete(key) } } } public async send( deviceId: string, type: APP_COMMAND_TYPES, data: object | ArrayBuffer | Uint8Array, onProgress?: (progress: number) => void, ): Promise { let payload: Uint8Array if (data instanceof ArrayBuffer) { console.debug('[BleProtocolService] Sending ArrayBuffer') payload = new Uint8Array(data) } else if (data instanceof Uint8Array) { console.debug('[BleProtocolService] Sending Uint8Array') payload = data } else { console.debug('[BleProtocolService] Sending JSON payload') const jsonStr = JSON.stringify(data) payload = new Uint8Array(Buffer.from(jsonStr)) } const device = this.client.getConnectedDevice() const mtu = device?.mtu || 23 // MTU - 3 bytes (ATT overhead) - Protocol Header - Protocol Footer const maxPayloadSize = mtu - 3 - FRAME_CONSTANTS.HEADER_SIZE - FRAME_CONSTANTS.FOOTER_SIZE // Ensure reasonable bounds (at least 1 byte, max constrained by protocol constant) const safeMaxDataSize = Math.max(1, Math.min(maxPayloadSize, FRAME_CONSTANTS.MAX_DATA_SIZE)) console.debug(`[BleProtocolService] Sending with MTU=${mtu}, maxDataSize=${safeMaxDataSize}`) const rawPayloadHex = payload.reduce((acc, val) => acc + val.toString(16).padStart(2, '0') + ' ', '') const formattedRawPayloadHex = rawPayloadHex.substring(0, 512 * 2) + '\n......\n' + rawPayloadHex.substring(rawPayloadHex.length - 512 * 2) console.debug( `[BleProtocolService] Sending payload size=${payload.byteLength}, raw payload hex=\n${formattedRawPayloadHex}`, ) const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize) const total = frames.length console.debug(`Sending ${total} frames`) for (let i = 0; i < total; i++) { const frame = frames[i] if (i < 3) { const rawFrame = Array.from(frame) .map((b) => b.toString(16).padStart(2, '0')) .join(' ') console.debug(`raw ${i + 1} frame \n ${rawFrame}`) } console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`) const base64 = Buffer.from(frame).toString('base64') const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false) await new Promise((resolve) => setTimeout(resolve, FRAME_CONSTANTS.FRAME_INTERVAL)) if (onProgress) { onProgress((i + 1) / total) } // console.debug("Wrote frame", result); } } private isExpectedDisconnectionError(error: any): boolean { const message = error?.message?.toLowerCase() || '' const reason = error?.reason?.toLowerCase() || '' return ( message.includes('disconnect') || message.includes('gatt_conn_terminate') || message.includes('connection terminated') || reason.includes('disconnect') || error?.errorCode === 19 ) // GATT_CONN_TERMINATE_PEER_USER } private isKnownNativeCrashError(error: any): boolean { const message = error?.message || '' const reason = error?.reason || '' return ( (error.errorCode === 0 && message.includes('Unknown error') && reason?.includes('PromiseImpl.reject')) || message.includes('Parameter specified as non-null is null') || reason?.includes('SafePromise.reject') ) } }