Compare commits
4
Commits
5e387c24f5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c0fcf1f4 | ||
|
|
145ec23526 | ||
|
|
d6f1e2f6b8 | ||
|
|
5350017085 |
No files matched your search
@@ -74,3 +74,7 @@ yarn-error.log
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Local SDK workspace
|
||||
sdk/
|
||||
sdk-repair/
|
||||
@@ -2,6 +2,8 @@ apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
apply from: rootProject.file("node.gradle")
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -26,6 +28,7 @@ react {
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
nodeExecutableAndArgs = [resolveNodeExecutable()]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
@@ -82,8 +85,8 @@ android {
|
||||
applicationId "com.zhixingpai.powerfundfuapp"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2
|
||||
versionName "1.0.2"
|
||||
versionCode 3
|
||||
versionName "1.0.3"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
||||
@@ -42,7 +42,3 @@ hermesEnabled=true
|
||||
# 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.
|
||||
edgeToEdgeEnabled=false
|
||||
|
||||
|
||||
|
||||
org.gradle.java.home=E:\\jdk\\jdk-17.0.12
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||
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'
|
||||
include ':app'
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
+2
-2
@@ -4,12 +4,12 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"clean-a": "cd android && gradlew clean",
|
||||
"clean-a": "cd android && ./gradlew clean",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"build-a": "set NODE_OPTIONS=--openssl-legacy-provider && cd android&&gradlew assembleRelease"
|
||||
"build-a": "cd android && ./gradlew assembleRelease"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
|
||||
+84
-46
@@ -5,10 +5,12 @@ 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 MyStatusbar from "./component/MyStatusbar";
|
||||
import MyHeader from "./component/MyHeader";
|
||||
|
||||
@@ -63,42 +65,45 @@ const trace = (step: string, payload?: unknown) => {
|
||||
export default function DfuScreen({ route, navigation }: Props) {
|
||||
const {
|
||||
deviceId,
|
||||
systemId,
|
||||
address,
|
||||
name,
|
||||
firmware: deviceFirmware,
|
||||
} = route.params;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [state, setState] = useState("准备中...");
|
||||
const [state, setState] = useState(t("dfu.preparing"));
|
||||
const [error, setError] = useState<string>();
|
||||
const [latestVersion, setLatestVersion] = useState("读取中...");
|
||||
const [latestVersion, setLatestVersion] = useState(t("dfu.reading"));
|
||||
const [isDfuRunning, setIsDfuRunning] = useState(false);
|
||||
const startedRef = useRef(false);
|
||||
|
||||
const mapDfuStateToChinese = (s: string): string => {
|
||||
const mapDfuStateToLabel = (s: string): string => {
|
||||
switch (s) {
|
||||
case "connecting":
|
||||
return "连接中…";
|
||||
return t("dfu.stateConnecting");
|
||||
case "starting":
|
||||
return "初始化中…";
|
||||
return t("dfu.stateStarting");
|
||||
case "enablingDfuMode":
|
||||
return "启用 DFU 模式…";
|
||||
return t("dfu.stateEnablingDfuMode");
|
||||
case "uploading":
|
||||
return "上传固件中…";
|
||||
return t("dfu.stateUploading");
|
||||
case "validating":
|
||||
return "校验固件…";
|
||||
return t("dfu.stateValidating");
|
||||
case "disconnecting":
|
||||
return "断开连接…";
|
||||
return t("dfu.stateDisconnecting");
|
||||
case "completed":
|
||||
return "升级完成";
|
||||
return t("dfu.stateCompleted");
|
||||
case "aborted":
|
||||
return "已取消";
|
||||
return t("dfu.stateAborted");
|
||||
case "failed":
|
||||
case "dfu_failed":
|
||||
return "升级失败";
|
||||
return t("dfu.stateFailed");
|
||||
case "initializing":
|
||||
return "启动中…";
|
||||
return t("dfu.stateInitializing");
|
||||
case "errored":
|
||||
return "升级出错!";
|
||||
return t("dfu.stateErrored");
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -108,11 +113,11 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
const unsubscribe = navigation.addListener("beforeRemove", (e) => {
|
||||
if (!isDfuRunning) return;
|
||||
e.preventDefault();
|
||||
Alert.alert("请稍候", "正在升级,请勿返回或关闭应用!");
|
||||
Alert.alert(t("dfu.pleaseWait"), t("dfu.doNotReturn"));
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [navigation, isDfuRunning]);
|
||||
}, [navigation, isDfuRunning, t]);
|
||||
|
||||
useEffect(() => {
|
||||
trace("screen mounted", {
|
||||
@@ -152,18 +157,30 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
setError(undefined);
|
||||
|
||||
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();
|
||||
|
||||
trace("resolved input", {
|
||||
rawDeviceId,
|
||||
rawSystemId,
|
||||
rawAddress,
|
||||
safeDeviceId,
|
||||
firmwareText,
|
||||
});
|
||||
|
||||
if (!safeDeviceId) {
|
||||
throw new Error("无法生成 DFU 目标设备 ID");
|
||||
throw new Error(t("dfu.invalidTargetDeviceId"));
|
||||
}
|
||||
|
||||
const currentFw = parseFirmwareVersion(firmwareText);
|
||||
@@ -178,7 +195,9 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
trace("manifest fetch response", { status: manifestResp.status });
|
||||
|
||||
if (!manifestResp.ok) {
|
||||
throw new Error(`manifest 下载失败,HTTP ${manifestResp.status}`);
|
||||
throw new Error(
|
||||
t("dfu.manifestDownloadFailed", { status: manifestResp.status })
|
||||
);
|
||||
}
|
||||
|
||||
const manifestText = await manifestResp.text();
|
||||
@@ -192,7 +211,11 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
try {
|
||||
manifest = JSON.parse(manifestText) as { devices: DeviceInfo[] };
|
||||
} 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(
|
||||
@@ -204,9 +227,9 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
if (!deviceInfo) {
|
||||
setIsDfuRunning(false);
|
||||
Alert.alert(
|
||||
"无法升级",
|
||||
`未找到 hardware=${currentFw.hardware} 的固件`,
|
||||
[{ text: "确认", onPress: () => navigation.goBack() }]
|
||||
t("dfu.cannotUpgrade"),
|
||||
t("dfu.hardwareNotFound", { hardware: currentFw.hardware }),
|
||||
[{ text: t("dfu.confirm"), onPress: () => navigation.goBack() }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -218,14 +241,17 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
|
||||
if (latestFw.hardware !== currentFw.hardware) {
|
||||
throw new Error(
|
||||
`服务器固件硬件号不匹配:当前 ${currentFw.hardware},服务器 ${latestFw.hardware}`
|
||||
t("dfu.hardwareMismatch", {
|
||||
current: currentFw.hardware,
|
||||
latest: latestFw.hardware,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (latestFw.iteration <= currentFw.iteration) {
|
||||
setIsDfuRunning(false);
|
||||
Alert.alert("无需升级", "已是最新固件,无需升级", [
|
||||
{ text: "确认", onPress: () => navigation.goBack() },
|
||||
Alert.alert(t("dfu.noNeedUpgrade"), t("dfu.alreadyLatest"), [
|
||||
{ text: t("dfu.confirm"), onPress: () => navigation.goBack() },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
@@ -258,14 +284,18 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
trace("firmware download result", downloadResult);
|
||||
|
||||
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);
|
||||
trace("firmware exists check", { fileExists });
|
||||
|
||||
if (!fileExists) {
|
||||
throw new Error(`固件包下载后文件不存在: ${localPath}`);
|
||||
throw new Error(t("dfu.firmwareFileMissing", { path: localPath }));
|
||||
}
|
||||
|
||||
const fileStat = await RNFS.stat(localPath);
|
||||
@@ -273,7 +303,9 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
trace("firmware stat", fileStat);
|
||||
|
||||
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;
|
||||
@@ -310,9 +342,9 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
|
||||
trace("startDfu resolved successfully");
|
||||
setIsDfuRunning(false);
|
||||
Alert.alert("升级成功", "升级成功,请重连设备", [
|
||||
Alert.alert(t("dfu.upgradeSuccess"), t("dfu.upgradeSuccessMessage"), [
|
||||
{
|
||||
text: "确认",
|
||||
text: t("dfu.confirm"),
|
||||
onPress: () => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
@@ -329,21 +361,21 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
error: err,
|
||||
});
|
||||
setIsDfuRunning(false);
|
||||
setError(err?.message || "DFU失败");
|
||||
Alert.alert("升级失败", err?.message || "DFU失败", [
|
||||
{ text: "确认", onPress: () => navigation.goBack() },
|
||||
setError(err?.message || t("dfu.dfuFailed"));
|
||||
Alert.alert(t("dfu.upgradeFailed"), err?.message || t("dfu.dfuFailed"), [
|
||||
{ text: t("dfu.confirm"), onPress: () => navigation.goBack() },
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
runDfu();
|
||||
}, [deviceId, name, deviceFirmware, navigation]);
|
||||
}, [deviceId, name, deviceFirmware, navigation, t]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<MyStatusbar backgroundColor="#FFFFFF" dark />
|
||||
<MyHeader
|
||||
title="固件升级"
|
||||
title={t("dfu.title")}
|
||||
textColor="#333"
|
||||
backgroundColor="#FFFFFF"
|
||||
navigation={navigation}
|
||||
@@ -351,20 +383,26 @@ export default function DfuScreen({ route, navigation }: Props) {
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.titleText}>蓝牙名称: {name || "--"}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.normalText}>最新版本: {latestVersion}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.normalText}>当前版本: {deviceFirmware || "--"}</Text>
|
||||
<Text style={styles.titleText}>
|
||||
{t("dfu.bluetoothName")}: {name || "--"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<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>
|
||||
</View>
|
||||
|
||||
|
||||
+2
-2
@@ -95,9 +95,9 @@ export default function HomeScreen({ navigation }: Props) {
|
||||
<TouchableOpacity style={[styles.button, { marginTop: pxToDp(20) }]} onPress={() => navigation.navigate("ScanScreen2")}>
|
||||
<Text style={styles.buttonText}>{t("home.paddle")}</Text>
|
||||
</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>
|
||||
</TouchableOpacity> */}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<UpdateModal
|
||||
|
||||
+16
-6
@@ -217,24 +217,34 @@ export default function InfoScreen({ route, navigation }: Props) {
|
||||
};
|
||||
|
||||
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 {
|
||||
const bytes = await readCharacteristicWithRetry("180f", "2a19", 1);
|
||||
const batteryValue = bytes[0];
|
||||
|
||||
if (Number.isInteger(batteryValue) && batteryValue >= 0 && batteryValue <= 100) {
|
||||
return `${batteryValue}%`;
|
||||
if (
|
||||
Number.isInteger(batteryValue) &&
|
||||
batteryValue >= 0 &&
|
||||
batteryValue <= 100
|
||||
) {
|
||||
if (lastValidBattery !== null && lastValidBattery === batteryValue) {
|
||||
return `${batteryValue}%`;
|
||||
}
|
||||
|
||||
lastValidBattery = batteryValue;
|
||||
}
|
||||
} catch {
|
||||
// Continue retrying with a short gap below.
|
||||
}
|
||||
|
||||
if (attempt < 2) {
|
||||
await sleep(180);
|
||||
if (attempt < 3) {
|
||||
await sleep(220);
|
||||
}
|
||||
}
|
||||
|
||||
return "未知";
|
||||
return lastValidBattery !== null ? `${lastValidBattery}%` : "未知";
|
||||
};
|
||||
|
||||
const subscribePowerDataIfNeeded = async () => {
|
||||
|
||||
@@ -189,9 +189,16 @@
|
||||
"doNotReturn": "Updating in progress, do not go back or close the app!",
|
||||
|
||||
"cannotUpgrade": "Cannot Update",
|
||||
"hardwareNotFound": "Firmware not found for hardware version {hardware}",
|
||||
"hardwareNotFound": "Firmware not found for hardware version {{hardware}}",
|
||||
"noNeedUpgrade": "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",
|
||||
"upgradeSuccessMessage": "Update successful, please reconnect the device",
|
||||
|
||||
@@ -189,9 +189,16 @@
|
||||
"doNotReturn": "正在升级,请勿返回或关闭应用!",
|
||||
|
||||
"cannotUpgrade": "无法升级",
|
||||
"hardwareNotFound": "未找到硬件版本 {hardware} 的固件",
|
||||
"hardwareNotFound": "未找到硬件版本 {{hardware}} 的固件",
|
||||
"noNeedUpgrade": "无需升级",
|
||||
"alreadyLatest": "已是最新固件,无需升级",
|
||||
"invalidTargetDeviceId": "无法生成 DFU 目标设备 ID",
|
||||
"manifestDownloadFailed": "manifest 下载失败,HTTP {{status}}",
|
||||
"manifestInvalidJson": "manifest 不是合法 JSON: {{preview}}",
|
||||
"hardwareMismatch": "服务器固件硬件号不匹配:当前 {{current}},服务器 {{latest}}",
|
||||
"firmwareDownloadFailed": "固件包下载失败,HTTP {{status}}",
|
||||
"firmwareFileMissing": "固件包下载后文件不存在: {{path}}",
|
||||
"firmwareFileInvalid": "固件包文件无效,大小为 {{size}}",
|
||||
|
||||
"upgradeSuccess": "升级成功",
|
||||
"upgradeSuccessMessage": "升级成功,请重连设备",
|
||||
|
||||
Reference in new issue
Block a user