307 lines
9.1 KiB
TypeScript
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;
|
|
}
|
|
}
|