初版
This commit is contained in:
commit
8a60654731
82 files changed
+22637
No files matched your search
@@ -0,0 +1,221 @@
|
||||
// src/ScanScreen.tsx
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
RefreshControl,
|
||||
} from "react-native";
|
||||
import {
|
||||
Central,
|
||||
CentralEventMap,
|
||||
ScannedPeripheral,
|
||||
} from "@systemic-games/react-native-bluetooth-le";
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import { RootStackParamList } from "../App";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons"; // 信号图标集
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, "Scan">;
|
||||
type DeviceWithTimestamp = ScannedPeripheral & { lastSeen: number };
|
||||
|
||||
export default function ScanScreen({ navigation }: Props) {
|
||||
const [devices, setDevices] = useState<DeviceWithTimestamp[]>([]);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const handlerRef = useRef<((payload: CentralEventMap["scannedPeripheral"]) => void) | null>(null);
|
||||
|
||||
// 将 RSSI 转换为信号格数(0-4)
|
||||
const getSignalLevel = (rssi?: number): number => {
|
||||
if (rssi === undefined) return 0;
|
||||
if (rssi >= -50) return 4; // 很强
|
||||
if (rssi >= -65) return 3; // 强
|
||||
if (rssi >= -80) return 2; // 一般
|
||||
if (rssi >= -90) return 1; // 弱
|
||||
return 0; // 无信号
|
||||
};
|
||||
|
||||
// 渲染信号图标(格数+颜色)
|
||||
const renderSignalIcon = (rssi?: number) => {
|
||||
const level = getSignalLevel(rssi);
|
||||
|
||||
let iconColor = "#aaa"; // 默认灰色
|
||||
switch (level) {
|
||||
case 4:
|
||||
iconColor = "#8BC34A"; // 亮绿
|
||||
break;
|
||||
case 3:
|
||||
iconColor = "#4CAF50"; // 深绿
|
||||
break;
|
||||
case 2:
|
||||
iconColor = "#FF9800"; // 橙色
|
||||
break;
|
||||
case 1:
|
||||
iconColor = "#F44336"; // 红色
|
||||
break;
|
||||
}
|
||||
|
||||
const iconName =
|
||||
level === 4
|
||||
? "wifi-strength-4"
|
||||
: level === 3
|
||||
? "wifi-strength-3"
|
||||
: level === 2
|
||||
? "wifi-strength-2"
|
||||
: level === 1
|
||||
? "wifi-strength-1"
|
||||
: "wifi-strength-outline";
|
||||
|
||||
return <Icon name={iconName} size={22} color={iconColor} />;
|
||||
};
|
||||
|
||||
// 更新或添加设备,并记录最后扫描时间
|
||||
const updatePeripherals = useCallback((p: ScannedPeripheral) => {
|
||||
if (!p?.name || !p.name.startsWith("POWERFUN")) return;
|
||||
if ((p.advertisementData?.rssi ?? -999) < -90) return; // 已过滤弱信号
|
||||
|
||||
setDevices((prev) => {
|
||||
const now = Date.now();
|
||||
const exists = prev.find((d) => d.systemId === p.systemId);
|
||||
if (exists) {
|
||||
return prev.map((d) =>
|
||||
d.systemId === p.systemId ? { ...p, lastSeen: now } : d
|
||||
);
|
||||
} else {
|
||||
return [...prev, { ...p, lastSeen: now }];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 清理消失或信号低的设备
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
setDevices((prev) =>
|
||||
prev.filter(
|
||||
(d) =>
|
||||
(d.advertisementData?.rssi ?? -999) >= -90 &&
|
||||
now - d.lastSeen < 3000
|
||||
)
|
||||
);
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const stopScan = useCallback(() => {
|
||||
setScanning(false);
|
||||
try { Central.stopScan(); } catch {}
|
||||
if (handlerRef.current) {
|
||||
try { Central.removeListener("scannedPeripheral", handlerRef.current); } catch {}
|
||||
handlerRef.current = null;
|
||||
}
|
||||
try { Central.shutdown(); } catch {}
|
||||
}, []);
|
||||
|
||||
const startScan = useCallback(() => {
|
||||
stopScan();
|
||||
setDevices([]);
|
||||
setScanning(true);
|
||||
|
||||
const onScanned = (payload: CentralEventMap["scannedPeripheral"]) => {
|
||||
const p = payload?.peripheral;
|
||||
if (p?.name && p.advertisementData?.isConnectable) {
|
||||
updatePeripherals(p);
|
||||
}
|
||||
};
|
||||
handlerRef.current = onScanned;
|
||||
|
||||
try {
|
||||
Central.initialize();
|
||||
Central.addListener("scannedPeripheral", onScanned);
|
||||
Central.startScan([]);
|
||||
} catch (e) {
|
||||
console.warn("Central scan start error:", e);
|
||||
setScanning(false);
|
||||
handlerRef.current = null;
|
||||
try { Central.shutdown(); } catch {}
|
||||
}
|
||||
}, [stopScan, updatePeripherals]);
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
stopScan();
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 200));
|
||||
startScan();
|
||||
setRefreshing(false);
|
||||
}, [stopScan, startScan]);
|
||||
|
||||
useEffect(() => {
|
||||
startScan();
|
||||
return () => { stopScan(); };
|
||||
}, [startScan, stopScan]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={devices}
|
||||
keyExtractor={(item) => item.systemId}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => navigation.navigate("Info", { peripheral: item })}
|
||||
style={styles.deviceRow}
|
||||
>
|
||||
<View style={styles.deviceInfo}>
|
||||
<Text style={styles.deviceName}>{item.name || "[no name]"}</Text>
|
||||
<View style={styles.rssiRow}>
|
||||
{renderSignalIcon(item.advertisementData?.rssi)}
|
||||
<Text style={styles.rssiText}>
|
||||
{item.advertisementData?.rssi ?? "--"} dBm
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyBox}>
|
||||
{scanning ? (
|
||||
<>
|
||||
<Text style={styles.emptyText}>搜索中...</Text>
|
||||
<Text style={styles.tips}>(请确保设备有电且被唤醒)</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.emptyText}>暂无设备</Text>
|
||||
<Text style={styles.tips}>(请在设置中打开蓝牙)</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: "#fff" },
|
||||
deviceRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "#eee",
|
||||
},
|
||||
deviceInfo: { flexDirection: "column" },
|
||||
deviceName: { fontSize: 16, fontWeight: "500" },
|
||||
rssiRow: { flexDirection: "row", alignItems: "center", marginTop: 4 },
|
||||
rssiText: { fontSize: 14, color: "#666", marginLeft: 6 },
|
||||
emptyBox: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
marginTop: 50,
|
||||
},
|
||||
emptyText: { fontSize: 18, fontWeight: "600", marginBottom: 8 },
|
||||
tips: { fontSize: 14, color: "#888", textAlign: "center" },
|
||||
});
|
||||
Reference in new issue
Block a user