@onekeyfe/hardware-cli 1.2.0-alpha.1 → 1.2.0-alpha.100
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 +83 -1
- package/dist/cli.js +501 -142
- 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/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +12 -2
- package/dist/sdk.js +22 -11
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +371 -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 +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +101 -0
- package/src/__tests__/noble-ble-plugin.test.ts +370 -0
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +687 -173
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +29 -13
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +487 -0
package/dist/cli.js
CHANGED
|
@@ -1,48 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.program = exports.runFirmwareUpdateV4WithRetry = exports.buildWallpaperUploadMetrics = exports.prepareSession = 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
|
-
function extractPassphraseSession(payload) {
|
|
11
|
-
if (typeof payload === 'string') {
|
|
12
|
-
return { passphraseState: payload };
|
|
13
|
-
}
|
|
14
|
-
if (!payload || typeof payload !== 'object') {
|
|
15
|
-
return {};
|
|
16
|
-
}
|
|
17
|
-
const statePayload = payload;
|
|
18
|
-
let passphraseState;
|
|
19
|
-
if (typeof statePayload.passphrase_state === 'string') {
|
|
20
|
-
passphraseState = statePayload.passphrase_state;
|
|
21
|
-
}
|
|
22
|
-
else if (typeof statePayload.passphraseState === 'string') {
|
|
23
|
-
passphraseState = statePayload.passphraseState;
|
|
24
|
-
}
|
|
25
|
-
let sessionId;
|
|
26
|
-
if (typeof statePayload.session_id === 'string') {
|
|
27
|
-
sessionId = statePayload.session_id;
|
|
28
|
-
}
|
|
29
|
-
else if (typeof statePayload.sessionId === 'string') {
|
|
30
|
-
sessionId = statePayload.sessionId;
|
|
31
|
-
}
|
|
32
|
-
return { passphraseState, sessionId };
|
|
33
|
-
}
|
|
34
15
|
const program = new commander_1.Command();
|
|
16
|
+
exports.program = program;
|
|
17
|
+
const { version: cliVersion } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'), 'utf8'));
|
|
35
18
|
program
|
|
36
19
|
.name('onekey-hw')
|
|
37
20
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
38
|
-
.version(
|
|
21
|
+
.version(cliVersion);
|
|
39
22
|
// ============================================================
|
|
40
23
|
// Global Options
|
|
41
24
|
// ============================================================
|
|
42
25
|
program.option('--connect-id <id>', 'Device connection ID (USB: serial, iOS: uuid, Android: MAC)');
|
|
43
26
|
program.option('--device-id <id>', 'Persistent device ID from getFeatures (changes when seed changes)');
|
|
27
|
+
program.option('--transport <transport>', 'Transport to use: usb or ble', 'usb');
|
|
44
28
|
program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
|
|
45
29
|
program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
|
|
30
|
+
program.option('--debug', 'Enable SDK debug logs');
|
|
46
31
|
// ============================================================
|
|
47
32
|
// Device Commands
|
|
48
33
|
// ============================================================
|
|
@@ -51,50 +36,103 @@ program
|
|
|
51
36
|
.description('Search for connected OneKey hardware wallet devices')
|
|
52
37
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
53
38
|
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?.toLowerCase();
|
|
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
39
|
outputResult(globalOpts, result);
|
|
76
40
|
}));
|
|
77
41
|
program
|
|
78
42
|
.command('get-features')
|
|
79
43
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
80
44
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
45
|
+
const result = await (0, deviceStateCommands_1.getCompatibleFeatures)(sdk, globalOpts.connectId);
|
|
46
|
+
outputResult(globalOpts, result);
|
|
47
|
+
}));
|
|
48
|
+
program
|
|
49
|
+
.command('get-state')
|
|
50
|
+
.description('Get canonical device state for Protocol V1 and Protocol V2 devices')
|
|
51
|
+
.option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
|
|
52
|
+
.action((opts) => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
53
|
+
const supportedScopes = ['runtime', 'settings', 'firmware'];
|
|
54
|
+
if (!supportedScopes.includes(opts.scope)) {
|
|
55
|
+
const error = new Error(`Unsupported device state scope: ${opts.scope}`);
|
|
56
|
+
error.code = 'INVALID_DEVICE_STATE_SCOPE';
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const result = await (0, deviceStateCommands_1.getCanonicalDeviceState)(sdk, globalOpts.connectId, opts.scope);
|
|
60
|
+
outputResult(globalOpts, result);
|
|
61
|
+
}));
|
|
62
|
+
program
|
|
63
|
+
.command('upload-wallpaper')
|
|
64
|
+
.description('Upload and activate a Pro2 wallpaper')
|
|
65
|
+
.requiredOption('--rgba <path>', '604x1024 raw RGBA file')
|
|
66
|
+
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
67
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
68
|
+
.action(opts => runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
69
|
+
const rgba = readBinaryParam(opts.rgba);
|
|
70
|
+
const expectedBytes = 604 * 1024 * 4;
|
|
71
|
+
if (rgba.byteLength !== expectedBytes) {
|
|
72
|
+
throw new Error(`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`);
|
|
73
|
+
}
|
|
74
|
+
let transferStartedAt;
|
|
75
|
+
let transferEndedAt;
|
|
76
|
+
let lastProgress = -1;
|
|
77
|
+
let lastPrintedProgress = -10;
|
|
78
|
+
let progressTotalBytes = 0;
|
|
79
|
+
let transferredBytes = 0;
|
|
80
|
+
const totalStartedAt = Date.now();
|
|
81
|
+
const onUiEvent = (message) => {
|
|
82
|
+
if (!message || typeof message !== 'object')
|
|
83
|
+
return;
|
|
84
|
+
const event = message;
|
|
85
|
+
if (event.type !== hd_core_1.UI_REQUEST.DEVICE_PROGRESS || !event.payload)
|
|
86
|
+
return;
|
|
87
|
+
const progress = Number(event.payload.progress);
|
|
88
|
+
if (!Number.isFinite(progress))
|
|
92
89
|
return;
|
|
90
|
+
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
91
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
92
|
+
const totalBytes = Number(event.payload.totalBytes);
|
|
93
|
+
if (Number.isFinite(totalBytes) && totalBytes > 0)
|
|
94
|
+
progressTotalBytes = totalBytes;
|
|
95
|
+
const confirmedBytes = Number(event.payload.transferredBytes);
|
|
96
|
+
if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
|
|
97
|
+
transferredBytes = Math.max(transferredBytes, confirmedBytes);
|
|
93
98
|
}
|
|
94
|
-
|
|
99
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
100
|
+
if (printableProgress > lastPrintedProgress || progress >= 100) {
|
|
101
|
+
const rate = Number(event.payload.rateBytesPerSecond);
|
|
102
|
+
const rateText = Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
|
|
103
|
+
process.stderr.write(`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`);
|
|
104
|
+
lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
|
|
105
|
+
}
|
|
106
|
+
if (progress >= 100)
|
|
107
|
+
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
108
|
+
};
|
|
109
|
+
sdk.on(hd_core_1.UI_EVENT, onUiEvent);
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
113
|
+
...params,
|
|
114
|
+
width: 604,
|
|
115
|
+
height: 1024,
|
|
116
|
+
rgba,
|
|
117
|
+
fileName: opts.fileName,
|
|
118
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
119
|
+
});
|
|
95
120
|
}
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
finally {
|
|
122
|
+
sdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
|
|
123
|
+
}
|
|
124
|
+
const endedAt = transferEndedAt ?? Date.now();
|
|
125
|
+
const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
|
|
126
|
+
outputResult(globalOpts, {
|
|
127
|
+
...result,
|
|
128
|
+
metrics: buildWallpaperUploadMetrics({
|
|
129
|
+
totalBytes,
|
|
130
|
+
transferredBytes: result?.success ? totalBytes : transferredBytes,
|
|
131
|
+
startedAt: transferStartedAt ?? totalStartedAt,
|
|
132
|
+
endedAt,
|
|
133
|
+
lastProgress,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
98
136
|
}));
|
|
99
137
|
// ============================================================
|
|
100
138
|
// Signing Commands
|
|
@@ -409,16 +447,66 @@ program
|
|
|
409
447
|
code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
|
|
410
448
|
},
|
|
411
449
|
}));
|
|
450
|
+
program
|
|
451
|
+
.command('firmware-update-legacy')
|
|
452
|
+
.description('Update Classic/Pure firmware through the legacy protocol')
|
|
453
|
+
.requiredOption('--binary <path>', 'Local firmware binary path')
|
|
454
|
+
.option('--device-name <name>', 'BLE advertising name, for example K1514')
|
|
455
|
+
.option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
|
|
456
|
+
.option('--no-reboot', 'Do not reboot the device after a successful update')
|
|
457
|
+
.action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
458
|
+
if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
|
|
459
|
+
throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
|
|
460
|
+
}
|
|
461
|
+
const connectId = await resolveLegacyFirmwareConnectId(sdk, globalOpts.connectId, opts.deviceName);
|
|
462
|
+
const result = await sdk.firmwareUpdate(connectId, {
|
|
463
|
+
binary: readBinaryParam(opts.binary),
|
|
464
|
+
updateType: opts.updateType,
|
|
465
|
+
rebootOnSuccess: opts.reboot,
|
|
466
|
+
timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
|
|
467
|
+
});
|
|
468
|
+
outputResult(globalOpts, result);
|
|
469
|
+
}));
|
|
470
|
+
function getLegacyFirmwareConnectTimeout(transport) {
|
|
471
|
+
return transport === 'usb' ? 90000 : undefined;
|
|
472
|
+
}
|
|
473
|
+
exports.getLegacyFirmwareConnectTimeout = getLegacyFirmwareConnectTimeout;
|
|
412
474
|
program
|
|
413
475
|
.command('firmware-update-ble')
|
|
414
|
-
.description('
|
|
476
|
+
.description('Run Protocol V2 firmware update over BLE')
|
|
415
477
|
.action(() => respondAndExit({
|
|
416
478
|
success: false,
|
|
417
479
|
payload: {
|
|
418
|
-
error: '
|
|
419
|
-
code: '
|
|
480
|
+
error: 'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
481
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
420
482
|
},
|
|
421
483
|
}));
|
|
484
|
+
program
|
|
485
|
+
.command('firmware-update-v4')
|
|
486
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
487
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
488
|
+
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
489
|
+
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
490
|
+
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
491
|
+
.option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
|
|
492
|
+
.option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
|
|
493
|
+
.option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
|
|
494
|
+
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
495
|
+
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
496
|
+
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
497
|
+
.option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
|
|
498
|
+
.option('--forced-update-res', 'Force resource update')
|
|
499
|
+
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
500
|
+
.action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
501
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
502
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
503
|
+
sdk,
|
|
504
|
+
globalOpts,
|
|
505
|
+
params,
|
|
506
|
+
retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
|
|
507
|
+
});
|
|
508
|
+
outputResult(globalOpts, result);
|
|
509
|
+
}));
|
|
422
510
|
program
|
|
423
511
|
.command('bootloader-check')
|
|
424
512
|
.description('Check bootloader version and status')
|
|
@@ -534,7 +622,7 @@ program
|
|
|
534
622
|
const sessionCmd = program.command('session').description('Manage device passphrase session cache');
|
|
535
623
|
sessionCmd
|
|
536
624
|
.command('connect')
|
|
537
|
-
.description('Connect device and
|
|
625
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
538
626
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
539
627
|
// 1. Search for device
|
|
540
628
|
const searchResult = await sdk.searchDevices();
|
|
@@ -545,7 +633,14 @@ sessionCmd
|
|
|
545
633
|
});
|
|
546
634
|
return;
|
|
547
635
|
}
|
|
548
|
-
const device = searchResult.payload
|
|
636
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
|
|
637
|
+
if (!device) {
|
|
638
|
+
outputResult(globalOpts, {
|
|
639
|
+
success: false,
|
|
640
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
549
644
|
const connectId = device.connectId || globalOpts.connectId;
|
|
550
645
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
551
646
|
// device session, which a locked device will reject with an obscure
|
|
@@ -554,56 +649,33 @@ sessionCmd
|
|
|
554
649
|
process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
|
|
555
650
|
await unlockWithRetry(sdk, connectId);
|
|
556
651
|
}
|
|
557
|
-
// 3.
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
useEmptyPassphrase: false,
|
|
652
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
653
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
654
|
+
mode: 'select-hidden',
|
|
561
655
|
});
|
|
562
|
-
if (!
|
|
563
|
-
outputResult(globalOpts,
|
|
656
|
+
if (!sessionResult.success) {
|
|
657
|
+
outputResult(globalOpts, sessionResult);
|
|
564
658
|
return;
|
|
565
659
|
}
|
|
566
|
-
|
|
567
|
-
if (!passphraseState) {
|
|
660
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
568
661
|
outputResult(globalOpts, {
|
|
569
662
|
success: false,
|
|
570
|
-
payload: { error: '
|
|
663
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
571
664
|
});
|
|
572
665
|
return;
|
|
573
666
|
}
|
|
667
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
574
668
|
// 4. Get address to verify + extract deviceId
|
|
575
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
669
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
576
670
|
path: "m/44'/60'/0'/0/0",
|
|
577
671
|
showOnOneKey: false,
|
|
578
672
|
passphraseState,
|
|
579
673
|
});
|
|
580
|
-
// 5. Fetch the now-active session_id via getFeatures.
|
|
581
|
-
//
|
|
582
|
-
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
583
|
-
// connectStateChange guard (core/index.ts) would see the payload's
|
|
584
|
-
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
585
|
-
// and call Initialize again with no passphrase_state / no session_id.
|
|
586
|
-
// That Initialize resets the device to the standard wallet and returns
|
|
587
|
-
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
588
|
-
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
589
|
-
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
590
|
-
const featResult = await sdk.getFeatures(connectId, {
|
|
591
|
-
passphraseState,
|
|
592
|
-
skipPassphraseCheck: true,
|
|
593
|
-
});
|
|
594
|
-
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
595
|
-
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
596
|
-
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
597
|
-
// 6. Save to keychain
|
|
598
|
-
if (passphraseState && deviceId && sessionId) {
|
|
599
|
-
await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
|
|
600
|
-
}
|
|
601
674
|
outputResult(globalOpts, {
|
|
602
675
|
success: true,
|
|
603
676
|
payload: {
|
|
604
677
|
passphraseState,
|
|
605
678
|
deviceId,
|
|
606
|
-
...(sessionId ? { sessionId } : {}),
|
|
607
679
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
608
680
|
},
|
|
609
681
|
});
|
|
@@ -613,8 +685,7 @@ sessionCmd
|
|
|
613
685
|
.description('Clear cached device session')
|
|
614
686
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
615
687
|
const searchResult = await sdk.searchDevices();
|
|
616
|
-
const device =
|
|
617
|
-
searchResult?.payload?.[0];
|
|
688
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult?.payload ?? [], globalOpts.connectId);
|
|
618
689
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
619
690
|
if (deviceId) {
|
|
620
691
|
await (0, session_1.clearSessionFromKeychain)(deviceId);
|
|
@@ -691,8 +762,8 @@ async function unlockWithRetry(sdk, connectId, maxAttempts = 3) {
|
|
|
691
762
|
* Prepare passphrase session before SDK calls.
|
|
692
763
|
*
|
|
693
764
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
694
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
695
|
-
* 3. Keychain miss →
|
|
765
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
766
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
696
767
|
*
|
|
697
768
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
698
769
|
*/
|
|
@@ -704,7 +775,7 @@ globalOpts) {
|
|
|
704
775
|
return globalOpts.passphraseState;
|
|
705
776
|
}
|
|
706
777
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
707
|
-
//
|
|
778
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
708
779
|
// catch block, which renders them as structured `{ success: false,
|
|
709
780
|
// payload: { error, code } }` output instead of silently falling through
|
|
710
781
|
// to a confusing downstream error 112 / 114.
|
|
@@ -715,18 +786,22 @@ globalOpts) {
|
|
|
715
786
|
searchResult.payload.length === 0) {
|
|
716
787
|
return undefined;
|
|
717
788
|
}
|
|
718
|
-
const device = searchResult.payload
|
|
719
|
-
|
|
789
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
|
|
790
|
+
if (!device) {
|
|
791
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
792
|
+
}
|
|
793
|
+
const selectedDevice = device;
|
|
794
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
720
795
|
if (!globalOpts.connectId && connectId) {
|
|
721
796
|
globalOpts.connectId = connectId;
|
|
722
797
|
}
|
|
723
798
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
724
799
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
725
800
|
// 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 =
|
|
801
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
802
|
+
let deviceType = selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? hd_shared_1.EDeviceType.Unknown;
|
|
803
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
804
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
730
805
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
731
806
|
try {
|
|
732
807
|
const featResult = await sdk.getFeatures(connectId);
|
|
@@ -761,7 +836,7 @@ globalOpts) {
|
|
|
761
836
|
if (passphraseProtection === false && deviceType !== hd_shared_1.EDeviceType.Pro2) {
|
|
762
837
|
return undefined;
|
|
763
838
|
}
|
|
764
|
-
// ── Step 5: Try keychain session reuse
|
|
839
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
765
840
|
// Only attempt if device was already unlocked — locking invalidates
|
|
766
841
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
767
842
|
if (!wasLocked && deviceId) {
|
|
@@ -771,38 +846,22 @@ globalOpts) {
|
|
|
771
846
|
return cached;
|
|
772
847
|
}
|
|
773
848
|
}
|
|
774
|
-
// ── Step 6: Keychain miss →
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
useEmptyPassphrase: false,
|
|
849
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
850
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
851
|
+
mode: 'select-hidden',
|
|
778
852
|
});
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
if (!passphraseState) {
|
|
853
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
854
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
782
855
|
return undefined;
|
|
783
856
|
}
|
|
857
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
858
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
784
859
|
globalOpts.passphraseState = passphraseState;
|
|
785
|
-
// Save session to keychain for next invocation.
|
|
786
|
-
//
|
|
787
|
-
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
788
|
-
// Initialize would be re-run without passphrase_state, resetting the
|
|
789
|
-
// device to the standard wallet and returning a mismatched session_id.
|
|
790
|
-
// See the matching comment in `session connect`.
|
|
791
|
-
if (deviceId) {
|
|
792
|
-
const featAfter = await sdk.getFeatures(connectId, {
|
|
793
|
-
passphraseState,
|
|
794
|
-
skipPassphraseCheck: true,
|
|
795
|
-
});
|
|
796
|
-
const sessionId = passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
797
|
-
if (sessionId) {
|
|
798
|
-
await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
|
|
799
|
-
await (0, session_1.preloadSessionFromKeychain)(deviceId);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
860
|
return passphraseState;
|
|
803
861
|
}
|
|
804
862
|
return undefined;
|
|
805
863
|
}
|
|
864
|
+
exports.prepareSession = prepareSession;
|
|
806
865
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
807
866
|
function outputResult(_globalOpts, result) {
|
|
808
867
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -812,12 +871,15 @@ function outputResult(_globalOpts, result) {
|
|
|
812
871
|
!result.success) {
|
|
813
872
|
process.exitCode = 1;
|
|
814
873
|
}
|
|
815
|
-
// No process.exit here — runCommand()
|
|
816
|
-
//
|
|
874
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
875
|
+
// exit naturally so leaked USB handles remain observable.
|
|
817
876
|
}
|
|
818
877
|
async function runCommand(options, handler) {
|
|
819
878
|
const globalOpts = program.opts();
|
|
820
879
|
try {
|
|
880
|
+
if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
|
|
881
|
+
throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
|
|
882
|
+
}
|
|
821
883
|
const sdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
822
884
|
if (options.needsSession) {
|
|
823
885
|
await prepareSession(sdk, globalOpts);
|
|
@@ -843,9 +905,7 @@ async function runCommand(options, handler) {
|
|
|
843
905
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
844
906
|
await (0, sdk_1.disposeSDK)();
|
|
845
907
|
}
|
|
846
|
-
//
|
|
847
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
848
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
908
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
849
909
|
}
|
|
850
910
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
851
911
|
function respondAndExit(result) {
|
|
@@ -866,6 +926,303 @@ function safeJsonParse(input, label) {
|
|
|
866
926
|
throw err;
|
|
867
927
|
}
|
|
868
928
|
}
|
|
929
|
+
function readBinaryParam(path) {
|
|
930
|
+
const buffer = (0, node_fs_1.readFileSync)(path);
|
|
931
|
+
return new Uint8Array(buffer).buffer;
|
|
932
|
+
}
|
|
933
|
+
async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName) {
|
|
934
|
+
if (explicitConnectId && !deviceName)
|
|
935
|
+
return explicitConnectId;
|
|
936
|
+
const searchResult = await sdk.searchDevices();
|
|
937
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
938
|
+
throw new Error('Unable to scan BLE devices');
|
|
939
|
+
}
|
|
940
|
+
const devices = searchResult.payload;
|
|
941
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
942
|
+
const matches = normalizedName
|
|
943
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
944
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
945
|
+
if (matches.length === 0) {
|
|
946
|
+
throw new Error(normalizedName
|
|
947
|
+
? `BLE device not found by name: ${deviceName}`
|
|
948
|
+
: 'No Classic/Pure BLE device found');
|
|
949
|
+
}
|
|
950
|
+
if (matches.length > 1) {
|
|
951
|
+
throw new Error(normalizedName
|
|
952
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
953
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name');
|
|
954
|
+
}
|
|
955
|
+
const [{ connectId, name }] = matches;
|
|
956
|
+
if (!connectId)
|
|
957
|
+
throw new Error(`BLE device has no connect ID: ${name}`);
|
|
958
|
+
return connectId;
|
|
959
|
+
}
|
|
960
|
+
function getFirmwareUpdateV4TotalBytes(params) {
|
|
961
|
+
return [
|
|
962
|
+
params.bootloaderBinary,
|
|
963
|
+
params.applicationP1Binary,
|
|
964
|
+
params.applicationP2Binary,
|
|
965
|
+
params.coprocessorBinary,
|
|
966
|
+
params.se01Binary,
|
|
967
|
+
params.se02Binary,
|
|
968
|
+
params.se03Binary,
|
|
969
|
+
params.se04Binary,
|
|
970
|
+
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
971
|
+
}
|
|
972
|
+
function getFirmwareUpdateV4ErrorText(result) {
|
|
973
|
+
if (!result || typeof result !== 'object')
|
|
974
|
+
return '';
|
|
975
|
+
const { payload } = result;
|
|
976
|
+
if (!payload || typeof payload !== 'object')
|
|
977
|
+
return '';
|
|
978
|
+
const { error } = payload;
|
|
979
|
+
return typeof error === 'string' ? error : '';
|
|
980
|
+
}
|
|
981
|
+
function isProtocolV2UsbProbeTransientResult(result) {
|
|
982
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
983
|
+
return (error.includes('Device protocol mismatch') &&
|
|
984
|
+
error.includes('expected V2') &&
|
|
985
|
+
error.includes('did not respond to expected protocol'));
|
|
986
|
+
}
|
|
987
|
+
function isSuccessResult(result) {
|
|
988
|
+
return (!!result && typeof result === 'object' && result.success === true);
|
|
989
|
+
}
|
|
990
|
+
function getFirmwareUpdatePayload(message) {
|
|
991
|
+
if (!message || typeof message !== 'object')
|
|
992
|
+
return undefined;
|
|
993
|
+
return message.payload;
|
|
994
|
+
}
|
|
995
|
+
function formatFirmwareProgress(progress) {
|
|
996
|
+
if (!Number.isFinite(progress))
|
|
997
|
+
return '0%';
|
|
998
|
+
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
999
|
+
}
|
|
1000
|
+
function formatFirmwareBytes(bytes) {
|
|
1001
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
1002
|
+
return '';
|
|
1003
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1004
|
+
}
|
|
1005
|
+
function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }) {
|
|
1006
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1007
|
+
return {
|
|
1008
|
+
totalBytes,
|
|
1009
|
+
transferredBytes,
|
|
1010
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1011
|
+
transferKiBPerSecond: elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1012
|
+
lastProgress,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
exports.buildWallpaperUploadMetrics = buildWallpaperUploadMetrics;
|
|
1016
|
+
function maybePrintFirmwareProgress({ progressType, progress, payload, lastPrintedProgress, }) {
|
|
1017
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
1018
|
+
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
1019
|
+
return lastPrintedProgress;
|
|
1020
|
+
}
|
|
1021
|
+
const transferredBytes = Number(payload.transferredBytes);
|
|
1022
|
+
const totalBytes = Number(payload.totalBytes);
|
|
1023
|
+
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1024
|
+
const sizeText = Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1025
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1026
|
+
: '';
|
|
1027
|
+
const speedText = Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
1028
|
+
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
1029
|
+
: '';
|
|
1030
|
+
process.stderr.write(`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(progress)}${sizeText}${speedText}\n`);
|
|
1031
|
+
return progress >= 100 ? 100 : printableProgress;
|
|
1032
|
+
}
|
|
1033
|
+
function buildFirmwareUpdateV4Metrics({ attempt, maxAttempts, totalBytes, totalStartedAt, transferStartedAt, transferEndedAt, installStartedAt, installEndedAt, progressEvents, lastProgress, installProgressEvents, lastInstallProgress, retried, }) {
|
|
1034
|
+
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
1035
|
+
const transferElapsedMs = transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
1036
|
+
? transferEndedAt - transferStartedAt
|
|
1037
|
+
: undefined;
|
|
1038
|
+
const installElapsedMs = installStartedAt !== undefined && installEndedAt !== undefined
|
|
1039
|
+
? installEndedAt - installStartedAt
|
|
1040
|
+
: undefined;
|
|
1041
|
+
return {
|
|
1042
|
+
attempt,
|
|
1043
|
+
maxAttempts,
|
|
1044
|
+
retried,
|
|
1045
|
+
totalBytes,
|
|
1046
|
+
progressEvents,
|
|
1047
|
+
lastProgress,
|
|
1048
|
+
installProgressEvents,
|
|
1049
|
+
lastInstallProgress,
|
|
1050
|
+
transferSeconds: transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
|
|
1051
|
+
transferKiBPerSecond: transferElapsedMs !== undefined && transferElapsedMs > 0
|
|
1052
|
+
? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
|
|
1053
|
+
: null,
|
|
1054
|
+
installSeconds: installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
|
|
1055
|
+
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }) {
|
|
1059
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1060
|
+
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1061
|
+
let currentSdk = sdk;
|
|
1062
|
+
let retried = false;
|
|
1063
|
+
let attempt = 1;
|
|
1064
|
+
let { connectId } = globalOpts;
|
|
1065
|
+
if (globalOpts.transport === 'usb') {
|
|
1066
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1067
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1068
|
+
scope: 'runtime',
|
|
1069
|
+
connectProtocol: 'V2',
|
|
1070
|
+
retryCount: 0,
|
|
1071
|
+
});
|
|
1072
|
+
if (isSuccessResult(probeResult))
|
|
1073
|
+
break;
|
|
1074
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1075
|
+
return probeResult;
|
|
1076
|
+
}
|
|
1077
|
+
retried = true;
|
|
1078
|
+
process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`);
|
|
1079
|
+
await (0, sdk_1.disposeSDK)();
|
|
1080
|
+
await new Promise(resolve => {
|
|
1081
|
+
setTimeout(resolve, 3000);
|
|
1082
|
+
});
|
|
1083
|
+
currentSdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
1084
|
+
if (globalOpts.connectId)
|
|
1085
|
+
connectId = undefined;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
let progressEvents = 0;
|
|
1089
|
+
let lastProgress = -1;
|
|
1090
|
+
let transferStartedAt;
|
|
1091
|
+
let transferEndedAt;
|
|
1092
|
+
let installProgressEvents = 0;
|
|
1093
|
+
let lastInstallProgress = -1;
|
|
1094
|
+
let installStartedAt;
|
|
1095
|
+
let installEndedAt;
|
|
1096
|
+
let lastPrintedTransferProgress = -10;
|
|
1097
|
+
let lastPrintedInstallProgress = -10;
|
|
1098
|
+
const totalStartedAt = Date.now();
|
|
1099
|
+
const onUiEvent = (message) => {
|
|
1100
|
+
if (!message || typeof message !== 'object')
|
|
1101
|
+
return;
|
|
1102
|
+
const messageType = message.type;
|
|
1103
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1104
|
+
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1105
|
+
const tipMessage = payload?.data?.message;
|
|
1106
|
+
if (typeof tipMessage === 'string') {
|
|
1107
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1108
|
+
}
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
|
|
1112
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1113
|
+
process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
|
|
1117
|
+
return;
|
|
1118
|
+
const progress = Number(payload.progress);
|
|
1119
|
+
if (!Number.isFinite(progress))
|
|
1120
|
+
return;
|
|
1121
|
+
if (payload.progressType === 'transferData') {
|
|
1122
|
+
progressEvents += 1;
|
|
1123
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1124
|
+
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1125
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1126
|
+
progressType: 'transfer',
|
|
1127
|
+
progress,
|
|
1128
|
+
payload,
|
|
1129
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1130
|
+
});
|
|
1131
|
+
if (progress >= 100) {
|
|
1132
|
+
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
1133
|
+
}
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1137
|
+
installProgressEvents += 1;
|
|
1138
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1139
|
+
installStartedAt ?? (installStartedAt = Date.now());
|
|
1140
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1141
|
+
progressType: 'install',
|
|
1142
|
+
progress,
|
|
1143
|
+
payload,
|
|
1144
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1145
|
+
});
|
|
1146
|
+
if (progress >= 100) {
|
|
1147
|
+
installEndedAt ?? (installEndedAt = Date.now());
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
|
|
1152
|
+
let result;
|
|
1153
|
+
try {
|
|
1154
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1155
|
+
}
|
|
1156
|
+
finally {
|
|
1157
|
+
currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
|
|
1158
|
+
}
|
|
1159
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1160
|
+
installEndedAt = Date.now();
|
|
1161
|
+
}
|
|
1162
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1163
|
+
attempt,
|
|
1164
|
+
maxAttempts,
|
|
1165
|
+
totalBytes,
|
|
1166
|
+
totalStartedAt,
|
|
1167
|
+
transferStartedAt,
|
|
1168
|
+
transferEndedAt,
|
|
1169
|
+
installStartedAt,
|
|
1170
|
+
installEndedAt,
|
|
1171
|
+
progressEvents,
|
|
1172
|
+
lastProgress,
|
|
1173
|
+
installProgressEvents,
|
|
1174
|
+
lastInstallProgress,
|
|
1175
|
+
retried,
|
|
1176
|
+
});
|
|
1177
|
+
if (result && typeof result === 'object') {
|
|
1178
|
+
const payload = (result.payload ?? {});
|
|
1179
|
+
return {
|
|
1180
|
+
...result,
|
|
1181
|
+
payload: {
|
|
1182
|
+
...payload,
|
|
1183
|
+
metrics,
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
return result;
|
|
1188
|
+
}
|
|
1189
|
+
exports.runFirmwareUpdateV4WithRetry = runFirmwareUpdateV4WithRetry;
|
|
1190
|
+
function buildFirmwareUpdateV4Params(opts) {
|
|
1191
|
+
const params = {
|
|
1192
|
+
platform: 'desktop',
|
|
1193
|
+
connectProtocol: 'V2',
|
|
1194
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1195
|
+
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1196
|
+
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1197
|
+
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1198
|
+
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
1199
|
+
applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
|
|
1200
|
+
coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
|
|
1201
|
+
se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
|
|
1202
|
+
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1203
|
+
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1204
|
+
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1205
|
+
resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
|
|
1206
|
+
};
|
|
1207
|
+
const hasPayload = [
|
|
1208
|
+
params.romloaderBinary,
|
|
1209
|
+
params.bootloaderBinary,
|
|
1210
|
+
params.applicationP1Binary,
|
|
1211
|
+
params.applicationP2Binary,
|
|
1212
|
+
params.coprocessorBinary,
|
|
1213
|
+
params.se01Binary,
|
|
1214
|
+
params.se02Binary,
|
|
1215
|
+
params.se03Binary,
|
|
1216
|
+
params.se04Binary,
|
|
1217
|
+
params.resourceArchiveBinary,
|
|
1218
|
+
].some(Boolean);
|
|
1219
|
+
if (!hasPayload) {
|
|
1220
|
+
const err = new Error('firmware-update-v4 requires at least one firmware binary or resource archive path');
|
|
1221
|
+
err.code = 'MISSING_FIRMWARE_BINARY';
|
|
1222
|
+
throw err;
|
|
1223
|
+
}
|
|
1224
|
+
return params;
|
|
1225
|
+
}
|
|
869
1226
|
/**
|
|
870
1227
|
* #9 FIX: Safe parseInt with NaN check
|
|
871
1228
|
*/
|
|
@@ -876,4 +1233,6 @@ function safeParseInt(input, label) {
|
|
|
876
1233
|
}
|
|
877
1234
|
return num;
|
|
878
1235
|
}
|
|
879
|
-
|
|
1236
|
+
if (require.main === module) {
|
|
1237
|
+
program.parse();
|
|
1238
|
+
}
|