expo-ble模块测试demo

This commit is contained in:
Yudi Xiao
2025-12-10 10:29:50 +08:00
parent 709c466792
commit 41f4080264
23 changed files with 3749 additions and 141 deletions

View File

@@ -1,112 +1,415 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { Collapsible } from '@/components/ui/collapsible';
import { ExternalLink } from '@/components/external-link';
import React from 'react';
import {Alert, StyleSheet, Button, Switch, ScrollView} from 'react-native';
import {ThemedText} from '@/components/themed-text';
import {ThemedView} from '@/components/themed-view';
import {useBleExplorer} from '@/hooks/useBleExplorer';
import {useBlePeripheral} from '@/hooks/useBlePeripheral';
import {BLE_UUIDS, PROTOCOL_VERSION} from '@/ble';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';
import {IconSymbol} from '@/components/ui/icon-symbol';
export default function TabTwoScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText
type="title"
style={{
fontFamily: Fonts.rounded,
}}>
Explore
</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<ThemedText>
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
different screen densities
</ThemedText>
<Image
source={require('@/assets/images/react-logo.png')}
style={{ width: 100, height: 100, alignSelf: 'center' }}
/>
<ExternalLink href="https://reactnative.dev/docs/images">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
<ThemedText type="link">Learn more</ThemedText>
</ExternalLink>
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
the powerful{' '}
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
react-native-reanimated
</ThemedText>{' '}
library to create a waving hand animation.
</ThemedText>
{Platform.select({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
component provides a parallax effect for the header image.
</ThemedText>
),
})}
</Collapsible>
</ParallaxScrollView>
);
const [deviceMode, setDeviceMode] = React.useState<'central' | 'peripheral'>('central');
const {
isScanning,
isConnected,
connectedDevice,
deviceInfo,
version,
isActivated,
transferProgress,
isTransferring,
discoveredDevices,
loading,
error,
startScan,
stopScan,
connectToDevice,
disconnectDevice,
queryActivationStatus,
queryDeviceVersion,
requestDeviceInfo,
updateActivationTime,
sendIdentityCheck,
transferSampleFile,
clearLogs,
} = useBleExplorer();
const {
isAdvertising,
connectedCentralCount,
logs: peripheralLogs,
deviceInfo: peripheralDeviceInfo,
loading: peripheralLoading,
error: peripheralError,
startAdvertising,
stopAdvertising,
getCharacteristicReadValue,
updateDeviceInfo,
resetDeviceInfo,
clearLogs: clearPeripheralLogs,
} = useBlePeripheral();
return (
<ParallaxScrollView
headerBackgroundColor={{light: '#D0D0D0', dark: '#353636'}}
headerImage={
<IconSymbol
size={310}
color="#808080"
name="paperplane.fill"
style={styles.headerImage}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">BLE Explorer</ThemedText>
</ThemedView>
{/* Device Mode Selection */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Device Mode</ThemedText>
<ThemedView style={styles.modeSwitchContainer}>
<ThemedText style={styles.modeLabel}>Central</ThemedText>
<Switch
value={deviceMode === 'peripheral'}
onValueChange={async (value) => {
if (value) {
// Switching to peripheral mode - stop central operations first
console.log('Switching to peripheral mode - stopping central operations');
if (isScanning) {
await stopScan();
// Add small delay to ensure cleanup
await new Promise(resolve => setTimeout(resolve, 500));
}
if (isConnected) {
await disconnectDevice();
// Add small delay to ensure cleanup
await new Promise(resolve => setTimeout(resolve, 500));
}
} else {
// Switching to central mode - stop peripheral operations first
console.log('Switching to central mode - stopping peripheral operations');
if (isAdvertising) {
await stopAdvertising();
// Add small delay to ensure cleanup
await new Promise(resolve => setTimeout(resolve, 500));
}
}
setDeviceMode(value ? 'peripheral' : 'central');
}}
disabled={isScanning || isConnected || isAdvertising || peripheralLoading.advertising}
/>
<ThemedText style={styles.modeLabel}>Peripheral</ThemedText>
</ThemedView>
<ThemedText style={styles.modeDescription}>
{deviceMode === 'central'
? 'Central mode: Scan for and connect to BLE peripherals'
: `Peripheral mode: Act as BLE receiver`}
</ThemedText>
</ThemedView>
{deviceMode === 'central' ? (
<>
{/* Connection Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Connection Status</ThemedText>
<ThemedText>Scanning: {isScanning ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Connected: {isConnected ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Device: {connectedDevice?.name || 'None'}</ThemedText>
{error && <ThemedText style={styles.errorText}>Error: {error}</ThemedText>}
</ThemedView>
{/* Discovered Devices */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Discovered Devices</ThemedText>
<ThemedText style={styles.deviceCount}>Total devices
found: {discoveredDevices.length}</ThemedText>
{discoveredDevices.length === 0 ? (
<ThemedText>No devices discovered yet. Start scanning to find devices.</ThemedText>
) : (
<ThemedView style={{gap: 8}}>
{discoveredDevices.map((item) => (
<ThemedView
key={item.id}
style={[styles.deviceItem, item.connected && styles.connectedDevice]}
lightColor="#eee"
darkColor="#2a2a2a"
>
<ThemedView style={styles.deviceInfo} lightColor="transparent"
darkColor="transparent">
<ThemedText style={item.connected && styles.connectedDeviceText}>
{item.name || 'Unknown Device'}
</ThemedText>
<ThemedText style={styles.deviceId}>{item.id}</ThemedText>
{item.serviceUUIDs && item.serviceUUIDs.length > 0 && (
<ThemedText style={styles.serviceUuids}>
Services: {item.serviceUUIDs.join(', ')}
</ThemedText>
)}
{item.connected && (
<ThemedText style={styles.connectionStatus}>Connected</ThemedText>
)}
</ThemedView>
<Button
title={loading.connecting ? 'Connecting...' : (item.connected ? 'Connected' : 'Connect')}
onPress={() => connectToDevice(item)}
disabled={isConnected || loading.connecting || item.connected}
/>
</ThemedView>
))}
</ThemedView>
)}
</ThemedView>
{/* Device Info */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Device Information</ThemedText>
<ThemedText>Activated: {isActivated ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Version: {version || 'Unknown'}</ThemedText>
{deviceInfo && (
<>
<ThemedText>Name: {deviceInfo.devname}</ThemedText>
<ThemedText>Total Space: {deviceInfo.allspace} KB</ThemedText>
<ThemedText>Free Space: {deviceInfo.freespace} KB</ThemedText>
<ThemedText>Brand: {deviceInfo.brand}</ThemedText>
</>
)}
</ThemedView>
{/* Transfer Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">File Transfer</ThemedText>
<ThemedText>Transferring: {isTransferring ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Progress: {transferProgress}%</ThemedText>
</ThemedView>
{/* Control Buttons */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Controls</ThemedText>
<ThemedView style={styles.buttonRow}>
<Button title={isScanning ? 'Stop Scan' : 'Start Scan'}
onPress={isScanning ? stopScan : startScan}/>
<Button
title="Disconnect"
onPress={disconnectDevice}
disabled={!isConnected}
/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button
title={loading.querying ? 'Querying...' : 'Query Activation'}
onPress={queryActivationStatus}
disabled={!isConnected || loading.querying}
/>
<Button title={loading.querying ? 'Querying...' : 'Query Version'}
onPress={queryDeviceVersion} disabled={!isConnected || loading.querying}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button
title={loading.querying ? 'Querying...' : 'Device Info'}
onPress={requestDeviceInfo}
disabled={!isConnected || loading.querying}
/>
<Button title="Update Time" onPress={updateActivationTime} disabled={!isConnected}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button title="Identity Check (Valid)" onPress={() => sendIdentityCheck()}
disabled={!isConnected}/>
<Button title="Identity Check (Invalid)" onPress={() => sendIdentityCheck()}
disabled={!isConnected}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button
title={loading.transferring ? 'Transferring...' : 'Transfer File'}
onPress={transferSampleFile}
disabled={!isConnected || loading.transferring}
/>
</ThemedView>
</ThemedView>
</>
) : (
<>
{/* Peripheral Status */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Peripheral Status</ThemedText>
<ThemedText>Advertising: {isAdvertising ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Connected Centrals: {connectedCentralCount}</ThemedText>
{peripheralError && <ThemedText style={styles.errorText}>Error: {peripheralError}</ThemedText>}
</ThemedView>
{/* Device Information */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Device Information</ThemedText>
<ThemedText>Activated: {peripheralDeviceInfo.activated ? 'Yes' : 'No'}</ThemedText>
<ThemedText>Version: {peripheralDeviceInfo.version || 'Unknown'}</ThemedText>
<ThemedText>Name: {peripheralDeviceInfo.devname || 'Unknown Device'}</ThemedText>
<ThemedText>Total Space: {peripheralDeviceInfo.allspace || 0} KB</ThemedText>
<ThemedText>Free Space: {peripheralDeviceInfo.freespace || 0} KB</ThemedText>
<ThemedText>Brand: {peripheralDeviceInfo.brand || 'Unknown'}</ThemedText>
</ThemedView>
{/* Peripheral Controls */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Peripheral Controls</ThemedText>
<ThemedView style={styles.buttonRow}>
<Button
title={peripheralLoading.advertising ? 'Starting...' : 'Start Advertising'}
onPress={startAdvertising}
disabled={isAdvertising || peripheralLoading.advertising}
/>
<Button title="Stop Advertising" onPress={stopAdvertising} disabled={!isAdvertising}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button
title="Test Read Response"
onPress={() => getCharacteristicReadValue(BLE_UUIDS.READ_CHARACTERISTIC)}
disabled={!isAdvertising}
/>
<Button title="Reset Device Info" onPress={resetDeviceInfo}/>
</ThemedView>
<ThemedView style={styles.buttonRow}>
<Button title="Toggle Activation"
onPress={() => updateDeviceInfo({activated: !(peripheralDeviceInfo.activated ?? false)})}/>
<Button title="Update Free Space"
onPress={() => updateDeviceInfo({freespace: Math.floor(Math.random() * 1024)})}/>
</ThemedView>
</ThemedView>
{/* Peripheral Logs */}
<ThemedView style={styles.section}>
<ThemedView style={styles.logHeader}>
<ThemedText type="subtitle">Peripheral Logs</ThemedText>
<Button title="Clear" onPress={clearPeripheralLogs}/>
</ThemedView>
<ThemedView style={styles.logContainer} lightColor="#eee" darkColor="#2a2a2a">
<ScrollView nestedScrollEnabled={false} showsVerticalScrollIndicator={true}>
{peripheralLogs.map((log, index) => (
<ThemedText key={index} style={styles.logText}>
{log}
</ThemedText>
))}
</ScrollView>
</ThemedView>
</ThemedView>
</>
)}
{/* Protocol Info - Available in both modes */}
<ThemedView style={styles.section}>
<ThemedText type="subtitle">Protocol Information</ThemedText>
<ThemedText>Version: {PROTOCOL_VERSION}</ThemedText>
<ThemedText>Service UUID: {BLE_UUIDS.SERVICE}</ThemedText>
<ThemedText>Write UUID: {BLE_UUIDS.WRITE_CHARACTERISTIC}</ThemedText>
<ThemedText>Read UUID: {BLE_UUIDS.READ_CHARACTERISTIC}</ThemedText>
</ThemedView>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
headerImage: {
color: '#808080',
bottom: -90,
left: -35,
position: 'absolute',
},
titleContainer: {
flexDirection: 'row',
gap: 8,
},
section: {
gap: 8,
marginBottom: 8,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
gap: 8,
},
logHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
logContainer: {
maxHeight: 200,
padding: 8,
borderRadius: 4,
},
logText: {
fontSize: 12,
marginBottom: 2,
},
deviceItem: {
padding: 12,
marginBottom: 8,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
deviceInfo: {
flex: 1,
marginRight: 12,
},
deviceId: {
fontSize: 12,
opacity: 0.7,
},
deviceCount: {
fontSize: 14,
opacity: 0.8,
marginTop: 4,
marginBottom: 8,
fontWeight: '500',
},
serviceUuids: {
fontSize: 11,
opacity: 0.6,
marginTop: 2,
fontStyle: 'italic',
},
connectionStatus: {
fontSize: 12,
fontWeight: 'bold',
marginTop: 4,
},
connectedDevice: {
borderWidth: 1,
borderColor: '#4CAF50',
},
connectedDeviceText: {
fontWeight: 'bold',
},
errorText: {
marginTop: 8,
fontWeight: 'bold',
},
modeSwitchContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginVertical: 12,
},
modeLabel: {
fontSize: 16,
marginHorizontal: 8,
},
modeDescription: {
fontSize: 12,
opacity: 0.7,
marginTop: 8,
},
});