forked from yudi_xiao/expo-ble-app-demo
162 lines
5.9 KiB
TypeScript
162 lines
5.9 KiB
TypeScript
import {ProtocolFrame} from './types';
|
|
import {FRAME_CONSTANTS} 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 checksumV1 = (0 - sum) & 0xff
|
|
const checksum = (~sum + 1) & 0xff
|
|
console.debug(`[ProtocolManager] Checksum V1 calculated: 0 - ${sum} = ${checksumV1}`);
|
|
console.debug(`[ProtocolManager] Checksum calculated: 0 - ${sum} = ${checksum}`);
|
|
return checksum;
|
|
}
|
|
|
|
static verifyChecksum(frameData: Uint8Array, expectedChecksum: number): boolean {
|
|
const calculated = this.calculateChecksum(frameData);
|
|
return calculated === expectedChecksum;
|
|
}
|
|
|
|
static createFrame(
|
|
type: number,
|
|
data: Uint8Array,
|
|
head: number = FRAME_CONSTANTS.HEAD_APP_TO_DEVICE,
|
|
requireFragmentation: boolean = true
|
|
): Uint8Array[] {
|
|
if (data.length <= FRAME_CONSTANTS.MAX_DATA_SIZE || !requireFragmentation) {
|
|
console.debug(`[ProtocolManager] Frame size ${data.length} is less than max size ${FRAME_CONSTANTS.MAX_DATA_SIZE}, not fragmented`);
|
|
return [this.createSingleFrame(type, data, head)];
|
|
} else {
|
|
console.debug(`[ProtocolManager] Frame size ${data.length} is greater than max size ${FRAME_CONSTANTS.MAX_DATA_SIZE}, fragmented`);
|
|
return this.createFragmentedFrames(type, data, head);
|
|
}
|
|
}
|
|
|
|
private static createSingleFrame(type: number, data: Uint8Array, head: number): 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;
|
|
|
|
// 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: number, data: Uint8Array, head: number): Uint8Array[] {
|
|
const frames: Uint8Array[] = [];
|
|
const totalSize = data.length;
|
|
const maxDataSize = FRAME_CONSTANTS.MAX_DATA_SIZE;
|
|
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;
|
|
|
|
// curPage (descending order usually? No, BleManagerV2 comments said: "Protocol specifies: page numbers count down from highest to 0")
|
|
// Wait, ProtocolUtilsV2 code:
|
|
// const curPageVal = totalPages - 1 - i;
|
|
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;
|
|
console.debug(`chunk length = ${chunk.length}, buffer 8 header = ${buffer.slice(0, 8).toString()}`)
|
|
// 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
|
|
};
|
|
}
|
|
}
|