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

473 lines
18 KiB
TypeScript

/**
* BLE Peripheral Manager for real peripheral functionality
* Handles advertising, characteristic setup, and communication with central devices
*
* SETUP REQUIREMENTS:
* 1. npm install buffer react-native-multi-ble-peripheral
* 2. npx expo prebuild (to generate native code)
* 3. Test on physical device (simulator doesn't support peripheral mode)
* 4. Android 12+ requires BLUETOOTH_ADVERTISE permission (handled in app.json)
* 5. iOS requires NSBluetoothPeripheralUsageDescription (handled in app.json)
*/
import {Platform} from 'react-native';
import Peripheral, {Permission, Property} from 'react-native-multi-ble-peripheral';
import {
BLE_UUIDS as PROTOCOL_UUIDS,
COMMAND_TYPES as APP_COMMAND_TYPES,
DeviceInfo,
FRAME_CONSTANTS as PROTOCOL_FRAME,
PeripheralConnectionEvent,
PeripheralReadRequest,
PeripheralWriteRequest,
RESPONSE_TYPES as DEVICE_RESPONSE_TYPES
} from '../index';
import {ProtocolUtilsV2} from '../utils/ProtocolUtilsV2';
import {Buffer} from 'buffer';
// Re-export DeviceInfo for useBlePeripheral hook
export type {DeviceInfo};
export interface PeripheralEventCallback {
onAdvertisingStateChange?: (isAdvertising: boolean, error?: string) => void;
onCentralConnected?: (centralId: string) => void;
onCentralDisconnected?: (centralId: string) => void;
onCharacteristicRead?: (centralId: string, characteristicUuid: string, data: ArrayBuffer) => void;
onCharacteristicWrite?: (centralId: string, characteristicUuid: string, data: ArrayBuffer) => void;
}
export class BlePeripheralManager {
private static instance: BlePeripheralManager;
private callbacks: Set<PeripheralEventCallback> = new Set();
private peripheral: Peripheral | null = null;
private isAdvertising = false;
private isReady = false;
private connectedCentrals = new Set<string>();
private deviceInfo: DeviceInfo;
private operationQueue: Array<() => Promise<void>> = [];
// Default device info - matches the DeviceInfo interface from BleTypesV2
private readonly DEFAULT_DEVICE_INFO: DeviceInfo = {
devname: 'BLE Test Device',
allspace: 1024,
freespace: 512,
size: 1, // Square screen
brand: 1, // Vendor
version: '2.1.1', // Firmware version
activated: true, // Activation status
};
private constructor() {
this.deviceInfo = {...this.DEFAULT_DEVICE_INFO};
this.initializePeripheral();
}
public static getInstance(): BlePeripheralManager {
if (!BlePeripheralManager.instance) {
BlePeripheralManager.instance = new BlePeripheralManager();
}
return BlePeripheralManager.instance;
}
private async initializePeripheral(): Promise<void> {
try {
if (Platform.OS === 'web') {
console.log('BlePeripheralManager: BLE peripheral not supported on web platform');
return;
}
this.peripheral = new Peripheral();
console.log('BlePeripheralManager: Peripheral instance created');
// Set up event listeners
this.peripheral.on('ready', this.handleReady.bind(this));
this.peripheral.on('connect', this.handleCentralConnected.bind(this));
this.peripheral.on('disconnect', this.handleCentralDisconnected.bind(this));
this.peripheral.on('read', this.handleCharacteristicRead.bind(this));
this.peripheral.on('write', this.handleCharacteristicWrite.bind(this));
console.log('BlePeripheralManager: Event listeners set up');
} catch (error) {
console.error('BlePeripheralManager: Failed to initialize peripheral', error);
this.notifyAdvertisingStateChange(false, error instanceof Error ? error.message : 'Unknown error');
}
}
private async handleReady(): Promise<void> {
console.log('BlePeripheralManager: Peripheral ready');
this.isReady = true;
// Execute queued operations
while (this.operationQueue.length > 0) {
const operation = this.operationQueue.shift();
if (operation) {
try {
await operation();
} catch (error) {
console.error('BlePeripheralManager: Queued operation failed', error);
}
}
}
}
private handleCentralConnected(central: PeripheralConnectionEvent): void {
console.log('BlePeripheralManager: Central connected', central);
const centralId = central.id || 'unknown';
this.connectedCentrals.add(centralId);
this.notifyCentralConnected(centralId);
}
private handleCentralDisconnected(central: PeripheralConnectionEvent): void {
console.log('BlePeripheralManager: Central disconnected', central);
const centralId = central.id || 'unknown';
this.connectedCentrals.delete(centralId);
this.notifyCentralDisconnected(centralId);
}
private handleCharacteristicRead(request: PeripheralReadRequest): void {
console.log('BlePeripheralManager: Characteristic read request', request);
if (request.characteristicUuid === PROTOCOL_UUIDS.READ_CHARACTERISTIC) {
// Return device info as JSON
const response = {
type: 0x02, // Device info response type
data: this.deviceInfo,
};
const jsonString = JSON.stringify(response);
// Update the characteristic value with the response
if (this.peripheral) {
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(jsonString, 'utf-8')
);
}
}
}
private handleCharacteristicWrite(request: PeripheralWriteRequest): void {
console.log('BlePeripheralManager: Characteristic write request', request);
if (request.characteristicUuid === PROTOCOL_UUIDS.WRITE_CHARACTERISTIC) {
let buffer: Buffer;
try {
buffer = Buffer.from(request.value);
} catch (e) {
console.error('BlePeripheralManager: Failed to convert value to buffer', e);
return;
}
const arrayBuffer = ProtocolUtilsV2.uint8ArrayToArrayBuffer(buffer);
// Check for V2 Binary Protocol (starts with HEAD_APP_TO_DEVICE = 0xc7)
if (buffer.length > 0 && buffer[0] === PROTOCOL_FRAME.HEAD_APP_TO_DEVICE) {
console.log('BlePeripheralManager: Received V2 binary frame');
try {
const frame = ProtocolUtilsV2.parseFrame(arrayBuffer);
if (frame && frame.data) {
const jsonData = ProtocolUtilsV2.parseJsonData(frame.data);
if (jsonData) {
console.log('BlePeripheralManager: Parsed V2 command:', jsonData);
// Handle Version Query (0x07)
if (jsonData.type === APP_COMMAND_TYPES.VERSION_QUERY) {
console.log('BlePeripheralManager: Handling VERSION_QUERY');
const response = {
type: DEVICE_RESPONSE_TYPES.VERSION_INFO,
version: this.deviceInfo.version || '2.1.1'
};
const jsonString = JSON.stringify(response);
// Use HEAD_DEVICE_TO_APP (0xB0) for response
const frames = ProtocolUtilsV2.createFrame(
DEVICE_RESPONSE_TYPES.VERSION_INFO,
jsonString,
false,
PROTOCOL_FRAME.HEAD_DEVICE_TO_APP
);
if (this.peripheral) {
for (const f of frames) {
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(f)
);
}
console.log('BlePeripheralManager: Sent VERSION_INFO response');
}
}
this.notifyCharacteristicWrite(request.centralId, request.characteristicUuid, arrayBuffer);
return;
}
}
} catch (e) {
console.error('BlePeripheralManager: Error processing V2 frame', e);
}
}
try {
const jsonString = buffer.toString('utf-8');
const command = JSON.parse(jsonString);
console.log('BlePeripheralManager: Received command type: 0x' + (command.type?.toString(16) || 'unknown'));
// Process different command types
switch (command.type) {
case 0x01: // General command
console.log('BlePeripheralManager: Processing general command');
break;
case 0x02: // Device info request
console.log('BlePeripheralManager: Device info request received');
break;
default:
console.log('BlePeripheralManager: Unknown command type:', command.type);
}
this.notifyCharacteristicWrite(request.centralId, request.characteristicUuid, arrayBuffer);
} catch (error) {
console.error('BlePeripheralManager: Failed to process write request', error);
}
}
}
private async executeOrQueue(operation: () => Promise<void>): Promise<void> {
if (this.isReady && this.peripheral) {
await operation();
} else {
this.operationQueue.push(operation);
}
}
public async startAdvertising(deviceName?: string): Promise<void> {
try {
if (Platform.OS === 'web') {
throw new Error('BLE peripheral not supported on web platform');
}
if (this.isAdvertising) {
console.log('BlePeripheralManager: Already advertising');
return;
}
if (!this.peripheral) {
throw new Error('Peripheral not initialized');
}
const currentDeviceInfo = this.getDeviceInfo();
console.log('BlePeripheralManager: Starting BLE advertising...');
console.log('Device Name: ' + currentDeviceInfo.devname);
console.log('Advertising with service UUID: ' + PROTOCOL_UUIDS.SERVICE);
// iOS platform limitation warning
if (Platform.OS === 'ios') {
console.log('⚠️ iOS: Advertising will stop when app goes to background');
}
await this.executeOrQueue(async () => {
// Set up service
await this.peripheral!.addService(PROTOCOL_UUIDS.SERVICE, true);
console.log('BlePeripheralManager: Service added successfully');
await this.peripheral!.addCharacteristic(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.SERVICE,
Property.BROADCAST,
Permission.READABLE
)
// Set up read characteristic
await this.peripheral!.addCharacteristic(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Property.READ | Property.NOTIFY,
Permission.READABLE
);
console.log('BlePeripheralManager: Read characteristic added successfully');
// Set up write characteristic
await this.peripheral!.addCharacteristic(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.WRITE_CHARACTERISTIC,
Property.WRITE | Property.WRITE_NO_RESPONSE,
Permission.WRITEABLE
);
console.log('BlePeripheralManager: Write characteristic added successfully');
// Set initial read characteristic value
const deviceInfoResponse = {
type: 0x02,
data: currentDeviceInfo,
};
await this.peripheral!.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(JSON.stringify(deviceInfoResponse), 'utf-8')
);
console.log('BlePeripheralManager: Initial characteristic value set');
// Start advertising
await this.peripheral!.startAdvertising();
console.log('BlePeripheralManager: Advertising started successfully');
});
this.isAdvertising = true;
this.notifyAdvertisingStateChange(true);
} catch (error) {
console.error('BlePeripheralManager: Failed to start advertising', error);
this.notifyAdvertisingStateChange(false, error instanceof Error ? error.message : 'Unknown error');
throw error;
}
}
public async stopAdvertising(): Promise<void> {
try {
if (Platform.OS === 'web') {
return;
}
if (!this.isAdvertising) {
console.log('BlePeripheralManager: Not advertising');
return;
}
if (!this.peripheral) {
throw new Error('Peripheral not initialized');
}
await this.executeOrQueue(async () => {
await this.peripheral!.stopAdvertising();
console.log('BlePeripheralManager: Advertising stopped');
});
this.isAdvertising = false;
this.connectedCentrals.clear();
this.notifyAdvertisingStateChange(false);
} catch (error) {
console.error('BlePeripheralManager: Failed to stop advertising', error);
throw error;
}
}
public getAdvertisingState(): boolean {
return this.isAdvertising;
}
public getConnectedCentrals(): string[] {
return Array.from(this.connectedCentrals);
}
public getConnectedCentralsCount(): number {
return this.connectedCentrals.size;
}
public updateDeviceInfo(updates: Partial<DeviceInfo>): void {
this.deviceInfo = {...this.deviceInfo, ...updates};
console.log('BlePeripheralManager: Device info updated', this.deviceInfo);
// Update the characteristic value if advertising
if (this.isAdvertising && this.peripheral && this.isReady) {
const response = {
type: 0x02,
data: this.deviceInfo,
};
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(JSON.stringify(response), 'utf-8')
).catch(error => {
console.error('BlePeripheralManager: Failed to update characteristic value', error);
});
}
}
public getDeviceInfo(): DeviceInfo {
return {...this.deviceInfo};
}
public resetDeviceInfo(): void {
this.deviceInfo = {...this.DEFAULT_DEVICE_INFO};
console.log('BlePeripheralManager: Device info reset to defaults');
// Update the characteristic value if advertising
if (this.isAdvertising && this.peripheral && this.isReady) {
const response = {
type: 0x02,
data: this.deviceInfo,
};
this.peripheral.updateValue(
PROTOCOL_UUIDS.SERVICE,
PROTOCOL_UUIDS.READ_CHARACTERISTIC,
Buffer.from(JSON.stringify(response), 'utf-8')
).catch(error => {
console.error('BlePeripheralManager: Failed to update characteristic value', error);
});
}
}
public addCallback(callback: PeripheralEventCallback): void {
this.callbacks.add(callback);
}
public removeCallback(callback: PeripheralEventCallback): void {
this.callbacks.delete(callback);
}
private notifyAdvertisingStateChange(isAdvertising: boolean, error?: string): void {
this.callbacks.forEach(callback => {
try {
callback.onAdvertisingStateChange?.(isAdvertising, error);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCentralConnected(centralId: string): void {
this.callbacks.forEach(callback => {
try {
callback.onCentralConnected?.(centralId);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCentralDisconnected(centralId: string): void {
this.callbacks.forEach(callback => {
try {
callback.onCentralDisconnected?.(centralId);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCharacteristicRead(centralId: string, characteristicUuid: string, data: ArrayBuffer): void {
this.callbacks.forEach(callback => {
try {
callback.onCharacteristicRead?.(centralId, characteristicUuid, data);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
private notifyCharacteristicWrite(centralId: string, characteristicUuid: string, data: ArrayBuffer): void {
this.callbacks.forEach(callback => {
try {
callback.onCharacteristicWrite?.(centralId, characteristicUuid, data);
} catch (err) {
console.error('BlePeripheralManager: Callback error', err);
}
});
}
public cleanup(): void {
this.stopAdvertising().catch(console.error);
this.callbacks.clear();
this.operationQueue = [];
}
}