import { Buffer } from 'buffer'; import { FRAME_CONSTANTS as PROTOCOL_V2_FRAME, ERROR_CODES as BLE_V2_ERROR_CODES } from '../protocol/Constants'; import { ProtocolFrame, JsonPayload } from '../protocol/types'; /** * Protocol utilities for BLE V2.1.1 communication * Handles JSON data parsing, binary framing, and checksum calculation */ export class ProtocolUtilsV2 { /** * Calculate checksum for protocol frame * New algorithm: 0 - (sum of all bytes except checksum) */ public static calculateChecksum(frameData: Uint8Array): number { let sum = 0; for (let i = 0; i < frameData.length; i++) { sum += frameData[i]; } return (256 - (sum % 256)) % 256; // Equivalent to 0 - sum modulo 256 } /** * Verify checksum of received frame */ public static verifyChecksum(frameData: Uint8Array, expectedChecksum: number): boolean { const calculatedChecksum = this.calculateChecksum(frameData); return calculatedChecksum === expectedChecksum; } /** * Convert ArrayBuffer to Uint8Array */ public static arrayBufferToUint8Array(buffer: ArrayBuffer): Uint8Array { return new Uint8Array(buffer); } /** * Convert Uint8Array to ArrayBuffer */ public static uint8ArrayToArrayBuffer(array: Uint8Array): ArrayBuffer { return new Uint8Array(array).buffer; } /** * Create protocol frame for sending to device */ public static createFrame( type: number, jsonData: string, requireFragmentation: boolean = false, head: number = PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE ): ArrayBuffer[] { const dataBytes = Buffer.from(jsonData, 'utf8'); const frames: ArrayBuffer[] = []; if (!requireFragmentation && dataBytes.length <= PROTOCOL_V2_FRAME.MAX_DATA_SIZE) { // Single frame const frame = this.createSingleFrame(type, dataBytes, head); frames.push(frame); } else { // Fragmented frames const fragmentedFrames = this.createFragmentedFrames(type, dataBytes, head); frames.push(...fragmentedFrames); } return frames; } /** * Create a single protocol frame */ private static createSingleFrame( type: number, dataBytes: Buffer, head: number = PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE ): ArrayBuffer { const frameSize = PROTOCOL_V2_FRAME.HEADER_SIZE + dataBytes.length + PROTOCOL_V2_FRAME.FOOTER_SIZE; const frame = new Uint8Array(frameSize); let offset = 0; // Header frame[offset++] = head; frame[offset++] = type; frame[offset++] = 0x00; // Subpage total (high byte) frame[offset++] = 0x00; // Subpage total (low byte) frame[offset++] = 0x00; // Current page (high byte) frame[offset++] = 0x00; // Current page (low byte) frame[offset++] = (dataBytes.length >> 8) & 0xFF; // Data length (high byte) frame[offset++] = dataBytes.length & 0xFF; // Data length (low byte) // Data for (let i = 0; i < dataBytes.length; i++) { frame[offset++] = dataBytes[i]; } // Calculate and add checksum const checksumData = frame.slice(0, -1); // All bytes except checksum const checksum = this.calculateChecksum(checksumData); frame[offset] = checksum; return this.uint8ArrayToArrayBuffer(frame); } /** * Create fragmented protocol frames */ private static createFragmentedFrames( type: number, dataBytes: Buffer, head: number = PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE ): ArrayBuffer[] { const frames: ArrayBuffer[] = []; const totalFrames = Math.ceil(dataBytes.length / PROTOCOL_V2_FRAME.MAX_DATA_SIZE); for (let i = 0; i < totalFrames; i++) { const startOffset = i * PROTOCOL_V2_FRAME.MAX_DATA_SIZE; const endOffset = Math.min(startOffset + PROTOCOL_V2_FRAME.MAX_DATA_SIZE, dataBytes.length); const chunkSize = endOffset - startOffset; const chunk = dataBytes.slice(startOffset, endOffset); const frameSize = PROTOCOL_V2_FRAME.HEADER_SIZE + chunkSize + PROTOCOL_V2_FRAME.FOOTER_SIZE; const frame = new Uint8Array(frameSize); let offset = 0; // Header frame[offset++] = head; frame[offset++] = type; frame[offset++] = (totalFrames >> 8) & 0xFF; // Subpage total (high byte) frame[offset++] = totalFrames & 0xFF; // Subpage total (low byte) frame[offset++] = (i >> 8) & 0xFF; // Current page (high byte) frame[offset++] = i & 0xFF; // Current page (low byte) frame[offset++] = (chunkSize >> 8) & 0xFF; // Data length (high byte) frame[offset++] = chunkSize & 0xFF; // Data length (low byte) // Data for (let j = 0; j < chunkSize; j++) { frame[offset++] = chunk[j]; } // Calculate and add checksum const checksumData = frame.slice(0, -1); // All bytes except checksum const checksum = this.calculateChecksum(checksumData); frame[offset] = checksum; frames.push(this.uint8ArrayToArrayBuffer(frame)); } return frames; } /** * Parse received protocol frame */ public static parseFrame(frameData: ArrayBuffer): ProtocolFrame | null { try { const bytes = this.arrayBufferToUint8Array(frameData); if (bytes.length < PROTOCOL_V2_FRAME.HEADER_SIZE + PROTOCOL_V2_FRAME.FOOTER_SIZE) { console.error('ProtocolUtilsV2: Frame too short'); return null; } let offset = 0; // Parse header const head = bytes[offset++]; const type = bytes[offset++]; const subpageTotal = (bytes[offset++] << 8) | bytes[offset++]; const curPage = (bytes[offset++] << 8) | bytes[offset++]; const dataLen = (bytes[offset++] << 8) | bytes[offset++]; // Validate data length - allow extra bytes for BLE MTU padding const expectedFrameSize = PROTOCOL_V2_FRAME.HEADER_SIZE + dataLen + PROTOCOL_V2_FRAME.FOOTER_SIZE; if (bytes.length < expectedFrameSize) { console.error(`ProtocolUtilsV2: Frame too short. Expected: ${expectedFrameSize}, Actual: ${bytes.length}`); return null; } // Trim to expected size to remove BLE padding bytes if (bytes.length > expectedFrameSize) { console.log(`ProtocolUtilsV2: Trimming frame from ${bytes.length} to ${expectedFrameSize} bytes (removing BLE padding)`); const trimmedBytes = bytes.slice(0, expectedFrameSize); return this.parseFrame(this.uint8ArrayToArrayBuffer(trimmedBytes)); } // Extract data const dataBytes = bytes.slice(offset, offset + dataLen); const data = this.uint8ArrayToArrayBuffer(dataBytes); offset += dataLen; // Verify checksum const checksum = bytes[offset]; const frameWithoutChecksum = bytes.slice(0, -1); if (!this.verifyChecksum(frameWithoutChecksum, checksum)) { console.error(`ProtocolUtilsV2: Checksum mismatch. Expected: ${checksum}, Calculated: ${this.calculateChecksum(frameWithoutChecksum)}`); return null; } return { head, type, subpageTotal, curPage, dataLen, data, checksum }; } catch (error) { console.error('ProtocolUtilsV2: Error parsing frame', error); return null; } } /** * Parse JSON data from frame */ public static parseJsonData(data: ArrayBuffer): JsonPayload | null { try { const jsonString = Buffer.from(data).toString('utf8'); const jsonData = JSON.parse(jsonString); return jsonData; } catch (error) { console.error('ProtocolUtilsV2: Error parsing JSON data', error); return null; } } /** * Convert object to JSON string */ public static toJsonString(obj: object): string { return JSON.stringify(obj); } /** * Convert binary data to base64 for JSON embedding */ public static arrayBufferToBase64(buffer: ArrayBuffer): string { return Buffer.from(buffer).toString('base64'); } /** * Convert base64 string to ArrayBuffer */ public static base64ToArrayBuffer(base64: string): ArrayBuffer { const buffer = Buffer.from(base64, 'base64'); return this.uint8ArrayToArrayBuffer(buffer); } /** * Validate frame header */ public static isValidFrameHead(head: number): boolean { return head === PROTOCOL_V2_FRAME.HEAD_DEVICE_TO_APP || head === PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE; } /** * Check if frame is from device */ public static isDeviceFrame(frame: ProtocolFrame): boolean { return frame.head === PROTOCOL_V2_FRAME.HEAD_DEVICE_TO_APP; } /** * Check if frame is from app */ public static isAppFrame(frame: ProtocolFrame): boolean { return frame.head === PROTOCOL_V2_FRAME.HEAD_APP_TO_DEVICE; } /** * Check if frame is fragmented */ public static isFragmentedFrame(frame: ProtocolFrame): boolean { return frame.subpageTotal > 0; } /** * Get hex string for debugging */ public static bytesToHex(bytes: ArrayBuffer): string { const buffer = Buffer.from(bytes); return buffer.toString('hex').toUpperCase(); } /** * Create error response */ public static createErrorResponse(errorCode: number, message: string): JsonPayload { return { type: 0xFF, // Custom error type error: errorCode, message } as any; } }