174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
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";
|
||
|
||
type Props = NativeStackScreenProps<RootStackParamList, "Dfu">;
|
||
|
||
interface DeviceInfo {
|
||
hardware: number;
|
||
latestFirmware: string;
|
||
download: string;
|
||
}
|
||
|
||
export default function DfuScreen({ route, navigation }: Props) {
|
||
const { deviceId, name, firmware: deviceFirmware } = route.params;
|
||
|
||
const [progress, setProgress] = useState(0);
|
||
const [state, setState] = useState("准备中...");
|
||
const [error, setError] = useState<string>();
|
||
const [latestVersion, setLatestVersion] = useState<string>("读取中...");
|
||
const [isDfuRunning, setIsDfuRunning] = useState(false);
|
||
|
||
const mapDfuStateToChinese = (state: string): string => {
|
||
switch (state) {
|
||
case "connecting": return "连接中…";
|
||
case "starting": return "初始化中…";
|
||
case "enablingDfuMode": return "启用 DFU 模式…";
|
||
case "uploading": return "上传固件中…";
|
||
case "validating": return "校验固件…";
|
||
case "disconnecting": return "断开连接…";
|
||
case "completed": return "升级完成";
|
||
case "aborted": return "已取消";
|
||
case "failed":
|
||
case "dfu_failed": return "升级失败";
|
||
case "initializing": return "启动中…";
|
||
case "errored": return "升级出错!";
|
||
default: return state;
|
||
}
|
||
};
|
||
|
||
|
||
// ✅ 拦截所有导航返回(iOS + Android)
|
||
useEffect(() => {
|
||
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
|
||
if (!isDfuRunning) return;
|
||
|
||
// 阻止返回
|
||
e.preventDefault();
|
||
Alert.alert("请稍候", "正在升级,请勿返回或关闭应用!");
|
||
});
|
||
|
||
return unsubscribe;
|
||
}, [navigation, isDfuRunning]);
|
||
|
||
|
||
|
||
|
||
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);
|
||
Alert.alert("无法升级", `未找到硬件版本 ${deviceHW} 的固件`, [
|
||
{ text: "确认", onPress: () => navigation.goBack() },
|
||
]);
|
||
return;
|
||
}
|
||
|
||
setLatestVersion(deviceInfo.latestFirmware);
|
||
|
||
const [, latestFWStr] = deviceInfo.latestFirmware.split(".");
|
||
const latestFW = parseInt(latestFWStr);
|
||
|
||
if (latestFW <= deviceFW) {
|
||
setIsDfuRunning(false);
|
||
Alert.alert("无需升级", "已是最新固件,无需升级", [
|
||
{ text: "确认", onPress: () => navigation.goBack() },
|
||
]);
|
||
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);
|
||
Alert.alert("升级成功", "升级成功,请重连设备", [
|
||
{ text: "确认", onPress: () => navigation.navigate("Home") },
|
||
]);
|
||
} catch (err: any) {
|
||
setIsDfuRunning(false);
|
||
setError(err.message || "DFU失败");
|
||
Alert.alert("升级失败", err.message || "DFU失败", [
|
||
{ text: "确认", onPress: () => navigation.goBack() },
|
||
]);
|
||
}
|
||
};
|
||
|
||
runDfu();
|
||
}, [deviceId, deviceFirmware, navigation]);
|
||
|
||
return (
|
||
<View style={{ flex: 1, padding: 20 }}>
|
||
<View style={styles.row}>
|
||
<Text style={{ fontSize: 16 }}>蓝牙名称: {name}</Text>
|
||
</View>
|
||
<View style={styles.row}>
|
||
<Text style={{ fontSize: 16 }}>最新版本: {latestVersion}</Text>
|
||
</View>
|
||
<View style={styles.row}>
|
||
<Text style={{ fontSize: 16 }}>当前版本: {deviceFirmware}</Text>
|
||
</View>
|
||
<View style={styles.row}>
|
||
<Text style={{ fontSize: 16 }}>升级状态: {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>}
|
||
</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",
|
||
},
|
||
});
|