expo-duooomi-app/ble/protocol/ProtocolManager.ts

182 lines
6.0 KiB
TypeScript

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,
}
}
}