expo-ble模块测试demo
This commit is contained in:
165
ble/services/BleProtocolService.ts
Normal file
165
ble/services/BleProtocolService.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import {BleClient} from '../core/BleClient';
|
||||
import {ProtocolManager} from '../protocol/ProtocolManager';
|
||||
import {BLE_UUIDS} from '../protocol/Constants';
|
||||
import {ProtocolFrame} from '../protocol/types';
|
||||
import {Buffer} from 'buffer';
|
||||
import {Subscription} from 'react-native-ble-plx';
|
||||
|
||||
export class BleProtocolService {
|
||||
private static instance: BleProtocolService;
|
||||
private client = BleClient.getInstance();
|
||||
private listeners: Map<number, Set<(data: ArrayBuffer, deviceId: string) => void>> = new Map();
|
||||
private subscription: Subscription | null = null;
|
||||
|
||||
// deviceId_type -> { total: number, frames: ArrayBuffer[] }
|
||||
private fragments: Map<string, { total: number, frames: (ArrayBuffer | null)[] }> = new Map();
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
public static getInstance(): BleProtocolService {
|
||||
if (!BleProtocolService.instance) {
|
||||
BleProtocolService.instance = new BleProtocolService();
|
||||
}
|
||||
return BleProtocolService.instance;
|
||||
}
|
||||
|
||||
public addListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) {
|
||||
if (!this.listeners.has(type)) {
|
||||
this.listeners.set(type, new Set());
|
||||
}
|
||||
this.listeners.get(type)!.add(callback);
|
||||
}
|
||||
|
||||
public removeListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) {
|
||||
if (this.listeners.has(type)) {
|
||||
this.listeners.get(type)!.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private emit(type: number, data: ArrayBuffer, deviceId: string) {
|
||||
if (this.listeners.has(type)) {
|
||||
this.listeners.get(type)!.forEach(cb => cb(data, deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
public async initialize(deviceId: string) {
|
||||
// Clean up previous subscription
|
||||
this.disconnect();
|
||||
|
||||
// Clear fragments for this device
|
||||
this.clearFragments(deviceId);
|
||||
|
||||
this.subscription = await this.client.monitor(
|
||||
deviceId,
|
||||
BLE_UUIDS.SERVICE,
|
||||
BLE_UUIDS.READ_CHARACTERISTIC,
|
||||
(error, value) => {
|
||||
if (error) {
|
||||
// Check for known native crash error and ignore/log as debug
|
||||
if (error.errorCode === 0 && error.message.includes('Unknown error') && error.reason?.includes('PromiseImpl.reject')) {
|
||||
console.debug("Ignored native monitor error", error);
|
||||
return;
|
||||
}
|
||||
console.warn("Monitor error", error);
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
const buffer = Buffer.from(value, 'base64');
|
||||
console.log(`[BleProtocol] Received ${buffer.byteLength} bytes:`, buffer.toString('hex'));
|
||||
this.handleRawData(deviceId, buffer);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.subscription) {
|
||||
try {
|
||||
this.subscription.remove();
|
||||
} catch (e) {
|
||||
console.warn("Failed to remove subscription", e);
|
||||
}
|
||||
this.subscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRawData(deviceId: string, data: Buffer) {
|
||||
const frame = ProtocolManager.parseFrame(data.buffer);
|
||||
if (!frame) return;
|
||||
|
||||
if (frame.subpageTotal > 0) {
|
||||
this.handleFragment(deviceId, frame);
|
||||
} else {
|
||||
this.emit(frame.type, frame.data, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private handleFragment(deviceId: string, frame: ProtocolFrame) {
|
||||
const key = `${deviceId}_${frame.type}`;
|
||||
|
||||
if (!this.fragments.has(key)) {
|
||||
this.fragments.set(key, {
|
||||
total: frame.subpageTotal,
|
||||
frames: new Array(frame.subpageTotal).fill(null)
|
||||
});
|
||||
}
|
||||
|
||||
const session = this.fragments.get(key)!;
|
||||
// Basic validation
|
||||
if (frame.curPage >= session.total) return;
|
||||
|
||||
session.frames[frame.curPage] = frame.data;
|
||||
|
||||
// Check if complete
|
||||
if (session.frames.every(f => f !== null)) {
|
||||
const combinedLength = session.frames.reduce((acc, val) => acc + (val ? val.byteLength : 0), 0);
|
||||
const combined = new Uint8Array(combinedLength);
|
||||
let offset = 0;
|
||||
|
||||
// Reassemble from High to Low pages
|
||||
for (let i = session.total - 1; i >= 0; i--) {
|
||||
const part = session.frames[i];
|
||||
if (part) {
|
||||
combined.set(new Uint8Array(part), offset);
|
||||
offset += part.byteLength;
|
||||
}
|
||||
}
|
||||
|
||||
this.fragments.delete(key);
|
||||
this.emit(frame.type, combined.buffer as ArrayBuffer, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private clearFragments(deviceId: string) {
|
||||
for (const key of this.fragments.keys()) {
|
||||
if (key.startsWith(deviceId)) {
|
||||
this.fragments.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async send(deviceId: string, type: number, data: object | ArrayBuffer, onProgress?: (progress: number) => void): Promise<void> {
|
||||
let payload: Uint8Array;
|
||||
if (data instanceof ArrayBuffer) {
|
||||
payload = new Uint8Array(data);
|
||||
} else {
|
||||
const jsonStr = JSON.stringify(data);
|
||||
payload = new Uint8Array(Buffer.from(jsonStr));
|
||||
}
|
||||
|
||||
const frames = ProtocolManager.createFrame(type, payload);
|
||||
const total = frames.length;
|
||||
console.debug(`Sending ${total} frames`);
|
||||
for (let i = 0; i < total; i++) {
|
||||
const frame = frames[i];
|
||||
console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`);
|
||||
const base64 = Buffer.from(frame).toString('base64');
|
||||
const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false);
|
||||
if (onProgress) {
|
||||
onProgress((i + 1) / total);
|
||||
}
|
||||
console.debug("Wrote frame", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
ble/services/DeviceInfoService.ts
Normal file
76
ble/services/DeviceInfoService.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {BleProtocolService} from './BleProtocolService';
|
||||
import {COMMAND_TYPES, RESPONSE_TYPES} from '../protocol/Constants';
|
||||
import {DeviceInfo, ActivationStatus} from '../protocol/types';
|
||||
import {Buffer} from 'buffer';
|
||||
|
||||
export class DeviceInfoService {
|
||||
private protocol = BleProtocolService.getInstance();
|
||||
private static instance: DeviceInfoService;
|
||||
private listeners: Map<string, Set<Function>> = new Map();
|
||||
|
||||
private constructor() {
|
||||
this.protocol.addListener(RESPONSE_TYPES.DEVICE_INFO_REPORT, this.onDeviceInfo);
|
||||
this.protocol.addListener(RESPONSE_TYPES.ACTIVATION_STATUS, this.onActivationStatus);
|
||||
}
|
||||
|
||||
public static getInstance(): DeviceInfoService {
|
||||
if (!DeviceInfoService.instance) {
|
||||
DeviceInfoService.instance = new DeviceInfoService();
|
||||
}
|
||||
return DeviceInfoService.instance;
|
||||
}
|
||||
|
||||
private onDeviceInfo = (data: ArrayBuffer, deviceId: string) => {
|
||||
try {
|
||||
const str = Buffer.from(data).toString('utf-8');
|
||||
// Remove null characters if any
|
||||
const cleanStr = str.replace(/\0/g, '');
|
||||
const json = JSON.parse(cleanStr) as DeviceInfo;
|
||||
this.emit('deviceInfo', json);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse device info", e);
|
||||
}
|
||||
}
|
||||
|
||||
private onActivationStatus = (data: ArrayBuffer, deviceId: string) => {
|
||||
try {
|
||||
const str = Buffer.from(data).toString('utf-8');
|
||||
const cleanStr = str.replace(/\0/g, '');
|
||||
const json = JSON.parse(cleanStr) as ActivationStatus;
|
||||
this.emit('activationStatus', json);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse activation status", e);
|
||||
}
|
||||
}
|
||||
|
||||
public addListener(event: string, cb: Function) {
|
||||
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
|
||||
this.listeners.get(event)!.add(cb);
|
||||
}
|
||||
|
||||
public removeListener(event: string, cb: Function) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event)!.delete(cb);
|
||||
}
|
||||
}
|
||||
|
||||
private emit(event: string, data: any) {
|
||||
this.listeners.get(event)?.forEach(cb => cb(data));
|
||||
}
|
||||
|
||||
public async queryDeviceInfo(deviceId: string) {
|
||||
await this.protocol.send(deviceId, COMMAND_TYPES.DEVICE_INFO_SETTINGS, {
|
||||
type: COMMAND_TYPES.DEVICE_INFO_SETTINGS
|
||||
});
|
||||
}
|
||||
|
||||
public async queryActivationStatus(deviceId: string) {
|
||||
await this.protocol.send(deviceId, COMMAND_TYPES.ACTIVATION_QUERY, {
|
||||
type: COMMAND_TYPES.ACTIVATION_QUERY
|
||||
});
|
||||
}
|
||||
|
||||
public async queryDeviceVersion(deviceId: string) {
|
||||
await this.protocol.send(deviceId, COMMAND_TYPES.VERSION_QUERY, {type: COMMAND_TYPES.VERSION_QUERY})
|
||||
}
|
||||
}
|
||||
51
ble/services/FileTransferService.ts
Normal file
51
ble/services/FileTransferService.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BleProtocolService } from './BleProtocolService';
|
||||
import { COMMAND_TYPES } from '../protocol/Constants';
|
||||
|
||||
|
||||
export class FileTransferService {
|
||||
private static instance: FileTransferService;
|
||||
private protocol = BleProtocolService.getInstance();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): FileTransferService {
|
||||
if (!FileTransferService.instance) {
|
||||
FileTransferService.instance = new FileTransferService();
|
||||
}
|
||||
return FileTransferService.instance;
|
||||
}
|
||||
|
||||
public async transferFile(deviceId: string, filePath: string, type: number, onProgress?: (progress: number) => void): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(filePath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.statusText}`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
|
||||
const reader = new FileReader();
|
||||
const arrayBuffer = await new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
|
||||
await this.protocol.send(deviceId, type, arrayBuffer, onProgress);
|
||||
} catch (e) {
|
||||
console.error("File transfer failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async transferOta(deviceId: string, filePath: string) {
|
||||
return this.transferFile(deviceId, filePath, COMMAND_TYPES.OTA_PACKAGE);
|
||||
}
|
||||
|
||||
public async transferBootAnimation(deviceId: string, filePath: string) {
|
||||
return this.transferFile(deviceId, filePath, COMMAND_TYPES.TRANSFER_BOOT_ANIMATION);
|
||||
}
|
||||
|
||||
public async transferWatchfaceStyle(deviceId: string, filePath: string) {
|
||||
return this.transferFile(deviceId, filePath, COMMAND_TYPES.TRANSFER_AVI_VIDEO);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user