expo-ble模块测试demo
This commit is contained in:
306
ble/utils/ProtocolUtilsV2.ts
Normal file
306
ble/utils/ProtocolUtilsV2.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user