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; 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(); 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 ( 蓝牙名称: {name || "--"} 最新版本: {latestVersion} 当前版本: {deviceFirmware || "--"} 升级状态: {mapDfuStateToChinese(state)} {progress}% {!!error && {error}} ); } 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, }, });