import { type APP_COMMAND_TYPES, FRAME_CONSTANTS, type FRAME_HEAD } from './Constants' import { type ProtocolFrame } from './types' 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 { const calculatedChecksum = this.calculateChecksum(frameData) const isValid = calculatedChecksum === expectedChecksum if (!isValid) { console.warn( `[ProtocolManager] Checksum mismatch: calculated=0x${calculatedChecksum.toString(16)}, expected=0x${expectedChecksum.toString(16)}`, ) } console.log('verifyChecksum-----------------', isValid) return isValid } 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, } } }