import { useState, useEffect, useCallback, useRef } from 'react'; import { Platform, PermissionsAndroid } from 'react-native'; import { BLE_UUIDS as PROTOCOL_UUIDS } from '../ble/index'; import { BlePeripheralManager, PeripheralEventCallback, DeviceInfo } from '@/ble/manager/BlePeripheralManager'; import { Buffer } from 'buffer'; interface PeripheralState { isAdvertising: boolean; connectedCentralCount: number; logs: string[]; deviceInfo: DeviceInfo; loading: { advertising: boolean; }; error: string | null; } const MAX_LOGS = 100; const DEFAULT_DEVICE_INFO: DeviceInfo = { devname: 'BLE Test Device', version: '2.1.1', activated: true, brand: 0, size: 1, allspace: 1024, freespace: 512, }; export const useBlePeripheral = () => { const [state, setState] = useState({ isAdvertising: false, connectedCentralCount: 0, logs: [], deviceInfo: DEFAULT_DEVICE_INFO, loading: { advertising: false, }, error: null, }); // Ref to track peripheral manager const peripheralManagerRef = useRef(BlePeripheralManager.getInstance()); // Ref to track advertising state for cleanup const isAdvertisingRef = useRef(false); // Ref to track component mount state const isMountedRef = useRef(true); // Add log message with limit const addLog = useCallback((message: string) => { const timestamp = new Date().toLocaleTimeString(); setState(prev => { const newLogs = [...prev.logs, `[${timestamp}] ${message}`]; const trimmedLogs = newLogs.length > MAX_LOGS ? newLogs.slice(-MAX_LOGS) : newLogs; return { ...prev, logs: trimmedLogs }; }); }, []); // Set error with user visibility const setError = useCallback((error: string | null) => { setState(prev => ({ ...prev, error })); if (error) { addLog(`ERROR: ${error}`); } }, [addLog]); // Initialize peripheral mode useEffect(() => { if (Platform.OS === 'web') { addLog('BLE peripheral mode not supported on web platform'); return; } addLog('BLE Peripheral mode initialized (Real BLE)'); addLog('Service UUID: ' + PROTOCOL_UUIDS.SERVICE); addLog('Write Characteristic UUID: ' + PROTOCOL_UUIDS.WRITE_CHARACTERISTIC); addLog('Read Characteristic UUID: ' + PROTOCOL_UUIDS.READ_CHARACTERISTIC); // Set up event callbacks const callbacks: PeripheralEventCallback = { onAdvertisingStateChange: (isAdvertising, error) => { if (isMountedRef.current) { setState(prev => ({ ...prev, isAdvertising, loading: { ...prev.loading, advertising: false }, error: error || null })); addLog(`Advertising ${isAdvertising ? 'started' : 'stopped'}${error ? ` with error: ${error}` : ''}`); } }, onCentralConnected: (centralId) => { if (isMountedRef.current) { setState(prev => ({ ...prev, connectedCentralCount: peripheralManagerRef.current.getConnectedCentralsCount() })); addLog(`Central device connected: ${centralId}`); } }, onCentralDisconnected: (centralId) => { if (isMountedRef.current) { setState(prev => ({ ...prev, connectedCentralCount: peripheralManagerRef.current.getConnectedCentralsCount() })); addLog(`Central device disconnected: ${centralId}`); } }, onCharacteristicRead: (centralId, characteristicUuid, data) => { if (isMountedRef.current) { addLog(`Characteristic read by ${centralId}: ${characteristicUuid}`); } }, onCharacteristicWrite: (centralId, characteristicUuid, data) => { if (isMountedRef.current) { const jsonString = Buffer.from(data).toString('utf-8'); addLog(`Characteristic write by ${centralId}: ${characteristicUuid}`); addLog(`Data: ${jsonString}`); } } }; peripheralManagerRef.current.addCallback(callbacks); return () => { isMountedRef.current = false; if (isAdvertisingRef.current) { stopAdvertising(); } peripheralManagerRef.current.removeCallback(callbacks); }; }, [addLog]); // Check and request necessary permissions for Android 12+ const checkPermissions = useCallback(async (): Promise => { if (Platform.OS !== 'android') { return true; // iOS handles permissions differently } try { const apiLevel = Platform.Version; if (typeof apiLevel === 'number' && apiLevel >= 31) { // Android 12+ (API 31+) requires multiple Bluetooth permissions for peripheral mode const requiredPermissions = [ PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN ]; for (const permission of requiredPermissions) { const granted = await PermissionsAndroid.check(permission); if (!granted) { const result = await PermissionsAndroid.request(permission, { title: 'Bluetooth Permission Required', message: 'This app needs Bluetooth permissions to act as a BLE peripheral device.', buttonNeutral: 'Ask Me Later', buttonNegative: 'Cancel', buttonPositive: 'OK', }); if (result !== PermissionsAndroid.RESULTS.GRANTED) { setError(`${permission} is required for peripheral mode`); return false; } } } } return true; } catch (error) { console.error('Permission check failed:', error); setError('Failed to check Bluetooth permissions'); return false; } }, [setError]); // Start advertising as peripheral (real BLE) const startAdvertising = useCallback(async () => { if (Platform.OS === 'web') { setError('BLE peripheral not supported on web'); return; } if (isAdvertisingRef.current) { addLog('Already advertising as peripheral'); return; } try { setError(null); setState(prev => ({ ...prev, loading: { ...prev.loading, advertising: true } })); // Check permissions first const hasPermissions = await checkPermissions(); if (!hasPermissions) { if (isMountedRef.current) { setState(prev => ({ ...prev, loading: { ...prev.loading, advertising: false } })); } return; } const currentDeviceInfo = peripheralManagerRef.current.getDeviceInfo(); addLog('Starting BLE advertising...'); addLog('Device Name: ' + currentDeviceInfo.devname); addLog('Advertising with service UUID: ' + PROTOCOL_UUIDS.SERVICE); // iOS platform limitation warning if (Platform.OS === 'ios') { addLog('⚠️ iOS: Advertising will stop when app goes to background'); } peripheralManagerRef await peripheralManagerRef.current.startAdvertising(currentDeviceInfo.devname); isAdvertisingRef.current = true; } catch (error) { if (isMountedRef.current) { const errorMsg = `Failed to start advertising: ${error}`; addLog(errorMsg); setError(errorMsg); setState(prev => ({ ...prev, loading: { ...prev.loading, advertising: false } })); } } }, [addLog, setError, checkPermissions]); // Stop advertising const stopAdvertising = useCallback(async () => { if (!isAdvertisingRef.current) { addLog('Not currently advertising'); return; } try { addLog('Stopping BLE advertising...'); await peripheralManagerRef.current.stopAdvertising(); isAdvertisingRef.current = false; setState(prev => ({ ...prev, isAdvertising: false, connectedCentralCount: 0 })); addLog('BLE advertising stopped'); } catch (error) { addLog(`Failed to stop advertising: ${error}`); } }, [addLog]); // Get characteristic read value (for testing/debugging: Shows what response will be sent when central reads this characteristic) const getCharacteristicReadValue = useCallback((characteristicUuid: string) => { if (!isAdvertisingRef.current) { addLog('Cannot get read value - not advertising'); return null; } addLog(`Getting characteristic read value: ${characteristicUuid}`); if (characteristicUuid === PROTOCOL_UUIDS.READ_CHARACTERISTIC) { // Return device info as JSON - use manager to get latest data instead of stale state const response = { type: 0x02, // Device info response type data: peripheralManagerRef.current.getDeviceInfo() }; addLog('Returning device info response'); return JSON.stringify(response); } addLog('Unknown characteristic for read'); return null; }, [addLog]); // Update device info const updateDeviceInfo = useCallback((updates: Partial) => { peripheralManagerRef.current.updateDeviceInfo(updates); // Sync state with manager to avoid stale state issues setState(prev => ({ ...prev, deviceInfo: peripheralManagerRef.current.getDeviceInfo() })); addLog('Device info updated'); }, [addLog]); // Reset device info const resetDeviceInfo = useCallback(() => { peripheralManagerRef.current.resetDeviceInfo(); // Sync state with manager to avoid stale state issues setState(prev => ({ ...prev, deviceInfo: peripheralManagerRef.current.getDeviceInfo() })); addLog('Device info reset to defaults'); }, [addLog]); // Clear logs const clearLogs = useCallback(() => { setState(prev => ({ ...prev, logs: [] })); }, []); return { ...state, startAdvertising, stopAdvertising, getCharacteristicReadValue, updateDeviceInfo, resetDeviceInfo, clearLogs, }; };