@onekeyfe/hardware-cli 1.2.0-alpha.13 → 1.2.0-alpha.131

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.js CHANGED
@@ -1,44 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.program = void 0;
4
+ exports.program = exports.runFirmwareUpdateV4WithRetry = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
5
5
  const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
6
7
  const commander_1 = require("commander");
7
8
  const hd_core_1 = require("@onekeyfe/hd-core");
8
9
  const hd_shared_1 = require("@onekeyfe/hd-shared");
9
10
  const chains_1 = require("./chains");
11
+ const deviceSelection_1 = require("./deviceSelection");
12
+ const deviceStateCommands_1 = require("./deviceStateCommands");
10
13
  const sdk_1 = require("./sdk");
11
14
  const session_1 = require("./session");
12
- function extractPassphraseSession(payload) {
13
- if (typeof payload === 'string') {
14
- return { passphraseState: payload };
15
- }
16
- if (!payload || typeof payload !== 'object') {
17
- return {};
18
- }
19
- const statePayload = payload;
20
- let passphraseState;
21
- if (typeof statePayload.passphrase_state === 'string') {
22
- passphraseState = statePayload.passphrase_state;
23
- }
24
- else if (typeof statePayload.passphraseState === 'string') {
25
- passphraseState = statePayload.passphraseState;
26
- }
27
- let sessionId;
28
- if (typeof statePayload.session_id === 'string') {
29
- sessionId = statePayload.session_id;
30
- }
31
- else if (typeof statePayload.sessionId === 'string') {
32
- sessionId = statePayload.sessionId;
33
- }
34
- return { passphraseState, sessionId };
35
- }
36
15
  const program = new commander_1.Command();
37
16
  exports.program = program;
17
+ const { version: cliVersion } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'), 'utf8'));
38
18
  program
39
19
  .name('onekey-hw')
40
20
  .description('OneKey hardware wallet CLI for AI agent integration')
41
- .version('1.1.26-alpha.1');
21
+ .version(cliVersion);
42
22
  // ============================================================
43
23
  // Global Options
44
24
  // ============================================================
@@ -56,50 +36,97 @@ program
56
36
  .description('Search for connected OneKey hardware wallet devices')
57
37
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
58
38
  const result = await sdk.searchDevices();
59
- // USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
60
- if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
61
- for (const device of result.payload) {
62
- if (device.connectId) {
63
- try {
64
- const features = await sdk.getFeatures(device.connectId);
65
- if (features?.success && features.payload) {
66
- device.features = features.payload;
67
- device.name = features.payload.label || features.payload.bleName || device.name;
68
- const devType = features.payload.deviceType?.toLowerCase();
69
- if (devType) {
70
- device.deviceType = devType;
71
- }
72
- }
73
- }
74
- catch {
75
- // Features fetch failed — device may need PIN, continue with basic info
76
- }
77
- }
78
- }
79
- }
80
39
  outputResult(globalOpts, result);
81
40
  }));
82
41
  program
83
42
  .command('get-features')
84
43
  .description('Get device features (firmware, unlock state, passphrase protection, etc.)')
85
44
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
86
- // Resolve connectId: explicit flag wins, else pick the first attached device
87
- let { connectId } = globalOpts;
88
- if (!connectId) {
89
- const searchResult = await sdk.searchDevices();
90
- if (!searchResult?.success ||
91
- !Array.isArray(searchResult.payload) ||
92
- searchResult.payload.length === 0) {
93
- outputResult(globalOpts, {
94
- success: false,
95
- payload: { error: 'No device found', code: 'NO_DEVICE' },
96
- });
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('--jpeg <path>', '604x1024 JPEG 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 jpegBase64 = (0, node_fs_1.readFileSync)(opts.jpeg).toString('base64');
70
+ let transferStartedAt;
71
+ let transferEndedAt;
72
+ let lastProgress = -1;
73
+ let lastPrintedProgress = -10;
74
+ let progressTotalBytes = 0;
75
+ let transferredBytes = 0;
76
+ const totalStartedAt = Date.now();
77
+ const onUiEvent = (message) => {
78
+ if (!message || typeof message !== 'object')
79
+ return;
80
+ const event = message;
81
+ if (event.type !== hd_core_1.UI_REQUEST.DEVICE_PROGRESS || !event.payload)
97
82
  return;
83
+ const progress = Number(event.payload.progress);
84
+ if (!Number.isFinite(progress))
85
+ return;
86
+ transferStartedAt ?? (transferStartedAt = Date.now());
87
+ lastProgress = Math.max(lastProgress, progress);
88
+ const totalBytes = Number(event.payload.totalBytes);
89
+ if (Number.isFinite(totalBytes) && totalBytes > 0)
90
+ progressTotalBytes = totalBytes;
91
+ const confirmedBytes = Number(event.payload.transferredBytes);
92
+ if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
93
+ transferredBytes = Math.max(transferredBytes, confirmedBytes);
94
+ }
95
+ const printableProgress = Math.floor(progress / 10) * 10;
96
+ if (printableProgress > lastPrintedProgress || progress >= 100) {
97
+ const rate = Number(event.payload.rateBytesPerSecond);
98
+ const rateText = Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
99
+ process.stderr.write(`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`);
100
+ lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
98
101
  }
99
- connectId = searchResult.payload[0].connectId ?? undefined;
102
+ if (progress >= 100)
103
+ transferEndedAt ?? (transferEndedAt = Date.now());
104
+ };
105
+ sdk.on(hd_core_1.UI_EVENT, onUiEvent);
106
+ let result;
107
+ try {
108
+ result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
109
+ ...params,
110
+ jpegBase64,
111
+ fileName: opts.fileName,
112
+ chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
113
+ });
100
114
  }
101
- const result = await sdk.getFeatures(connectId || '');
102
- outputResult(globalOpts, result);
115
+ finally {
116
+ sdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
117
+ }
118
+ const endedAt = transferEndedAt ?? Date.now();
119
+ const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
120
+ outputResult(globalOpts, {
121
+ ...result,
122
+ metrics: buildWallpaperUploadMetrics({
123
+ totalBytes,
124
+ transferredBytes: result?.success ? totalBytes : transferredBytes,
125
+ startedAt: transferStartedAt ?? totalStartedAt,
126
+ endedAt,
127
+ lastProgress,
128
+ }),
129
+ });
103
130
  }));
104
131
  // ============================================================
105
132
  // Signing Commands
@@ -414,6 +441,30 @@ program
414
441
  code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
415
442
  },
416
443
  }));
444
+ program
445
+ .command('firmware-update-legacy')
446
+ .description('Update Classic/Pure firmware through the legacy protocol')
447
+ .requiredOption('--binary <path>', 'Local firmware binary path')
448
+ .option('--device-name <name>', 'BLE advertising name, for example K1514')
449
+ .option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
450
+ .option('--no-reboot', 'Do not reboot the device after a successful update')
451
+ .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
452
+ if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
453
+ throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
454
+ }
455
+ const connectId = await resolveLegacyFirmwareConnectId(sdk, globalOpts.connectId, opts.deviceName);
456
+ const result = await sdk.firmwareUpdate(connectId, {
457
+ binary: readBinaryParam(opts.binary),
458
+ updateType: opts.updateType,
459
+ rebootOnSuccess: opts.reboot,
460
+ timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
461
+ });
462
+ outputResult(globalOpts, result);
463
+ }));
464
+ function getLegacyFirmwareConnectTimeout(transport) {
465
+ return transport === 'usb' ? 90000 : undefined;
466
+ }
467
+ exports.getLegacyFirmwareConnectTimeout = getLegacyFirmwareConnectTimeout;
417
468
  program
418
469
  .command('firmware-update-ble')
419
470
  .description('Run Protocol V2 firmware update over BLE')
@@ -428,7 +479,6 @@ program
428
479
  .command('firmware-update-v4')
429
480
  .description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
430
481
  .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
431
- .option('--resource-bundle <spec...>', 'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg')
432
482
  .option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
433
483
  .option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
434
484
  .option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
@@ -438,6 +488,7 @@ program
438
488
  .option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
439
489
  .option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
440
490
  .option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
491
+ .option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
441
492
  .option('--forced-update-res', 'Force resource update')
442
493
  .option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
443
494
  .action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
@@ -565,7 +616,7 @@ program
565
616
  const sessionCmd = program.command('session').description('Manage device passphrase session cache');
566
617
  sessionCmd
567
618
  .command('connect')
568
- .description('Connect device and establish passphrase session (cached for subsequent commands)')
619
+ .description('Connect device and select a hidden wallet for this invocation')
569
620
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
570
621
  // 1. Search for device
571
622
  const searchResult = await sdk.searchDevices();
@@ -576,7 +627,14 @@ sessionCmd
576
627
  });
577
628
  return;
578
629
  }
579
- const device = searchResult.payload[0];
630
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
631
+ if (!device) {
632
+ outputResult(globalOpts, {
633
+ success: false,
634
+ payload: { error: 'No matching device found', code: 'NO_DEVICE' },
635
+ });
636
+ return;
637
+ }
580
638
  const connectId = device.connectId || globalOpts.connectId;
581
639
  // 2. Unlock if locked — getPassphraseState below talks to a live
582
640
  // device session, which a locked device will reject with an obscure
@@ -585,56 +643,33 @@ sessionCmd
585
643
  process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
586
644
  await unlockWithRetry(sdk, connectId);
587
645
  }
588
- // 3. Get passphraseState (triggers 1/2/3 selection)
589
- const psResult = await sdk.getPassphraseState(connectId, {
590
- initSession: true,
591
- useEmptyPassphrase: false,
646
+ // 3. Open a hidden wallet session (triggers 1/2/3 selection).
647
+ const sessionResult = await sdk.openWalletSession(connectId, {
648
+ mode: 'select-hidden',
592
649
  });
593
- if (!psResult.success) {
594
- outputResult(globalOpts, psResult);
650
+ if (!sessionResult.success) {
651
+ outputResult(globalOpts, sessionResult);
595
652
  return;
596
653
  }
597
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(psResult.payload);
598
- if (!passphraseState) {
654
+ if (sessionResult.payload.walletType !== 'hidden') {
599
655
  outputResult(globalOpts, {
600
656
  success: false,
601
- payload: { error: 'getPassphraseState did not return passphraseState' },
657
+ payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
602
658
  });
603
659
  return;
604
660
  }
661
+ const { deviceId, passphraseState } = sessionResult.payload;
605
662
  // 4. Get address to verify + extract deviceId
606
- const addrResult = await sdk.evmGetAddress(connectId, device.deviceId || '', {
663
+ const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
607
664
  path: "m/44'/60'/0'/0/0",
608
665
  showOnOneKey: false,
609
666
  passphraseState,
610
667
  });
611
- // 5. Fetch the now-active session_id via getFeatures.
612
- //
613
- // IMPORTANT: pass `passphraseState` here. Without it, the SDK's
614
- // connectStateChange guard (core/index.ts) would see the payload's
615
- // passphraseState flip from mnNy → undefined, clear the cached Device,
616
- // and call Initialize again with no passphrase_state / no session_id.
617
- // That Initialize resets the device to the standard wallet and returns
618
- // a *standard-wallet* session_id — which we'd then save in the keychain
619
- // paired with the hidden-wallet passphraseState. On the next CLI run
620
- // the mismatch would trigger PassphraseRequest (1/2/3 again).
621
- const featResult = await sdk.getFeatures(connectId, {
622
- passphraseState,
623
- skipPassphraseCheck: true,
624
- });
625
- const featPayload = featResult?.success ? featResult.payload : undefined;
626
- const deviceId = featPayload?.deviceId || device.deviceId || '';
627
- const sessionId = passphraseSessionId || featPayload?.sessionId || '';
628
- // 6. Save to keychain
629
- if (passphraseState && deviceId && sessionId) {
630
- await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
631
- }
632
668
  outputResult(globalOpts, {
633
669
  success: true,
634
670
  payload: {
635
671
  passphraseState,
636
672
  deviceId,
637
- ...(sessionId ? { sessionId } : {}),
638
673
  ...(addrResult?.success ? { address: addrResult.payload.address } : {}),
639
674
  },
640
675
  });
@@ -644,8 +679,7 @@ sessionCmd
644
679
  .description('Clear cached device session')
645
680
  .action(() => runCommand({}, async ({ sdk, globalOpts }) => {
646
681
  const searchResult = await sdk.searchDevices();
647
- const device = // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
648
- searchResult?.payload?.[0];
682
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult?.payload ?? [], globalOpts.connectId);
649
683
  const deviceId = device?.deviceId || device?.features?.device_id;
650
684
  if (deviceId) {
651
685
  await (0, session_1.clearSessionFromKeychain)(deviceId);
@@ -722,8 +756,8 @@ async function unlockWithRetry(sdk, connectId, maxAttempts = 3) {
722
756
  * Prepare passphrase session before SDK calls.
723
757
  *
724
758
  * 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
725
- * 2. Try keychain → preloadSessionCache → use cached session
726
- * 3. Keychain miss → getPassphraseState (triggers 1/2/3 prompt) → save to keychain
759
+ * 2. Try a legacy keychain entry → preloadSessionCache → use cached session
760
+ * 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
727
761
  *
728
762
  * After this, globalOpts.passphraseState is set and getCommonParams will include it.
729
763
  */
@@ -735,7 +769,7 @@ globalOpts) {
735
769
  return globalOpts.passphraseState;
736
770
  }
737
771
  // Errors from the SDK calls below (PIN cancelled, transport broken,
738
- // getPassphraseState rejection) intentionally propagate to runCommand's
772
+ // openWalletSession rejection) intentionally propagate to runCommand's
739
773
  // catch block, which renders them as structured `{ success: false,
740
774
  // payload: { error, code } }` output instead of silently falling through
741
775
  // to a confusing downstream error 112 / 114.
@@ -746,18 +780,22 @@ globalOpts) {
746
780
  searchResult.payload.length === 0) {
747
781
  return undefined;
748
782
  }
749
- const device = searchResult.payload[0];
750
- const connectId = device.connectId || globalOpts.connectId || '';
783
+ const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
784
+ if (!device) {
785
+ throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
786
+ }
787
+ const selectedDevice = device;
788
+ const connectId = selectedDevice.connectId || globalOpts.connectId || '';
751
789
  if (!globalOpts.connectId && connectId) {
752
790
  globalOpts.connectId = connectId;
753
791
  }
754
792
  // ── Step 2: Get features if searchDevices didn't populate them ──
755
793
  // getFeatures failures here are non-fatal — we fall through to Step 3
756
794
  // which will fail with a clearer error if the device is truly unreachable.
757
- let deviceId = device.features?.deviceId || device.deviceId || '';
758
- let deviceType = (0, hd_core_1.getDeviceType)(device.features);
759
- let unlocked = device.features?.unlocked;
760
- let passphraseProtection = device.features?.passphraseProtection;
795
+ let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
796
+ let deviceType = selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? hd_shared_1.EDeviceType.Unknown;
797
+ let unlocked = selectedDevice.features?.unlocked;
798
+ let passphraseProtection = selectedDevice.features?.passphraseProtection;
761
799
  if (!deviceId || unlocked == null || passphraseProtection == null) {
762
800
  try {
763
801
  const featResult = await sdk.getFeatures(connectId);
@@ -792,7 +830,7 @@ globalOpts) {
792
830
  if (passphraseProtection === false && deviceType !== hd_shared_1.EDeviceType.Pro2) {
793
831
  return undefined;
794
832
  }
795
- // ── Step 5: Try keychain session reuse ───────────────────────────
833
+ // ── Step 5: Try legacy keychain session reuse ────────────────────
796
834
  // Only attempt if device was already unlocked — locking invalidates
797
835
  // all passphrase sessions, so cached session_id is useless after unlock.
798
836
  if (!wasLocked && deviceId) {
@@ -802,38 +840,22 @@ globalOpts) {
802
840
  return cached;
803
841
  }
804
842
  }
805
- // ── Step 6: Keychain miss → getPassphraseState (triggers 1/2/3 prompt) ──
806
- const psResult = await sdk.getPassphraseState(connectId, {
807
- initSession: true,
808
- useEmptyPassphrase: false,
843
+ // ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
844
+ const sessionResult = await sdk.openWalletSession(connectId, {
845
+ mode: 'select-hidden',
809
846
  });
810
- if (psResult.success && psResult.payload) {
811
- const { passphraseState, sessionId: passphraseSessionId } = extractPassphraseSession(psResult.payload);
812
- if (!passphraseState) {
847
+ if (sessionResult.success && sessionResult.payload) {
848
+ if (sessionResult.payload.walletType !== 'hidden') {
813
849
  return undefined;
814
850
  }
851
+ const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
852
+ globalOpts.deviceId = sessionDeviceId;
815
853
  globalOpts.passphraseState = passphraseState;
816
- // Save session to keychain for next invocation.
817
- //
818
- // Pass passphraseState to keep connectStateChange=false — otherwise
819
- // Initialize would be re-run without passphrase_state, resetting the
820
- // device to the standard wallet and returning a mismatched session_id.
821
- // See the matching comment in `session connect`.
822
- if (deviceId) {
823
- const featAfter = await sdk.getFeatures(connectId, {
824
- passphraseState,
825
- skipPassphraseCheck: true,
826
- });
827
- const sessionId = passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
828
- if (sessionId) {
829
- await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
830
- await (0, session_1.preloadSessionFromKeychain)(deviceId);
831
- }
832
- }
833
854
  return passphraseState;
834
855
  }
835
856
  return undefined;
836
857
  }
858
+ exports.prepareSession = prepareSession;
837
859
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
838
860
  function outputResult(_globalOpts, result) {
839
861
  console.log(JSON.stringify(result, null, 2));
@@ -843,8 +865,8 @@ function outputResult(_globalOpts, result) {
843
865
  !result.success) {
844
866
  process.exitCode = 1;
845
867
  }
846
- // No process.exit here — runCommand() below handles dispose + exit so SDK
847
- // async cleanup (USB release, event listener teardown) finishes first.
868
+ // No process.exit here — runCommand() waits for SDK cleanup, then lets Node
869
+ // exit naturally so leaked USB handles remain observable.
848
870
  }
849
871
  async function runCommand(options, handler) {
850
872
  const globalOpts = program.opts();
@@ -877,9 +899,7 @@ async function runCommand(options, handler) {
877
899
  // promise reference. Idempotent, safe to call even if init failed.
878
900
  await (0, sdk_1.disposeSDK)();
879
901
  }
880
- // SDK event listeners can keep the event loop alive after dispose.
881
- // setImmediate lets any trailing stdout/stderr writes flush first.
882
- setImmediate(() => process.exit(process.exitCode ?? 0));
902
+ // disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
883
903
  }
884
904
  /** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
885
905
  function respondAndExit(result) {
@@ -904,24 +924,35 @@ function readBinaryParam(path) {
904
924
  const buffer = (0, node_fs_1.readFileSync)(path);
905
925
  return new Uint8Array(buffer).buffer;
906
926
  }
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>`);
927
+ async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName) {
928
+ if (explicitConnectId && !deviceName)
929
+ return explicitConnectId;
930
+ const searchResult = await sdk.searchDevices();
931
+ if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
932
+ throw new Error('Unable to scan BLE devices');
911
933
  }
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`);
934
+ const devices = searchResult.payload;
935
+ const normalizedName = deviceName?.trim().toLowerCase();
936
+ const matches = normalizedName
937
+ ? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
938
+ : devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
939
+ if (matches.length === 0) {
940
+ throw new Error(normalizedName
941
+ ? `BLE device not found by name: ${deviceName}`
942
+ : 'No Classic/Pure BLE device found');
916
943
  }
917
- return {
918
- binary: readBinaryParam(localPath),
919
- devicePath,
920
- };
944
+ if (matches.length > 1) {
945
+ throw new Error(normalizedName
946
+ ? `Multiple BLE devices found by name: ${deviceName}`
947
+ : 'Multiple Classic/Pure BLE devices found; specify --device-name');
948
+ }
949
+ const [{ connectId, name }] = matches;
950
+ if (!connectId)
951
+ throw new Error(`BLE device has no connect ID: ${name}`);
952
+ return connectId;
921
953
  }
922
954
  function getFirmwareUpdateV4TotalBytes(params) {
923
955
  return [
924
- ...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
925
956
  params.bootloaderBinary,
926
957
  params.applicationP1Binary,
927
958
  params.applicationP2Binary,
@@ -965,6 +996,17 @@ function formatFirmwareBytes(bytes) {
965
996
  return '';
966
997
  return `${(bytes / 1024).toFixed(1)} KiB`;
967
998
  }
999
+ function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }) {
1000
+ const elapsedMs = Math.max(endedAt - startedAt, 0);
1001
+ return {
1002
+ totalBytes,
1003
+ transferredBytes,
1004
+ totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
1005
+ transferKiBPerSecond: elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
1006
+ lastProgress,
1007
+ };
1008
+ }
1009
+ exports.buildWallpaperUploadMetrics = buildWallpaperUploadMetrics;
968
1010
  function maybePrintFirmwareProgress({ progressType, progress, payload, lastPrintedProgress, }) {
969
1011
  const printableProgress = Math.floor(progress / 10) * 10;
970
1012
  if (printableProgress <= lastPrintedProgress && progress < 100) {
@@ -1011,135 +1053,140 @@ async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries,
1011
1053
  const totalBytes = getFirmwareUpdateV4TotalBytes(params);
1012
1054
  const maxAttempts = Math.max((retries ?? 2) + 1, 1);
1013
1055
  let currentSdk = sdk;
1014
- let lastResult;
1015
1056
  let retried = false;
1016
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1017
- let progressEvents = 0;
1018
- let lastProgress = -1;
1019
- let transferStartedAt;
1020
- let transferEndedAt;
1021
- let installProgressEvents = 0;
1022
- let lastInstallProgress = -1;
1023
- let installStartedAt;
1024
- let installEndedAt;
1025
- let lastPrintedTransferProgress = -10;
1026
- let lastPrintedInstallProgress = -10;
1027
- const totalStartedAt = Date.now();
1028
- const connectId = retried && globalOpts.transport === 'usb' && globalOpts.connectId
1029
- ? undefined
1030
- : globalOpts.connectId;
1031
- const onUiEvent = (message) => {
1032
- if (!message || typeof message !== 'object')
1033
- return;
1034
- const messageType = message.type;
1035
- const payload = getFirmwareUpdatePayload(message);
1036
- if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1037
- const tipMessage = payload?.data?.message;
1038
- if (typeof tipMessage === 'string') {
1039
- process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1040
- }
1041
- return;
1042
- }
1043
- if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1044
- const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1045
- process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1046
- return;
1047
- }
1048
- if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1049
- return;
1050
- const progress = Number(payload.progress);
1051
- if (!Number.isFinite(progress))
1052
- return;
1053
- if (payload.progressType === 'transferData') {
1054
- progressEvents += 1;
1055
- lastProgress = Math.max(lastProgress, progress);
1056
- transferStartedAt ?? (transferStartedAt = Date.now());
1057
- lastPrintedTransferProgress = maybePrintFirmwareProgress({
1058
- progressType: 'transfer',
1059
- progress,
1060
- payload,
1061
- lastPrintedProgress: lastPrintedTransferProgress,
1062
- });
1063
- if (progress >= 100) {
1064
- transferEndedAt ?? (transferEndedAt = Date.now());
1065
- }
1066
- return;
1067
- }
1068
- if (payload.progressType === 'installingFirmware') {
1069
- installProgressEvents += 1;
1070
- lastInstallProgress = Math.max(lastInstallProgress, progress);
1071
- installStartedAt ?? (installStartedAt = Date.now());
1072
- lastPrintedInstallProgress = maybePrintFirmwareProgress({
1073
- progressType: 'install',
1074
- progress,
1075
- payload,
1076
- lastPrintedProgress: lastPrintedInstallProgress,
1077
- });
1078
- if (progress >= 100) {
1079
- installEndedAt ?? (installEndedAt = Date.now());
1080
- }
1057
+ let attempt = 1;
1058
+ let { connectId } = globalOpts;
1059
+ if (globalOpts.transport === 'usb') {
1060
+ for (; attempt <= maxAttempts; attempt += 1) {
1061
+ const probeResult = await currentSdk.getDeviceState(connectId, {
1062
+ scope: 'runtime',
1063
+ connectProtocol: 'V2',
1064
+ retryCount: 0,
1065
+ });
1066
+ if (isSuccessResult(probeResult))
1067
+ break;
1068
+ if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
1069
+ return probeResult;
1081
1070
  }
1082
- };
1083
- currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1084
- try {
1085
- lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1086
- }
1087
- finally {
1088
- currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1071
+ retried = true;
1072
+ process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`);
1073
+ await (0, sdk_1.disposeSDK)();
1074
+ await new Promise(resolve => {
1075
+ setTimeout(resolve, 3000);
1076
+ });
1077
+ currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1078
+ if (globalOpts.connectId)
1079
+ connectId = undefined;
1089
1080
  }
1090
- if (installStartedAt !== undefined && installEndedAt === undefined) {
1091
- installEndedAt = Date.now();
1081
+ }
1082
+ let progressEvents = 0;
1083
+ let lastProgress = -1;
1084
+ let transferStartedAt;
1085
+ let transferEndedAt;
1086
+ let installProgressEvents = 0;
1087
+ let lastInstallProgress = -1;
1088
+ let installStartedAt;
1089
+ let installEndedAt;
1090
+ let lastPrintedTransferProgress = -10;
1091
+ let lastPrintedInstallProgress = -10;
1092
+ const totalStartedAt = Date.now();
1093
+ const onUiEvent = (message) => {
1094
+ if (!message || typeof message !== 'object')
1095
+ return;
1096
+ const messageType = message.type;
1097
+ const payload = getFirmwareUpdatePayload(message);
1098
+ if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1099
+ const tipMessage = payload?.data?.message;
1100
+ if (typeof tipMessage === 'string') {
1101
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1102
+ }
1103
+ return;
1092
1104
  }
1093
- const metrics = buildFirmwareUpdateV4Metrics({
1094
- attempt,
1095
- maxAttempts,
1096
- totalBytes,
1097
- totalStartedAt,
1098
- transferStartedAt,
1099
- transferEndedAt,
1100
- installStartedAt,
1101
- installEndedAt,
1102
- progressEvents,
1103
- lastProgress,
1104
- installProgressEvents,
1105
- lastInstallProgress,
1106
- retried,
1107
- });
1108
- if (lastResult && typeof lastResult === 'object') {
1109
- const payload = (lastResult.payload ?? {});
1110
- lastResult = {
1111
- ...lastResult,
1112
- payload: {
1113
- ...payload,
1114
- metrics,
1115
- },
1116
- };
1105
+ if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1106
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1107
+ process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1108
+ return;
1117
1109
  }
1118
- if (isSuccessResult(lastResult)) {
1119
- return lastResult;
1110
+ if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1111
+ return;
1112
+ const progress = Number(payload.progress);
1113
+ if (!Number.isFinite(progress))
1114
+ return;
1115
+ if (payload.progressType === 'transferData') {
1116
+ progressEvents += 1;
1117
+ lastProgress = Math.max(lastProgress, progress);
1118
+ transferStartedAt ?? (transferStartedAt = Date.now());
1119
+ lastPrintedTransferProgress = maybePrintFirmwareProgress({
1120
+ progressType: 'transfer',
1121
+ progress,
1122
+ payload,
1123
+ lastPrintedProgress: lastPrintedTransferProgress,
1124
+ });
1125
+ if (progress >= 100) {
1126
+ transferEndedAt ?? (transferEndedAt = Date.now());
1127
+ }
1128
+ return;
1120
1129
  }
1121
- if (attempt >= maxAttempts ||
1122
- globalOpts.transport !== 'usb' ||
1123
- !isProtocolV2UsbProbeTransientResult(lastResult)) {
1124
- return lastResult;
1130
+ if (payload.progressType === 'installingFirmware') {
1131
+ installProgressEvents += 1;
1132
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1133
+ installStartedAt ?? (installStartedAt = Date.now());
1134
+ lastPrintedInstallProgress = maybePrintFirmwareProgress({
1135
+ progressType: 'install',
1136
+ progress,
1137
+ payload,
1138
+ lastPrintedProgress: lastPrintedInstallProgress,
1139
+ });
1140
+ if (progress >= 100) {
1141
+ installEndedAt ?? (installEndedAt = Date.now());
1142
+ }
1125
1143
  }
1126
- retried = true;
1127
- process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`);
1128
- await (0, sdk_1.disposeSDK)();
1129
- await new Promise(resolve => {
1130
- setTimeout(resolve, 3000);
1131
- });
1132
- currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1144
+ };
1145
+ currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1146
+ let result;
1147
+ try {
1148
+ result = await currentSdk.firmwareUpdateV4(connectId, params);
1149
+ }
1150
+ finally {
1151
+ currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1152
+ }
1153
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1154
+ installEndedAt = Date.now();
1155
+ }
1156
+ const metrics = buildFirmwareUpdateV4Metrics({
1157
+ attempt,
1158
+ maxAttempts,
1159
+ totalBytes,
1160
+ totalStartedAt,
1161
+ transferStartedAt,
1162
+ transferEndedAt,
1163
+ installStartedAt,
1164
+ installEndedAt,
1165
+ progressEvents,
1166
+ lastProgress,
1167
+ installProgressEvents,
1168
+ lastInstallProgress,
1169
+ retried,
1170
+ });
1171
+ if (result && typeof result === 'object') {
1172
+ const payload = (result.payload ?? {});
1173
+ return {
1174
+ ...result,
1175
+ payload: {
1176
+ ...payload,
1177
+ metrics,
1178
+ },
1179
+ };
1133
1180
  }
1134
- return lastResult;
1181
+ return result;
1135
1182
  }
1183
+ exports.runFirmwareUpdateV4WithRetry = runFirmwareUpdateV4WithRetry;
1136
1184
  function buildFirmwareUpdateV4Params(opts) {
1137
1185
  const params = {
1138
1186
  platform: 'desktop',
1139
1187
  connectProtocol: 'V2',
1140
1188
  chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
1141
1189
  forcedUpdateRes: opts.forcedUpdateRes,
1142
- resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
1143
1190
  romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
1144
1191
  bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
1145
1192
  applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
@@ -1149,9 +1196,9 @@ function buildFirmwareUpdateV4Params(opts) {
1149
1196
  se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
1150
1197
  se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
1151
1198
  se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
1199
+ resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
1152
1200
  };
1153
1201
  const hasPayload = [
1154
- params.resourceBundleFiles,
1155
1202
  params.romloaderBinary,
1156
1203
  params.bootloaderBinary,
1157
1204
  params.applicationP1Binary,
@@ -1161,9 +1208,10 @@ function buildFirmwareUpdateV4Params(opts) {
1161
1208
  params.se02Binary,
1162
1209
  params.se03Binary,
1163
1210
  params.se04Binary,
1211
+ params.resourceArchiveBinary,
1164
1212
  ].some(Boolean);
1165
1213
  if (!hasPayload) {
1166
- const err = new Error('firmware-update-v4 requires at least one binary path');
1214
+ const err = new Error('firmware-update-v4 requires at least one firmware binary or resource archive path');
1167
1215
  err.code = 'MISSING_FIRMWARE_BINARY';
1168
1216
  throw err;
1169
1217
  }