@onekeyfe/hardware-cli 1.2.0-alpha.2 → 1.2.0-alpha.20
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 +17 -1
- package/dist/cli.js +480 -54
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.js +8 -6
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +384 -0
- package/package.json +8 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +20 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +19 -0
- package/src/__tests__/noble-ble-plugin.test.ts +225 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +691 -66
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/sdk.ts +15 -7
- package/src/transports/nobleBlePlugin.ts +498 -0
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
declare const program: Command;
|
|
3
|
+
export declare function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble'): 90000 | undefined;
|
|
4
|
+
export declare function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }: {
|
|
5
|
+
totalBytes: number;
|
|
6
|
+
transferredBytes: number;
|
|
7
|
+
startedAt: number;
|
|
8
|
+
endedAt: number;
|
|
9
|
+
lastProgress: number;
|
|
10
|
+
}): {
|
|
11
|
+
totalBytes: number;
|
|
12
|
+
transferredBytes: number;
|
|
13
|
+
totalSeconds: number;
|
|
14
|
+
transferKiBPerSecond: number | null;
|
|
15
|
+
lastProgress: number;
|
|
16
|
+
};
|
|
17
|
+
export { program };
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.program = exports.buildWallpaperUploadMetrics = exports.getLegacyFirmwareConnectTimeout = void 0;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
4
7
|
const commander_1 = require("commander");
|
|
8
|
+
const hd_core_1 = require("@onekeyfe/hd-core");
|
|
9
|
+
const hd_shared_1 = require("@onekeyfe/hd-shared");
|
|
10
|
+
const chains_1 = require("./chains");
|
|
11
|
+
const deviceSelection_1 = require("./deviceSelection");
|
|
12
|
+
const deviceStateCommands_1 = require("./deviceStateCommands");
|
|
5
13
|
const sdk_1 = require("./sdk");
|
|
6
14
|
const session_1 = require("./session");
|
|
7
|
-
const chains_1 = require("./chains");
|
|
8
|
-
const hd_shared_1 = require("@onekeyfe/hd-shared");
|
|
9
|
-
const hd_core_1 = require("@onekeyfe/hd-core");
|
|
10
15
|
function extractPassphraseSession(payload) {
|
|
11
16
|
if (typeof payload === 'string') {
|
|
12
17
|
return { passphraseState: payload };
|
|
@@ -32,17 +37,21 @@ function extractPassphraseSession(payload) {
|
|
|
32
37
|
return { passphraseState, sessionId };
|
|
33
38
|
}
|
|
34
39
|
const program = new commander_1.Command();
|
|
40
|
+
exports.program = program;
|
|
41
|
+
const { version: cliVersion } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'), 'utf8'));
|
|
35
42
|
program
|
|
36
43
|
.name('onekey-hw')
|
|
37
44
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
38
|
-
.version(
|
|
45
|
+
.version(cliVersion);
|
|
39
46
|
// ============================================================
|
|
40
47
|
// Global Options
|
|
41
48
|
// ============================================================
|
|
42
49
|
program.option('--connect-id <id>', 'Device connection ID (USB: serial, iOS: uuid, Android: MAC)');
|
|
43
50
|
program.option('--device-id <id>', 'Persistent device ID from getFeatures (changes when seed changes)');
|
|
51
|
+
program.option('--transport <transport>', 'Transport to use: usb or ble', 'usb');
|
|
44
52
|
program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
|
|
45
53
|
program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
|
|
54
|
+
program.option('--debug', 'Enable SDK debug logs');
|
|
46
55
|
// ============================================================
|
|
47
56
|
// Device Commands
|
|
48
57
|
// ============================================================
|
|
@@ -51,50 +60,103 @@ program
|
|
|
51
60
|
.description('Search for connected OneKey hardware wallet devices')
|
|
52
61
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
53
62
|
const result = await sdk.searchDevices();
|
|
54
|
-
// Auto-fetch features for each discovered device (doesn't require PIN)
|
|
55
|
-
if (result?.success && Array.isArray(result.payload)) {
|
|
56
|
-
for (const device of result.payload) {
|
|
57
|
-
if (device.connectId) {
|
|
58
|
-
try {
|
|
59
|
-
const features = await sdk.getFeatures(device.connectId);
|
|
60
|
-
if (features?.success && features.payload) {
|
|
61
|
-
device.features = features.payload;
|
|
62
|
-
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
63
|
-
const devType = features.payload.deviceType;
|
|
64
|
-
if (devType) {
|
|
65
|
-
device.deviceType = devType;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// Features fetch failed — device may need PIN, continue with basic info
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
63
|
outputResult(globalOpts, result);
|
|
76
64
|
}));
|
|
77
65
|
program
|
|
78
66
|
.command('get-features')
|
|
79
67
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
80
68
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
69
|
+
const result = await (0, deviceStateCommands_1.getCompatibleFeatures)(sdk, globalOpts.connectId);
|
|
70
|
+
outputResult(globalOpts, result);
|
|
71
|
+
}));
|
|
72
|
+
program
|
|
73
|
+
.command('get-state')
|
|
74
|
+
.description('Get canonical device state for Protocol V1 and Protocol V2 devices')
|
|
75
|
+
.option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
|
|
76
|
+
.action((opts) => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
77
|
+
const supportedScopes = ['runtime', 'settings', 'firmware'];
|
|
78
|
+
if (!supportedScopes.includes(opts.scope)) {
|
|
79
|
+
const error = new Error(`Unsupported device state scope: ${opts.scope}`);
|
|
80
|
+
error.code = 'INVALID_DEVICE_STATE_SCOPE';
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
const result = await (0, deviceStateCommands_1.getCanonicalDeviceState)(sdk, globalOpts.connectId, opts.scope);
|
|
84
|
+
outputResult(globalOpts, result);
|
|
85
|
+
}));
|
|
86
|
+
program
|
|
87
|
+
.command('upload-wallpaper')
|
|
88
|
+
.description('Upload and activate a Pro2 wallpaper')
|
|
89
|
+
.requiredOption('--rgba <path>', '604x1024 raw RGBA file')
|
|
90
|
+
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
91
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
92
|
+
.action(opts => runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
93
|
+
const rgba = readBinaryParam(opts.rgba);
|
|
94
|
+
const expectedBytes = 604 * 1024 * 4;
|
|
95
|
+
if (rgba.byteLength !== expectedBytes) {
|
|
96
|
+
throw new Error(`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`);
|
|
97
|
+
}
|
|
98
|
+
let transferStartedAt;
|
|
99
|
+
let transferEndedAt;
|
|
100
|
+
let lastProgress = -1;
|
|
101
|
+
let lastPrintedProgress = -10;
|
|
102
|
+
let progressTotalBytes = 0;
|
|
103
|
+
let transferredBytes = 0;
|
|
104
|
+
const totalStartedAt = Date.now();
|
|
105
|
+
const onUiEvent = (message) => {
|
|
106
|
+
if (!message || typeof message !== 'object')
|
|
107
|
+
return;
|
|
108
|
+
const event = message;
|
|
109
|
+
if (event.type !== hd_core_1.UI_REQUEST.DEVICE_PROGRESS || !event.payload)
|
|
110
|
+
return;
|
|
111
|
+
const progress = Number(event.payload.progress);
|
|
112
|
+
if (!Number.isFinite(progress))
|
|
92
113
|
return;
|
|
114
|
+
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
115
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
116
|
+
const totalBytes = Number(event.payload.totalBytes);
|
|
117
|
+
if (Number.isFinite(totalBytes) && totalBytes > 0)
|
|
118
|
+
progressTotalBytes = totalBytes;
|
|
119
|
+
const confirmedBytes = Number(event.payload.transferredBytes);
|
|
120
|
+
if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
|
|
121
|
+
transferredBytes = Math.max(transferredBytes, confirmedBytes);
|
|
93
122
|
}
|
|
94
|
-
|
|
123
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
124
|
+
if (printableProgress > lastPrintedProgress || progress >= 100) {
|
|
125
|
+
const rate = Number(event.payload.rateBytesPerSecond);
|
|
126
|
+
const rateText = Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
|
|
127
|
+
process.stderr.write(`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`);
|
|
128
|
+
lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
|
|
129
|
+
}
|
|
130
|
+
if (progress >= 100)
|
|
131
|
+
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
132
|
+
};
|
|
133
|
+
sdk.on(hd_core_1.UI_EVENT, onUiEvent);
|
|
134
|
+
let result;
|
|
135
|
+
try {
|
|
136
|
+
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
137
|
+
...params,
|
|
138
|
+
width: 604,
|
|
139
|
+
height: 1024,
|
|
140
|
+
rgba,
|
|
141
|
+
fileName: opts.fileName,
|
|
142
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
143
|
+
});
|
|
95
144
|
}
|
|
96
|
-
|
|
97
|
-
|
|
145
|
+
finally {
|
|
146
|
+
sdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
|
|
147
|
+
}
|
|
148
|
+
const endedAt = transferEndedAt ?? Date.now();
|
|
149
|
+
const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
|
|
150
|
+
outputResult(globalOpts, {
|
|
151
|
+
...result,
|
|
152
|
+
metrics: buildWallpaperUploadMetrics({
|
|
153
|
+
totalBytes,
|
|
154
|
+
transferredBytes: result?.success ? totalBytes : transferredBytes,
|
|
155
|
+
startedAt: transferStartedAt ?? totalStartedAt,
|
|
156
|
+
endedAt,
|
|
157
|
+
lastProgress,
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
98
160
|
}));
|
|
99
161
|
// ============================================================
|
|
100
162
|
// Signing Commands
|
|
@@ -409,16 +471,66 @@ program
|
|
|
409
471
|
code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
|
|
410
472
|
},
|
|
411
473
|
}));
|
|
474
|
+
program
|
|
475
|
+
.command('firmware-update-legacy')
|
|
476
|
+
.description('Update Classic/Pure firmware through the legacy protocol')
|
|
477
|
+
.requiredOption('--binary <path>', 'Local firmware binary path')
|
|
478
|
+
.option('--device-name <name>', 'BLE advertising name, for example K1514')
|
|
479
|
+
.option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
|
|
480
|
+
.option('--no-reboot', 'Do not reboot the device after a successful update')
|
|
481
|
+
.action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
482
|
+
if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
|
|
483
|
+
throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
|
|
484
|
+
}
|
|
485
|
+
const connectId = await resolveLegacyFirmwareConnectId(sdk, globalOpts.connectId, opts.deviceName);
|
|
486
|
+
const result = await sdk.firmwareUpdate(connectId, {
|
|
487
|
+
binary: readBinaryParam(opts.binary),
|
|
488
|
+
updateType: opts.updateType,
|
|
489
|
+
rebootOnSuccess: opts.reboot,
|
|
490
|
+
timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
|
|
491
|
+
});
|
|
492
|
+
outputResult(globalOpts, result);
|
|
493
|
+
}));
|
|
494
|
+
function getLegacyFirmwareConnectTimeout(transport) {
|
|
495
|
+
return transport === 'usb' ? 90000 : undefined;
|
|
496
|
+
}
|
|
497
|
+
exports.getLegacyFirmwareConnectTimeout = getLegacyFirmwareConnectTimeout;
|
|
412
498
|
program
|
|
413
499
|
.command('firmware-update-ble')
|
|
414
|
-
.description('
|
|
500
|
+
.description('Run Protocol V2 firmware update over BLE')
|
|
415
501
|
.action(() => respondAndExit({
|
|
416
502
|
success: false,
|
|
417
503
|
payload: {
|
|
418
|
-
error: '
|
|
419
|
-
code: '
|
|
504
|
+
error: 'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
505
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
420
506
|
},
|
|
421
507
|
}));
|
|
508
|
+
program
|
|
509
|
+
.command('firmware-update-v4')
|
|
510
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
511
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
512
|
+
.option('--resource-bundle <spec...>', 'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg')
|
|
513
|
+
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
514
|
+
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
515
|
+
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
516
|
+
.option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
|
|
517
|
+
.option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
|
|
518
|
+
.option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
|
|
519
|
+
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
520
|
+
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
521
|
+
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
522
|
+
.option('--forced-update-res', 'Force resource update')
|
|
523
|
+
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
524
|
+
.action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
525
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
526
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
527
|
+
sdk,
|
|
528
|
+
globalOpts,
|
|
529
|
+
params,
|
|
530
|
+
retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
|
|
531
|
+
});
|
|
532
|
+
outputResult(globalOpts, result);
|
|
533
|
+
}));
|
|
422
534
|
program
|
|
423
535
|
.command('bootloader-check')
|
|
424
536
|
.description('Check bootloader version and status')
|
|
@@ -715,18 +827,22 @@ globalOpts) {
|
|
|
715
827
|
searchResult.payload.length === 0) {
|
|
716
828
|
return undefined;
|
|
717
829
|
}
|
|
718
|
-
const device = searchResult.payload
|
|
719
|
-
|
|
830
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
|
|
831
|
+
if (!device) {
|
|
832
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
833
|
+
}
|
|
834
|
+
const selectedDevice = device;
|
|
835
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
720
836
|
if (!globalOpts.connectId && connectId) {
|
|
721
837
|
globalOpts.connectId = connectId;
|
|
722
838
|
}
|
|
723
839
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
724
840
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
725
841
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
726
|
-
let deviceId =
|
|
727
|
-
let deviceType =
|
|
728
|
-
let unlocked =
|
|
729
|
-
let passphraseProtection =
|
|
842
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
843
|
+
let deviceType = selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? hd_shared_1.EDeviceType.Unknown;
|
|
844
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
845
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
730
846
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
731
847
|
try {
|
|
732
848
|
const featResult = await sdk.getFeatures(connectId);
|
|
@@ -812,12 +928,15 @@ function outputResult(_globalOpts, result) {
|
|
|
812
928
|
!result.success) {
|
|
813
929
|
process.exitCode = 1;
|
|
814
930
|
}
|
|
815
|
-
// No process.exit here — runCommand()
|
|
816
|
-
//
|
|
931
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
932
|
+
// exit naturally so leaked USB handles remain observable.
|
|
817
933
|
}
|
|
818
934
|
async function runCommand(options, handler) {
|
|
819
935
|
const globalOpts = program.opts();
|
|
820
936
|
try {
|
|
937
|
+
if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
|
|
938
|
+
throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
|
|
939
|
+
}
|
|
821
940
|
const sdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
822
941
|
if (options.needsSession) {
|
|
823
942
|
await prepareSession(sdk, globalOpts);
|
|
@@ -843,9 +962,7 @@ async function runCommand(options, handler) {
|
|
|
843
962
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
844
963
|
await (0, sdk_1.disposeSDK)();
|
|
845
964
|
}
|
|
846
|
-
//
|
|
847
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
848
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
965
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
849
966
|
}
|
|
850
967
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
851
968
|
function respondAndExit(result) {
|
|
@@ -866,6 +983,313 @@ function safeJsonParse(input, label) {
|
|
|
866
983
|
throw err;
|
|
867
984
|
}
|
|
868
985
|
}
|
|
986
|
+
function readBinaryParam(path) {
|
|
987
|
+
const buffer = (0, node_fs_1.readFileSync)(path);
|
|
988
|
+
return new Uint8Array(buffer).buffer;
|
|
989
|
+
}
|
|
990
|
+
async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName) {
|
|
991
|
+
if (explicitConnectId && !deviceName)
|
|
992
|
+
return explicitConnectId;
|
|
993
|
+
const searchResult = await sdk.searchDevices();
|
|
994
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
995
|
+
throw new Error('Unable to scan BLE devices');
|
|
996
|
+
}
|
|
997
|
+
const devices = searchResult.payload;
|
|
998
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
999
|
+
const matches = normalizedName
|
|
1000
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1001
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1002
|
+
if (matches.length === 0) {
|
|
1003
|
+
throw new Error(normalizedName
|
|
1004
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1005
|
+
: 'No Classic/Pure BLE device found');
|
|
1006
|
+
}
|
|
1007
|
+
if (matches.length > 1) {
|
|
1008
|
+
throw new Error(normalizedName
|
|
1009
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1010
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name');
|
|
1011
|
+
}
|
|
1012
|
+
const [{ connectId, name }] = matches;
|
|
1013
|
+
if (!connectId)
|
|
1014
|
+
throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1015
|
+
return connectId;
|
|
1016
|
+
}
|
|
1017
|
+
function parseResourceBundleParam(spec) {
|
|
1018
|
+
const sep = spec.indexOf(':');
|
|
1019
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
1020
|
+
throw new Error(`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`);
|
|
1021
|
+
}
|
|
1022
|
+
const localPath = spec.slice(0, sep);
|
|
1023
|
+
const devicePath = spec.slice(sep + 1);
|
|
1024
|
+
if (!devicePath.startsWith('vol')) {
|
|
1025
|
+
throw new Error(`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`);
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
binary: readBinaryParam(localPath),
|
|
1029
|
+
devicePath,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
function getFirmwareUpdateV4TotalBytes(params) {
|
|
1033
|
+
return [
|
|
1034
|
+
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1035
|
+
params.bootloaderBinary,
|
|
1036
|
+
params.applicationP1Binary,
|
|
1037
|
+
params.applicationP2Binary,
|
|
1038
|
+
params.coprocessorBinary,
|
|
1039
|
+
params.se01Binary,
|
|
1040
|
+
params.se02Binary,
|
|
1041
|
+
params.se03Binary,
|
|
1042
|
+
params.se04Binary,
|
|
1043
|
+
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1044
|
+
}
|
|
1045
|
+
function getFirmwareUpdateV4ErrorText(result) {
|
|
1046
|
+
if (!result || typeof result !== 'object')
|
|
1047
|
+
return '';
|
|
1048
|
+
const { payload } = result;
|
|
1049
|
+
if (!payload || typeof payload !== 'object')
|
|
1050
|
+
return '';
|
|
1051
|
+
const { error } = payload;
|
|
1052
|
+
return typeof error === 'string' ? error : '';
|
|
1053
|
+
}
|
|
1054
|
+
function isProtocolV2UsbProbeTransientResult(result) {
|
|
1055
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1056
|
+
return (error.includes('Device protocol mismatch') &&
|
|
1057
|
+
error.includes('expected V2') &&
|
|
1058
|
+
error.includes('did not respond to expected protocol'));
|
|
1059
|
+
}
|
|
1060
|
+
function isSuccessResult(result) {
|
|
1061
|
+
return (!!result && typeof result === 'object' && result.success === true);
|
|
1062
|
+
}
|
|
1063
|
+
function getFirmwareUpdatePayload(message) {
|
|
1064
|
+
if (!message || typeof message !== 'object')
|
|
1065
|
+
return undefined;
|
|
1066
|
+
return message.payload;
|
|
1067
|
+
}
|
|
1068
|
+
function formatFirmwareProgress(progress) {
|
|
1069
|
+
if (!Number.isFinite(progress))
|
|
1070
|
+
return '0%';
|
|
1071
|
+
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1072
|
+
}
|
|
1073
|
+
function formatFirmwareBytes(bytes) {
|
|
1074
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
1075
|
+
return '';
|
|
1076
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1077
|
+
}
|
|
1078
|
+
function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }) {
|
|
1079
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1080
|
+
return {
|
|
1081
|
+
totalBytes,
|
|
1082
|
+
transferredBytes,
|
|
1083
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1084
|
+
transferKiBPerSecond: elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1085
|
+
lastProgress,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
exports.buildWallpaperUploadMetrics = buildWallpaperUploadMetrics;
|
|
1089
|
+
function maybePrintFirmwareProgress({ progressType, progress, payload, lastPrintedProgress, }) {
|
|
1090
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
1091
|
+
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
1092
|
+
return lastPrintedProgress;
|
|
1093
|
+
}
|
|
1094
|
+
const transferredBytes = Number(payload.transferredBytes);
|
|
1095
|
+
const totalBytes = Number(payload.totalBytes);
|
|
1096
|
+
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1097
|
+
const sizeText = Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1098
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1099
|
+
: '';
|
|
1100
|
+
const speedText = Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
1101
|
+
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
1102
|
+
: '';
|
|
1103
|
+
process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(progress)}${sizeText}${speedText}\n`);
|
|
1104
|
+
return progress >= 100 ? 100 : printableProgress;
|
|
1105
|
+
}
|
|
1106
|
+
function buildFirmwareUpdateV4Metrics({ attempt, maxAttempts, totalBytes, totalStartedAt, transferStartedAt, transferEndedAt, installStartedAt, installEndedAt, progressEvents, lastProgress, installProgressEvents, lastInstallProgress, retried, }) {
|
|
1107
|
+
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
1108
|
+
const transferElapsedMs = transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
1109
|
+
? transferEndedAt - transferStartedAt
|
|
1110
|
+
: undefined;
|
|
1111
|
+
const installElapsedMs = installStartedAt !== undefined && installEndedAt !== undefined
|
|
1112
|
+
? installEndedAt - installStartedAt
|
|
1113
|
+
: undefined;
|
|
1114
|
+
return {
|
|
1115
|
+
attempt,
|
|
1116
|
+
maxAttempts,
|
|
1117
|
+
retried,
|
|
1118
|
+
totalBytes,
|
|
1119
|
+
progressEvents,
|
|
1120
|
+
lastProgress,
|
|
1121
|
+
installProgressEvents,
|
|
1122
|
+
lastInstallProgress,
|
|
1123
|
+
transferSeconds: transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
|
|
1124
|
+
transferKiBPerSecond: transferElapsedMs !== undefined && transferElapsedMs > 0
|
|
1125
|
+
? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
|
|
1126
|
+
: null,
|
|
1127
|
+
installSeconds: installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
|
|
1128
|
+
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }) {
|
|
1132
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1133
|
+
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1134
|
+
let currentSdk = sdk;
|
|
1135
|
+
let lastResult;
|
|
1136
|
+
let retried = false;
|
|
1137
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1138
|
+
let progressEvents = 0;
|
|
1139
|
+
let lastProgress = -1;
|
|
1140
|
+
let transferStartedAt;
|
|
1141
|
+
let transferEndedAt;
|
|
1142
|
+
let installProgressEvents = 0;
|
|
1143
|
+
let lastInstallProgress = -1;
|
|
1144
|
+
let installStartedAt;
|
|
1145
|
+
let installEndedAt;
|
|
1146
|
+
let lastPrintedTransferProgress = -10;
|
|
1147
|
+
let lastPrintedInstallProgress = -10;
|
|
1148
|
+
const totalStartedAt = Date.now();
|
|
1149
|
+
const connectId = retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1150
|
+
? undefined
|
|
1151
|
+
: globalOpts.connectId;
|
|
1152
|
+
const onUiEvent = (message) => {
|
|
1153
|
+
if (!message || typeof message !== 'object')
|
|
1154
|
+
return;
|
|
1155
|
+
const messageType = message.type;
|
|
1156
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1157
|
+
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1158
|
+
const tipMessage = payload?.data?.message;
|
|
1159
|
+
if (typeof tipMessage === 'string') {
|
|
1160
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1161
|
+
}
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
|
|
1165
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1166
|
+
process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
|
|
1170
|
+
return;
|
|
1171
|
+
const progress = Number(payload.progress);
|
|
1172
|
+
if (!Number.isFinite(progress))
|
|
1173
|
+
return;
|
|
1174
|
+
if (payload.progressType === 'transferData') {
|
|
1175
|
+
progressEvents += 1;
|
|
1176
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1177
|
+
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1178
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1179
|
+
progressType: 'transfer',
|
|
1180
|
+
progress,
|
|
1181
|
+
payload,
|
|
1182
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1183
|
+
});
|
|
1184
|
+
if (progress >= 100) {
|
|
1185
|
+
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
1186
|
+
}
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1190
|
+
installProgressEvents += 1;
|
|
1191
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1192
|
+
installStartedAt ?? (installStartedAt = Date.now());
|
|
1193
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1194
|
+
progressType: 'install',
|
|
1195
|
+
progress,
|
|
1196
|
+
payload,
|
|
1197
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1198
|
+
});
|
|
1199
|
+
if (progress >= 100) {
|
|
1200
|
+
installEndedAt ?? (installEndedAt = Date.now());
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
|
|
1205
|
+
try {
|
|
1206
|
+
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1207
|
+
}
|
|
1208
|
+
finally {
|
|
1209
|
+
currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
|
|
1210
|
+
}
|
|
1211
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1212
|
+
installEndedAt = Date.now();
|
|
1213
|
+
}
|
|
1214
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1215
|
+
attempt,
|
|
1216
|
+
maxAttempts,
|
|
1217
|
+
totalBytes,
|
|
1218
|
+
totalStartedAt,
|
|
1219
|
+
transferStartedAt,
|
|
1220
|
+
transferEndedAt,
|
|
1221
|
+
installStartedAt,
|
|
1222
|
+
installEndedAt,
|
|
1223
|
+
progressEvents,
|
|
1224
|
+
lastProgress,
|
|
1225
|
+
installProgressEvents,
|
|
1226
|
+
lastInstallProgress,
|
|
1227
|
+
retried,
|
|
1228
|
+
});
|
|
1229
|
+
if (lastResult && typeof lastResult === 'object') {
|
|
1230
|
+
const payload = (lastResult.payload ?? {});
|
|
1231
|
+
lastResult = {
|
|
1232
|
+
...lastResult,
|
|
1233
|
+
payload: {
|
|
1234
|
+
...payload,
|
|
1235
|
+
metrics,
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
if (isSuccessResult(lastResult)) {
|
|
1240
|
+
return lastResult;
|
|
1241
|
+
}
|
|
1242
|
+
if (attempt >= maxAttempts ||
|
|
1243
|
+
globalOpts.transport !== 'usb' ||
|
|
1244
|
+
!isProtocolV2UsbProbeTransientResult(lastResult)) {
|
|
1245
|
+
return lastResult;
|
|
1246
|
+
}
|
|
1247
|
+
retried = true;
|
|
1248
|
+
process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`);
|
|
1249
|
+
await (0, sdk_1.disposeSDK)();
|
|
1250
|
+
await new Promise(resolve => {
|
|
1251
|
+
setTimeout(resolve, 3000);
|
|
1252
|
+
});
|
|
1253
|
+
currentSdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
1254
|
+
}
|
|
1255
|
+
return lastResult;
|
|
1256
|
+
}
|
|
1257
|
+
function buildFirmwareUpdateV4Params(opts) {
|
|
1258
|
+
const params = {
|
|
1259
|
+
platform: 'desktop',
|
|
1260
|
+
connectProtocol: 'V2',
|
|
1261
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1262
|
+
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1263
|
+
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1264
|
+
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1265
|
+
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1266
|
+
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
1267
|
+
applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
|
|
1268
|
+
coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
|
|
1269
|
+
se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
|
|
1270
|
+
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1271
|
+
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1272
|
+
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1273
|
+
};
|
|
1274
|
+
const hasPayload = [
|
|
1275
|
+
params.resourceBundleFiles,
|
|
1276
|
+
params.romloaderBinary,
|
|
1277
|
+
params.bootloaderBinary,
|
|
1278
|
+
params.applicationP1Binary,
|
|
1279
|
+
params.applicationP2Binary,
|
|
1280
|
+
params.coprocessorBinary,
|
|
1281
|
+
params.se01Binary,
|
|
1282
|
+
params.se02Binary,
|
|
1283
|
+
params.se03Binary,
|
|
1284
|
+
params.se04Binary,
|
|
1285
|
+
].some(Boolean);
|
|
1286
|
+
if (!hasPayload) {
|
|
1287
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1288
|
+
err.code = 'MISSING_FIRMWARE_BINARY';
|
|
1289
|
+
throw err;
|
|
1290
|
+
}
|
|
1291
|
+
return params;
|
|
1292
|
+
}
|
|
869
1293
|
/**
|
|
870
1294
|
* #9 FIX: Safe parseInt with NaN check
|
|
871
1295
|
*/
|
|
@@ -876,4 +1300,6 @@ function safeParseInt(input, label) {
|
|
|
876
1300
|
}
|
|
877
1301
|
return num;
|
|
878
1302
|
}
|
|
879
|
-
|
|
1303
|
+
if (require.main === module) {
|
|
1304
|
+
program.parse();
|
|
1305
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectSearchDevice = void 0;
|
|
4
|
+
function selectSearchDevice(devices, preferredConnectId) {
|
|
5
|
+
if (preferredConnectId) {
|
|
6
|
+
return (devices.find(device => device.connectId === preferredConnectId) ??
|
|
7
|
+
{ connectId: preferredConnectId });
|
|
8
|
+
}
|
|
9
|
+
return devices[0];
|
|
10
|
+
}
|
|
11
|
+
exports.selectSearchDevice = selectSearchDevice;
|