Files
expo-ble-app-demo/ble/utils/ProtocolUtilsV2.ts
2025-12-10 10:29:50 +08:00

307 lines
9.1 KiB
TypeScript

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