/** * 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 = new Set(); private peripheral: Peripheral | null = null; private isAdvertising = false; private isReady = false; private connectedCentrals = new Set(); private deviceInfo: DeviceInfo; private operationQueue: Array<() => Promise> = []; // 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 { 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 { 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): Promise { if (this.isReady && this.peripheral) { await operation(); } else { this.operationQueue.push(operation); } } public async startAdvertising(deviceName?: string): Promise { 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 { 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): 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 = []; } }