Compare commits

...
4 Commits
Author SHA1 Message Date
yecong 20c0fcf1f4 同步应用代码并排除 SDK 交付目录 2026-11-12 16:11:49 +08:00
taoran 145ec23526 版本号 2026-07-15 20:34:09 +08:00
yecong d6f1e2f6b8 Fix Android DFU target id handling 2026-06-17 14:39:05 +08:00
yecong 5350017085 Update DFU translations and ignore local sdk workspace 2026-06-17 14:09:47 +08:00
11 changed files with 184 additions and 65 deletions

No files matched your search

+4
View File
@@ -74,3 +74,7 @@ yarn-error.log
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
# Local SDK workspace
sdk/
sdk-repair/
+5 -2
View File
@@ -2,6 +2,8 @@ apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android" apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react" apply plugin: "com.facebook.react"
apply from: rootProject.file("node.gradle")
/** /**
* This is the configuration block to customize your React Native Android app. * This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need. * By default you don't need to apply any configuration, just uncomment the lines you need.
@@ -26,6 +28,7 @@ react {
/* Bundling */ /* Bundling */
// A list containing the node command and its flags. Default is just 'node'. // A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"] // nodeExecutableAndArgs = ["node"]
nodeExecutableAndArgs = [resolveNodeExecutable()]
// //
// The command to run when bundling. By default is 'bundle' // The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle" // bundleCommand = "ram-bundle"
@@ -82,8 +85,8 @@ android {
applicationId "com.zhixingpai.powerfundfuapp" applicationId "com.zhixingpai.powerfundfuapp"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2 versionCode 3
versionName "1.0.2" versionName "1.0.3"
} }
signingConfigs { signingConfigs {
debug { debug {
-4
View File
@@ -42,7 +42,3 @@ hermesEnabled=true
# This allows your app to draw behind system bars for an immersive UI. # This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity. # Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=false edgeToEdgeEnabled=false
org.gradle.java.home=E:\\jdk\\jdk-17.0.12
+48
View File
@@ -0,0 +1,48 @@
def parseNodeVersion = { String value ->
value.replaceFirst("^v", "")
.tokenize(".")
.collect { part ->
def matcher = part =~ /\d+/
matcher.find() ? matcher.group().toInteger() : 0
}
}
ext.resolveNodeExecutable = {
def fromEnv = System.getenv("NODE_BINARY")
if (fromEnv != null && !fromEnv.trim().isEmpty()) {
return fromEnv.trim()
}
def home = System.getProperty("user.home")
def candidates = []
def nvmVersionsDir = new File(home, ".nvm/versions/node")
if (nvmVersionsDir.isDirectory()) {
def versionDirs = nvmVersionsDir.listFiles()?.findAll {
new File(it, "bin/node").canExecute()
} ?: []
versionDirs.sort { a, b ->
def aVersion = parseNodeVersion(a.name)
def bVersion = parseNodeVersion(b.name)
for (int i = 0; i < Math.max(aVersion.size(), bVersion.size()); i++) {
def aPart = i < aVersion.size() ? aVersion[i] : 0
def bPart = i < bVersion.size() ? bVersion[i] : 0
if (aPart != bPart) {
return bPart <=> aPart
}
}
return b.name <=> a.name
}.each {
candidates.add(new File(it, "bin/node").absolutePath)
}
}
candidates.addAll([
"${home}/.nvm/current/bin/node",
"/opt/homebrew/bin/node",
"/usr/local/bin/node"
])
return candidates.find { new File(it).canExecute() } ?: "node"
}
+7 -1
View File
@@ -1,6 +1,12 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") } plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } apply from: new File(settingsDir, "node.gradle")
def nodeExecutable = resolveNodeExecutable()
def reactNativeCli = new File(settingsDir, "../node_modules/@react-native-community/cli/build/bin.js")
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
ex.autolinkLibrariesFromCommand([nodeExecutable, reactNativeCli.absolutePath, "config"])
}
rootProject.name = 'dfuapp' rootProject.name = 'dfuapp'
include ':app' include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin') includeBuild('../node_modules/@react-native/gradle-plugin')
+2 -2
View File
@@ -4,12 +4,12 @@
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
"clean-a": "cd android && gradlew clean", "clean-a": "cd android && ./gradlew clean",
"ios": "react-native run-ios", "ios": "react-native run-ios",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start", "start": "react-native start",
"test": "jest", "test": "jest",
"build-a": "set NODE_OPTIONS=--openssl-legacy-provider && cd android&&gradlew assembleRelease" "build-a": "cd android && ./gradlew assembleRelease"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@react-native-async-storage/async-storage": "^2.2.0",
+84 -46
View File
@@ -5,10 +5,12 @@ import { RootStackParamList } from "../App";
import RNFS from "react-native-fs"; import RNFS from "react-native-fs";
import { import {
startDfu, startDfu,
getDfuTargetId,
addDfuEventListener, 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 MyStatusbar from "./component/MyStatusbar"; import MyStatusbar from "./component/MyStatusbar";
import MyHeader from "./component/MyHeader"; import MyHeader from "./component/MyHeader";
@@ -63,42 +65,45 @@ const trace = (step: string, payload?: unknown) => {
export default function DfuScreen({ route, navigation }: Props) { export default function DfuScreen({ route, navigation }: Props) {
const { const {
deviceId, deviceId,
systemId,
address,
name, name,
firmware: deviceFirmware, firmware: deviceFirmware,
} = route.params; } = route.params;
const { t } = useTranslation();
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [state, setState] = useState("准备中..."); const [state, setState] = useState(t("dfu.preparing"));
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [latestVersion, setLatestVersion] = useState("读取中..."); const [latestVersion, setLatestVersion] = useState(t("dfu.reading"));
const [isDfuRunning, setIsDfuRunning] = useState(false); const [isDfuRunning, setIsDfuRunning] = useState(false);
const startedRef = useRef(false); const startedRef = useRef(false);
const mapDfuStateToChinese = (s: string): string => { const mapDfuStateToLabel = (s: string): string => {
switch (s) { switch (s) {
case "connecting": case "connecting":
return "连接中…"; return t("dfu.stateConnecting");
case "starting": case "starting":
return "初始化中…"; return t("dfu.stateStarting");
case "enablingDfuMode": case "enablingDfuMode":
return "启用 DFU 模式…"; return t("dfu.stateEnablingDfuMode");
case "uploading": case "uploading":
return "上传固件中…"; return t("dfu.stateUploading");
case "validating": case "validating":
return "校验固件…"; return t("dfu.stateValidating");
case "disconnecting": case "disconnecting":
return "断开连接…"; return t("dfu.stateDisconnecting");
case "completed": case "completed":
return "升级完成"; return t("dfu.stateCompleted");
case "aborted": case "aborted":
return "已取消"; return t("dfu.stateAborted");
case "failed": case "failed":
case "dfu_failed": case "dfu_failed":
return "升级失败"; return t("dfu.stateFailed");
case "initializing": case "initializing":
return "启动中…"; return t("dfu.stateInitializing");
case "errored": case "errored":
return "升级出错!"; return t("dfu.stateErrored");
default: default:
return s; return s;
} }
@@ -108,11 +113,11 @@ export default function DfuScreen({ route, navigation }: Props) {
const unsubscribe = navigation.addListener("beforeRemove", (e) => { const unsubscribe = navigation.addListener("beforeRemove", (e) => {
if (!isDfuRunning) return; if (!isDfuRunning) return;
e.preventDefault(); e.preventDefault();
Alert.alert("请稍候", "正在升级,请勿返回或关闭应用!"); Alert.alert(t("dfu.pleaseWait"), t("dfu.doNotReturn"));
}); });
return unsubscribe; return unsubscribe;
}, [navigation, isDfuRunning]); }, [navigation, isDfuRunning, t]);
useEffect(() => { useEffect(() => {
trace("screen mounted", { trace("screen mounted", {
@@ -152,18 +157,30 @@ export default function DfuScreen({ route, navigation }: Props) {
setError(undefined); setError(undefined);
const rawDeviceId = String(deviceId ?? "").trim(); const rawDeviceId = String(deviceId ?? "").trim();
const safeDeviceId = rawDeviceId; 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: Number.isFinite(rawAddress) ? rawAddress : undefined,
});
const firmwareText = String(deviceFirmware ?? "").trim(); const firmwareText = String(deviceFirmware ?? "").trim();
trace("resolved input", { trace("resolved input", {
rawDeviceId, rawDeviceId,
rawSystemId,
rawAddress,
safeDeviceId, safeDeviceId,
firmwareText, firmwareText,
}); });
if (!safeDeviceId) { if (!safeDeviceId) {
throw new Error("无法生成 DFU 目标设备 ID"); throw new Error(t("dfu.invalidTargetDeviceId"));
} }
const currentFw = parseFirmwareVersion(firmwareText); const currentFw = parseFirmwareVersion(firmwareText);
@@ -178,7 +195,9 @@ export default function DfuScreen({ route, navigation }: Props) {
trace("manifest fetch response", { 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(
t("dfu.manifestDownloadFailed", { status: manifestResp.status })
);
} }
const manifestText = await manifestResp.text(); const manifestText = await manifestResp.text();
@@ -192,7 +211,11 @@ export default function DfuScreen({ route, navigation }: Props) {
try { try {
manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] }; manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] };
} catch (e) { } catch (e) {
throw new Error("manifest 不是合法 JSON: " + manifestText.slice(0, 200)); throw new Error(
t("dfu.manifestInvalidJson", {
preview: manifestText.slice(0, 200),
})
);
} }
const deviceInfo = manifest.devices.find( const deviceInfo = manifest.devices.find(
@@ -204,9 +227,9 @@ export default function DfuScreen({ route, navigation }: Props) {
if (!deviceInfo) { if (!deviceInfo) {
setIsDfuRunning(false); setIsDfuRunning(false);
Alert.alert( Alert.alert(
"无法升级", t("dfu.cannotUpgrade"),
`未找到 hardware=${currentFw.hardware} 的固件`, t("dfu.hardwareNotFound", { hardware: currentFw.hardware }),
[{ text: "确认", onPress: () => navigation.goBack() }] [{ text: t("dfu.confirm"), onPress: () => navigation.goBack() }]
); );
return; return;
} }
@@ -218,14 +241,17 @@ export default function DfuScreen({ route, navigation }: Props) {
if (latestFw.hardware !== currentFw.hardware) { if (latestFw.hardware !== currentFw.hardware) {
throw new Error( throw new Error(
`服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}` t("dfu.hardwareMismatch", {
current: currentFw.hardware,
latest: latestFw.hardware,
})
); );
} }
if (latestFw.iteration <= currentFw.iteration) { if (latestFw.iteration <= currentFw.iteration) {
setIsDfuRunning(false); setIsDfuRunning(false);
Alert.alert("无需升级", "已是最新固件,无需升级", [ Alert.alert(t("dfu.noNeedUpgrade"), t("dfu.alreadyLatest"), [
{ text: "确认", onPress: () => navigation.goBack() }, { text: t("dfu.confirm"), onPress: () => navigation.goBack() },
]); ]);
return; return;
} }
@@ -258,14 +284,18 @@ export default function DfuScreen({ route, navigation }: Props) {
trace("firmware download result", downloadResult); trace("firmware download result", downloadResult);
if (downloadResult.statusCode !== 200) { if (downloadResult.statusCode !== 200) {
throw new Error(`固件包下载失败,HTTP ${downloadResult.statusCode}`); throw new Error(
t("dfu.firmwareDownloadFailed", {
status: downloadResult.statusCode,
})
);
} }
const fileExists = await RNFS.exists(localPath); const fileExists = await RNFS.exists(localPath);
trace("firmware exists check", { fileExists }); trace("firmware exists check", { fileExists });
if (!fileExists) { if (!fileExists) {
throw new Error(`固件包下载后文件不存在: ${localPath}`); throw new Error(t("dfu.firmwareFileMissing", { path: localPath }));
} }
const fileStat = await RNFS.stat(localPath); const fileStat = await RNFS.stat(localPath);
@@ -273,7 +303,9 @@ export default function DfuScreen({ route, navigation }: Props) {
trace("firmware stat", fileStat); trace("firmware stat", fileStat);
if (!Number.isFinite(fileSize) || fileSize <= 0) { if (!Number.isFinite(fileSize) || fileSize <= 0) {
throw new Error(`固件包文件无效,大小为 ${fileStat.size}`); throw new Error(
t("dfu.firmwareFileInvalid", { size: String(fileStat.size) })
);
} }
const dfuFilePath = "file://" + localPath; const dfuFilePath = "file://" + localPath;
@@ -310,9 +342,9 @@ export default function DfuScreen({ route, navigation }: Props) {
trace("startDfu resolved successfully"); trace("startDfu resolved successfully");
setIsDfuRunning(false); setIsDfuRunning(false);
Alert.alert("升级成功", "升级成功,请重连设备", [ Alert.alert(t("dfu.upgradeSuccess"), t("dfu.upgradeSuccessMessage"), [
{ {
text: "确认", text: t("dfu.confirm"),
onPress: () => { onPress: () => {
navigation.reset({ navigation.reset({
index: 0, index: 0,
@@ -329,21 +361,21 @@ export default function DfuScreen({ route, navigation }: Props) {
error: err, error: err,
}); });
setIsDfuRunning(false); setIsDfuRunning(false);
setError(err?.message || "DFU失败"); setError(err?.message || t("dfu.dfuFailed"));
Alert.alert("升级失败", err?.message || "DFU失败", [ Alert.alert(t("dfu.upgradeFailed"), err?.message || t("dfu.dfuFailed"), [
{ text: "确认", onPress: () => navigation.goBack() }, { text: t("dfu.confirm"), onPress: () => navigation.goBack() },
]); ]);
} }
}; };
runDfu(); runDfu();
}, [deviceId, name, deviceFirmware, navigation]); }, [deviceId, name, deviceFirmware, navigation, t]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<MyStatusbar backgroundColor="#FFFFFF" dark /> <MyStatusbar backgroundColor="#FFFFFF" dark />
<MyHeader <MyHeader
title="固件升级" title={t("dfu.title")}
textColor="#333" textColor="#333"
backgroundColor="#FFFFFF" backgroundColor="#FFFFFF"
navigation={navigation} navigation={navigation}
@@ -351,20 +383,26 @@ export default function DfuScreen({ route, navigation }: Props) {
<View style={styles.content}> <View style={styles.content}>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.titleText}>蓝牙名称: {name || "--"}</Text> <Text style={styles.titleText}>
</View> {t("dfu.bluetoothName")}: {name || "--"}
</Text>
<View style={styles.row}>
<Text style={styles.normalText}>最新版本: {latestVersion}</Text>
</View>
<View style={styles.row}>
<Text style={styles.normalText}>当前版本: {deviceFirmware || "--"}</Text>
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<Text style={styles.normalText}> <Text style={styles.normalText}>
升级状态: {mapDfuStateToChinese(state)} {t("dfu.latestVersion")}: {latestVersion}
</Text>
</View>
<View style={styles.row}>
<Text style={styles.normalText}>
{t("dfu.currentVersion")}: {deviceFirmware || "--"}
</Text>
</View>
<View style={styles.row}>
<Text style={styles.normalText}>
{t("dfu.upgradeStatus")}: {mapDfuStateToLabel(state)}
</Text> </Text>
</View> </View>
+2 -2
View File
@@ -95,9 +95,9 @@ export default function HomeScreen({ navigation }: Props) {
<TouchableOpacity style={[styles.button, { marginTop: pxToDp(20) }]} onPress={() => navigation.navigate("ScanScreen2")}> <TouchableOpacity style={[styles.button, { marginTop: pxToDp(20) }]} onPress={() => navigation.navigate("ScanScreen2")}>
<Text style={styles.buttonText}>{t("home.paddle")}</Text> <Text style={styles.buttonText}>{t("home.paddle")}</Text>
</TouchableOpacity> </TouchableOpacity>
{/* <TouchableOpacity style={[styles.button, { marginTop: pxToDp(20) }]} onPress={() => navigation.navigate("ScanScreen3")}> <TouchableOpacity style={[styles.button, { marginTop: pxToDp(20) }]} onPress={() => navigation.navigate("ScanScreen3")}>
<Text style={styles.buttonText}>{t("home.T5trainer")}</Text> <Text style={styles.buttonText}>{t("home.T5trainer")}</Text>
</TouchableOpacity> */} </TouchableOpacity>
</View> </View>
<UpdateModal <UpdateModal
+16 -6
View File
@@ -217,24 +217,34 @@ export default function InfoScreen({ route, navigation }: Props) {
}; };
const readBatteryCharacteristic = async () => { const readBatteryCharacteristic = async () => {
for (let attempt = 0; attempt < 3; attempt += 1) { let lastValidBattery: number | null = null;
for (let attempt = 0; attempt < 4; attempt += 1) {
try { try {
const bytes = await readCharacteristicWithRetry("180f", "2a19", 1); const bytes = await readCharacteristicWithRetry("180f", "2a19", 1);
const batteryValue = bytes[0]; const batteryValue = bytes[0];
if (Number.isInteger(batteryValue) && batteryValue >= 0 && batteryValue <= 100) { if (
return `${batteryValue}%`; Number.isInteger(batteryValue) &&
batteryValue >= 0 &&
batteryValue <= 100
) {
if (lastValidBattery !== null && lastValidBattery === batteryValue) {
return `${batteryValue}%`;
}
lastValidBattery = batteryValue;
} }
} catch { } catch {
// Continue retrying with a short gap below. // Continue retrying with a short gap below.
} }
if (attempt < 2) { if (attempt < 3) {
await sleep(180); await sleep(220);
} }
} }
return "未知"; return lastValidBattery !== null ? `${lastValidBattery}%` : "未知";
}; };
const subscribePowerDataIfNeeded = async () => { const subscribePowerDataIfNeeded = async () => {
+8 -1
View File
@@ -189,9 +189,16 @@
"doNotReturn": "Updating in progress, do not go back or close the app!", "doNotReturn": "Updating in progress, do not go back or close the app!",
"cannotUpgrade": "Cannot Update", "cannotUpgrade": "Cannot Update",
"hardwareNotFound": "Firmware not found for hardware version {hardware}", "hardwareNotFound": "Firmware not found for hardware version {{hardware}}",
"noNeedUpgrade": "No Update Needed", "noNeedUpgrade": "No Update Needed",
"alreadyLatest": "Already the latest firmware, no update needed", "alreadyLatest": "Already the latest firmware, no update needed",
"invalidTargetDeviceId": "Unable to generate DFU target device ID",
"manifestDownloadFailed": "Manifest download failed, HTTP {{status}}",
"manifestInvalidJson": "Manifest is not valid JSON: {{preview}}",
"hardwareMismatch": "Firmware hardware mismatch: current {{current}}, server {{latest}}",
"firmwareDownloadFailed": "Firmware package download failed, HTTP {{status}}",
"firmwareFileMissing": "Firmware package file not found after download: {{path}}",
"firmwareFileInvalid": "Firmware package file is invalid, size {{size}}",
"upgradeSuccess": "Update Successful", "upgradeSuccess": "Update Successful",
"upgradeSuccessMessage": "Update successful, please reconnect the device", "upgradeSuccessMessage": "Update successful, please reconnect the device",
+8 -1
View File
@@ -189,9 +189,16 @@
"doNotReturn": "正在升级,请勿返回或关闭应用!", "doNotReturn": "正在升级,请勿返回或关闭应用!",
"cannotUpgrade": "无法升级", "cannotUpgrade": "无法升级",
"hardwareNotFound": "未找到硬件版本 {hardware} 的固件", "hardwareNotFound": "未找到硬件版本 {{hardware}} 的固件",
"noNeedUpgrade": "无需升级", "noNeedUpgrade": "无需升级",
"alreadyLatest": "已是最新固件,无需升级", "alreadyLatest": "已是最新固件,无需升级",
"invalidTargetDeviceId": "无法生成 DFU 目标设备 ID",
"manifestDownloadFailed": "manifest 下载失败,HTTP {{status}}",
"manifestInvalidJson": "manifest 不是合法 JSON: {{preview}}",
"hardwareMismatch": "服务器固件硬件号不匹配:当前 {{current}},服务器 {{latest}}",
"firmwareDownloadFailed": "固件包下载失败,HTTP {{status}}",
"firmwareFileMissing": "固件包下载后文件不存在: {{path}}",
"firmwareFileInvalid": "固件包文件无效,大小为 {{size}}",
"upgradeSuccess": "升级成功", "upgradeSuccess": "升级成功",
"upgradeSuccessMessage": "升级成功,请重连设备", "upgradeSuccessMessage": "升级成功,请重连设备",