import {ProtocolFrame} from './types'; import {FRAME_CONSTANTS, APP_COMMAND_TYPES, FRAME_HEAD} from './Constants'; export class ProtocolManager { static calculateChecksum(frameData: Uint8Array): number { let sum = 0; // console.debug(`[ProtocolManager] Calculating checksum for frame count: ${frameData.length}`); // Checksum is calculated on all bytes except the last one (which is the checksum itself) // Example: 0xA0 03 00 01 01 5B // 0xA0 + 0x03 + 0x00 + 0x01 + 0x01 = 0xA5 // 0 - 0xA5 = 0x5B for (let i = 0; i < frameData.length; i++) { sum += frameData[i]; } const checksum = (~sum + 1) & 0xff // console.debug(`[ProtocolManager] Checksum calculated: 0 - ${sum} = ${checksum.toString(16).padStart(2, '0')}`); return checksum; } static verifyChecksum(frameData: Uint8Array, expectedChecksum: number): boolean { return (frameData.length + expectedChecksum) === 0; } static createFrame( type: APP_COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD = FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, maxDataSize: number = FRAME_CONSTANTS.MAX_DATA_SIZE ): Uint8Array[] { // Max pages index is 4 bytes, so we can fit up to 65535 pages of data const maxTotalSize = 0xffff * (FRAME_CONSTANTS.HEADER_SIZE + maxDataSize + FRAME_CONSTANTS.FOOTER_SIZE); if (data.length > maxTotalSize) { throw new Error(`Data size ${data.length} exceeds max size ${maxTotalSize}`); } if (data.length <= maxDataSize) { console.debug(`[ProtocolManager] Frame size ${data.length} is less than max size ${maxDataSize}, not fragmented`); return [this.createSingleFrame(type, data, head)]; } else { console.debug(`[ProtocolManager] Frame size ${data.length} is greater than max size ${maxDataSize}, fragmented`); return this.createFragmentedFrames(type, data, head, maxDataSize); } } private static createSingleFrame(type: APP_COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD): Uint8Array { const buffer = new Uint8Array(FRAME_CONSTANTS.HEADER_SIZE + data.length + FRAME_CONSTANTS.FOOTER_SIZE); let offset = 0; buffer[offset++] = head; buffer[offset++] = type; // subpageTotal = 0 buffer[offset++] = 0; buffer[offset++] = 0; // curPage = 0 buffer[offset++] = 0; buffer[offset++] = 0; // dataLen buffer[offset++] = (data.length >> 8) & 0xff; buffer[offset++] = data.length & 0xff; const hexHeader = Array.from(buffer.slice(0, offset)).map(b => b.toString(16).padStart(2, '0')).join(' '); // console.debug(`chunk length = ${data.length}, buffer 8 header = ${hexHeader}`) // data buffer.set(data, offset); offset += data.length; // checksum // Logic from ProtocolUtilsV2: calculate sum of everything before checksum byte const checksum = this.calculateChecksum(buffer.slice(0, offset)); buffer[offset] = checksum; return buffer; } private static createFragmentedFrames(type: APP_COMMAND_TYPES, data: Uint8Array, head: FRAME_HEAD, maxDataSize: number): Uint8Array[] { const frames: Uint8Array[] = []; const totalSize = data.length; const totalPages = Math.ceil(totalSize / maxDataSize); for (let i = 0; i < totalPages; i++) { const start = i * maxDataSize; const end = Math.min(start + maxDataSize, totalSize); const chunk = data.slice(start, end); const buffer = new Uint8Array(FRAME_CONSTANTS.HEADER_SIZE + chunk.length + FRAME_CONSTANTS.FOOTER_SIZE); let offset = 0; buffer[offset++] = head; buffer[offset++] = type; // subpageTotal buffer[offset++] = (totalPages >> 8) & 0xff; buffer[offset++] = totalPages & 0xff; // Protocol specifies: page numbers count down from highest to 0 const curPageVal = totalPages - 1 - i; buffer[offset++] = (curPageVal >> 8) & 0xff; buffer[offset++] = curPageVal & 0xff; // dataLen buffer[offset++] = (chunk.length >> 8) & 0xff; buffer[offset++] = chunk.length & 0xff; // const hexHeader = Array.from(buffer.slice(0, offset)).map(b => b.toString(16).padStart(2, '0')).join(' '); // console.debug(`chunk length = ${chunk.length}, buffer 8 header = ${hexHeader}`) // data buffer.set(chunk, offset); offset += chunk.length; buffer[offset] = this.calculateChecksum(buffer.slice(0, offset)); frames.push(buffer); } return frames; } static parseFrame(data: ArrayBufferLike): ProtocolFrame | null { const bytes = new Uint8Array(data); if (bytes.length < FRAME_CONSTANTS.HEADER_SIZE + FRAME_CONSTANTS.FOOTER_SIZE) { return null; } const head = bytes[0]; if (head !== FRAME_CONSTANTS.HEAD_DEVICE_TO_APP && head !== FRAME_CONSTANTS.HEAD_APP_TO_DEVICE) { console.warn(`[ProtocolManager] Invalid frame header: 0x${head.toString(16)}`); return null; } const type = bytes[1]; const subpageTotal = (bytes[2] << 8) | bytes[3]; const curPage = (bytes[4] << 8) | bytes[5]; const dataLen = (bytes[6] << 8) | bytes[7]; if (bytes.length < FRAME_CONSTANTS.HEADER_SIZE + dataLen + FRAME_CONSTANTS.FOOTER_SIZE) { // Incomplete return null; } const frameData = bytes.slice(FRAME_CONSTANTS.HEADER_SIZE, FRAME_CONSTANTS.HEADER_SIZE + dataLen); const checksum = bytes[FRAME_CONSTANTS.HEADER_SIZE + dataLen]; // Verify checksum const dataToCheck = bytes.slice(0, FRAME_CONSTANTS.HEADER_SIZE + dataLen); if (!this.verifyChecksum(dataToCheck, checksum)) { console.warn("Checksum mismatch"); return null; } return { head, type, subpageTotal, curPage, dataLen, data: frameData.buffer as ArrayBuffer, checksum }; } }