Files
powerfun-setting/src/InfoScreen2.tsx
T
yecong 8dd0edd2ce 20260603-① 适配桨频器,做了连接和设置页面,已经内测ok。
② 适配T5骑行台,做了相关页面。内测还不够充分,产品也不着急上市,所以此版更新中,应该需要隐藏掉。
③ 修复了之前 盘爪设备信息 读取时,有概率出现乱码的情况。读取时做了排序和延迟,修复了此问题。
④ 多语言我们这边做更新时,拉取的是仅有中英文的版本,当时你们那好像有更新更多语言?所以需要适配一下。
⑤ 蓝牙名搜索时,适配最新的固件名称:
盘爪为:PF-PM5-前缀,桨频器为PF-STK-前缀,骑行台为PF-T5-前缀。同时允许前缀POWERFUN-也能显示并连接。
2026-06-03 14:35:08 +08:00

1228 lines
34 KiB
TypeScript

// InfoScreen2.tsx 此页面为桨频器信息页面
import React, { useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
Alert,
} from "react-native";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { decode as atob } from "base-64";
import {
Central,
ScannedPeripheral,
ConnectionStatus,
} from "@systemic-games/react-native-bluetooth-le";
import { RootStackParamList } from "../App";
import { useTranslation } from "react-i18next";
import MyStatusbar from "./component/MyStatusbar";
import MyHeader from "./component/MyHeader";
type Props = NativeStackScreenProps<RootStackParamList, "Info2">;
const CSC_SERVICE_UUID = "1816";
const CSC_MEASUREMENT_UUID = "2a5b";
const CUSTOM_SERVICE_UUID = "fff1";
const CUSTOM_WRITE_UUID = "fff2";
const CUSTOM_NOTIFY_UUID = "fff3";
const DFU_MANIFEST_URL =
"https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json";
type BoatType = 0x00 | 0x01 | 0x02;
type ParsedFirmware = {
hardware: number;
iteration: number;
build: string;
raw: string;
};
type DeviceInfo = {
hardware: number;
latestFirmware: string;
download: string;
};
const fullUUID = (uuid: string) =>
uuid.length === 4
? `0000${uuid}-0000-1000-8000-00805f9b34fb`
: uuid;
const sleep = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
const base64ToBytes = (base64: string): number[] => {
const binary = atob(base64);
const bytes: number[] = [];
for (let i = 0; i < binary.length; i += 1) {
bytes.push(binary.charCodeAt(i) & 0xff);
}
return bytes;
};
const bytesToArrayBuffer = (bytes: number[]): ArrayBuffer => {
return new Uint8Array(bytes).buffer;
};
const normalizeBytes = (arr: number[]): number[] => {
return arr.map((v) => v & 0xff);
};
const toHexByte = (value: number): string =>
(value & 0xff).toString(16).toUpperCase().padStart(2, "0");
const bytesToHexString = (bytes: number[]): string =>
normalizeBytes(bytes)
.map((v) => toHexByte(v))
.join(" ");
const parseFirmwareVersion = (text: string): ParsedFirmware => {
const raw = String(text ?? "").trim();
const parts = raw.split(".");
if (parts.length < 2) {
throw new Error(`固件版本格式不正确: ${raw}`);
}
const hardware = parseInt(parts[0], 10);
const iteration = parseInt(parts[1], 10);
const build = parts.length >= 3 ? parts[2] : "";
if (Number.isNaN(hardware) || Number.isNaN(iteration)) {
throw new Error(`固件版本无法解析: ${raw}`);
}
return {
hardware,
iteration,
build,
raw,
};
};
export default function InfoScreen2({ route, navigation }: Props) {
const { t } = useTranslation();
const { peripheral } = route.params;
const getBoatNameByValue = (value?: number | null): string => {
if (value === 0x00) return t("info2.boatKayak");
if (value === 0x01) return t("info2.boatRowing");
if (value === 0x02) return t("info2.boatRacing");
return t("common.unknown");
};
const deviceKey =
typeof peripheral.address === "string" && peripheral.address.length > 0
? peripheral.address
: String(peripheral.systemId ?? "");
const [cadence, setCadence] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [selectedBoat, setSelectedBoat] = useState<BoatType | null>(0x00);
const [boatBusy, setBoatBusy] = useState(false);
const [boatStatus, setBoatStatus] = useState(t("info2.waitingAction"));
const [boatNotifyHex, setBoatNotifyHex] = useState("--");
const [boatVerified, setBoatVerified] = useState("--");
const [isConnected, setIsConnected] = useState(false);
const [serial, setSerial] = useState(t("info.reading"));
const [firmware, setFirmware] = useState(t("info.reading"));
const [battery, setBattery] = useState(t("info.reading"));
// 新增:固件升级状态
const [latestFirmware, setLatestFirmware] = useState(t("info.reading"));
const [upgradeAvailable, setUpgradeAvailable] = useState(false);
const [upgradeCheckDone, setUpgradeCheckDone] = useState(false);
const lastCrankRef = useRef<number | null>(null);
const lastEventTimeRef = useRef<number | null>(null);
const lastCadenceUpdateAppTsRef = useRef<number | null>(null);
const boatReadResolverRef = useRef<((bytes: number[]) => void) | null>(null);
const mountedRef = useRef(true);
const disconnectingRef = useRef(false);
const hasConnectedOnceRef = useRef(false);
const hasNavigatedBackRef = useRef(false);
const skipDisconnectOnLeaveRef = useRef(false);
const checkUpgradeNeeded = async (firmwareText: string) => {
try {
const text = String(firmwareText ?? "").trim();
if (!text) {
if (mountedRef.current) {
setLatestFirmware(t("common.unknown"));
setUpgradeAvailable(false);
setUpgradeCheckDone(true);
}
return;
}
const currentFw = parseFirmwareVersion(text);
console.log("🔥 InfoScreen currentFw =", currentFw);
const manifestResp = await fetch(DFU_MANIFEST_URL);
console.log("🔥 InfoScreen manifest status =", manifestResp.status);
if (!manifestResp.ok) {
throw new Error(`manifest 下载失败,HTTP ${manifestResp.status}`);
}
const manifestText = await manifestResp.text();
console.log("🔥 InfoScreen manifest text =", manifestText);
let manifest: { devices: DeviceInfo[] };
try {
manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] };
} catch (e) {
throw new Error("manifest 不是合法 JSON");
}
const deviceInfo = manifest.devices.find(
(d) => d.hardware === currentFw.hardware
);
console.log("🔥 InfoScreen matched deviceInfo =", deviceInfo);
if (!deviceInfo) {
if (mountedRef.current) {
setLatestFirmware(t("info2.notMatched"));
setUpgradeAvailable(false);
setUpgradeCheckDone(true);
}
return;
}
const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware);
console.log("🔥 InfoScreen latestFw =", latestFw);
if (latestFw.hardware !== currentFw.hardware) {
throw new Error(
`服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}`
);
}
const needUpgrade = latestFw.iteration > currentFw.iteration;
if (mountedRef.current) {
setLatestFirmware(deviceInfo.latestFirmware);
setUpgradeAvailable(needUpgrade);
setUpgradeCheckDone(true);
}
} catch (err: any) {
console.warn("⚠️ checkUpgradeNeeded failed:", err);
if (mountedRef.current) {
setLatestFirmware(t("info2.checkFailed"));
setUpgradeAvailable(false);
setUpgradeCheckDone(true);
}
}
};
const readDeviceInfo = async () => {
console.log("📘 start read device info...");
try {
try {
const v = await Central.readCharacteristic(
peripheral,
fullUUID("180a"),
fullUUID("2a25")
);
if (v && (v as any).length) {
const idText = String.fromCharCode(...(v as any));
console.log("📘 serial raw:", v, "=>", idText);
if (mountedRef.current) {
setSerial(idText);
}
} else if (mountedRef.current) {
setSerial(
(peripheral.address ||
peripheral.systemId ||
peripheral.name ||
t("common.unknown")) as string
);
}
} catch (err) {
console.warn("⚠️ read serial failed:", err);
if (mountedRef.current) {
setSerial(
(peripheral.address ||
peripheral.systemId ||
peripheral.name ||
t("common.unknown")) as string
);
}
}
try {
const v = await Central.readCharacteristic(
peripheral,
fullUUID("180a"),
fullUUID("2a28")
);
if (v && (v as any).length) {
const fwText = String.fromCharCode(...(v as any));
console.log("📘 firmware raw:", v, "=>", fwText);
if (mountedRef.current) {
setFirmware(fwText);
}
// 新增:在 Info 页提前检查是否需要升级
await checkUpgradeNeeded(fwText);
} else if (mountedRef.current) {
setFirmware(t("common.unknown"));
setLatestFirmware(t("common.unknown"));
setUpgradeAvailable(false);
setUpgradeCheckDone(true);
}
} catch (err) {
console.warn("⚠️ read firmware failed:", err);
if (mountedRef.current) {
setFirmware(t("common.unknown"));
setLatestFirmware(t("common.unknown"));
setUpgradeAvailable(false);
setUpgradeCheckDone(true);
}
}
try {
const v = await Central.readCharacteristic(
peripheral,
fullUUID("180f"),
fullUUID("2a19")
);
if (v && (v as any).length) {
const batteryValue = (v as any)[0];
console.log("📘 battery raw:", v, "=>", batteryValue);
if (mountedRef.current) {
setBattery(`${batteryValue}%`);
}
} else if (mountedRef.current) {
setBattery(t("common.unknown"));
}
} catch (err) {
console.warn("⚠️ read battery failed:", err);
if (mountedRef.current) {
setBattery(t("common.unknown"));
}
}
} catch (err) {
console.warn("⚠️ readDeviceInfo failed:", err);
if (mountedRef.current) {
setSerial(
(peripheral.address ||
peripheral.systemId ||
peripheral.name ||
t("common.unknown")) as string
);
setFirmware(t("common.unknown"));
setBattery(t("common.unknown"));
setLatestFirmware(t("common.unknown"));
setUpgradeAvailable(false);
setUpgradeCheckDone(true);
}
}
};
const writeBytesToFff2 = async (bytes: number[]) => {
const normalized = normalizeBytes(bytes);
const buffer = bytesToArrayBuffer(normalized);
console.log("✍️ write FFF2 hex:", bytesToHexString(normalized));
await Central.writeCharacteristic(
peripheral,
fullUUID(CUSTOM_SERVICE_UUID),
fullUUID(CUSTOM_WRITE_UUID),
buffer,
{ withoutResponse: false }
);
};
const waitBoatNotifyOnce = (timeoutMs = 3000): Promise<number[]> => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (boatReadResolverRef.current) {
boatReadResolverRef.current = null;
}
reject(new Error(t("info2.waitFff3Timeout")));
}, timeoutMs);
boatReadResolverRef.current = (bytes: number[]) => {
const normalized = normalizeBytes(bytes);
if (normalized.length < 1) {
console.log("⚠️ 忽略 FFF3:长度不足 1 字节");
return;
}
clearTimeout(timer);
boatReadResolverRef.current = null;
resolve(normalized);
};
});
};
const applyBoatTypeFromNotify = (notifyBytes: number[]) => {
let returnedBoatType: number | null = null;
if (notifyBytes.length === 1) {
returnedBoatType = notifyBytes[0];
} else if (notifyBytes[0] === 0x01 && notifyBytes.length >= 2) {
returnedBoatType = notifyBytes[1];
} else {
returnedBoatType = notifyBytes[0];
}
if (
returnedBoatType === 0x00 ||
returnedBoatType === 0x01 ||
returnedBoatType === 0x02
) {
setSelectedBoat(returnedBoatType as BoatType);
setBoatVerified(
t("info2.currentBoat", { boatName: getBoatNameByValue(returnedBoatType) })
);
setBoatStatus(
t("info2.readCurrentBoat", {
boatName: getBoatNameByValue(returnedBoatType),
})
);
console.log(
"🚣 apply boat type:",
returnedBoatType,
getBoatNameByValue(returnedBoatType)
);
} else {
setBoatVerified(`❌ ${t("info2.unknownBoatType")}`);
setBoatStatus(t("info2.readAbnormal", { hex: bytesToHexString(notifyBytes) }));
console.log("⚠️ unknown boat notify:", bytesToHexString(notifyBytes));
}
};
const handleSetBoatType = async (boatType: BoatType) => {
if (boatBusy) return;
try {
setBoatBusy(true);
setBoatVerified("--");
setBoatNotifyHex("--");
const boatName = getBoatNameByValue(boatType);
setBoatStatus(t("info2.writingBoat", { boatName }));
await writeBytesToFff2([0x01, boatType]);
setBoatStatus(t("info2.writtenWaitRead", { boatName }));
await sleep(200);
setBoatStatus(t("info2.requestingBoatRead"));
const waitPromise = waitBoatNotifyOnce(3000);
await writeBytesToFff2([0x02]);
const notifyBytes = await waitPromise;
const hex = bytesToHexString(notifyBytes);
setBoatNotifyHex(hex);
let returnedBoatType: number | null = null;
if (notifyBytes.length === 1) {
returnedBoatType = notifyBytes[0];
} else if (notifyBytes[0] === 0x01) {
returnedBoatType = notifyBytes[1];
} else {
returnedBoatType = notifyBytes[0];
}
const ok = returnedBoatType === boatType;
if (ok) {
applyBoatTypeFromNotify(notifyBytes);
setBoatStatus(t("info2.setSuccess", { boatName }));
} else {
setBoatVerified(
`❌ ${t("info2.notMatch", {
expected: boatName,
actual: getBoatNameByValue(returnedBoatType),
})}`
);
setBoatStatus(t("info2.readResult", { hex }));
}
} catch (error: any) {
console.warn("❌ set boat type failed:", error);
setBoatStatus(
t("info2.actionFailed", { message: error?.message ?? "unknown error" })
);
setBoatVerified(`❌ ${t("info2.failed")}`);
} finally {
setBoatBusy(false);
}
};
const goToDfu = () => {
if (!isConnected) {
Alert.alert(t("common.notice"), t("info2.disconnectedNeedReconnect"), [
{
text: t("info.confirm"),
onPress: () => {
navigation.goBack();
},
},
]);
return;
}
skipDisconnectOnLeaveRef.current = true;
console.log("🔥 peripheral =", JSON.stringify(peripheral, null, 2));
console.log("🔥 peripheral.address =", peripheral.address);
console.log("🔥 peripheral.systemId =", peripheral.systemId);
console.log("🔥 peripheral.id =", peripheral.id);
console.log("🔥 peripheral.identifier =", peripheral.identifier);
console.log("🔥 peripheral.uuid =", peripheral.uuid);
console.log("🔥 current deviceKey =", deviceKey);
navigation.navigate("Dfu", {
deviceId: deviceKey,
systemId: peripheral.systemId,
address: peripheral.address,
name: peripheral.name ?? "",
firmware: firmware ?? "",
});
};
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);
const connected =
ev.connectionStatus === "connected" || ev.connectionStatus === "ready";
setIsConnected(connected);
if (connected) {
hasConnectedOnceRef.current = true;
return;
}
if (
hasConnectedOnceRef.current &&
mountedRef.current &&
!disconnectingRef.current &&
!hasNavigatedBackRef.current
) {
hasNavigatedBackRef.current = true;
console.log("↩️ 检测到设备断开,自动返回上一级");
if (navigation.canGoBack()) {
navigation.goBack();
}
}
};
Central.addListener("peripheralConnectionStatus", connectionHandler);
return () => {
Central.removeListener("peripheralConnectionStatus", connectionHandler);
};
}, [deviceKey, navigation]);
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;
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]);
useEffect(() => {
mountedRef.current = true;
let cscSubscription: any = null;
let customSubscription: any = null;
let cadenceZeroTimer: ReturnType<typeof setInterval> | null = null;
const handleMeasurement = (ev: any) => {
const rawData: number[] = Array.isArray(ev?.value)
? ev.value
: typeof ev?.value === "string"
? base64ToBytes(ev.value)
: ev?.value instanceof ArrayBuffer
? Array.from(new Uint8Array(ev.value))
: [];
const data = normalizeBytes(rawData);
if (data.length === 0) {
console.log("📥 CSC raw data: <empty>");
return;
}
console.log("📥 CSC raw data hex:", bytesToHexString(data));
if (data.length < 5) {
console.log("⚠️ invalid CSC packet length:", data.length);
return;
}
const flags = data[0];
if (flags !== 0x02) {
console.log("⚠️ unexpected CSC flags:", toHexByte(flags));
return;
}
const crankRevolutions = data[1] | (data[2] << 8);
const crankEventTime = data[3] | (data[4] << 8);
const lastCrank = lastCrankRef.current;
const lastEventTime = lastEventTimeRef.current;
console.log(
"📊 CSC parsed:",
"crankRevolutions=",
crankRevolutions,
"crankEventTime=",
crankEventTime,
"lastCrank=",
lastCrank,
"lastEventTime=",
lastEventTime
);
if (lastCrank !== null && lastEventTime !== null) {
let deltaCrank = crankRevolutions - lastCrank;
let deltaTime = crankEventTime - lastEventTime;
if (deltaCrank < 0) deltaCrank += 0x10000;
if (deltaTime < 0) deltaTime += 0x10000;
console.log(
"📊 CSC delta:",
"deltaCrank=",
deltaCrank,
"deltaTime=",
deltaTime
);
if (deltaCrank > 0 && deltaTime > 0) {
if (deltaCrank <= 4) {
const rpm = (60 * 1024 * deltaCrank) / deltaTime;
console.log("📊 CSC rpm raw:", rpm);
if (rpm >= 1 && rpm <= 220) {
const roundedCadence = Math.round(rpm);
const appNow = Date.now();
if (mountedRef.current) {
setCadence(roundedCadence);
}
lastCadenceUpdateAppTsRef.current = appNow;
console.log(
"✅ cadence updated:",
"cadence=",
roundedCadence,
"appTs=",
appNow,
"bleCrank=",
crankRevolutions,
"bleEventTime=",
crankEventTime
);
} else {
console.log("⚠️ rpm out of range, ignored:", rpm);
}
} else {
console.log("⚠️ deltaCrank too large, ignored:", deltaCrank);
}
} else {
console.log(
"⚠️ delta invalid, ignored:",
"deltaCrank=",
deltaCrank,
"deltaTime=",
deltaTime
);
}
} else {
console.log(
"🟡 first valid CSC packet, baseline saved only:",
"crankRevolutions=",
crankRevolutions,
"crankEventTime=",
crankEventTime
);
}
lastCrankRef.current = crankRevolutions;
lastEventTimeRef.current = crankEventTime;
};
const handleBoatNotify = (ev: any) => {
const rawData: number[] = Array.isArray(ev?.value)
? ev.value
: typeof ev?.value === "string"
? base64ToBytes(ev.value)
: ev?.value instanceof ArrayBuffer
? Array.from(new Uint8Array(ev.value))
: [];
const data = normalizeBytes(rawData);
if (data.length === 0) {
console.log("📥 FFF3 raw data: <empty>");
return;
}
const hex = bytesToHexString(data);
console.log("📥 FFF3 raw data hex:", hex);
if (mountedRef.current) {
setBoatNotifyHex(hex);
}
if (boatReadResolverRef.current) {
boatReadResolverRef.current(data);
}
};
(async () => {
setIsLoading(true);
try {
await Central.connectPeripheral(peripheral);
await new Promise<void>((resolve) => setTimeout(resolve, 500));
setIsConnected(true);
hasConnectedOnceRef.current = true;
hasNavigatedBackRef.current = false;
console.log("📡 subscribe CSC:", CSC_MEASUREMENT_UUID);
cscSubscription = await Central.subscribeCharacteristic(
peripheral,
fullUUID(CSC_SERVICE_UUID),
fullUUID(CSC_MEASUREMENT_UUID),
handleMeasurement
);
console.log("📡 subscribe FFF3:", CUSTOM_NOTIFY_UUID);
customSubscription = await Central.subscribeCharacteristic(
peripheral,
fullUUID(CUSTOM_SERVICE_UUID),
fullUUID(CUSTOM_NOTIFY_UUID),
handleBoatNotify
);
await readDeviceInfo();
cadenceZeroTimer = setInterval(() => {
const lastUpdateTs = lastCadenceUpdateAppTsRef.current;
const now = Date.now();
if (lastUpdateTs === null) {
return;
}
const elapsed = now - lastUpdateTs;
if (elapsed > 3000) {
if (mountedRef.current) {
setCadence((prev) => {
if (prev !== 0) {
console.log(
"⏱️ cadence reset to 0 and baseline cleared:",
"now=",
now,
"lastCadenceUpdateAppTs=",
lastUpdateTs,
"elapsedMs=",
elapsed
);
return 0;
}
return prev;
});
}
lastCrankRef.current = null;
lastEventTimeRef.current = null;
lastCadenceUpdateAppTsRef.current = null;
}
}, 500);
console.log("✅ all subscribe success");
try {
console.log("🔎 default read boat type: write FFF2 = 02");
const waitPromise = waitBoatNotifyOnce(3000);
await writeBytesToFff2([0x02]);
const notifyBytes = await waitPromise;
if (mountedRef.current) {
setBoatNotifyHex(bytesToHexString(notifyBytes));
applyBoatTypeFromNotify(notifyBytes);
}
} catch (err) {
console.warn("⚠️ default read boat type failed:", err);
if (mountedRef.current) {
setBoatStatus(t("info2.defaultReadFailed"));
}
}
if (mountedRef.current) {
setIsLoading(false);
}
} catch (e) {
console.warn("❌ 读取失败", e);
if (mountedRef.current) {
setIsLoading(false);
setIsConnected(false);
setBoatStatus(t("info2.connectOrReadFailed"));
Alert.alert(t("common.notice"), t("info2.connectOrReadFailedAlert"));
}
}
})();
return () => {
mountedRef.current = false;
hasConnectedOnceRef.current = false;
hasNavigatedBackRef.current = false;
setIsConnected(false);
try {
if (cscSubscription?.remove) {
cscSubscription.remove();
}
} catch (err) {
console.warn("⚠️ csc subscription remove failed:", err);
}
try {
if (customSubscription?.remove) {
customSubscription.remove();
}
} catch (err) {
console.warn("⚠️ custom subscription remove failed:", err);
}
if (cadenceZeroTimer) {
clearInterval(cadenceZeroTimer);
cadenceZeroTimer = null;
}
boatReadResolverRef.current = null;
lastCrankRef.current = null;
lastEventTimeRef.current = null;
lastCadenceUpdateAppTsRef.current = null;
try {
if (!skipDisconnectOnLeaveRef.current) {
Central.disconnectPeripheral(peripheral);
} else {
console.log("➡️ cleanup: 跳转到 DFU,不断开蓝牙");
}
} catch {}
};
}, [peripheral]);
return (
<>
<MyStatusbar backgroundColor="#f2f3f7" dark />
<MyHeader
title={t("info.title")}
textColor="#333"
backgroundColor="#f2f3f7"
navigation={navigation}
/>
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
{isLoading ? (
<ActivityIndicator
size="large"
color="#E7141E"
style={{ marginTop: 40 }}
/>
) : (
<>
{/* 1. 桨频数据框 */}
<View style={styles.cadenceCard}>
<Text style={styles.cadenceLabel}>{t("info2.cadence")}</Text>
<Text style={styles.cadenceValue}>{cadence}</Text>
<Text style={styles.cadenceUnit}>{t("info2.cadenceUnit")}</Text>
</View>
{/* 2. 船型选择 */}
<View style={styles.sectionCard}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{t("info2.boatSelect")}</Text>
<View style={styles.statusContainer}>
{boatBusy ? (
<Text style={styles.statusTextBusy}>{t("info2.boatSwitching")}</Text>
) : boatVerified.startsWith("✅") ? (
<Text style={styles.statusTextSuccess}>✅</Text>
) : boatVerified.startsWith("❌") ? (
<Text style={styles.statusTextError}>❌{t("info2.retry")}</Text>
) : (
<Text style={styles.statusText}>-</Text>
)}
</View>
</View>
<View style={styles.buttonRow}>
<Pressable
style={[
styles.boatButton,
selectedBoat === 0x00 && styles.boatButtonActive,
]}
onPress={() => handleSetBoatType(0x00)}
disabled={boatBusy}
>
<Text
style={[
styles.boatButtonText,
selectedBoat === 0x00 && styles.boatButtonTextActive,
]}
>
{t("info2.boatKayak")}
</Text>
</Pressable>
<Pressable
style={[
styles.boatButton,
selectedBoat === 0x01 && styles.boatButtonActive,
]}
onPress={() => handleSetBoatType(0x01)}
disabled={boatBusy}
>
<Text
style={[
styles.boatButtonText,
selectedBoat === 0x01 && styles.boatButtonTextActive,
]}
>
{t("info2.boatRowing")}
</Text>
</Pressable>
<Pressable
style={[
styles.boatButton,
selectedBoat === 0x02 && styles.boatButtonActive,
]}
onPress={() => handleSetBoatType(0x02)}
disabled={boatBusy}
>
<Text
style={[
styles.boatButtonText,
selectedBoat === 0x02 && styles.boatButtonTextActive,
]}
>
{t("info2.boatRacing")}
</Text>
</Pressable>
</View>
</View>
{/* 3. 设备信息(内含固件升级按钮) */}
<View style={styles.infoCard}>
<View style={styles.infoHeader}>
<Text style={styles.infoTitle}>{t("info.title")}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{t("info.bluetoothName")}</Text>
<Text style={styles.infoValue}>{peripheral?.name || t("common.unknown")}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{t("info.idNumber")}</Text>
<Text style={styles.infoValue}>{serial}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{t("info.firmwareVersion")}</Text>
<View style={styles.firmwareRightWrap}>
<Text style={styles.infoValue}>{firmware}</Text>
<Pressable
style={[
styles.inlineDfuButton,
upgradeCheckDone &&
upgradeAvailable &&
styles.inlineDfuButtonHot,
(!isConnected || isLoading || boatBusy) &&
styles.inlineDfuButtonDisabled,
]}
onPress={goToDfu}
disabled={!isConnected || isLoading || boatBusy}
>
<Text
style={[
styles.inlineDfuButtonText,
upgradeCheckDone &&
upgradeAvailable &&
styles.inlineDfuButtonTextHot,
]}
>
{t("info.firmwareUpgrade")}
</Text>
</Pressable>
</View>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{t("info.battery")}</Text>
<Text style={styles.infoValue}>{battery}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{t("info2.latestFirmware")}</Text>
<Text style={styles.infoValue}>
{upgradeCheckDone ? latestFirmware : t("info2.checking")}
</Text>
</View>
<View style={[styles.infoRow, styles.infoRowLast]}>
<Text style={styles.infoLabel}>{t("info.connectionStatus")}</Text>
<Text
style={[
styles.infoValue,
isConnected ? styles.connectedText : styles.disconnectedText,
]}
>
{isConnected ? t("info.connected") : t("info.disconnected")}
</Text>
</View>
</View>
</>
)}
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f2f3f7",
},
contentContainer: {
padding: 16,
gap: 12,
backgroundColor: "#f2f3f7",
},
cadenceCard: {
backgroundColor: "#FFFFFF",
borderRadius: 12,
borderWidth: 1,
borderColor: "#E7141E",
paddingVertical: 58,
paddingHorizontal: 20,
alignItems: "center",
marginBottom: 4,
},
cadenceLabel: {
color: "#666666",
fontSize: 20,
marginBottom: 12,
fontWeight: "500",
},
cadenceValue: {
color: "#E7141E",
fontSize: 100,
fontWeight: "700",
lineHeight: 108,
},
cadenceUnit: {
color: "#666666",
fontSize: 18,
marginTop: 10,
},
sectionCard: {
backgroundColor: "#FFFFFF",
borderRadius: 12,
borderWidth: 1,
borderColor: "#E7141E",
padding: 16,
marginBottom: 4,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
},
sectionTitle: {
color: "#111111",
fontSize: 16,
fontWeight: "600",
},
statusContainer: {
flexDirection: "row",
alignItems: "center",
},
statusTextBusy: {
color: "#FF9800",
fontSize: 14,
fontWeight: "500",
},
statusTextSuccess: {
color: "#4CAF50",
fontSize: 20,
fontWeight: "bold",
},
statusTextError: {
color: "#F44336",
fontSize: 16,
fontWeight: "bold",
},
statusText: {
color: "#444444",
fontSize: 14,
},
buttonRow: {
flexDirection: "row",
justifyContent: "space-between",
gap: 10,
},
boatButton: {
flex: 1,
backgroundColor: "#FFFFFF",
borderWidth: 1,
borderColor: "#E7141E",
borderRadius: 10,
paddingVertical: 10,
alignItems: "center",
},
boatButtonActive: {
backgroundColor: "#E7141E",
},
boatButtonText: {
color: "#E7141E",
fontSize: 15,
fontWeight: "600",
},
boatButtonTextActive: {
color: "#FFFFFF",
},
infoCard: {
backgroundColor: "#FFFFFF",
borderRadius: 12,
borderWidth: 1,
borderColor: "#E7141E",
paddingHorizontal: 14,
paddingVertical: 4,
},
infoHeader: {
paddingBottom: 4,
},
infoTitle: {
color: "#111111",
fontSize: 16,
fontWeight: "600",
},
infoRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 5,
borderBottomWidth: 1,
borderBottomColor: "#F0F0F0",
},
infoRowLast: {
borderBottomWidth: 0,
},
infoLabel: {
color: "#666666",
fontSize: 13,
flexShrink: 0,
marginRight: 12,
},
infoValue: {
color: "#111111",
fontSize: 14,
fontWeight: "600",
},
firmwareRightWrap: {
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-end",
flexShrink: 1,
},
inlineDfuButton: {
marginLeft: 10,
backgroundColor: "#D9D9D9",
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 5,
},
inlineDfuButtonHot: {
backgroundColor: "#E7141E",
},
inlineDfuButtonDisabled: {
opacity: 0.6,
},
inlineDfuButtonText: {
color: "#333333",
fontSize: 12,
fontWeight: "600",
},
inlineDfuButtonTextHot: {
color: "#FFFFFF",
},
connectedText: {
color: "#0A9F4B",
},
disconnectedText: {
color: "#D43C3C",
},
});