powerfun-setting/src/DfuScreen.tsx

431 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useRef, useState } from "react";
import { View, Text, StyleSheet, Alert, Platform } from "react-native";
2025-11-05 15:18:15 +08:00
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { RootStackParamList } from "../App";
import RNFS from "react-native-fs";
import {
startDfu,
addDfuEventListener,
DfuProgressEvent,
DfuStateEvent,
} from "@systemic-games/react-native-nordic-nrf5-dfu";
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;
}
interface ParsedFirmware {
hardware: number;
iteration: number;
build: string;
raw: string;
}
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,
};
};
const trace = (step: string, payload?: unknown) => {
const timestamp = new Date().toISOString();
if (payload === undefined) {
console.log(`[DFU-TRACE ${timestamp}] ${step}`);
} else {
console.log(`[DFU-TRACE ${timestamp}] ${step}`, payload);
}
};
2025-11-05 15:18:15 +08:00
export default function DfuScreen({ route, navigation }: Props) {
const {
deviceId,
name,
firmware: deviceFirmware,
} = route.params;
2025-11-05 15:18:15 +08:00
const [progress, setProgress] = useState(0);
const [state, setState] = useState("准备中...");
2025-11-05 15:18:15 +08:00
const [error, setError] = useState<string>();
const [latestVersion, setLatestVersion] = useState("读取中...");
2025-11-05 15:18:15 +08:00
const [isDfuRunning, setIsDfuRunning] = useState(false);
const startedRef = useRef(false);
2025-11-05 15:18:15 +08:00
const mapDfuStateToChinese = (s: string): string => {
switch (s) {
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 "已取消";
2025-11-05 15:18:15 +08:00
case "failed":
case "dfu_failed":
return "升级失败";
case "initializing":
return "启动中…";
case "errored":
return "升级出错!";
default:
return s;
2025-11-05 15:18:15 +08:00
}
};
useEffect(() => {
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
if (!isDfuRunning) return;
e.preventDefault();
Alert.alert("请稍候", "正在升级,请勿返回或关闭应用!");
2025-11-05 15:18:15 +08:00
});
return unsubscribe;
}, [navigation, isDfuRunning]);
useEffect(() => {
trace("screen mounted", {
routeParams: {
deviceId,
name,
deviceFirmware,
},
});
return () => {
trace("screen unmounted");
};
}, [deviceId, name, deviceFirmware]);
useEffect(() => {
const sub = addDfuEventListener("log", (ev) => {
trace("dfu native log", ev);
});
return () => {
sub.remove();
};
}, []);
useEffect(() => {
if (startedRef.current) {
trace("runDfu duplicate effect skipped");
return;
}
startedRef.current = true;
2025-11-05 15:18:15 +08:00
const runDfu = async () => {
try {
trace("runDfu begin");
2025-11-05 15:18:15 +08:00
setIsDfuRunning(true);
setError(undefined);
const rawDeviceId = String(deviceId ?? "").trim();
const safeDeviceId = rawDeviceId;
const firmwareText = String(deviceFirmware ?? "").trim();
2025-11-05 15:18:15 +08:00
trace("resolved input", {
rawDeviceId,
safeDeviceId,
firmwareText,
});
if (!safeDeviceId) {
throw new Error("无法生成 DFU 目标设备 ID");
}
const currentFw = parseFirmwareVersion(firmwareText);
trace("parsed current firmware", currentFw);
const manifestUrl =
"https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json";
2025-11-05 15:18:15 +08:00
const manifestPath = RNFS.CachesDirectoryPath + "/latest.json";
trace("manifest fetch start", { manifestUrl, manifestPath });
const manifestResp = await fetch(manifestUrl);
trace("manifest fetch response", { status: manifestResp.status });
if (!manifestResp.ok) {
throw new Error(`manifest 下载失败HTTP ${manifestResp.status}`);
}
2025-11-05 15:18:15 +08:00
const manifestText = await manifestResp.text();
trace("manifest text loaded", { length: manifestText.length });
await RNFS.writeFile(manifestPath, manifestText, "utf8");
trace("manifest saved", { manifestPath });
let manifest: { devices: DeviceInfo[] };
try {
manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] };
} catch (e) {
throw new Error("manifest 不是合法 JSON: " + manifestText.slice(0, 200));
}
const deviceInfo = manifest.devices.find(
(d) => d.hardware === currentFw.hardware
);
trace("manifest device matched", { deviceInfo });
2025-11-05 15:18:15 +08:00
if (!deviceInfo) {
setIsDfuRunning(false);
Alert.alert(
"无法升级",
`未找到 hardware=${currentFw.hardware} 的固件`,
[{ text: "确认", onPress: () => navigation.goBack() }]
);
2025-11-05 15:18:15 +08:00
return;
}
setLatestVersion(deviceInfo.latestFirmware);
const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware);
trace("parsed latest firmware", latestFw);
2025-11-05 15:18:15 +08:00
if (latestFw.hardware !== currentFw.hardware) {
throw new Error(
`服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}`
);
}
if (latestFw.iteration <= currentFw.iteration) {
2025-11-05 15:18:15 +08:00
setIsDfuRunning(false);
Alert.alert("无需升级", "已是最新固件,无需升级", [
{ text: "确认", onPress: () => navigation.goBack() },
2025-11-05 15:18:15 +08:00
]);
return;
}
trace("upgrade allowed", {
currentIteration: currentFw.iteration,
latestIteration: latestFw.iteration,
});
const localPath = RNFS.CachesDirectoryPath + "/firmware.zip";
const oldFileExists = await RNFS.exists(localPath);
if (oldFileExists) {
await RNFS.unlink(localPath);
trace("removed stale firmware zip", { localPath });
}
trace("firmware download start", {
downloadUrl: deviceInfo.download,
localPath,
});
const downloadResult = await RNFS.downloadFile({
fromUrl: deviceInfo.download,
toFile: localPath,
background: false,
discretionary: false,
}).promise;
trace("firmware download result", downloadResult);
2025-11-05 15:18:15 +08:00
if (downloadResult.statusCode !== 200) {
throw new Error(`固件包下载失败HTTP ${downloadResult.statusCode}`);
}
const fileExists = await RNFS.exists(localPath);
trace("firmware exists check", { fileExists });
if (!fileExists) {
throw new Error(`固件包下载后文件不存在: ${localPath}`);
}
const fileStat = await RNFS.stat(localPath);
const fileSize = Number(fileStat.size ?? 0);
trace("firmware stat", fileStat);
if (!Number.isFinite(fileSize) || fileSize <= 0) {
throw new Error(`固件包文件无效,大小为 ${fileStat.size}`);
}
const dfuFilePath = "file://" + localPath;
let iosBootloaderName: string | undefined;
if (Platform.OS === "ios") {
if (name?.startsWith("PF-PM5-")) {
iosBootloaderName = "PF-PM5-DFU";
} else if (name?.startsWith("PF-STK-")) {
iosBootloaderName = "PF-STK-DFU";
}
}
trace("startDfu call", {
safeDeviceId,
dfuFilePath,
localPath,
fileSize,
currentFirmware: currentFw.raw,
latestFirmware: latestFw.raw,
iosBootloaderName,
});
await startDfu(safeDeviceId, dfuFilePath, {
alternativeAdvertisingName: iosBootloaderName,
dfuStateListener: (ev: DfuStateEvent) => {
trace("dfu state", ev);
setState(ev.state);
},
dfuProgressListener: (ev: DfuProgressEvent) => {
trace("dfu progress", ev);
setProgress(ev.percent);
},
2025-11-05 15:18:15 +08:00
});
trace("startDfu resolved successfully");
2025-11-05 15:18:15 +08:00
setIsDfuRunning(false);
Alert.alert("升级成功", "升级成功,请重连设备", [
{
text: "确认",
onPress: () => {
navigation.reset({
index: 0,
routes: [{ name: "Home" }],
});
},
},
]);
2025-11-05 15:18:15 +08:00
} catch (err: any) {
trace("runDfu error", {
message: err?.message,
code: err?.code,
name: err?.name,
error: err,
});
2025-11-05 15:18:15 +08:00
setIsDfuRunning(false);
setError(err?.message || "DFU失败");
Alert.alert("升级失败", err?.message || "DFU失败", [
{ text: "确认", onPress: () => navigation.goBack() },
2025-11-05 15:18:15 +08:00
]);
}
};
runDfu();
}, [deviceId, name, deviceFirmware, navigation]);
2025-11-05 15:18:15 +08:00
return (
<View style={styles.container}>
<MyStatusbar backgroundColor="#FFFFFF" dark />
<MyHeader
title="固件升级"
textColor="#333"
backgroundColor="#FFFFFF"
navigation={navigation}
/>
<View style={styles.content}>
<View style={styles.row}>
<Text style={styles.titleText}>: {name || "--"}</Text>
</View>
<View style={styles.row}>
<Text style={styles.normalText}>: {latestVersion}</Text>
</View>
<View style={styles.row}>
<Text style={styles.normalText}>: {deviceFirmware || "--"}</Text>
</View>
<View style={styles.row}>
<Text style={styles.normalText}>
: {mapDfuStateToChinese(state)}
</Text>
</View>
<View style={styles.progressContainer}>
<View style={[styles.progressBar, { width: `${progress}%` }]} />
<Text style={styles.progressText}>{progress}%</Text>
</View>
{!!error && <Text style={styles.errorText}>{error}</Text>}
2025-11-05 15:18:15 +08:00
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#FFFFFF",
},
content: {
flex: 1,
padding: 20,
backgroundColor: "#FFFFFF",
},
2025-11-05 15:18:15 +08:00
row: {
borderBottomWidth: 1,
borderBottomColor: "#E7141E",
paddingBottom: 6,
marginBottom: 12,
},
titleText: {
fontSize: 18,
color: "#111111",
fontWeight: "600",
},
normalText: {
fontSize: 16,
color: "#222222",
2025-11-05 15:18:15 +08:00
},
progressContainer: {
height: 30,
backgroundColor: "#EEEEEE",
2025-11-05 15:18:15 +08:00
borderRadius: 15,
overflow: "hidden",
marginTop: 40,
justifyContent: "center",
},
progressBar: {
height: "100%",
backgroundColor: "#E7141E",
},
progressText: {
position: "absolute",
alignSelf: "center",
fontWeight: "bold",
color: "#000000",
},
errorText: {
color: "red",
marginTop: 20,
fontSize: 14,
2025-11-05 15:18:15 +08:00
},
});