From 5e387c24f5bb7b8eac02d5403f54ecd2080dca9c Mon Sep 17 00:00:00 2001 From: yecong Date: Wed, 17 Jun 2026 11:53:20 +0800 Subject: [PATCH] Fix DFU flow and preserve disconnect handling --- src/DfuScreen.tsx | 186 +++++++++++++++++++++++++++++---------------- src/InfoScreen.tsx | 41 ++++++++-- 2 files changed, 153 insertions(+), 74 deletions(-) diff --git a/src/DfuScreen.tsx b/src/DfuScreen.tsx index 4a9d83b..b9c1597 100644 --- a/src/DfuScreen.tsx +++ b/src/DfuScreen.tsx @@ -1,16 +1,14 @@ -import React, { useEffect, useState } from "react"; +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, - getDfuTargetId, + addDfuEventListener, DfuProgressEvent, DfuStateEvent, } from "@systemic-games/react-native-nordic-nrf5-dfu"; -import { useTranslation } from "react-i18next"; -import { encode as btoa } from "base-64"; import MyStatusbar from "./component/MyStatusbar"; import MyHeader from "./component/MyHeader"; @@ -29,18 +27,6 @@ interface ParsedFirmware { raw: string; } -const bytesToBase64 = (bytes: Uint8Array): string => { - let binary = ""; - const chunkSize = 0x8000; - - for (let i = 0; i < bytes.length; i += chunkSize) { - const chunk = bytes.subarray(i, i + chunkSize); - binary += String.fromCharCode(...chunk); - } - - return btoa(binary); -}; - const parseFirmwareVersion = (text: string): ParsedFirmware => { const raw = String(text ?? "").trim(); const parts = raw.split("."); @@ -65,12 +51,18 @@ const parseFirmwareVersion = (text: string): ParsedFirmware => { }; }; +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 { t } = useTranslation(); const { deviceId, - systemId, - address, name, firmware: deviceFirmware, } = route.params; @@ -80,6 +72,7 @@ export default function DfuScreen({ route, navigation }: Props) { const [error, setError] = useState(); const [latestVersion, setLatestVersion] = useState("读取中..."); const [isDfuRunning, setIsDfuRunning] = useState(false); + const startedRef = useRef(false); const mapDfuStateToChinese = (s: string): string => { switch (s) { @@ -122,57 +115,77 @@ export default function DfuScreen({ route, navigation }: Props) { }, [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 rawSystemId = String(systemId ?? rawDeviceId).trim(); - const rawAddress = - typeof address === "number" - ? address - : address !== undefined && address !== null - ? Number(address) - : undefined; - - const safeDeviceId = getDfuTargetId({ - systemId: rawSystemId, - address: rawAddress, - }); + const safeDeviceId = rawDeviceId; const firmwareText = String(deviceFirmware ?? "").trim(); - console.log("🔥 rawDeviceId =", rawDeviceId); - console.log("🔥 rawSystemId =", rawSystemId); - console.log("🔥 rawAddress =", rawAddress); - console.log("🔥 safeDeviceId =", safeDeviceId); - console.log("🔥 firmwareText =", JSON.stringify(firmwareText)); + trace("resolved input", { + rawDeviceId, + safeDeviceId, + firmwareText, + }); if (!safeDeviceId) { throw new Error("无法生成 DFU 目标设备 ID"); } const currentFw = parseFirmwareVersion(firmwareText); - console.log("🔥 currentFw =", currentFw); + trace("parsed current firmware", currentFw); const manifestUrl = "https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json"; const manifestPath = RNFS.CachesDirectoryPath + "/latest.json"; - console.log("🔥 before fetch manifest"); + trace("manifest fetch start", { manifestUrl, manifestPath }); const manifestResp = await fetch(manifestUrl); - console.log("🔥 manifest status =", manifestResp.status); + trace("manifest fetch response", { status: manifestResp.status }); if (!manifestResp.ok) { throw new Error(`manifest 下载失败,HTTP ${manifestResp.status}`); } const manifestText = await manifestResp.text(); - console.log("🔥 manifest text =", manifestText); + trace("manifest text loaded", { length: manifestText.length }); await RNFS.writeFile(manifestPath, manifestText, "utf8"); - console.log("🔥 manifest saved =", manifestPath); + trace("manifest saved", { manifestPath }); let manifest: { devices: DeviceInfo[] }; @@ -186,7 +199,7 @@ export default function DfuScreen({ route, navigation }: Props) { (d) => d.hardware === currentFw.hardware ); - console.log("🔥 matched deviceInfo =", deviceInfo); + trace("manifest device matched", { deviceInfo }); if (!deviceInfo) { setIsDfuRunning(false); @@ -201,7 +214,7 @@ export default function DfuScreen({ route, navigation }: Props) { setLatestVersion(deviceInfo.latestFirmware); const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware); - console.log("🔥 latestFw =", latestFw); + trace("parsed latest firmware", latestFw); if (latestFw.hardware !== currentFw.hardware) { throw new Error( @@ -217,49 +230,85 @@ export default function DfuScreen({ route, navigation }: Props) { return; } - console.log("🔥 upgrade allowed", { + trace("upgrade allowed", { currentIteration: currentFw.iteration, latestIteration: latestFw.iteration, }); - const zipResp = await fetch(deviceInfo.download); - console.log("🔥 zip status =", zipResp.status); - - if (!zipResp.ok) { - throw new Error(`固件包下载失败,HTTP ${zipResp.status}`); - } - - const zipArrayBuffer = await zipResp.arrayBuffer(); - const zipBytes = new Uint8Array(zipArrayBuffer); - console.log("🔥 zip bytes =", zipBytes.length); - - const zipBase64 = bytesToBase64(zipBytes); const localPath = RNFS.CachesDirectoryPath + "/firmware.zip"; - await RNFS.writeFile(localPath, zipBase64, "base64"); - console.log("🔥 zip saved =", localPath); + const oldFileExists = await RNFS.exists(localPath); + if (oldFileExists) { + await RNFS.unlink(localPath); + trace("removed stale firmware zip", { localPath }); + } - const dfuFilePath = - Platform.OS === "android" ? "file://" + localPath : localPath; + trace("firmware download start", { + downloadUrl: deviceInfo.download, + localPath, + }); - console.log("🔥 before startDfu =", { + 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) => { - console.log("🔥 dfu state =", ev.state); + trace("dfu state", ev); setState(ev.state); }, dfuProgressListener: (ev: DfuProgressEvent) => { - console.log("🔥 dfu progress =", ev.percent); + trace("dfu progress", ev); setProgress(ev.percent); }, }); + trace("startDfu resolved successfully"); setIsDfuRunning(false); Alert.alert("升级成功", "升级成功,请重连设备", [ { @@ -273,7 +322,12 @@ export default function DfuScreen({ route, navigation }: Props) { }, ]); } catch (err: any) { - console.log("❌ runDfu error =", err); + 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失败", [ @@ -283,7 +337,7 @@ export default function DfuScreen({ route, navigation }: Props) { }; runDfu(); - }, [deviceId, systemId, address, deviceFirmware, navigation]); + }, [deviceId, name, deviceFirmware, navigation]); return ( @@ -373,4 +427,4 @@ const styles = StyleSheet.create({ marginTop: 20, fontSize: 14, }, -}); \ No newline at end of file +}); diff --git a/src/InfoScreen.tsx b/src/InfoScreen.tsx index a82a254..1751fd3 100644 --- a/src/InfoScreen.tsx +++ b/src/InfoScreen.tsx @@ -73,6 +73,7 @@ export default function InfoScreen({ route, navigation }: Props) { const notifySubscribedRef = useRef(false); const disconnectingRef = useRef(false); + const skipDisconnectOnLeaveRef = useRef(false); const mountedRef = useRef(true); const deviceReadyRef = useRef(false); const readyResolverRef = useRef<(() => void) | null>(null); @@ -326,6 +327,10 @@ export default function InfoScreen({ route, navigation }: Props) { // ========== 监听连接状态变化,断开时提示重新连接 ========== useEffect(() => { if (prevConnectedRef.current && !isConnected && isActiveRef.current) { + if (skipDisconnectOnLeaveRef.current) { + prevConnectedRef.current = isConnected; + return; + } Alert.alert(t('info.disconnectTitle'), t('info.disconnectMessage'), [ { text: t('info.confirm'), onPress: () => navigation.goBack() }, ]); @@ -336,6 +341,7 @@ export default function InfoScreen({ route, navigation }: Props) { // ========== 页面即将返回时,强制断开蓝牙 ========== useEffect(() => { const unsubscribe = navigation.addListener("beforeRemove", async (e) => { + if (skipDisconnectOnLeaveRef.current) return; if (disconnectingRef.current) return; disconnectingRef.current = true; @@ -377,6 +383,16 @@ export default function InfoScreen({ route, navigation }: Props) { ev.connectionStatus === "connected" || ev.connectionStatus === "ready"; const isReady = ev.connectionStatus === "ready"; + if (skipDisconnectOnLeaveRef.current && !connected) { + console.log("⏭️ DFU flow disconnect ignored on InfoScreen"); + setIsConnected(false); + setIsDeviceReady(false); + deviceReadyRef.current = false; + notifySubscribedRef.current = false; + powerDataSubscribedRef.current = false; + return; + } + setIsConnected(connected); setIsDeviceReady(isReady); deviceReadyRef.current = isReady; @@ -918,14 +934,23 @@ export default function InfoScreen({ route, navigation }: Props) { ]); return; } - // 蓝牙已连接,正常跳转 DFU - navigation.navigate("Dfu", { - deviceId: deviceKey, - systemId: peripheral.systemId, - address: peripheral.address, - name: peripheral.name ?? "", - firmware: firmware ?? "", -}); + + const dfuRouteParams = { + deviceId: deviceKey, + systemId: peripheral.systemId, + address: peripheral.address, + name: peripheral.name ?? "", + firmware: firmware ?? "", + }; + + console.log("[DFU-TRACE] navigate to Dfu", { + ...dfuRouteParams, + isConnected, + isDeviceReady, + }); + + skipDisconnectOnLeaveRef.current = true; + navigation.navigate("Dfu", dfuRouteParams); }} style={styles.pressable} disabled={isLoading || powerTrimLoading} // ❌ 禁用点击