From 539c425a898da7b0609720d909dfb3fa91a11b2c Mon Sep 17 00:00:00 2001 From: yecong Date: Fri, 19 Dec 2025 00:05:42 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.tsx | 2 +- src/DfuScreen.tsx | 2 +- src/InfoScreen.tsx | 508 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 476 insertions(+), 36 deletions(-) diff --git a/App.tsx b/App.tsx index c7fb293..24207aa 100644 --- a/App.tsx +++ b/App.tsx @@ -55,7 +55,7 @@ export default function App() { - 蓝牙名称: {name} + 蓝牙名称: {name} 最新版本: {latestVersion} diff --git a/src/InfoScreen.tsx b/src/InfoScreen.tsx index d8197a7..fead836 100644 --- a/src/InfoScreen.tsx +++ b/src/InfoScreen.tsx @@ -49,6 +49,11 @@ export default function InfoScreen({ route, navigation }: Props) { 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("读取中..."); @@ -56,7 +61,7 @@ export default function InfoScreen({ route, navigation }: Props) { const [hardware, setHardware] = useState("读取中..."); const [battery, setBattery] = useState("读取中..."); const [powerTrim, setPowerTrim] = useState("读取中"); - const [inputTrim, setInputTrim] = useState("100"); + const [inputTrim, setInputTrim] = useState(""); const [readSuccessToast, setReadSuccessToast] = useState(false); // ✅ 读取成功提示 const [powerTrimLoading, setPowerTrimLoading] = useState(false); // 正在写入功率微调 @@ -69,6 +74,13 @@ export default function InfoScreen({ route, navigation }: Props) { 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(() => { // 页面获得焦点 @@ -84,7 +96,7 @@ export default function InfoScreen({ route, navigation }: Props) { // ========== 监听连接状态变化,断开时提示重新连接 ========== useEffect(() => { if (prevConnectedRef.current && !isConnected && isActiveRef.current) { - Alert.alert("提示", "请重新连接设备", [ + Alert.alert("提示", "设备已断开,请重新连接设备", [ { text: "确定", onPress: () => navigation.goBack() }, ]); } @@ -121,7 +133,7 @@ export default function InfoScreen({ route, navigation }: Props) { return unsubscribe; }, [navigation, peripheral]); - // ========== BLE notify 订阅 ========== + // ========== BLE notify 订阅FFF3 ========== useEffect(() => { const connectionHandler = async (ev: { peripheral: ScannedPeripheral; @@ -151,14 +163,18 @@ export default function InfoScreen({ route, navigation }: Props) { try { const raw = (notifyEv as any).value; if (!raw) return; - const arr = - raw instanceof Uint8Array - ? Array.from(raw) - : Array.from(new Uint8Array(raw)); - if (arr[0] === 0x02 && arr.length >= 2) { - setPowerTrim(arr[1].toString()); - setInputTrim(arr[1].toString()); - } + 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); } @@ -172,6 +188,8 @@ export default function InfoScreen({ route, navigation }: Props) { } }; + + Central.addListener("peripheralConnectionStatus", connectionHandler); return () => { Central.removeListener("peripheralConnectionStatus", connectionHandler); @@ -249,11 +267,182 @@ export default function InfoScreen({ route, navigation }: Props) { })(); }, [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("提示", "请输入有效数字(50~200)"); + Alert.alert("提示", "功率微调可调整功率计的高低偏差,默认值100%。可调整的范围是50%-200%。请输入50至200的纯数字,不需要包含%符号。输入后点击下方按钮更新进功率计设备。"); return; } if (val === Number(powerTrim)) { @@ -295,40 +484,215 @@ export default function InfoScreen({ route, navigation }: Props) { } }; +// ========== 校准按钮逻辑 ========== +const handleCalibration = async () => { + if (!isConnected) { + Alert.alert("提示", "设备未连接"); + 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("校准错误", "设备未响应,请重试"); + setCalibrating(false); + isCalibratingRef.current = false; + } + }, 5000); + + } catch (err) { + console.warn("❌ 发送校准命令失败", err); + Alert.alert("错误", "发送校准命令失败"); + 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( + "校准成功", + `校准值: ${value}`, + [ + { + text: "确定", + onPress: () => { + setCalibrating(false); + isCalibratingRef.current = false; + setCalibrationSuccess(false); + } + } + ] + ); + } else { + console.warn("❌ 校准响应格式错误", byteArray); + Alert.alert("校准错误", "设备返回数据格式错误"); + setCalibrating(false); + isCalibratingRef.current = false; + } +}; + +useEffect(() => { + return () => { + // 清理校准超时定时器 + if (calibrationTimeoutRef.current) { + clearTimeout(calibrationTimeoutRef.current); + calibrationTimeoutRef.current = null; + } + }; +}, []); + + // ========== UI ========== return ( - + + 蓝牙名称: {peripheral.name} + ID号: {serial} + 固件版本: {firmware} + 电量: {battery} + 连接状态: {isConnected ? "已连接" : "未连接"} + +{/* ====== 实时数据三卡片 ====== */} + + {/* 功率 */} + + + 功率/W + {power} + - - 功率微调: {powerTrim}% - - - 更新功率微调 - - + {/* 踏频 */} + + + 踏频/RPM + {cadence} + - + {/* 左右平衡 */} + + L / R + 左右平衡/% + + {leftbalance}/{rightbalance} + + + + + + + {/* ========== 功率微调卡片部分 ========== */} + + + 功率微调设置 + + + + 当前微调: {powerTrim}% + + + + + 更新数值 + + + + {/* ========== 新增校准按钮 ========== */} + + + + {calibrating ? "校准中...等待设备反应" : "校准归零"} + + + + + + + { if (!isConnected) { @@ -343,7 +707,6 @@ export default function InfoScreen({ route, navigation }: Props) { ]); return; } - // 蓝牙已连接,正常跳转 DFU navigation.navigate("Dfu", { deviceId: deviceKey, @@ -354,13 +717,15 @@ export default function InfoScreen({ route, navigation }: Props) { style={styles.pressable} disabled={isLoading || powerTrimLoading} // ❌ 禁用点击 > - 升级固件 + 固件升级 + + {isLoading && ( - + 正在读取信息... )} @@ -368,7 +733,7 @@ export default function InfoScreen({ route, navigation }: Props) { {/* ✅ 读取成功提示,固定在页面底部 */} {readSuccessToast && ( - + 读取成功! )} @@ -376,7 +741,7 @@ export default function InfoScreen({ route, navigation }: Props) { {/* 正在写入功率微调 */} {powerTrimLoading && ( - + 正在写入功率微调... )} @@ -384,7 +749,7 @@ export default function InfoScreen({ route, navigation }: Props) { {/* 功率微调写入成功 */} {powerTrimSuccessToast && ( - + 功率微调更新成功! )} @@ -397,7 +762,7 @@ const styles = StyleSheet.create({ row: { borderBottomWidth: 1, borderBottomColor: "#E7141E", - paddingBottom: 4, // 横线和文字保持一定间距 + paddingBottom: 3, // 横线和文字保持一定间距 marginBottom: 8, // 行间距 }, input: { @@ -414,4 +779,79 @@ const styles = StyleSheet.create({ 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", +}, + + + });