Fix DFU flow and preserve disconnect handling
This commit is contained in:
parent
1f09babe86
commit
5e387c24f5
@ -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}>
|
||||||
|
|||||||
@ -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} // ❌ 禁用点击
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user