powerfun-setting/src/DfuScreen.tsx

431 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useRef, useState } from "react";
import { View, Text, StyleSheet, Alert, Platform } from "react-native";
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";
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);
}
};
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("读取中...");
const [isDfuRunning, setIsDfuRunning] = useState(false);
const startedRef = useRef(false);
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 "已取消";
case "failed":
case "dfu_failed":
return "升级失败";
case "initializing":
return "启动中…";
case "errored":
return "升级出错!";
default:
return s;
}
};
useEffect(() => {
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
if (!isDfuRunning) return;
e.preventDefault();
Alert.alert("请稍候", "正在升级,请勿返回或关闭应用!");
});
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;
const runDfu = async () => {
try {
trace("runDfu begin");
setIsDfuRunning(true);
setError(undefined);
const rawDeviceId = String(deviceId ?? "").trim();
const safeDeviceId = rawDeviceId;
const firmwareText = String(deviceFirmware ?? "").trim();
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";
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}`);
}
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 });
if (!deviceInfo) {
setIsDfuRunning(false);
Alert.alert(
"无法升级",
`未找到 hardware=${currentFw.hardware} 的固件`,
[{ text: "确认", onPress: () => navigation.goBack() }]
);
return;
}
setLatestVersion(deviceInfo.latestFirmware);
const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware);
trace("parsed latest firmware", latestFw);
if (latestFw.hardware !== currentFw.hardware) {
throw new Error(
`服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}`
);
}
if (latestFw.iteration <= currentFw.iteration) {
setIsDfuRunning(false);
Alert.alert("无需升级", "已是最新固件,无需升级", [
{ text: "确认", onPress: () => navigation.goBack() },
]);
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);
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);
},
});
trace("startDfu resolved successfully");
setIsDfuRunning(false);
Alert.alert("升级成功", "升级成功,请重连设备", [
{
text: "确认",
onPress: () => {
navigation.reset({
index: 0,
routes: [{ name: "Home" }],
});
},
},
]);
} catch (err: any) {
trace("runDfu error", {
message: err?.message,
code: err?.code,
name: err?.name,
error: err,
});
setIsDfuRunning(false);
setError(err?.message || "DFU失败");
Alert.alert("升级失败", err?.message || "DFU失败", [
{ text: "确认", onPress: () => navigation.goBack() },
]);
}
};
runDfu();
}, [deviceId, name, deviceFirmware, navigation]);
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>}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#FFFFFF",
},
content: {
flex: 1,
padding: 20,
backgroundColor: "#FFFFFF",
},
row: {
borderBottomWidth: 1,
borderBottomColor: "#E7141E",
paddingBottom: 6,
marginBottom: 12,
},
titleText: {
fontSize: 18,
color: "#111111",
fontWeight: "600",
},
normalText: {
fontSize: 16,
color: "#222222",
},
progressContainer: {
height: 30,
backgroundColor: "#EEEEEE",
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,
},
});