@onekeyfe/hardware-cli 1.2.0-alpha.8 → 1.2.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +54 -33
- package/dist/transports/nobleBlePlugin.js +83 -24
- package/package.json +6 -6
- package/src/__tests__/firmware-update-v4-command.test.ts +19 -0
- package/src/__tests__/noble-ble-plugin.test.ts +113 -0
- package/src/cli.ts +64 -37
- package/src/transports/nobleBlePlugin.ts +107 -24
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.program = void 0;
|
|
4
5
|
const node_fs_1 = require("node:fs");
|
|
5
6
|
const commander_1 = require("commander");
|
|
6
7
|
const hd_core_1 = require("@onekeyfe/hd-core");
|
|
@@ -33,6 +34,7 @@ function extractPassphraseSession(payload) {
|
|
|
33
34
|
return { passphraseState, sessionId };
|
|
34
35
|
}
|
|
35
36
|
const program = new commander_1.Command();
|
|
37
|
+
exports.program = program;
|
|
36
38
|
program
|
|
37
39
|
.name('onekey-hw')
|
|
38
40
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
@@ -418,15 +420,15 @@ program
|
|
|
418
420
|
.action(() => respondAndExit({
|
|
419
421
|
success: false,
|
|
420
422
|
payload: {
|
|
421
|
-
error: 'Use `onekey-hw --transport ble firmware-update-v4
|
|
422
|
-
code: '
|
|
423
|
+
error: 'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
424
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
423
425
|
},
|
|
424
426
|
}));
|
|
425
427
|
program
|
|
426
|
-
.command('firmware-update-v4
|
|
427
|
-
.description('
|
|
428
|
+
.command('firmware-update-v4')
|
|
429
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
428
430
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
429
|
-
.option('--resource <
|
|
431
|
+
.option('--resource-bundle <spec...>', 'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg')
|
|
430
432
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
431
433
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
432
434
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -439,8 +441,8 @@ program
|
|
|
439
441
|
.option('--forced-update-res', 'Force resource update')
|
|
440
442
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
441
443
|
.action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
442
|
-
const params =
|
|
443
|
-
const result = await
|
|
444
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
445
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
444
446
|
sdk,
|
|
445
447
|
globalOpts,
|
|
446
448
|
params,
|
|
@@ -902,9 +904,24 @@ function readBinaryParam(path) {
|
|
|
902
904
|
const buffer = (0, node_fs_1.readFileSync)(path);
|
|
903
905
|
return new Uint8Array(buffer).buffer;
|
|
904
906
|
}
|
|
905
|
-
function
|
|
907
|
+
function parseResourceBundleParam(spec) {
|
|
908
|
+
const sep = spec.indexOf(':');
|
|
909
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
910
|
+
throw new Error(`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`);
|
|
911
|
+
}
|
|
912
|
+
const localPath = spec.slice(0, sep);
|
|
913
|
+
const devicePath = spec.slice(sep + 1);
|
|
914
|
+
if (!devicePath.startsWith('vol')) {
|
|
915
|
+
throw new Error(`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`);
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
binary: readBinaryParam(localPath),
|
|
919
|
+
devicePath,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function getFirmwareUpdateV4TotalBytes(params) {
|
|
906
923
|
return [
|
|
907
|
-
...(params.
|
|
924
|
+
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
908
925
|
params.bootloaderBinary,
|
|
909
926
|
params.applicationP1Binary,
|
|
910
927
|
params.applicationP2Binary,
|
|
@@ -915,17 +932,17 @@ function getFirmwareUpdateV4DebugTotalBytes(params) {
|
|
|
915
932
|
params.se04Binary,
|
|
916
933
|
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
917
934
|
}
|
|
918
|
-
function
|
|
935
|
+
function getFirmwareUpdateV4ErrorText(result) {
|
|
919
936
|
if (!result || typeof result !== 'object')
|
|
920
937
|
return '';
|
|
921
|
-
const payload = result
|
|
938
|
+
const { payload } = result;
|
|
922
939
|
if (!payload || typeof payload !== 'object')
|
|
923
940
|
return '';
|
|
924
|
-
const error = payload
|
|
941
|
+
const { error } = payload;
|
|
925
942
|
return typeof error === 'string' ? error : '';
|
|
926
943
|
}
|
|
927
944
|
function isProtocolV2UsbProbeTransientResult(result) {
|
|
928
|
-
const error =
|
|
945
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
929
946
|
return (error.includes('Device protocol mismatch') &&
|
|
930
947
|
error.includes('expected V2') &&
|
|
931
948
|
error.includes('did not respond to expected protocol'));
|
|
@@ -933,22 +950,22 @@ function isProtocolV2UsbProbeTransientResult(result) {
|
|
|
933
950
|
function isSuccessResult(result) {
|
|
934
951
|
return (!!result && typeof result === 'object' && result.success === true);
|
|
935
952
|
}
|
|
936
|
-
function
|
|
953
|
+
function getFirmwareUpdatePayload(message) {
|
|
937
954
|
if (!message || typeof message !== 'object')
|
|
938
955
|
return undefined;
|
|
939
956
|
return message.payload;
|
|
940
957
|
}
|
|
941
|
-
function
|
|
958
|
+
function formatFirmwareProgress(progress) {
|
|
942
959
|
if (!Number.isFinite(progress))
|
|
943
960
|
return '0%';
|
|
944
961
|
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
945
962
|
}
|
|
946
|
-
function
|
|
963
|
+
function formatFirmwareBytes(bytes) {
|
|
947
964
|
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
948
965
|
return '';
|
|
949
966
|
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
950
967
|
}
|
|
951
|
-
function
|
|
968
|
+
function maybePrintFirmwareProgress({ progressType, progress, payload, lastPrintedProgress, }) {
|
|
952
969
|
const printableProgress = Math.floor(progress / 10) * 10;
|
|
953
970
|
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
954
971
|
return lastPrintedProgress;
|
|
@@ -957,15 +974,15 @@ function maybePrintFirmwareDebugProgress({ progressType, progress, payload, last
|
|
|
957
974
|
const totalBytes = Number(payload.totalBytes);
|
|
958
975
|
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
959
976
|
const sizeText = Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
960
|
-
? ` ${
|
|
977
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
961
978
|
: '';
|
|
962
979
|
const speedText = Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
963
980
|
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
964
981
|
: '';
|
|
965
|
-
process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${
|
|
982
|
+
process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(progress)}${sizeText}${speedText}\n`);
|
|
966
983
|
return progress >= 100 ? 100 : printableProgress;
|
|
967
984
|
}
|
|
968
|
-
function
|
|
985
|
+
function buildFirmwareUpdateV4Metrics({ attempt, maxAttempts, totalBytes, totalStartedAt, transferStartedAt, transferEndedAt, installStartedAt, installEndedAt, progressEvents, lastProgress, installProgressEvents, lastInstallProgress, retried, }) {
|
|
969
986
|
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
970
987
|
const transferElapsedMs = transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
971
988
|
? transferEndedAt - transferStartedAt
|
|
@@ -990,8 +1007,8 @@ function buildFirmwareUpdateV4DebugMetrics({ attempt, maxAttempts, totalBytes, t
|
|
|
990
1007
|
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
991
1008
|
};
|
|
992
1009
|
}
|
|
993
|
-
async function
|
|
994
|
-
const totalBytes =
|
|
1010
|
+
async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }) {
|
|
1011
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
995
1012
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
996
1013
|
let currentSdk = sdk;
|
|
997
1014
|
let lastResult;
|
|
@@ -1015,7 +1032,7 @@ async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retr
|
|
|
1015
1032
|
if (!message || typeof message !== 'object')
|
|
1016
1033
|
return;
|
|
1017
1034
|
const messageType = message.type;
|
|
1018
|
-
const payload =
|
|
1035
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1019
1036
|
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1020
1037
|
const tipMessage = payload?.data?.message;
|
|
1021
1038
|
if (typeof tipMessage === 'string') {
|
|
@@ -1037,7 +1054,7 @@ async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retr
|
|
|
1037
1054
|
progressEvents += 1;
|
|
1038
1055
|
lastProgress = Math.max(lastProgress, progress);
|
|
1039
1056
|
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1040
|
-
lastPrintedTransferProgress =
|
|
1057
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1041
1058
|
progressType: 'transfer',
|
|
1042
1059
|
progress,
|
|
1043
1060
|
payload,
|
|
@@ -1052,7 +1069,7 @@ async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retr
|
|
|
1052
1069
|
installProgressEvents += 1;
|
|
1053
1070
|
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1054
1071
|
installStartedAt ?? (installStartedAt = Date.now());
|
|
1055
|
-
lastPrintedInstallProgress =
|
|
1072
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1056
1073
|
progressType: 'install',
|
|
1057
1074
|
progress,
|
|
1058
1075
|
payload,
|
|
@@ -1073,7 +1090,7 @@ async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retr
|
|
|
1073
1090
|
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1074
1091
|
installEndedAt = Date.now();
|
|
1075
1092
|
}
|
|
1076
|
-
const
|
|
1093
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1077
1094
|
attempt,
|
|
1078
1095
|
maxAttempts,
|
|
1079
1096
|
totalBytes,
|
|
@@ -1094,7 +1111,7 @@ async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retr
|
|
|
1094
1111
|
...lastResult,
|
|
1095
1112
|
payload: {
|
|
1096
1113
|
...payload,
|
|
1097
|
-
|
|
1114
|
+
metrics,
|
|
1098
1115
|
},
|
|
1099
1116
|
};
|
|
1100
1117
|
}
|
|
@@ -1109,18 +1126,20 @@ async function runFirmwareUpdateV4DebugWithRetry({ sdk, globalOpts, params, retr
|
|
|
1109
1126
|
retried = true;
|
|
1110
1127
|
process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`);
|
|
1111
1128
|
await (0, sdk_1.disposeSDK)();
|
|
1112
|
-
await new Promise(resolve =>
|
|
1129
|
+
await new Promise(resolve => {
|
|
1130
|
+
setTimeout(resolve, 3000);
|
|
1131
|
+
});
|
|
1113
1132
|
currentSdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
1114
1133
|
}
|
|
1115
1134
|
return lastResult;
|
|
1116
1135
|
}
|
|
1117
|
-
function
|
|
1136
|
+
function buildFirmwareUpdateV4Params(opts) {
|
|
1118
1137
|
const params = {
|
|
1119
1138
|
platform: 'desktop',
|
|
1120
1139
|
connectProtocol: 'V2',
|
|
1121
1140
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1122
1141
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1123
|
-
|
|
1142
|
+
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1124
1143
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1125
1144
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1126
1145
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1132,7 +1151,7 @@ function buildFirmwareUpdateV4DebugParams(opts) {
|
|
|
1132
1151
|
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1133
1152
|
};
|
|
1134
1153
|
const hasPayload = [
|
|
1135
|
-
params.
|
|
1154
|
+
params.resourceBundleFiles,
|
|
1136
1155
|
params.romloaderBinary,
|
|
1137
1156
|
params.bootloaderBinary,
|
|
1138
1157
|
params.applicationP1Binary,
|
|
@@ -1144,7 +1163,7 @@ function buildFirmwareUpdateV4DebugParams(opts) {
|
|
|
1144
1163
|
params.se04Binary,
|
|
1145
1164
|
].some(Boolean);
|
|
1146
1165
|
if (!hasPayload) {
|
|
1147
|
-
const err = new Error('firmware-update-v4
|
|
1166
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1148
1167
|
err.code = 'MISSING_FIRMWARE_BINARY';
|
|
1149
1168
|
throw err;
|
|
1150
1169
|
}
|
|
@@ -1160,4 +1179,6 @@ function safeParseInt(input, label) {
|
|
|
1160
1179
|
}
|
|
1161
1180
|
return num;
|
|
1162
1181
|
}
|
|
1163
|
-
|
|
1182
|
+
if (require.main === module) {
|
|
1183
|
+
program.parse();
|
|
1184
|
+
}
|
|
@@ -14,6 +14,7 @@ const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
|
14
14
|
const DEVICE_SCAN_TIMEOUT = 8000;
|
|
15
15
|
const CONNECTION_TIMEOUT = 8000;
|
|
16
16
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
17
|
+
const BLE_CLEANUP_TIMEOUT = 100;
|
|
17
18
|
const BLE_PACKET_SIZE = 192;
|
|
18
19
|
const BLE_WRITE_DELAY = 5;
|
|
19
20
|
const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
|
|
@@ -22,8 +23,8 @@ let nobleReadyPromise = null;
|
|
|
22
23
|
const discoveredDevices = new Map();
|
|
23
24
|
const connectedDevices = new Map();
|
|
24
25
|
const deviceCharacteristics = new Map();
|
|
25
|
-
const
|
|
26
|
-
const
|
|
26
|
+
const notificationStates = new Map();
|
|
27
|
+
const notificationGenerations = new Map();
|
|
27
28
|
function getBleUuidKey(uuid) {
|
|
28
29
|
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
29
30
|
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
@@ -43,14 +44,63 @@ function isOneKeyPeripheral(peripheral) {
|
|
|
43
44
|
const deviceName = peripheral.advertisement?.localName || null;
|
|
44
45
|
return (0, hd_shared_1.isOnekeyDevice)(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
|
|
45
46
|
}
|
|
46
|
-
function enqueueNotification(data) {
|
|
47
|
+
function enqueueNotification(deviceId, generation, data) {
|
|
48
|
+
const state = notificationStates.get(deviceId);
|
|
49
|
+
if (!state || state.generation !== generation)
|
|
50
|
+
return;
|
|
47
51
|
const hex = data.toString('hex');
|
|
48
|
-
const receiver = pendingReceivers
|
|
52
|
+
const [receiver] = state.pendingReceivers;
|
|
49
53
|
if (receiver) {
|
|
50
|
-
receiver
|
|
54
|
+
state.pendingReceivers.delete(receiver);
|
|
55
|
+
receiver.resolve(hex);
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
53
|
-
|
|
58
|
+
state.queue.push(hex);
|
|
59
|
+
}
|
|
60
|
+
function createNotificationState(deviceId) {
|
|
61
|
+
const existing = notificationStates.get(deviceId);
|
|
62
|
+
if (existing) {
|
|
63
|
+
const error = new Error(`BLE notification state replaced for ${deviceId}`);
|
|
64
|
+
existing.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
65
|
+
}
|
|
66
|
+
const generation = (notificationGenerations.get(deviceId) ?? 0) + 1;
|
|
67
|
+
notificationGenerations.set(deviceId, generation);
|
|
68
|
+
const state = {
|
|
69
|
+
generation,
|
|
70
|
+
queue: [],
|
|
71
|
+
pendingReceivers: new Set(),
|
|
72
|
+
};
|
|
73
|
+
notificationStates.set(deviceId, state);
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
function clearNotificationState(deviceId, reason) {
|
|
77
|
+
const state = notificationStates.get(deviceId);
|
|
78
|
+
if (!state)
|
|
79
|
+
return;
|
|
80
|
+
notificationStates.delete(deviceId);
|
|
81
|
+
const error = new Error(reason);
|
|
82
|
+
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
83
|
+
state.pendingReceivers.clear();
|
|
84
|
+
state.queue.length = 0;
|
|
85
|
+
}
|
|
86
|
+
function waitForNobleCleanup(registerCallback) {
|
|
87
|
+
return new Promise(resolve => {
|
|
88
|
+
let completed = false;
|
|
89
|
+
const complete = () => {
|
|
90
|
+
if (completed)
|
|
91
|
+
return;
|
|
92
|
+
completed = true;
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
const timeout = setTimeout(complete, BLE_CLEANUP_TIMEOUT);
|
|
97
|
+
try {
|
|
98
|
+
registerCallback(complete);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
complete();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
54
104
|
}
|
|
55
105
|
async function initializeNoble() {
|
|
56
106
|
if (!noble) {
|
|
@@ -210,10 +260,8 @@ async function discoverCharacteristics(peripheral) {
|
|
|
210
260
|
notify: notifyCharacteristic,
|
|
211
261
|
};
|
|
212
262
|
}
|
|
213
|
-
function subscribeNotifications(deviceId, notifyCharacteristic) {
|
|
214
|
-
return
|
|
215
|
-
notifyCharacteristic.unsubscribe(() => resolve());
|
|
216
|
-
})
|
|
263
|
+
function subscribeNotifications(deviceId, generation, notifyCharacteristic) {
|
|
264
|
+
return waitForNobleCleanup(callback => notifyCharacteristic.unsubscribe(callback))
|
|
217
265
|
.then(() => new Promise((resolve, reject) => {
|
|
218
266
|
notifyCharacteristic.subscribe((error) => {
|
|
219
267
|
if (error) {
|
|
@@ -230,7 +278,7 @@ function subscribeNotifications(deviceId, notifyCharacteristic) {
|
|
|
230
278
|
}))
|
|
231
279
|
.then(() => {
|
|
232
280
|
notifyCharacteristic.removeAllListeners('data');
|
|
233
|
-
notifyCharacteristic.on('data', enqueueNotification);
|
|
281
|
+
notifyCharacteristic.on('data', data => enqueueNotification(deviceId, generation, data));
|
|
234
282
|
})
|
|
235
283
|
.catch(error => {
|
|
236
284
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -254,21 +302,16 @@ function writeCharacteristic(characteristic, buffer) {
|
|
|
254
302
|
async function disconnectDevice(uuid) {
|
|
255
303
|
const peripheral = connectedDevices.get(uuid);
|
|
256
304
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
305
|
+
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
257
306
|
if (characteristics) {
|
|
258
307
|
characteristics.notify.removeAllListeners('data');
|
|
259
|
-
await
|
|
260
|
-
characteristics.notify.unsubscribe(() => resolve());
|
|
261
|
-
});
|
|
308
|
+
await waitForNobleCleanup(callback => characteristics.notify.unsubscribe(callback));
|
|
262
309
|
}
|
|
263
310
|
connectedDevices.delete(uuid);
|
|
264
311
|
deviceCharacteristics.delete(uuid);
|
|
265
|
-
notificationQueue.length = 0;
|
|
266
|
-
pendingReceivers.splice(0).forEach(resolve => resolve(''));
|
|
267
312
|
if (!peripheral || peripheral.state === 'disconnected')
|
|
268
313
|
return;
|
|
269
|
-
await
|
|
270
|
-
peripheral.disconnect(() => resolve());
|
|
271
|
-
});
|
|
314
|
+
await waitForNobleCleanup(callback => peripheral.disconnect(callback));
|
|
272
315
|
}
|
|
273
316
|
function createNobleBlePlugin() {
|
|
274
317
|
return {
|
|
@@ -294,7 +337,14 @@ function createNobleBlePlugin() {
|
|
|
294
337
|
}
|
|
295
338
|
await connectPeripheral(peripheral);
|
|
296
339
|
const characteristics = await discoverCharacteristics(peripheral);
|
|
297
|
-
|
|
340
|
+
const notificationState = createNotificationState(uuid);
|
|
341
|
+
try {
|
|
342
|
+
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
298
348
|
connectedDevices.set(uuid, peripheral);
|
|
299
349
|
deviceCharacteristics.set(uuid, characteristics);
|
|
300
350
|
},
|
|
@@ -315,12 +365,21 @@ function createNobleBlePlugin() {
|
|
|
315
365
|
}
|
|
316
366
|
}
|
|
317
367
|
},
|
|
318
|
-
async receive() {
|
|
319
|
-
const
|
|
368
|
+
async receive(uuid) {
|
|
369
|
+
const resolvedUuid = uuid ??
|
|
370
|
+
(notificationStates.size === 1 ? notificationStates.keys().next().value : undefined);
|
|
371
|
+
if (!resolvedUuid) {
|
|
372
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.RuntimeError, 'BLE receive requires a device UUID when multiple devices are connected');
|
|
373
|
+
}
|
|
374
|
+
const state = notificationStates.get(resolvedUuid);
|
|
375
|
+
if (!state) {
|
|
376
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.TransportNotFound, `BLE notification state not found: ${resolvedUuid}`);
|
|
377
|
+
}
|
|
378
|
+
const queued = state.queue.shift();
|
|
320
379
|
if (queued !== undefined)
|
|
321
380
|
return queued;
|
|
322
|
-
return new Promise(resolve => {
|
|
323
|
-
pendingReceivers.
|
|
381
|
+
return new Promise((resolve, reject) => {
|
|
382
|
+
state.pendingReceivers.add({ resolve, reject });
|
|
324
383
|
});
|
|
325
384
|
},
|
|
326
385
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hardware-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.9",
|
|
4
4
|
"description": "OneKey hardware wallet CLI for testing device communication",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"test": "jest"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.
|
|
34
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
35
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
36
|
-
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.
|
|
33
|
+
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.9",
|
|
34
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.9",
|
|
35
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.9",
|
|
36
|
+
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.9",
|
|
37
37
|
"@stoprocent/noble": "2.3.16",
|
|
38
38
|
"commander": "^12.0.0"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "f90ef9921b742f68cce9f4e7a0425e45e8ae7ec9"
|
|
41
41
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { program } from '../cli';
|
|
2
|
+
|
|
3
|
+
describe('firmware-update-v4 CLI command', () => {
|
|
4
|
+
test('exposes firmware-update-v4 as the formal command', () => {
|
|
5
|
+
const command = program.commands.find(item => item.name() === 'firmware-update-v4');
|
|
6
|
+
|
|
7
|
+
expect(command).toBeDefined();
|
|
8
|
+
expect(command?.description()).toBe(
|
|
9
|
+
'Run Protocol V2 firmware update through sdk.firmwareUpdateV4'
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('does not expose the pre-release firmware-update-v4-debug command', () => {
|
|
14
|
+
expect(program.commands.some(item => item.name() === 'firmware-update-v4-debug')).toBe(false);
|
|
15
|
+
expect(program.commands.some(item => item.aliases().includes('firmware-update-v4-debug'))).toBe(
|
|
16
|
+
false
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
type MockCharacteristic = EventEmitter & {
|
|
4
|
+
uuid: string;
|
|
5
|
+
unsubscribe: jest.Mock;
|
|
6
|
+
subscribe: jest.Mock;
|
|
7
|
+
write: jest.Mock;
|
|
8
|
+
removeAllListeners: jest.Mock;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const createCharacteristic = (uuid: string): MockCharacteristic => {
|
|
12
|
+
const characteristic = new EventEmitter() as MockCharacteristic;
|
|
13
|
+
characteristic.uuid = uuid;
|
|
14
|
+
characteristic.unsubscribe = jest.fn(callback => callback());
|
|
15
|
+
characteristic.subscribe = jest.fn(callback => callback());
|
|
16
|
+
characteristic.write = jest.fn((_buffer, _withoutResponse, callback) => callback());
|
|
17
|
+
characteristic.removeAllListeners = jest.fn(
|
|
18
|
+
characteristic.removeAllListeners.bind(characteristic)
|
|
19
|
+
);
|
|
20
|
+
return characteristic;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createPeripheral = (id: string) => {
|
|
24
|
+
const write = createCharacteristic('0002');
|
|
25
|
+
const notify = createCharacteristic('0003');
|
|
26
|
+
const service = {
|
|
27
|
+
uuid: '0001',
|
|
28
|
+
discoverCharacteristics: jest.fn((_uuids, callback) => callback(null, [write, notify])),
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
peripheral: {
|
|
32
|
+
id,
|
|
33
|
+
state: 'connected',
|
|
34
|
+
advertisement: {
|
|
35
|
+
localName: `OneKey Pro 2 ${id}`,
|
|
36
|
+
serviceUuids: ['fffd'],
|
|
37
|
+
},
|
|
38
|
+
discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
|
|
39
|
+
connect: jest.fn(callback => callback()),
|
|
40
|
+
disconnect: jest.fn(callback => callback()),
|
|
41
|
+
},
|
|
42
|
+
notify,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe('Noble BLE plugin notification routing', () => {
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
jest.resetModules();
|
|
49
|
+
jest.clearAllMocks();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('routes notifications to the receiver waiting for the same device', async () => {
|
|
53
|
+
const deviceA = createPeripheral('device-a');
|
|
54
|
+
const deviceB = createPeripheral('device-b');
|
|
55
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
56
|
+
state: string;
|
|
57
|
+
startScanning: jest.Mock;
|
|
58
|
+
stopScanning: jest.Mock;
|
|
59
|
+
};
|
|
60
|
+
noble.state = 'poweredOn';
|
|
61
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
62
|
+
callback?.();
|
|
63
|
+
noble.emit('discover', deviceA.peripheral);
|
|
64
|
+
noble.emit('discover', deviceB.peripheral);
|
|
65
|
+
});
|
|
66
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
67
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
68
|
+
|
|
69
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
70
|
+
const plugin = createNobleBlePlugin();
|
|
71
|
+
await plugin.init();
|
|
72
|
+
await plugin.connect('device-a');
|
|
73
|
+
await plugin.connect('device-b');
|
|
74
|
+
|
|
75
|
+
const receiveA = plugin.receive('device-a');
|
|
76
|
+
const receiveB = plugin.receive('device-b');
|
|
77
|
+
deviceB.notify.emit('data', Buffer.from('bb', 'hex'));
|
|
78
|
+
deviceA.notify.emit('data', Buffer.from('aa', 'hex'));
|
|
79
|
+
|
|
80
|
+
await expect(Promise.all([receiveA, receiveB])).resolves.toEqual(['aa', 'bb']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('finishes disconnect cleanup when Noble never calls unsubscribe back', async () => {
|
|
84
|
+
const device = createPeripheral('device-a');
|
|
85
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
86
|
+
state: string;
|
|
87
|
+
startScanning: jest.Mock;
|
|
88
|
+
stopScanning: jest.Mock;
|
|
89
|
+
};
|
|
90
|
+
noble.state = 'poweredOn';
|
|
91
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
92
|
+
callback?.();
|
|
93
|
+
noble.emit('discover', device.peripheral);
|
|
94
|
+
});
|
|
95
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
96
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
97
|
+
|
|
98
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
99
|
+
const plugin = createNobleBlePlugin();
|
|
100
|
+
await plugin.init();
|
|
101
|
+
await plugin.connect('device-a');
|
|
102
|
+
device.notify.unsubscribe.mockImplementation(() => undefined);
|
|
103
|
+
|
|
104
|
+
const result = await Promise.race([
|
|
105
|
+
plugin.disconnect('device-a').then(() => 'completed'),
|
|
106
|
+
new Promise(resolve => {
|
|
107
|
+
setTimeout(() => resolve('blocked'), 300);
|
|
108
|
+
}),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
expect(result).toBe('completed');
|
|
112
|
+
});
|
|
113
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -549,17 +549,20 @@ program
|
|
|
549
549
|
success: false,
|
|
550
550
|
payload: {
|
|
551
551
|
error:
|
|
552
|
-
'Use `onekey-hw --transport ble firmware-update-v4
|
|
553
|
-
code: '
|
|
552
|
+
'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
553
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
554
554
|
},
|
|
555
555
|
})
|
|
556
556
|
);
|
|
557
557
|
|
|
558
558
|
program
|
|
559
|
-
.command('firmware-update-v4
|
|
560
|
-
.description('
|
|
559
|
+
.command('firmware-update-v4')
|
|
560
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
561
561
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
562
|
-
.option(
|
|
562
|
+
.option(
|
|
563
|
+
'--resource-bundle <spec...>',
|
|
564
|
+
'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
|
|
565
|
+
)
|
|
563
566
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
564
567
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
565
568
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -573,8 +576,8 @@ program
|
|
|
573
576
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
574
577
|
.action(opts =>
|
|
575
578
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
576
|
-
const params =
|
|
577
|
-
const result = await
|
|
579
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
580
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
578
581
|
sdk,
|
|
579
582
|
globalOpts,
|
|
580
583
|
params,
|
|
@@ -1159,11 +1162,29 @@ function readBinaryParam(path: string): ArrayBuffer {
|
|
|
1159
1162
|
return new Uint8Array(buffer).buffer;
|
|
1160
1163
|
}
|
|
1161
1164
|
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
) {
|
|
1165
|
+
function parseResourceBundleParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
|
|
1166
|
+
const sep = spec.indexOf(':');
|
|
1167
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
const localPath = spec.slice(0, sep);
|
|
1173
|
+
const devicePath = spec.slice(sep + 1);
|
|
1174
|
+
if (!devicePath.startsWith('vol')) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
binary: readBinaryParam(localPath),
|
|
1181
|
+
devicePath,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1165
1186
|
return [
|
|
1166
|
-
...(params.
|
|
1187
|
+
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1167
1188
|
params.bootloaderBinary,
|
|
1168
1189
|
params.applicationP1Binary,
|
|
1169
1190
|
params.applicationP2Binary,
|
|
@@ -1175,16 +1196,16 @@ function getFirmwareUpdateV4DebugTotalBytes(
|
|
|
1175
1196
|
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1176
1197
|
}
|
|
1177
1198
|
|
|
1178
|
-
function
|
|
1199
|
+
function getFirmwareUpdateV4ErrorText(result: unknown) {
|
|
1179
1200
|
if (!result || typeof result !== 'object') return '';
|
|
1180
|
-
const payload =
|
|
1201
|
+
const { payload } = result as { payload?: unknown };
|
|
1181
1202
|
if (!payload || typeof payload !== 'object') return '';
|
|
1182
|
-
const error =
|
|
1203
|
+
const { error } = payload as { error?: unknown };
|
|
1183
1204
|
return typeof error === 'string' ? error : '';
|
|
1184
1205
|
}
|
|
1185
1206
|
|
|
1186
1207
|
function isProtocolV2UsbProbeTransientResult(result: unknown) {
|
|
1187
|
-
const error =
|
|
1208
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1188
1209
|
return (
|
|
1189
1210
|
error.includes('Device protocol mismatch') &&
|
|
1190
1211
|
error.includes('expected V2') &&
|
|
@@ -1198,22 +1219,22 @@ function isSuccessResult(result: unknown) {
|
|
|
1198
1219
|
);
|
|
1199
1220
|
}
|
|
1200
1221
|
|
|
1201
|
-
function
|
|
1222
|
+
function getFirmwareUpdatePayload(message: unknown) {
|
|
1202
1223
|
if (!message || typeof message !== 'object') return undefined;
|
|
1203
1224
|
return (message as { payload?: Record<string, unknown> }).payload;
|
|
1204
1225
|
}
|
|
1205
1226
|
|
|
1206
|
-
function
|
|
1227
|
+
function formatFirmwareProgress(progress: number) {
|
|
1207
1228
|
if (!Number.isFinite(progress)) return '0%';
|
|
1208
1229
|
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1209
1230
|
}
|
|
1210
1231
|
|
|
1211
|
-
function
|
|
1232
|
+
function formatFirmwareBytes(bytes: number) {
|
|
1212
1233
|
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
1213
1234
|
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1214
1235
|
}
|
|
1215
1236
|
|
|
1216
|
-
function
|
|
1237
|
+
function maybePrintFirmwareProgress({
|
|
1217
1238
|
progressType,
|
|
1218
1239
|
progress,
|
|
1219
1240
|
payload,
|
|
@@ -1234,7 +1255,7 @@ function maybePrintFirmwareDebugProgress({
|
|
|
1234
1255
|
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1235
1256
|
const sizeText =
|
|
1236
1257
|
Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1237
|
-
? ` ${
|
|
1258
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1238
1259
|
: '';
|
|
1239
1260
|
const speedText =
|
|
1240
1261
|
Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
@@ -1242,14 +1263,14 @@ function maybePrintFirmwareDebugProgress({
|
|
|
1242
1263
|
: '';
|
|
1243
1264
|
|
|
1244
1265
|
process.stderr.write(
|
|
1245
|
-
`[onekey-hw] Firmware ${progressType}: ${
|
|
1266
|
+
`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(
|
|
1246
1267
|
progress
|
|
1247
1268
|
)}${sizeText}${speedText}\n`
|
|
1248
1269
|
);
|
|
1249
1270
|
return progress >= 100 ? 100 : printableProgress;
|
|
1250
1271
|
}
|
|
1251
1272
|
|
|
1252
|
-
function
|
|
1273
|
+
function buildFirmwareUpdateV4Metrics({
|
|
1253
1274
|
attempt,
|
|
1254
1275
|
maxAttempts,
|
|
1255
1276
|
totalBytes,
|
|
@@ -1309,7 +1330,7 @@ function buildFirmwareUpdateV4DebugMetrics({
|
|
|
1309
1330
|
};
|
|
1310
1331
|
}
|
|
1311
1332
|
|
|
1312
|
-
async function
|
|
1333
|
+
async function runFirmwareUpdateV4WithRetry({
|
|
1313
1334
|
sdk,
|
|
1314
1335
|
globalOpts,
|
|
1315
1336
|
params,
|
|
@@ -1317,10 +1338,10 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1317
1338
|
}: {
|
|
1318
1339
|
sdk: AnySdk;
|
|
1319
1340
|
globalOpts: Record<string, any>;
|
|
1320
|
-
params: ReturnType<typeof
|
|
1341
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
1321
1342
|
retries?: number;
|
|
1322
1343
|
}) {
|
|
1323
|
-
const totalBytes =
|
|
1344
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1324
1345
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1325
1346
|
let currentSdk = sdk;
|
|
1326
1347
|
let lastResult: unknown;
|
|
@@ -1346,7 +1367,7 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1346
1367
|
const onUiEvent = (message: unknown) => {
|
|
1347
1368
|
if (!message || typeof message !== 'object') return;
|
|
1348
1369
|
const messageType = (message as { type?: string }).type;
|
|
1349
|
-
const payload =
|
|
1370
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1350
1371
|
|
|
1351
1372
|
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1352
1373
|
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
@@ -1372,7 +1393,7 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1372
1393
|
progressEvents += 1;
|
|
1373
1394
|
lastProgress = Math.max(lastProgress, progress);
|
|
1374
1395
|
transferStartedAt ??= Date.now();
|
|
1375
|
-
lastPrintedTransferProgress =
|
|
1396
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1376
1397
|
progressType: 'transfer',
|
|
1377
1398
|
progress,
|
|
1378
1399
|
payload,
|
|
@@ -1388,7 +1409,7 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1388
1409
|
installProgressEvents += 1;
|
|
1389
1410
|
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1390
1411
|
installStartedAt ??= Date.now();
|
|
1391
|
-
lastPrintedInstallProgress =
|
|
1412
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1392
1413
|
progressType: 'install',
|
|
1393
1414
|
progress,
|
|
1394
1415
|
payload,
|
|
@@ -1410,7 +1431,7 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1410
1431
|
installEndedAt = Date.now();
|
|
1411
1432
|
}
|
|
1412
1433
|
|
|
1413
|
-
const
|
|
1434
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1414
1435
|
attempt,
|
|
1415
1436
|
maxAttempts,
|
|
1416
1437
|
totalBytes,
|
|
@@ -1435,7 +1456,7 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1435
1456
|
...(lastResult as Record<string, unknown>),
|
|
1436
1457
|
payload: {
|
|
1437
1458
|
...payload,
|
|
1438
|
-
|
|
1459
|
+
metrics,
|
|
1439
1460
|
},
|
|
1440
1461
|
};
|
|
1441
1462
|
}
|
|
@@ -1457,16 +1478,18 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1457
1478
|
`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
|
|
1458
1479
|
);
|
|
1459
1480
|
await disposeSDK();
|
|
1460
|
-
await new Promise(resolve =>
|
|
1481
|
+
await new Promise(resolve => {
|
|
1482
|
+
setTimeout(resolve, 3000);
|
|
1483
|
+
});
|
|
1461
1484
|
currentSdk = await createSDK(globalOpts);
|
|
1462
1485
|
}
|
|
1463
1486
|
|
|
1464
1487
|
return lastResult;
|
|
1465
1488
|
}
|
|
1466
1489
|
|
|
1467
|
-
function
|
|
1490
|
+
function buildFirmwareUpdateV4Params(opts: {
|
|
1468
1491
|
chunkSize?: string;
|
|
1469
|
-
|
|
1492
|
+
resourceBundle?: string[];
|
|
1470
1493
|
romloader?: string;
|
|
1471
1494
|
bootloader?: string;
|
|
1472
1495
|
applicationP1?: string;
|
|
@@ -1483,7 +1506,7 @@ function buildFirmwareUpdateV4DebugParams(opts: {
|
|
|
1483
1506
|
connectProtocol: 'V2' as const,
|
|
1484
1507
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1485
1508
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1486
|
-
|
|
1509
|
+
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1487
1510
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1488
1511
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1489
1512
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1496,7 +1519,7 @@ function buildFirmwareUpdateV4DebugParams(opts: {
|
|
|
1496
1519
|
};
|
|
1497
1520
|
|
|
1498
1521
|
const hasPayload = [
|
|
1499
|
-
params.
|
|
1522
|
+
params.resourceBundleFiles,
|
|
1500
1523
|
params.romloaderBinary,
|
|
1501
1524
|
params.bootloaderBinary,
|
|
1502
1525
|
params.applicationP1Binary,
|
|
@@ -1509,7 +1532,7 @@ function buildFirmwareUpdateV4DebugParams(opts: {
|
|
|
1509
1532
|
].some(Boolean);
|
|
1510
1533
|
|
|
1511
1534
|
if (!hasPayload) {
|
|
1512
|
-
const err = new Error('firmware-update-v4
|
|
1535
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1513
1536
|
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1514
1537
|
throw err;
|
|
1515
1538
|
}
|
|
@@ -1528,4 +1551,8 @@ function safeParseInt(input: string, label: string): number {
|
|
|
1528
1551
|
return num;
|
|
1529
1552
|
}
|
|
1530
1553
|
|
|
1531
|
-
program
|
|
1554
|
+
export { program };
|
|
1555
|
+
|
|
1556
|
+
if (require.main === module) {
|
|
1557
|
+
program.parse();
|
|
1558
|
+
}
|
|
@@ -28,6 +28,17 @@ type CharacteristicPair = {
|
|
|
28
28
|
notify: Characteristic;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
type NoblePendingReceiver = {
|
|
32
|
+
resolve: (data: string) => void;
|
|
33
|
+
reject: (error: Error) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type NobleNotificationState = {
|
|
37
|
+
generation: number;
|
|
38
|
+
queue: string[];
|
|
39
|
+
pendingReceivers: Set<NoblePendingReceiver>;
|
|
40
|
+
};
|
|
41
|
+
|
|
31
42
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
32
43
|
const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']);
|
|
33
44
|
const NORMALIZED_WRITE_UUID = '0002';
|
|
@@ -41,6 +52,7 @@ const BLUETOOTH_INIT_TIMEOUT = 10_000;
|
|
|
41
52
|
const DEVICE_SCAN_TIMEOUT = 8_000;
|
|
42
53
|
const CONNECTION_TIMEOUT = 8_000;
|
|
43
54
|
const SERVICE_DISCOVERY_TIMEOUT = 10_000;
|
|
55
|
+
const BLE_CLEANUP_TIMEOUT = 100;
|
|
44
56
|
const BLE_PACKET_SIZE = 192;
|
|
45
57
|
const BLE_WRITE_DELAY = 5;
|
|
46
58
|
const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
|
|
@@ -50,8 +62,8 @@ let nobleReadyPromise: Promise<void> | null = null;
|
|
|
50
62
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
51
63
|
const connectedDevices = new Map<string, Peripheral>();
|
|
52
64
|
const deviceCharacteristics = new Map<string, CharacteristicPair>();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
65
|
+
const notificationStates = new Map<string, NobleNotificationState>();
|
|
66
|
+
const notificationGenerations = new Map<string, number>();
|
|
55
67
|
|
|
56
68
|
function getBleUuidKey(uuid?: string | null) {
|
|
57
69
|
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
@@ -78,14 +90,65 @@ function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
|
78
90
|
return isOnekeyDevice(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
function enqueueNotification(data: Buffer) {
|
|
93
|
+
function enqueueNotification(deviceId: string, generation: number, data: Buffer) {
|
|
94
|
+
const state = notificationStates.get(deviceId);
|
|
95
|
+
if (!state || state.generation !== generation) return;
|
|
96
|
+
|
|
82
97
|
const hex = data.toString('hex');
|
|
83
|
-
const receiver = pendingReceivers
|
|
98
|
+
const [receiver] = state.pendingReceivers;
|
|
84
99
|
if (receiver) {
|
|
85
|
-
receiver
|
|
100
|
+
state.pendingReceivers.delete(receiver);
|
|
101
|
+
receiver.resolve(hex);
|
|
86
102
|
return;
|
|
87
103
|
}
|
|
88
|
-
|
|
104
|
+
state.queue.push(hex);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createNotificationState(deviceId: string) {
|
|
108
|
+
const existing = notificationStates.get(deviceId);
|
|
109
|
+
if (existing) {
|
|
110
|
+
const error = new Error(`BLE notification state replaced for ${deviceId}`);
|
|
111
|
+
existing.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const generation = (notificationGenerations.get(deviceId) ?? 0) + 1;
|
|
115
|
+
notificationGenerations.set(deviceId, generation);
|
|
116
|
+
const state: NobleNotificationState = {
|
|
117
|
+
generation,
|
|
118
|
+
queue: [],
|
|
119
|
+
pendingReceivers: new Set(),
|
|
120
|
+
};
|
|
121
|
+
notificationStates.set(deviceId, state);
|
|
122
|
+
return state;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function clearNotificationState(deviceId: string, reason: string) {
|
|
126
|
+
const state = notificationStates.get(deviceId);
|
|
127
|
+
if (!state) return;
|
|
128
|
+
|
|
129
|
+
notificationStates.delete(deviceId);
|
|
130
|
+
const error = new Error(reason);
|
|
131
|
+
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
132
|
+
state.pendingReceivers.clear();
|
|
133
|
+
state.queue.length = 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function waitForNobleCleanup(registerCallback: (callback: () => void) => void) {
|
|
137
|
+
return new Promise<void>(resolve => {
|
|
138
|
+
let completed = false;
|
|
139
|
+
const complete = () => {
|
|
140
|
+
if (completed) return;
|
|
141
|
+
completed = true;
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
resolve();
|
|
144
|
+
};
|
|
145
|
+
const timeout = setTimeout(complete, BLE_CLEANUP_TIMEOUT);
|
|
146
|
+
try {
|
|
147
|
+
registerCallback(complete);
|
|
148
|
+
} catch {
|
|
149
|
+
complete();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
89
152
|
}
|
|
90
153
|
|
|
91
154
|
async function initializeNoble() {
|
|
@@ -270,10 +333,12 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
|
|
|
270
333
|
};
|
|
271
334
|
}
|
|
272
335
|
|
|
273
|
-
function subscribeNotifications(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
336
|
+
function subscribeNotifications(
|
|
337
|
+
deviceId: string,
|
|
338
|
+
generation: number,
|
|
339
|
+
notifyCharacteristic: Characteristic
|
|
340
|
+
) {
|
|
341
|
+
return waitForNobleCleanup(callback => notifyCharacteristic.unsubscribe(callback))
|
|
277
342
|
.then(
|
|
278
343
|
() =>
|
|
279
344
|
new Promise<void>((resolve, reject) => {
|
|
@@ -303,7 +368,7 @@ function subscribeNotifications(deviceId: string, notifyCharacteristic: Characte
|
|
|
303
368
|
)
|
|
304
369
|
.then(() => {
|
|
305
370
|
notifyCharacteristic.removeAllListeners('data');
|
|
306
|
-
notifyCharacteristic.on('data', enqueueNotification);
|
|
371
|
+
notifyCharacteristic.on('data', data => enqueueNotification(deviceId, generation, data));
|
|
307
372
|
})
|
|
308
373
|
.catch(error => {
|
|
309
374
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -329,23 +394,18 @@ function writeCharacteristic(characteristic: Characteristic, buffer: Buffer) {
|
|
|
329
394
|
async function disconnectDevice(uuid: string) {
|
|
330
395
|
const peripheral = connectedDevices.get(uuid);
|
|
331
396
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
397
|
+
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
332
398
|
if (characteristics) {
|
|
333
399
|
characteristics.notify.removeAllListeners('data');
|
|
334
|
-
await
|
|
335
|
-
characteristics.notify.unsubscribe(() => resolve());
|
|
336
|
-
});
|
|
400
|
+
await waitForNobleCleanup(callback => characteristics.notify.unsubscribe(callback));
|
|
337
401
|
}
|
|
338
402
|
|
|
339
403
|
connectedDevices.delete(uuid);
|
|
340
404
|
deviceCharacteristics.delete(uuid);
|
|
341
|
-
notificationQueue.length = 0;
|
|
342
|
-
pendingReceivers.splice(0).forEach(resolve => resolve(''));
|
|
343
405
|
|
|
344
406
|
if (!peripheral || peripheral.state === 'disconnected') return;
|
|
345
407
|
|
|
346
|
-
await
|
|
347
|
-
peripheral.disconnect(() => resolve());
|
|
348
|
-
});
|
|
408
|
+
await waitForNobleCleanup(callback => peripheral.disconnect(callback));
|
|
349
409
|
}
|
|
350
410
|
|
|
351
411
|
export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
@@ -376,7 +436,13 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
|
376
436
|
|
|
377
437
|
await connectPeripheral(peripheral);
|
|
378
438
|
const characteristics = await discoverCharacteristics(peripheral);
|
|
379
|
-
|
|
439
|
+
const notificationState = createNotificationState(uuid);
|
|
440
|
+
try {
|
|
441
|
+
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
380
446
|
connectedDevices.set(uuid, peripheral);
|
|
381
447
|
deviceCharacteristics.set(uuid, characteristics);
|
|
382
448
|
},
|
|
@@ -404,11 +470,28 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
|
404
470
|
}
|
|
405
471
|
},
|
|
406
472
|
|
|
407
|
-
async receive() {
|
|
408
|
-
const
|
|
473
|
+
async receive(uuid?: string) {
|
|
474
|
+
const resolvedUuid =
|
|
475
|
+
uuid ??
|
|
476
|
+
(notificationStates.size === 1 ? notificationStates.keys().next().value : undefined);
|
|
477
|
+
if (!resolvedUuid) {
|
|
478
|
+
throw ERRORS.TypedError(
|
|
479
|
+
HardwareErrorCode.RuntimeError,
|
|
480
|
+
'BLE receive requires a device UUID when multiple devices are connected'
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const state = notificationStates.get(resolvedUuid);
|
|
485
|
+
if (!state) {
|
|
486
|
+
throw ERRORS.TypedError(
|
|
487
|
+
HardwareErrorCode.TransportNotFound,
|
|
488
|
+
`BLE notification state not found: ${resolvedUuid}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
const queued = state.queue.shift();
|
|
409
492
|
if (queued !== undefined) return queued;
|
|
410
|
-
return new Promise<string>(resolve => {
|
|
411
|
-
pendingReceivers.
|
|
493
|
+
return new Promise<string>((resolve, reject) => {
|
|
494
|
+
state.pendingReceivers.add({ resolve, reject });
|
|
412
495
|
});
|
|
413
496
|
},
|
|
414
497
|
};
|