473 lines
18 KiB
TypeScript
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 = [];
|
|
}
|
|
}
|