② 适配T5骑行台,做了相关页面。内测还不够充分,产品也不着急上市,所以此版更新中,应该需要隐藏掉。 ③ 修复了之前 盘爪设备信息 读取时,有概率出现乱码的情况。读取时做了排序和延迟,修复了此问题。 ④ 多语言我们这边做更新时,拉取的是仅有中英文的版本,当时你们那好像有更新更多语言?所以需要适配一下。 ⑤ 蓝牙名搜索时,适配最新的固件名称: 盘爪为:PF-PM5-前缀,桨频器为PF-STK-前缀,骑行台为PF-T5-前缀。同时允许前缀POWERFUN-也能显示并连接。
231 lines
7.0 KiB
TypeScript
231 lines
7.0 KiB
TypeScript
// 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>;
|
||
type DeviceWithTimestamp = ScannedPeripheral & { lastSeen: number };
|
||
import { useTranslation } from 'react-i18next';
|
||
import MyStatusbar from "./component/MyStatusbar";
|
||
import MyHeader from "./component/MyHeader";
|
||
|
||
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);
|
||
const { t } = useTranslation();
|
||
// 将 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") && !p.name.startsWith("PF-PM5"))) 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}>
|
||
<MyStatusbar backgroundColor="#f2f3f7" dark></MyStatusbar>
|
||
<MyHeader
|
||
title={t('scan.title')}
|
||
textColor="#333"
|
||
backgroundColor="#f2f3f7"
|
||
navigation={navigation}
|
||
></MyHeader>
|
||
<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 || t("scan.noName")}</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}>{t("scan.scanning")}</Text>
|
||
<Text style={styles.tips}>{t("scan.tipScanning")}</Text>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Text style={styles.emptyText}>{t("scan.noDevice")}</Text>
|
||
<Text style={styles.tips}>{t("scan.tipBluetooth")}</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" },
|
||
}); |