324 lines
9.9 KiB
TypeScript
324 lines
9.9 KiB
TypeScript
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<PeripheralState>({
|
|
isAdvertising: false,
|
|
connectedCentralCount: 0,
|
|
logs: [],
|
|
deviceInfo: DEFAULT_DEVICE_INFO,
|
|
loading: {
|
|
advertising: false,
|
|
},
|
|
error: null,
|
|
});
|
|
|
|
// Ref to track peripheral manager
|
|
const peripheralManagerRef = useRef<BlePeripheralManager>(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<boolean> => {
|
|
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<DeviceInfo>) => {
|
|
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,
|
|
};
|
|
};
|