import React, { useEffect, useState, useCallback, useRef } from "react"; import { View, Text as RNText, TextInput, ScrollView, StyleSheet, ActivityIndicator, useColorScheme, Alert, Pressable, } from "react-native"; import { useFocusEffect } from "@react-navigation/native"; import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { RootStackParamList } from "../App"; import { Central, ScannedPeripheral, ConnectionStatus, } from "@systemic-games/react-native-bluetooth-le"; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { useTranslation } from 'react-i18next'; import MyStatusbar from "./component/MyStatusbar"; import MyHeader from "./component/MyHeader"; type Props = NativeStackScreenProps; const powerServiceUuid = "fff1"; const powerWriteUuid = "fff2"; const powerNotifyUuid = "fff3"; const fullUUID = (uuid: string) => uuid.length === 4 ? `0000${uuid}-0000-1000-8000-00805f9b34fb` : uuid; const Text = ({ children, style, ...props }: any) => { return ( {children} ); }; export default function InfoScreen({ route, navigation }: Props) { const { t } = useTranslation(); const { peripheral } = route.params; const deviceKey = peripheral.address || peripheral.systemId; const [power, setPower] = useState(0); const [cadence, setCadence] = useState(0); const [leftbalance, setLeftBalance] = useState(0); const [rightbalance, setRightBalance] = useState(0); const [isConnected, setIsConnected] = useState(false); const [isLoading, setIsLoading] = useState(true); const [serial, setSerial] = useState(t('info.reading')); const [firmware, setFirmware] = useState(t('info.reading')); const [hardware, setHardware] = useState(t('info.reading')); const [battery, setBattery] = useState(t('info.reading')); const [powerTrim, setPowerTrim] = useState(t('info.reading')); const [inputTrim, setInputTrim] = useState(""); const [readSuccessToast, setReadSuccessToast] = useState(false); // ✅ 读取成功提示 const [powerTrimLoading, setPowerTrimLoading] = useState(false); // 正在写入功率微调 const [powerTrimSuccessToast, setPowerTrimSuccessToast] = useState(false); // 功率微调写入成功 const notifySubscribedRef = useRef(false); const disconnectingRef = useRef(false); const prevConnectedRef = useRef(isConnected); const isActiveRef = useRef(true); //增加校准按钮相关状态 const [calibrating, setCalibrating] = useState(false); const [calibrationSuccess, setCalibrationSuccess] = useState(false); const [calibrationValue, setCalibrationValue] = useState(""); const calibrationTimeoutRef = useRef(null); const isCalibratingRef = useRef(false); useFocusEffect( React.useCallback(() => { // 页面获得焦点 isActiveRef.current = true; return () => { // 页面失去焦点 isActiveRef.current = false; }; }, []) ); // ========== 监听连接状态变化,断开时提示重新连接 ========== useEffect(() => { if (prevConnectedRef.current && !isConnected && isActiveRef.current) { Alert.alert(t('info.disconnectTitle'), t('info.disconnectMessage'), [ { text: t('info.confirm'), onPress: () => navigation.goBack() }, ]); } prevConnectedRef.current = isConnected; }, [isConnected]); // ========== 页面即将返回时,强制断开蓝牙 ========== useEffect(() => { const unsubscribe = navigation.addListener("beforeRemove", async (e) => { if (disconnectingRef.current) return; disconnectingRef.current = true; const next = e.data.action?.type; const action = e.data.action as any; // ✅ 判断是否跳转到 Dfu 页面 if (next === "NAVIGATE" && action?.payload?.name === "Dfu") { console.log("➡️ 跳转到 DFU,不断开蓝牙"); disconnectingRef.current = false; return; } // 🔌 其他页面离开则断开蓝牙 console.log("📴 页面关闭,断开蓝牙..."); try { await Central.disconnectPeripheral(peripheral); console.log("✅ 已断开蓝牙"); } catch (err) { console.warn("❌ 断开失败:", err); } finally { disconnectingRef.current = false; } }); return unsubscribe; }, [navigation, peripheral]); // ========== BLE notify 订阅FFF3 ========== useEffect(() => { const connectionHandler = async (ev: { peripheral: ScannedPeripheral; connectionStatus: ConnectionStatus; }) => { const addr = ev.peripheral.address || ev.peripheral.systemId; if (addr !== deviceKey) return; console.log("🔌 connection event:", ev.connectionStatus); setIsConnected( ev.connectionStatus === "connected" || ev.connectionStatus === "ready" ); if (ev.connectionStatus === "ready" && !notifySubscribedRef.current) { console.log("✅ device ready - subscribe notify FFF3"); try { await Central.unsubscribeCharacteristic( peripheral, fullUUID(powerServiceUuid), fullUUID(powerNotifyUuid) ).catch(() => { }); await Central.subscribeCharacteristic( peripheral, fullUUID(powerServiceUuid), fullUUID(powerNotifyUuid), (notifyEv) => { try { const raw = (notifyEv as any).value; if (!raw) return; const byteArray = raw instanceof Uint8Array ? raw : new Uint8Array(raw); if (byteArray[0] === 0x02 && byteArray.length >= 2) { // 功率微调数据 setPowerTrim(byteArray[1].toString()); //setInputTrim(byteArray[1].toString()); } else if (byteArray[0] === 0x05 && byteArray.length >= 3) { // 校准响应数据 (05XXXX格式) handleFFF3Response(byteArray); } } catch (err) { console.warn("notify parse error", err); } } ); notifySubscribedRef.current = true; console.log("✅ notify 已订阅"); } catch (err) { console.warn("❌ notify 订阅失败:", err); } } }; Central.addListener("peripheralConnectionStatus", connectionHandler); return () => { Central.removeListener("peripheralConnectionStatus", connectionHandler); notifySubscribedRef.current = false; }; }, [deviceKey, peripheral]); // ========== 首次连接并读取信息 ========== useEffect(() => { (async () => { setIsLoading(true); try { await Central.connectPeripheral(peripheral); await new Promise((resolve) => setTimeout(resolve, 500)); const readStr = async (srv: string, char: string) => { try { const v = await Central.readCharacteristic( peripheral, fullUUID(srv), fullUUID(char) ); return v ? String.fromCharCode(...(v as any)) : "未知"; } catch { return "未知"; } }; setSerial(await readStr("180a", "2a25")); setFirmware(await readStr("180a", "2a28")); setHardware(await readStr("180a", "2a27")); try { const v = await Central.readCharacteristic( peripheral, fullUUID("180f"), fullUUID("2a19") ); if (v && (v as any).length) setBattery(`${(v as any)[0]}%`); } catch { setBattery("未知"); } console.log("✅ info 读取完成"); try { await Central.writeCharacteristic( peripheral, fullUUID(powerServiceUuid), fullUUID(powerWriteUuid), new Uint8Array([0x04]).buffer, { withoutResponse: false } ); console.log("✅ 已发送 0x04,等待 notify 更新功率微调"); // notify 回调里已经订阅了,所以这里不用再重复订阅 // 可以稍等 300ms,确保 notify 回来后 UI 会更新 await new Promise(resolve => setTimeout(() => resolve(), 300)); } catch (err) { console.warn("❌ 首次读取功率微调失败", err); } } catch (e) { console.warn("❌ 读取失败", e); } finally { setIsLoading(false); // ✅ 显示读取成功提示 2 秒 setReadSuccessToast(true); setTimeout(() => setReadSuccessToast(false), 2000); } })(); }, [peripheral]); // ========== 订阅 1818 服务的 2A63 特性 (通知) ========== useEffect(() => { const subscribeToPowerData = async () => { try { // 确保设备已经连接 if (!isConnected) return; console.log("✅ 订阅 2A63 特性通知..."); // 先取消之前的订阅(防止重复订阅) await Central.unsubscribeCharacteristic( peripheral, fullUUID("1818"), fullUUID("2a63") ).catch(() => { }); // 订阅 2A63 特性通知 await Central.subscribeCharacteristic( peripheral, fullUUID("1818"), fullUUID("2a63"), (notifyEv) => { try { const raw = notifyEv.value; if (raw) { // 将 ArrayBuffer 转换为字节数组 const byteArray = new Uint8Array(raw); // 解析数据 parseData(byteArray); } } catch (err) { console.warn("❌ 处理通知数据失败", err); } } ); console.log("✅ 已订阅 2A63 特性通知"); } catch (err) { console.warn("❌ 订阅 2A63 特性失败", err); } }; // 只在设备已连接时进行订阅 if (peripheral && isConnected) { subscribeToPowerData(); } // 清理订阅(当组件卸载或者连接状态变化时) return () => { if (peripheral && isConnected) { Central.unsubscribeCharacteristic(peripheral, fullUUID("1818"), fullUUID("2a63")).catch(() => { }); } }; }, [peripheral, isConnected]); const cadenceStateRef = useRef({ lastCadenceCount: 0, // 上一次的踏频翻转次数,用于计算差值 lastCadenceTimestamp: 0, // 上一次的踏频时间戳(设备时间),用于计算差值 lastCadenceChangedTime: 0, // 最后一次踏频变化的时间(本地时间),用于检测超时 }); const parseData = (byteArray: Uint8Array) => { // 当前时间(用于超时检测) const currentTime = Date.now(); // ========== 1. 解析功率 ========== // 字节2-3:功率值(小端序) const powerValue = (byteArray[3] << 8) | byteArray[2]; setPower(powerValue); // ========== 3. 解析踏频 ========== // 字节5-6:踏频翻转次数(小端序) const cadenceCount = (byteArray[6] << 8) | byteArray[5]; // 字节7-8:踏频时间戳(小端序) const timestamp = (byteArray[8] << 8) | byteArray[7]; // 获取上一次的状态 const lastState = cadenceStateRef.current; // 检测踏频翻转次数是否有变化 const hasCadenceChanged = cadenceCount !== lastState.lastCadenceCount; if (hasCadenceChanged) { // 踏频有变化,更新变化时间戳 cadenceStateRef.current.lastCadenceChangedTime = currentTime; } // 检查是否超时(3秒无变化) if (lastState.lastCadenceChangedTime > 0 && currentTime - lastState.lastCadenceChangedTime > 3000) { //setPower("0 W"); setCadence(0); setLeftBalance(0); setRightBalance(0); cadenceStateRef.current = { lastCadenceCount: 0, lastCadenceTimestamp: 0, lastCadenceChangedTime: 0, }; return; // 超时后直接返回,不再计算踏频 } // 如果是第一次收到踏频数据,只记录不计算 if (lastState.lastCadenceTimestamp === 0) { cadenceStateRef.current = { lastCadenceCount: cadenceCount, lastCadenceTimestamp: timestamp, lastCadenceChangedTime: hasCadenceChanged ? currentTime : 0, }; setCadence(0); return; } // 计算踏频RPM(只在踏频有变化时计算) if (hasCadenceChanged) { // 计算翻转次数差(处理16位计数器翻转) let revDiff = cadenceCount - lastState.lastCadenceCount; if (revDiff < 0) { revDiff += 65536; // 16位计数器最大值65535 } // 计算时间差(处理16位计数器翻转) let timeDiff = timestamp - lastState.lastCadenceTimestamp; if (timeDiff < 0) { timeDiff += 65536; // 16位计数器最大值65535 } // 计算踏频 if (timeDiff > 0 && revDiff > 0) { // 假设时间单位是1/1024秒(蓝牙设备常用) const timeInSeconds = timeDiff / 1024.0; // RPM = (圈数 / 时间(秒)) * 60 const cadenceRPM = (revDiff / timeInSeconds) * 60; // 限制合理范围 if (cadenceRPM > 10 && cadenceRPM < 250) { setCadence(Math.round(cadenceRPM)); } else { setCadence(0); } } else { // 如果没有翻转,踏频为0 setCadence(0); } // ========== 解析左平衡 ========== // 字节4:左平衡值,单位0.5% const leftBalanceValue = byteArray[4]; const leftBalancePercent = Math.round(leftBalanceValue * 0.5 * 10) / 10; setLeftBalance(leftBalancePercent); // 计算右平衡:100% - 左平衡百分比 const rightBalancePercent = Math.round((100 - leftBalancePercent) * 10) / 10; setRightBalance(rightBalancePercent); // 更新踏频计算状态 cadenceStateRef.current = { ...cadenceStateRef.current, lastCadenceCount: cadenceCount, lastCadenceTimestamp: timestamp, }; } }; // ========== 写入功率微调 ========== const updatePowerTrim = async () => { const val = parseInt(inputTrim); if (isNaN(val) || val < 50 || val > 200) { Alert.alert(t('info.disconnectTitle'), t('info.trimRangeAlert')); return; } if (val === Number(powerTrim)) { console.log("⚙️ 相同数值,跳过发送"); return; } if (!isConnected) { Alert.alert(t('info.disconnectTitle'), t('info.deviceNotConnected')); return; } try { setPowerTrimLoading(true); console.log("🚀 写入功率微调", val); await Central.writeCharacteristic( peripheral, fullUUID(powerServiceUuid), fullUUID(powerWriteUuid), new Uint8Array([0x02, val]).buffer, { withoutResponse: false } ); await new Promise((resolve) => setTimeout(() => resolve(), 150)); await Central.writeCharacteristic( peripheral, fullUUID(powerServiceUuid), fullUUID(powerWriteUuid), new Uint8Array([0x04]).buffer, { withoutResponse: false } ); setPowerTrimLoading(false); setPowerTrimSuccessToast(true); setTimeout(() => setPowerTrimSuccessToast(false), 2000); } catch (err) { console.warn("❌ 写入失败", err); Alert.alert(t('info.writeFailed')); } }; // ========== 校准按钮逻辑 ========== const handleCalibration = async () => { if (!isConnected) { Alert.alert(t('info.disconnectTitle'), t('info.deviceNotConnected')); return; } if (calibrating) { return; // 正在校准中,不重复点击 } try { setCalibrating(true); isCalibratingRef.current = true; setCalibrationSuccess(false); setCalibrationValue(""); console.log("🔧 开始校准,向FFF2写入05"); // 向FFF2写入05 await Central.writeCharacteristic( peripheral, fullUUID("FFF1"), // 服务UUID fullUUID("FFF2"), // 写入特征UUID new Uint8Array([0x05]).buffer, { withoutResponse: false } ); console.log("✅ 已发送校准命令,等待响应..."); // 设置5秒超时 calibrationTimeoutRef.current = setTimeout(() => { if (isCalibratingRef.current) { console.log("❌ 校准超时,未收到响应"); Alert.alert(t('info.calibrationError'), t('info.calibrationTimeout')); setCalibrating(false); isCalibratingRef.current = false; } }, 5000); } catch (err) { console.warn("❌ 发送校准命令失败", err); Alert.alert(t('info.error'), t('info.calibrationSendError')); setCalibrating(false); isCalibratingRef.current = false; if (calibrationTimeoutRef.current) { clearTimeout(calibrationTimeoutRef.current); } } }; const handleFFF3Response = (byteArray: Uint8Array) => { // 如果不是正在校准中,不处理 if (!isCalibratingRef.current) return; // 清除超时定时器 if (calibrationTimeoutRef.current) { clearTimeout(calibrationTimeoutRef.current); calibrationTimeoutRef.current = null; } // 检查响应数据格式:05XXXX (05开头,后面至少2个字节) // 假设响应是05 + 2字节的小端序数值 if (byteArray[0] === 0x05 && byteArray.length >= 3) { // 解析XXXX(小端序):低位在前,高位在后 // byteArray[1] 是低位字节,byteArray[2] 是高位字节 const value = Math.round(((byteArray[2] << 8) | byteArray[1]) / 10); //除以10发送出来 不然数据容易乱跳 和码表端处理保持一致 console.log(`✅ 收到校准响应,值: ${value}`); // 更新状态 setCalibrationValue(value.toString()); setCalibrationSuccess(true); // 显示成功弹框 Alert.alert( t('info.calibrationSuccess'), `${t('info.calibrationValue') + ": "} ${value}`, [ { text: t('info.confirm'), onPress: () => { setCalibrating(false); isCalibratingRef.current = false; setCalibrationSuccess(false); } } ] ); } else { console.warn("❌ 校准响应格式错误", byteArray); Alert.alert(t('info.calibrationError'), t('info.calibrationFormatError')); setCalibrating(false); isCalibratingRef.current = false; } }; useEffect(() => { return () => { // 清理校准超时定时器 if (calibrationTimeoutRef.current) { clearTimeout(calibrationTimeoutRef.current); calibrationTimeoutRef.current = null; } }; }, []); // ========== UI ========== return ( {t('info.bluetoothName') + ": "} {peripheral.name} {t('info.idNumber') + ": "} {serial} {t('info.firmwareVersion') + ": "} {firmware} {t('info.battery') + ": "} {battery} {t('info.connectionStatus') + ": "} {isConnected ? t('info.connected') : t('info.disconnected')} {/* ====== 实时数据三卡片 ====== */} {/* 功率 */} {t('info.power')} {power} {/* 踏频 */} {t('info.cadence')} {cadence} {/* 左右平衡 */} L / R {t('info.balance')} {leftbalance}/{rightbalance} {/* ========== 功率微调卡片部分 ========== */} {t('info.powerTrimTitle')} {t('info.currentTrim') + ": "} {powerTrim}% {t('info.updateValue')} {/* ========== 新增校准按钮 ========== */} {calibrating ? t('info.calibrating') : t('info.calibrateButton')} { if (!isConnected) { Alert.alert(t('info.disconnectTitle'), t('info.reconnectMessage'), [ { text: t('info.confirm'), onPress: () => { // 返回 ScanScreen navigation.goBack(); }, }, ]); return; } // 蓝牙已连接,正常跳转 DFU navigation.navigate("Dfu", { deviceId: deviceKey, name: peripheral.name, firmware, }); }} style={styles.pressable} disabled={isLoading || powerTrimLoading} // ❌ 禁用点击 > {t('info.firmwareUpgrade')} {isLoading && ( {t('info.readingInfo')} )} {/* ✅ 读取成功提示,固定在页面底部 */} {readSuccessToast && ( {t('info.readSuccess')} )} {/* 正在写入功率微调 */} {powerTrimLoading && ( {t('info.writingTrim')} )} {/* 功率微调写入成功 */} {powerTrimSuccessToast && ( {t('info.trimUpdateSuccess')} )} ); } const styles = StyleSheet.create({ row: { borderBottomWidth: 1, borderBottomColor: "#E7141E", paddingBottom: 3, // 横线和文字保持一定间距 marginBottom: 8, // 行间距 }, input: { borderWidth: 1, borderColor: "#ccc", padding: 6, marginTop: 5, borderRadius: 4, }, pressable: { padding: 10, backgroundColor: "#E7141E", marginTop: 10, borderRadius: 6, alignItems: "center", }, disabledButton: { backgroundColor: "#999", // 禁用时的颜色 opacity: 0.6, }, cardContainer: { borderWidth: 1, borderColor: "#ddd", borderRadius: 10, marginTop: 10, backgroundColor: "#fff", overflow: 'hidden', // 确保圆角效果完整 shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2, // Android阴影 }, cardTitle: { backgroundColor: "#f5f5f5", paddingVertical: 8, paddingHorizontal: 15, borderBottomWidth: 1, borderBottomColor: "#eee", }, cardTitleText: { fontWeight: 'bold', color: "#333", }, realtimeContainer: { flexDirection: "row", justifyContent: "space-between", marginVertical: 10, }, realtimeCard: { flex: 1, marginHorizontal: 4, paddingVertical: 12, borderRadius: 10, backgroundColor: "#fff", alignItems: "center", borderWidth: 1, borderColor: "#E7141E", shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2, }, realtimeLabel: { marginTop: 6, fontSize: 14, color: "#666", }, realtimeValue: { marginTop: 4, fontSize: 20, fontWeight: "bold", color: "#000", }, balanceHeader: { fontSize: 16, fontWeight: "bold", color: "#E7141E", }, });