2025-11-05 15:18:15 +08:00
|
|
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
|
|
import { View, Text, StyleSheet, Alert, BackHandler } from "react-native";
|
|
|
|
|
|
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
|
|
|
|
|
import { RootStackParamList } from "../App";
|
|
|
|
|
|
import RNFS from "react-native-fs";
|
|
|
|
|
|
import { startDfu, DfuProgressEvent, DfuStateEvent } from "@systemic-games/react-native-nordic-nrf5-dfu";
|
2025-12-25 16:16:01 +08:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
import MyStatusbar from "./component/MyStatusbar";
|
|
|
|
|
|
import MyHeader from "./component/MyHeader";
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
type Props = NativeStackScreenProps<RootStackParamList, "Dfu">;
|
|
|
|
|
|
|
|
|
|
|
|
interface DeviceInfo {
|
|
|
|
|
|
hardware: number;
|
|
|
|
|
|
latestFirmware: string;
|
|
|
|
|
|
download: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function DfuScreen({ route, navigation }: Props) {
|
2025-12-25 16:16:01 +08:00
|
|
|
|
const { t } = useTranslation();
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const { deviceId, name, firmware: deviceFirmware } = route.params;
|
|
|
|
|
|
|
|
|
|
|
|
const [progress, setProgress] = useState(0);
|
2025-12-25 16:16:01 +08:00
|
|
|
|
const [state, setState] = useState(t('dfu.preparing'));
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const [error, setError] = useState<string>();
|
2025-12-25 16:16:01 +08:00
|
|
|
|
const [latestVersion, setLatestVersion] = useState<string>(t('dfu.reading'));
|
2025-11-05 15:18:15 +08:00
|
|
|
|
const [isDfuRunning, setIsDfuRunning] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const mapDfuStateToChinese = (state: string): string => {
|
|
|
|
|
|
switch (state) {
|
2025-12-25 16:16:01 +08:00
|
|
|
|
case "connecting": return t('dfu.stateConnecting');
|
|
|
|
|
|
case "starting": return t('dfu.stateStarting');
|
|
|
|
|
|
case "enablingDfuMode": return t('dfu.stateEnablingDfuMode');
|
|
|
|
|
|
case "uploading": return t('dfu.stateUploading');
|
|
|
|
|
|
case "validating": return t('dfu.stateValidating');
|
|
|
|
|
|
case "disconnecting": return t('dfu.stateDisconnecting');
|
|
|
|
|
|
case "completed": return t('dfu.stateCompleted');
|
|
|
|
|
|
case "aborted": return t('dfu.stateAborted');
|
2025-11-05 15:18:15 +08:00
|
|
|
|
case "failed":
|
2025-12-25 16:16:01 +08:00
|
|
|
|
case "dfu_failed": return t('dfu.stateFailed');
|
|
|
|
|
|
case "initializing": return t('dfu.stateInitializing');
|
|
|
|
|
|
case "errored": return t('dfu.stateErrored');
|
2025-11-05 15:18:15 +08:00
|
|
|
|
default: return state;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-25 16:16:01 +08:00
|
|
|
|
|
2025-11-05 15:18:15 +08:00
|
|
|
|
// ✅ 拦截所有导航返回(iOS + Android)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
|
|
|
|
|
|
if (!isDfuRunning) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 阻止返回
|
|
|
|
|
|
e.preventDefault();
|
2025-12-25 16:16:01 +08:00
|
|
|
|
Alert.alert(t('dfu.pleaseWait'), t('dfu.doNotReturn'));
|
2025-11-05 15:18:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return unsubscribe;
|
|
|
|
|
|
}, [navigation, isDfuRunning]);
|
|
|
|
|
|
|
2025-12-25 16:16:01 +08:00
|
|
|
|
|
2025-11-05 15:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const runDfu = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setIsDfuRunning(true);
|
|
|
|
|
|
|
|
|
|
|
|
const manifestUrl = "https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json";
|
|
|
|
|
|
const manifestPath = RNFS.CachesDirectoryPath + "/latest.json";
|
|
|
|
|
|
|
|
|
|
|
|
await RNFS.downloadFile({ fromUrl: manifestUrl, toFile: manifestPath }).promise;
|
|
|
|
|
|
const manifestContent = await RNFS.readFile(manifestPath);
|
|
|
|
|
|
const manifest = JSON.parse(manifestContent) as { devices: DeviceInfo[] };
|
|
|
|
|
|
|
|
|
|
|
|
const [deviceHWStr, deviceFWStr] = deviceFirmware.split(".");
|
|
|
|
|
|
const deviceHW = parseInt(deviceHWStr);
|
|
|
|
|
|
const deviceFW = parseInt(deviceFWStr);
|
|
|
|
|
|
|
|
|
|
|
|
const deviceInfo = manifest.devices.find(d => d.hardware === deviceHW);
|
|
|
|
|
|
if (!deviceInfo) {
|
|
|
|
|
|
setIsDfuRunning(false);
|
2025-12-25 16:16:01 +08:00
|
|
|
|
Alert.alert(t('dfu.cannotUpgrade'), t('dfu.hardwareNotFound', { hardware: deviceHW }), [
|
|
|
|
|
|
{ text: t('dfu.confirm'), onPress: () => navigation.goBack() },
|
2025-11-05 15:18:15 +08:00
|
|
|
|
]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLatestVersion(deviceInfo.latestFirmware);
|
|
|
|
|
|
|
|
|
|
|
|
const [, latestFWStr] = deviceInfo.latestFirmware.split(".");
|
|
|
|
|
|
const latestFW = parseInt(latestFWStr);
|
|
|
|
|
|
|
|
|
|
|
|
if (latestFW <= deviceFW) {
|
|
|
|
|
|
setIsDfuRunning(false);
|
2025-12-25 16:16:01 +08:00
|
|
|
|
Alert.alert(t('dfu.noNeedUpgrade'), t('dfu.alreadyLatest'), [
|
|
|
|
|
|
{ text: t('dfu.confirm'), onPress: () => navigation.goBack() },
|
2025-11-05 15:18:15 +08:00
|
|
|
|
]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const localPath = RNFS.CachesDirectoryPath + "/firmware.zip";
|
|
|
|
|
|
await RNFS.downloadFile({ fromUrl: deviceInfo.download, toFile: localPath }).promise;
|
|
|
|
|
|
|
|
|
|
|
|
await startDfu(deviceId, "file://" + localPath, {
|
|
|
|
|
|
dfuStateListener: (ev: DfuStateEvent) => setState(ev.state),
|
|
|
|
|
|
dfuProgressListener: (ev: DfuProgressEvent) => setProgress(ev.percent),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setIsDfuRunning(false);
|
2025-12-25 16:16:01 +08:00
|
|
|
|
Alert.alert(t('dfu.upgradeSuccess'), t('dfu.upgradeSuccessMessage'), [
|
|
|
|
|
|
{ text: t('dfu.confirm'), onPress: () => navigation.navigate("Home") },
|
2025-11-05 15:18:15 +08:00
|
|
|
|
]);
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
setIsDfuRunning(false);
|
2025-12-25 16:16:01 +08:00
|
|
|
|
setError(err.message || t('dfu.dfuFailed'));
|
|
|
|
|
|
Alert.alert(t('dfu.upgradeFailed'), err.message || t('dfu.dfuFailed'), [
|
|
|
|
|
|
{ text: t('dfu.confirm'), onPress: () => navigation.goBack() },
|
2025-11-05 15:18:15 +08:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
runDfu();
|
|
|
|
|
|
}, [deviceId, deviceFirmware, navigation]);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-25 16:16:01 +08:00
|
|
|
|
<View style={{flex:1,backgroundColor:'#f2f3f7'}}>
|
|
|
|
|
|
<MyStatusbar backgroundColor="#f2f3f7" dark></MyStatusbar>
|
|
|
|
|
|
<MyHeader title={t("dfu.title")} textColor="#333" backgroundColor="#f2f3f7" navigation={navigation}></MyHeader>
|
|
|
|
|
|
<View style={{ flex: 1, padding: 20 }}>
|
|
|
|
|
|
|
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<Text style={{ fontSize: 16, color: "#333" }}>
|
|
|
|
|
|
{t('dfu.bluetoothName')}: {name}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<Text style={{ fontSize: 16, color: "#333" }}>
|
|
|
|
|
|
{t('dfu.latestVersion')}: {latestVersion}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<Text style={{ fontSize: 16, color: "#333" }}>
|
|
|
|
|
|
{t('dfu.currentVersion')}: {deviceFirmware}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<Text style={{ fontSize: 16, color: "#333" }}>
|
|
|
|
|
|
{t('dfu.upgradeStatus')}: {mapDfuStateToChinese(state)}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 横向进度条 */}
|
|
|
|
|
|
<View style={styles.progressContainer}>
|
|
|
|
|
|
<View style={[styles.progressBar, { width: `${progress}%` }]} />
|
|
|
|
|
|
<Text style={styles.progressText}>{progress}%</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{error && <Text style={{ color: "red", marginTop: 20 }}>{error}</Text>}
|
2025-11-05 15:18:15 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
|
row: {
|
|
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: "red",
|
|
|
|
|
|
paddingBottom: 4,
|
|
|
|
|
|
marginBottom: 8,
|
|
|
|
|
|
},
|
|
|
|
|
|
progressContainer: {
|
|
|
|
|
|
height: 30,
|
|
|
|
|
|
backgroundColor: "#eee",
|
|
|
|
|
|
borderRadius: 15,
|
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
|
marginTop: 40,
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
progressBar: {
|
|
|
|
|
|
height: "100%",
|
|
|
|
|
|
backgroundColor: "#E7141E",
|
|
|
|
|
|
},
|
|
|
|
|
|
progressText: {
|
|
|
|
|
|
position: "absolute",
|
|
|
|
|
|
alignSelf: "center",
|
|
|
|
|
|
fontWeight: "bold",
|
|
|
|
|
|
color: "#000",
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|