说明文字

This commit is contained in:
yecong 2025-12-19 00:05:42 +08:00
parent d72b18f713
commit 539c425a89
3 changed files with 476 additions and 36 deletions

View File

@ -55,7 +55,7 @@ export default function App() {
<Stack.Screen
name="Info"
component={InfoScreen}
options={{ title: "设备信息" }}
options={{ title: "设备详情" }}
/>
<Stack.Screen
name="Dfu"

View File

@ -122,7 +122,7 @@ export default function DfuScreen({ route, navigation }: Props) {
return (
<View style={{ flex: 1, padding: 20 }}>
<View style={styles.row}>
<Text style={{ fontSize: 18 }}>: {name}</Text>
<Text style={{ fontSize: 16 }}>: {name}</Text>
</View>
<View style={styles.row}>
<Text style={{ fontSize: 16 }}>: {latestVersion}</Text>

View File

@ -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<number>(0);
const [cadence, setCadence] = useState<number>(0);
const [leftbalance, setLeftBalance] = useState<number>(0);
const [rightbalance, setRightBalance] = useState<number>(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<boolean>(false);
const [calibrationSuccess, setCalibrationSuccess] = useState<boolean>(false);
const [calibrationValue, setCalibrationValue] = useState<string>("");
const calibrationTimeoutRef = useRef<number | null>(null);
const isCalibratingRef = useRef<boolean>(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 (
<ScrollView contentContainerStyle={{ padding: 20, gap: 8 }}>
<View style={styles.row}>
<View style={styles.row}>
<Text>: {peripheral.name}</Text>
</View>
<View style={styles.row}>
<Text>ID号: {serial}</Text>
</View>
<View style={styles.row}>
<Text>: {firmware}</Text>
</View>
<View style={styles.row}>
<Text>: {battery}</Text>
</View>
<View style={styles.row}>
<Text>: {isConnected ? "已连接" : "未连接"}</Text>
</View>
{/* ====== 实时数据三卡片 ====== */}
<View style={styles.realtimeContainer}>
{/* 功率 */}
<View style={styles.realtimeCard}>
<Icon name="flash" size={22} color="#E7141E" />
<Text style={styles.realtimeLabel}>/W</Text>
<Text style={styles.realtimeValue}>{power}</Text>
</View>
<View style={{ marginTop: 20 }}>
<Text>: {powerTrim}%</Text>
<TextInput
style={styles.input}
value={inputTrim}
onChangeText={setInputTrim}
keyboardType="numeric"
onEndEditing={updatePowerTrim}
/>
<Pressable onPress={updatePowerTrim} style={styles.pressable} disabled={isLoading || powerTrimLoading}>
<Text style={{ color: "white" }}></Text>
</Pressable>
</View>
{/* 踏频 */}
<View style={styles.realtimeCard}>
<Icon name="sync" size={22} color="#E7141E" />
<Text style={styles.realtimeLabel}>/RPM</Text>
<Text style={styles.realtimeValue}>{cadence}</Text>
</View>
<View style={{ marginTop: 30 }}>
{/* 左右平衡 */}
<View style={styles.realtimeCard}>
<Text style={styles.balanceHeader}>L / R</Text>
<Text style={styles.realtimeLabel}>/%</Text>
<Text style={styles.realtimeValue}>
{leftbalance}/{rightbalance}
</Text>
</View>
</View>
{/* ========== 功率微调卡片部分 ========== */}
<View style={styles.cardContainer}>
<View style={styles.cardTitle}>
<Text style={[styles.cardTitleText, { textAlign: 'center' }]}></Text>
</View>
<View style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 0,
paddingHorizontal: 10
}}>
<Text style={{ marginTop: 10,flex: 1, marginRight: 10 }}>: {powerTrim}%</Text>
<TextInput
style={[styles.input, { flex: 1 , marginTop: 10 }]}
value={inputTrim}
onChangeText={setInputTrim}
keyboardType="numeric"
onEndEditing={updatePowerTrim}
placeholder="输入50-200"
/>
</View>
<Pressable
onPress={updatePowerTrim}
style={styles.pressable}
disabled={isLoading || powerTrimLoading}
>
<Text style={{ color: "white" }}></Text>
</Pressable>
</View>
{/* ========== 新增校准按钮 ========== */}
<View style={{ marginTop: 0 }}>
<Pressable
onPress={handleCalibration}
style={[styles.pressable, calibrating && styles.disabledButton]}
disabled={calibrating || !isConnected}
>
<Text style={{ color: "white" }}>
{calibrating ? "校准中...等待设备反应" : "校准归零"}
</Text>
</Pressable>
</View>
<View style={{ marginTop: 0 }}>
<Pressable
onPress={() => {
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} // ❌ 禁用点击
>
<Text style={{ color: "white" }}></Text>
<Text style={{ color: "white" }}></Text>
</Pressable>
</View>
{isLoading && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<ActivityIndicator size={80} color="#E7141E" />
<ActivityIndicator size={30} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}>...</Text>
</View>
)}
@ -368,7 +733,7 @@ export default function InfoScreen({ route, navigation }: Props) {
{/* ✅ 读取成功提示,固定在页面底部 */}
{readSuccessToast && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<Icon name="check-circle" size={60} color="#E7141E" />
<Icon name="check-circle" size={30} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}></Text>
</View>
)}
@ -376,7 +741,7 @@ export default function InfoScreen({ route, navigation }: Props) {
{/* 正在写入功率微调 */}
{powerTrimLoading && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<ActivityIndicator size={60} color="#E7141E" />
<ActivityIndicator size={30} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}>...</Text>
</View>
)}
@ -384,7 +749,7 @@ export default function InfoScreen({ route, navigation }: Props) {
{/* 功率微调写入成功 */}
{powerTrimSuccessToast && (
<View style={{ marginVertical: 60, alignItems: "center" }}>
<Icon name="check-circle" size={60} color="#E7141E" />
<Icon name="check-circle" size={30} color="#E7141E" />
<Text style={{ marginTop: 10, fontSize: 16 }}></Text>
</View>
)}
@ -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",
},
});