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 { this.subscription = await this.client.monitor( deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.READ_CHARACTERISTIC, (error, value) => { if (error) { // Check for known native crash error and ignore/log as debug if ( error.errorCode === 0 && error.message.includes('Unknown error') && error.reason?.includes('PromiseImpl.reject') ) { console.debug('Ignored native monitor error', error) return } console.warn('Monitor error', error) return } if (value) { 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 (error) { console.error('Failed to initialize protocol service:', error) throw error } } public disconnect() { if (this.subscription) { try { this.subscription.remove() } catch (e) { console.warn('Failed to remove subscription', e) } this.subscription = null } } 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() console.log('send-------------device', device) 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); } } }