Fix DFU flow and preserve disconnect handling

This commit is contained in:
yecong 2026-06-17 11:53:20 +08:00
parent 1f09babe86
commit 5e387c24f5
2 changed files with 153 additions and 74 deletions

View File

@ -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 { View, Text, StyleSheet, Alert, Platform } from "react-native";
import { NativeStackScreenProps } from "@react-navigation/native-stack"; import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { RootStackParamList } from "../App"; import { RootStackParamList } from "../App";
import RNFS from "react-native-fs"; import RNFS from "react-native-fs";
import { import {
startDfu, startDfu,
getDfuTargetId, addDfuEventListener,
DfuProgressEvent, DfuProgressEvent,
DfuStateEvent, DfuStateEvent,
} from "@systemic-games/react-native-nordic-nrf5-dfu"; } 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 MyStatusbar from "./component/MyStatusbar";
import MyHeader from "./component/MyHeader"; import MyHeader from "./component/MyHeader";
@ -29,18 +27,6 @@ interface ParsedFirmware {
raw: string; 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 parseFirmwareVersion = (text: string): ParsedFirmware => {
const raw = String(text ?? "").trim(); const raw = String(text ?? "").trim();
const parts = raw.split("."); 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) { export default function DfuScreen({ route, navigation }: Props) {
const { t } = useTranslation();
const { const {
deviceId, deviceId,
systemId,
address,
name, name,
firmware: deviceFirmware, firmware: deviceFirmware,
} = route.params; } = route.params;
@ -80,6 +72,7 @@ export default function DfuScreen({ route, navigation }: Props) {
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [latestVersion, setLatestVersion] = useState("读取中..."); const [latestVersion, setLatestVersion] = useState("读取中...");
const [isDfuRunning, setIsDfuRunning] = useState(false); const [isDfuRunning, setIsDfuRunning] = useState(false);
const startedRef = useRef(false);
const mapDfuStateToChinese = (s: string): string => { const mapDfuStateToChinese = (s: string): string => {
switch (s) { switch (s) {
@ -122,57 +115,77 @@ export default function DfuScreen({ route, navigation }: Props) {
}, [navigation, isDfuRunning]); }, [navigation, isDfuRunning]);
useEffect(() => { 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 () => { const runDfu = async () => {
try { try {
trace("runDfu begin");
setIsDfuRunning(true); setIsDfuRunning(true);
setError(undefined); setError(undefined);
const rawDeviceId = String(deviceId ?? "").trim(); const rawDeviceId = String(deviceId ?? "").trim();
const rawSystemId = String(systemId ?? rawDeviceId).trim(); const safeDeviceId = rawDeviceId;
const rawAddress =
typeof address === "number"
? address
: address !== undefined && address !== null
? Number(address)
: undefined;
const safeDeviceId = getDfuTargetId({
systemId: rawSystemId,
address: rawAddress,
});
const firmwareText = String(deviceFirmware ?? "").trim(); const firmwareText = String(deviceFirmware ?? "").trim();
console.log("🔥 rawDeviceId =", rawDeviceId); trace("resolved input", {
console.log("🔥 rawSystemId =", rawSystemId); rawDeviceId,
console.log("🔥 rawAddress =", rawAddress); safeDeviceId,
console.log("🔥 safeDeviceId =", safeDeviceId); firmwareText,
console.log("🔥 firmwareText =", JSON.stringify(firmwareText)); });
if (!safeDeviceId) { if (!safeDeviceId) {
throw new Error("无法生成 DFU 目标设备 ID"); throw new Error("无法生成 DFU 目标设备 ID");
} }
const currentFw = parseFirmwareVersion(firmwareText); const currentFw = parseFirmwareVersion(firmwareText);
console.log("🔥 currentFw =", currentFw); trace("parsed current firmware", currentFw);
const manifestUrl = const manifestUrl =
"https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json"; "https://powerfun.oss-cn-shanghai.aliyuncs.com/yecongdfu/latest.json";
const manifestPath = RNFS.CachesDirectoryPath + "/latest.json"; const manifestPath = RNFS.CachesDirectoryPath + "/latest.json";
console.log("🔥 before fetch manifest"); trace("manifest fetch start", { manifestUrl, manifestPath });
const manifestResp = await fetch(manifestUrl); const manifestResp = await fetch(manifestUrl);
console.log("🔥 manifest status =", manifestResp.status); trace("manifest fetch response", { status: manifestResp.status });
if (!manifestResp.ok) { if (!manifestResp.ok) {
throw new Error(`manifest 下载失败HTTP ${manifestResp.status}`); throw new Error(`manifest 下载失败HTTP ${manifestResp.status}`);
} }
const manifestText = await manifestResp.text(); const manifestText = await manifestResp.text();
console.log("🔥 manifest text =", manifestText); trace("manifest text loaded", { length: manifestText.length });
await RNFS.writeFile(manifestPath, manifestText, "utf8"); await RNFS.writeFile(manifestPath, manifestText, "utf8");
console.log("🔥 manifest saved =", manifestPath); trace("manifest saved", { manifestPath });
let manifest: { devices: DeviceInfo[] }; let manifest: { devices: DeviceInfo[] };
@ -186,7 +199,7 @@ export default function DfuScreen({ route, navigation }: Props) {
(d) => d.hardware === currentFw.hardware (d) => d.hardware === currentFw.hardware
); );
console.log("🔥 matched deviceInfo =", deviceInfo); trace("manifest device matched", { deviceInfo });
if (!deviceInfo) { if (!deviceInfo) {
setIsDfuRunning(false); setIsDfuRunning(false);
@ -201,7 +214,7 @@ export default function DfuScreen({ route, navigation }: Props) {
setLatestVersion(deviceInfo.latestFirmware); setLatestVersion(deviceInfo.latestFirmware);
const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware); const latestFw = parseFirmwareVersion(deviceInfo.latestFirmware);
console.log("🔥 latestFw =", latestFw); trace("parsed latest firmware", latestFw);
if (latestFw.hardware !== currentFw.hardware) { if (latestFw.hardware !== currentFw.hardware) {
throw new Error( throw new Error(
@ -217,49 +230,85 @@ export default function DfuScreen({ route, navigation }: Props) {
return; return;
} }
console.log("🔥 upgrade allowed", { trace("upgrade allowed", {
currentIteration: currentFw.iteration, currentIteration: currentFw.iteration,
latestIteration: latestFw.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"; const localPath = RNFS.CachesDirectoryPath + "/firmware.zip";
await RNFS.writeFile(localPath, zipBase64, "base64"); const oldFileExists = await RNFS.exists(localPath);
console.log("🔥 zip saved =", localPath); if (oldFileExists) {
await RNFS.unlink(localPath);
trace("removed stale firmware zip", { localPath });
}
const dfuFilePath = trace("firmware download start", {
Platform.OS === "android" ? "file://" + localPath : localPath; 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, safeDeviceId,
dfuFilePath, dfuFilePath,
localPath,
fileSize,
currentFirmware: currentFw.raw, currentFirmware: currentFw.raw,
latestFirmware: latestFw.raw, latestFirmware: latestFw.raw,
iosBootloaderName,
}); });
await startDfu(safeDeviceId, dfuFilePath, { await startDfu(safeDeviceId, dfuFilePath, {
alternativeAdvertisingName: iosBootloaderName,
dfuStateListener: (ev: DfuStateEvent) => { dfuStateListener: (ev: DfuStateEvent) => {
console.log("🔥 dfu state =", ev.state); trace("dfu state", ev);
setState(ev.state); setState(ev.state);
}, },
dfuProgressListener: (ev: DfuProgressEvent) => { dfuProgressListener: (ev: DfuProgressEvent) => {
console.log("🔥 dfu progress =", ev.percent); trace("dfu progress", ev);
setProgress(ev.percent); setProgress(ev.percent);
}, },
}); });
trace("startDfu resolved successfully");
setIsDfuRunning(false); setIsDfuRunning(false);
Alert.alert("升级成功", "升级成功,请重连设备", [ Alert.alert("升级成功", "升级成功,请重连设备", [
{ {
@ -273,7 +322,12 @@ export default function DfuScreen({ route, navigation }: Props) {
}, },
]); ]);
} catch (err: any) { } catch (err: any) {
console.log("❌ runDfu error =", err); trace("runDfu error", {
message: err?.message,
code: err?.code,
name: err?.name,
error: err,
});
setIsDfuRunning(false); setIsDfuRunning(false);
setError(err?.message || "DFU失败"); setError(err?.message || "DFU失败");
Alert.alert("升级失败", err?.message || "DFU失败", [ Alert.alert("升级失败", err?.message || "DFU失败", [
@ -283,7 +337,7 @@ export default function DfuScreen({ route, navigation }: Props) {
}; };
runDfu(); runDfu();
}, [deviceId, systemId, address, deviceFirmware, navigation]); }, [deviceId, name, deviceFirmware, navigation]);
return ( return (
<View style={styles.container}> <View style={styles.container}>

View File

@ -73,6 +73,7 @@ export default function InfoScreen({ route, navigation }: Props) {
const notifySubscribedRef = useRef(false); const notifySubscribedRef = useRef(false);
const disconnectingRef = useRef(false); const disconnectingRef = useRef(false);
const skipDisconnectOnLeaveRef = useRef(false);
const mountedRef = useRef(true); const mountedRef = useRef(true);
const deviceReadyRef = useRef(false); const deviceReadyRef = useRef(false);
const readyResolverRef = useRef<(() => void) | null>(null); const readyResolverRef = useRef<(() => void) | null>(null);
@ -326,6 +327,10 @@ export default function InfoScreen({ route, navigation }: Props) {
// ========== 监听连接状态变化,断开时提示重新连接 ========== // ========== 监听连接状态变化,断开时提示重新连接 ==========
useEffect(() => { useEffect(() => {
if (prevConnectedRef.current && !isConnected && isActiveRef.current) { if (prevConnectedRef.current && !isConnected && isActiveRef.current) {
if (skipDisconnectOnLeaveRef.current) {
prevConnectedRef.current = isConnected;
return;
}
Alert.alert(t('info.disconnectTitle'), t('info.disconnectMessage'), [ Alert.alert(t('info.disconnectTitle'), t('info.disconnectMessage'), [
{ text: t('info.confirm'), onPress: () => navigation.goBack() }, { text: t('info.confirm'), onPress: () => navigation.goBack() },
]); ]);
@ -336,6 +341,7 @@ export default function InfoScreen({ route, navigation }: Props) {
// ========== 页面即将返回时,强制断开蓝牙 ========== // ========== 页面即将返回时,强制断开蓝牙 ==========
useEffect(() => { useEffect(() => {
const unsubscribe = navigation.addListener("beforeRemove", async (e) => { const unsubscribe = navigation.addListener("beforeRemove", async (e) => {
if (skipDisconnectOnLeaveRef.current) return;
if (disconnectingRef.current) return; if (disconnectingRef.current) return;
disconnectingRef.current = true; disconnectingRef.current = true;
@ -377,6 +383,16 @@ export default function InfoScreen({ route, navigation }: Props) {
ev.connectionStatus === "connected" || ev.connectionStatus === "ready"; ev.connectionStatus === "connected" || ev.connectionStatus === "ready";
const isReady = 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); setIsConnected(connected);
setIsDeviceReady(isReady); setIsDeviceReady(isReady);
deviceReadyRef.current = isReady; deviceReadyRef.current = isReady;
@ -918,14 +934,23 @@ export default function InfoScreen({ route, navigation }: Props) {
]); ]);
return; return;
} }
// 蓝牙已连接,正常跳转 DFU
navigation.navigate("Dfu", { const dfuRouteParams = {
deviceId: deviceKey, deviceId: deviceKey,
systemId: peripheral.systemId, systemId: peripheral.systemId,
address: peripheral.address, address: peripheral.address,
name: peripheral.name ?? "", name: peripheral.name ?? "",
firmware: firmware ?? "", firmware: firmware ?? "",
}); };
console.log("[DFU-TRACE] navigate to Dfu", {
...dfuRouteParams,
isConnected,
isDeviceReady,
});
skipDisconnectOnLeaveRef.current = true;
navigation.navigate("Dfu", dfuRouteParams);
}} }}
style={styles.pressable} style={styles.pressable}
disabled={isLoading || powerTrimLoading} // ❌ 禁用点击 disabled={isLoading || powerTrimLoading} // ❌ 禁用点击