expo-ble模块测试demo

This commit is contained in:
Yudi Xiao
2025-12-10 10:29:50 +08:00
parent 709c466792
commit 41f4080264
23 changed files with 3749 additions and 141 deletions

52
ble/protocol/Constants.ts Normal file
View File

@@ -0,0 +1,52 @@
export const PROTOCOL_VERSION = "1.0.0";
export const BLE_UUIDS = {
SERVICE: '000002c4-0000-1000-8000-00805f9b34fb',
SERVICE_SHORT: 'ae00',
BROADCAST_CHARACTERISTIC: "000002c1-0000-1000-8000-00805f9b34fb",
WRITE_CHARACTERISTIC: '000002c5-0000-1000-8000-00805f9b34fb',
READ_CHARACTERISTIC: '000002c6-0000-1000-8000-00805f9b34fb',
} as const;
export const FRAME_CONSTANTS = {
HEAD_DEVICE_TO_APP: 0xb1,
HEAD_APP_TO_DEVICE: 0xc7,
MAX_DATA_SIZE: 496,
HEADER_SIZE: 8,
FOOTER_SIZE: 1,
} as const;
// 562包 427包 384字节
// b1 05 02 32 01 ab 01 f0 e2cfbfc2199a5ea27f028e208c0257c40e18e4631c6cb63356db638528f2d82973cb70dc331968fec529061c3761c78ffda94dea8e2f42c8116e569eccc7b5f764c2463f9f922829090f0b6a03341a6956cdb1ab38e1117ee3ea76f75b8516b87cf83c1ba91b010a3eef22a8aa26d5483b68aa2aa680996b77d7ebdc87fd88d565cc2b7134cc90fc6135394a5478527ba31a7d455605344798061797cc1eb78e2c1060f26f86f7d89be5acb6c96de2cb9e91216db74a3c221b1c9ad6afabb587c81ba9720cb2e5851d94b18ca55247191a607993d91c3886368167ab19109809a18f200ecf75738a006832517a0ffa712c4c59c3fd36d0c80800176598aa506faeb26597ac1cf5d7278f3a0bfb40fd2768680efeec6e67dfbbd628b83b1fe91860001917820e3f2b1e14bde5ebd838f6c785f50aee6ed1438bc2e924068038d68340dc5b27dbe8ca38b3c4d00e257d9e2b76ce9546283e988cc505e52a3eeeed2496c4d552826e9473ac9a93af3df73415a2fbddf9bc2d0bbfafa5330ba4a19eda04e034c31809d56f37d8800e0491385477003c436d7a60f3df41a3257fd38bbed251b087ca18ae7071758e65d9cdb907a052bad5b4cbf648627a9d28cbcc19e208891c4f7008469d1ee76032902ba4fee15ab970523a5a0f639ceeefaf59ca16ebe71b96fbc16069ef3719d139e514b0
export const COMMAND_TYPES = {
ACTIVATION_QUERY: 0x01,
OTA_PACKAGE: 0x02,
TRANSFER_BOOT_ANIMATION: 0x03,
TRANSFER_AVI_VIDEO: 0x04,
TRANSFER_ANI_VIDEO: 0x05,
TRANSFER_JPEG_IMAGE: 0x06,
VERSION_QUERY: 0x07,
UPDATE_ACTIVATION_TIME: 0x08,
DEVICE_INFO_SETTINGS: 0x0d,
DEVICE_IDENTITY_CHECK: 0x0e,
} as const;
export const RESPONSE_TYPES = {
ACTIVATION_STATUS: 0x01,
VERSION_INFO: 0x07,
DEVICE_INFO_REPORT: 0x0d,
IDENTITY_CHECK_RESULT: 0x0e,
} as const;
export const ERROR_CODES = {
DISCONNECT: -100,
TRANSFER_TIMEOUT: -101,
INVALID_RESPONSE: -102,
PROTOCOL_ERROR: -103,
CHECKSUM_MISMATCH: -104,
JSON_PARSE_ERROR: -105,
DEVICE_NOT_ACTIVATED: -106,
INVALID_FILE_FORMAT: -107,
TRANSFER_IN_PROGRESS: -108,
INSUFFICIENT_SPACE: -109,
} as const;

View File

@@ -0,0 +1,155 @@
import {Buffer} from 'buffer';
import {ProtocolFrame} from './types';
import {FRAME_CONSTANTS} from './Constants';
export class ProtocolManager {
static calculateChecksum(frameData: Uint8Array): number {
let sum = 0;
// Checksum is calculated on all bytes except the last one (which is the checksum itself)
// But here we are calculating FOR the last byte.
for (let i = 0; i < frameData.length; i++) {
sum += frameData[i];
}
return sum & 0xff;
}
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
};
}
}

89
ble/protocol/types.ts Normal file
View File

@@ -0,0 +1,89 @@
export interface ProtocolFrame {
head: number;
type: number;
subpageTotal: number;
curPage: number;
dataLen: number;
data: ArrayBuffer;
checksum: number;
}
export interface DeviceInfo {
allspace: number;
freespace: number;
devname: string;
size: number;
brand: number;
version?: string;
activated?: boolean;
}
export interface ActivationStatus {
type: number;
state: number;
}
export interface VersionInfo {
type: number;
version: string;
}
export interface IdentityCheckResult {
type: number;
IdCheck: string;
}
export interface ActivationTimeUpdate {
type: number;
year: number;
mon: number;
day: number;
hour: number;
min: number;
mes: number;
}
export interface PeripheralConnectionEvent {
id: string;
}
export interface PeripheralReadRequest {
centralId: string;
serviceUuid: string;
characteristicUuid: string;
}
export interface PeripheralWriteRequest {
centralId: string;
serviceUuid: string;
characteristicUuid: string;
value: string;
}
export interface FileTransferData {
type: number;
data: ArrayBuffer;
}
export interface DeviceInfoReport {
type: number;
allspace: number;
freespace: number;
devname: string;
size: number;
brand: number;
}
export interface IdentityCheckRequest {
type: number;
Ret: number;
}
export type JsonPayload =
| ActivationStatus
| VersionInfo
| DeviceInfoReport
| IdentityCheckResult
| ActivationTimeUpdate
| IdentityCheckRequest
| FileTransferData;