import {BleClient} from '../core/BleClient'; import {ProtocolManager} from '../protocol/ProtocolManager'; import {BLE_UUIDS, APP_COMMAND_TYPES, FRAME_CONSTANTS} from '../protocol/Constants'; import {ProtocolFrame} from '../protocol/types'; import {Buffer} from 'buffer'; import {Subscription} from 'react-native-ble-plx'; 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); 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'); console.log(`[BleProtocol] Received ${buffer.byteLength} bytes:`, buffer.toString('hex')); this.handleRawData(deviceId, buffer); } } ); } 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(); 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); } } }