expo-ble模块测试demo
This commit is contained in:
323
hooks/useBlePeripheral.ts
Normal file
323
hooks/useBlePeripheral.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user