@onekeyfe/hardware-cli 1.2.0-alpha.17 → 1.2.0-alpha.170
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 +66 -0
- package/dist/cli.js +191 -284
- package/dist/deviceSelection.d.ts +1 -1
- 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 +10 -2
- package/dist/sdk.js +17 -8
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.js +26 -39
- package/package.json +7 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +1 -3
- package/src/__tests__/firmware-update-v4-command.test.ts +83 -1
- package/src/__tests__/noble-ble-plugin.test.ts +147 -2
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +1 -0
- package/src/cli.ts +222 -339
- package/src/deviceSelection.ts +1 -1
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +18 -10
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +34 -45
package/dist/cli.js
CHANGED
|
@@ -1,45 +1,24 @@
|
|
|
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;
|
|
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");
|
|
10
11
|
const deviceSelection_1 = require("./deviceSelection");
|
|
12
|
+
const deviceStateCommands_1 = require("./deviceStateCommands");
|
|
11
13
|
const sdk_1 = require("./sdk");
|
|
12
14
|
const session_1 = require("./session");
|
|
13
|
-
function extractPassphraseSession(payload) {
|
|
14
|
-
if (typeof payload === 'string') {
|
|
15
|
-
return { passphraseState: payload };
|
|
16
|
-
}
|
|
17
|
-
if (!payload || typeof payload !== 'object') {
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
const statePayload = payload;
|
|
21
|
-
let passphraseState;
|
|
22
|
-
if (typeof statePayload.passphrase_state === 'string') {
|
|
23
|
-
passphraseState = statePayload.passphrase_state;
|
|
24
|
-
}
|
|
25
|
-
else if (typeof statePayload.passphraseState === 'string') {
|
|
26
|
-
passphraseState = statePayload.passphraseState;
|
|
27
|
-
}
|
|
28
|
-
let sessionId;
|
|
29
|
-
if (typeof statePayload.session_id === 'string') {
|
|
30
|
-
sessionId = statePayload.session_id;
|
|
31
|
-
}
|
|
32
|
-
else if (typeof statePayload.sessionId === 'string') {
|
|
33
|
-
sessionId = statePayload.sessionId;
|
|
34
|
-
}
|
|
35
|
-
return { passphraseState, sessionId };
|
|
36
|
-
}
|
|
37
15
|
const program = new commander_1.Command();
|
|
38
16
|
exports.program = program;
|
|
17
|
+
const { version: cliVersion } = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, '../package.json'), 'utf8'));
|
|
39
18
|
program
|
|
40
19
|
.name('onekey-hw')
|
|
41
20
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
42
|
-
.version(
|
|
21
|
+
.version(cliVersion);
|
|
43
22
|
// ============================================================
|
|
44
23
|
// Global Options
|
|
45
24
|
// ============================================================
|
|
@@ -57,63 +36,37 @@ program
|
|
|
57
36
|
.description('Search for connected OneKey hardware wallet devices')
|
|
58
37
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
59
38
|
const result = await sdk.searchDevices();
|
|
60
|
-
// USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
|
|
61
|
-
if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
|
|
62
|
-
for (const device of result.payload) {
|
|
63
|
-
if (device.connectId) {
|
|
64
|
-
try {
|
|
65
|
-
const features = await sdk.getFeatures(device.connectId);
|
|
66
|
-
if (features?.success && features.payload) {
|
|
67
|
-
device.features = features.payload;
|
|
68
|
-
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
69
|
-
const devType = features.payload.deviceType?.toLowerCase();
|
|
70
|
-
if (devType) {
|
|
71
|
-
device.deviceType = devType;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// Features fetch failed — device may need PIN, continue with basic info
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
39
|
outputResult(globalOpts, result);
|
|
82
40
|
}));
|
|
83
41
|
program
|
|
84
42
|
.command('get-features')
|
|
85
43
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
86
44
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
connectId = searchResult.payload[0].connectId ?? undefined;
|
|
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;
|
|
101
58
|
}
|
|
102
|
-
const result = await sdk.
|
|
59
|
+
const result = await (0, deviceStateCommands_1.getCanonicalDeviceState)(sdk, globalOpts.connectId, opts.scope);
|
|
103
60
|
outputResult(globalOpts, result);
|
|
104
61
|
}));
|
|
105
62
|
program
|
|
106
63
|
.command('upload-wallpaper')
|
|
107
64
|
.description('Upload and activate a Pro2 wallpaper')
|
|
108
|
-
.requiredOption('--
|
|
65
|
+
.requiredOption('--jpeg <path>', '604x1024 JPEG file')
|
|
109
66
|
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
110
67
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
111
68
|
.action(opts => runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
112
|
-
const
|
|
113
|
-
const expectedBytes = 604 * 1024 * 4;
|
|
114
|
-
if (rgba.byteLength !== expectedBytes) {
|
|
115
|
-
throw new Error(`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`);
|
|
116
|
-
}
|
|
69
|
+
const jpegBase64 = (0, node_fs_1.readFileSync)(opts.jpeg).toString('base64');
|
|
117
70
|
let transferStartedAt;
|
|
118
71
|
let transferEndedAt;
|
|
119
72
|
let lastProgress = -1;
|
|
@@ -154,9 +107,7 @@ program
|
|
|
154
107
|
try {
|
|
155
108
|
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
156
109
|
...params,
|
|
157
|
-
|
|
158
|
-
height: 1024,
|
|
159
|
-
rgba,
|
|
110
|
+
jpegBase64,
|
|
160
111
|
fileName: opts.fileName,
|
|
161
112
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
162
113
|
});
|
|
@@ -528,7 +479,6 @@ program
|
|
|
528
479
|
.command('firmware-update-v4')
|
|
529
480
|
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
530
481
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
531
|
-
.option('--resource-bundle <spec...>', 'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg')
|
|
532
482
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
533
483
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
534
484
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -538,6 +488,7 @@ program
|
|
|
538
488
|
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
539
489
|
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
540
490
|
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
491
|
+
.option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
|
|
541
492
|
.option('--forced-update-res', 'Force resource update')
|
|
542
493
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
543
494
|
.action(opts => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
@@ -665,7 +616,7 @@ program
|
|
|
665
616
|
const sessionCmd = program.command('session').description('Manage device passphrase session cache');
|
|
666
617
|
sessionCmd
|
|
667
618
|
.command('connect')
|
|
668
|
-
.description('Connect device and
|
|
619
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
669
620
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
670
621
|
// 1. Search for device
|
|
671
622
|
const searchResult = await sdk.searchDevices();
|
|
@@ -676,7 +627,14 @@ sessionCmd
|
|
|
676
627
|
});
|
|
677
628
|
return;
|
|
678
629
|
}
|
|
679
|
-
const device = searchResult.payload
|
|
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
|
+
}
|
|
680
638
|
const connectId = device.connectId || globalOpts.connectId;
|
|
681
639
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
682
640
|
// device session, which a locked device will reject with an obscure
|
|
@@ -685,56 +643,33 @@ sessionCmd
|
|
|
685
643
|
process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
|
|
686
644
|
await unlockWithRetry(sdk, connectId);
|
|
687
645
|
}
|
|
688
|
-
// 3.
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
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',
|
|
692
649
|
});
|
|
693
|
-
if (!
|
|
694
|
-
outputResult(globalOpts,
|
|
650
|
+
if (!sessionResult.success) {
|
|
651
|
+
outputResult(globalOpts, sessionResult);
|
|
695
652
|
return;
|
|
696
653
|
}
|
|
697
|
-
|
|
698
|
-
if (!passphraseState) {
|
|
654
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
699
655
|
outputResult(globalOpts, {
|
|
700
656
|
success: false,
|
|
701
|
-
payload: { error: '
|
|
657
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
702
658
|
});
|
|
703
659
|
return;
|
|
704
660
|
}
|
|
661
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
705
662
|
// 4. Get address to verify + extract deviceId
|
|
706
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
663
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
707
664
|
path: "m/44'/60'/0'/0/0",
|
|
708
665
|
showOnOneKey: false,
|
|
709
666
|
passphraseState,
|
|
710
667
|
});
|
|
711
|
-
// 5. Fetch the now-active session_id via getFeatures.
|
|
712
|
-
//
|
|
713
|
-
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
714
|
-
// connectStateChange guard (core/index.ts) would see the payload's
|
|
715
|
-
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
716
|
-
// and call Initialize again with no passphrase_state / no session_id.
|
|
717
|
-
// That Initialize resets the device to the standard wallet and returns
|
|
718
|
-
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
719
|
-
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
720
|
-
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
721
|
-
const featResult = await sdk.getFeatures(connectId, {
|
|
722
|
-
passphraseState,
|
|
723
|
-
skipPassphraseCheck: true,
|
|
724
|
-
});
|
|
725
|
-
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
726
|
-
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
727
|
-
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
728
|
-
// 6. Save to keychain
|
|
729
|
-
if (passphraseState && deviceId && sessionId) {
|
|
730
|
-
await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
|
|
731
|
-
}
|
|
732
668
|
outputResult(globalOpts, {
|
|
733
669
|
success: true,
|
|
734
670
|
payload: {
|
|
735
671
|
passphraseState,
|
|
736
672
|
deviceId,
|
|
737
|
-
...(sessionId ? { sessionId } : {}),
|
|
738
673
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
739
674
|
},
|
|
740
675
|
});
|
|
@@ -744,8 +679,7 @@ sessionCmd
|
|
|
744
679
|
.description('Clear cached device session')
|
|
745
680
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
746
681
|
const searchResult = await sdk.searchDevices();
|
|
747
|
-
const device =
|
|
748
|
-
searchResult?.payload?.[0];
|
|
682
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult?.payload ?? [], globalOpts.connectId);
|
|
749
683
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
750
684
|
if (deviceId) {
|
|
751
685
|
await (0, session_1.clearSessionFromKeychain)(deviceId);
|
|
@@ -822,8 +756,8 @@ async function unlockWithRetry(sdk, connectId, maxAttempts = 3) {
|
|
|
822
756
|
* Prepare passphrase session before SDK calls.
|
|
823
757
|
*
|
|
824
758
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
825
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
826
|
-
* 3. Keychain miss →
|
|
759
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
760
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
827
761
|
*
|
|
828
762
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
829
763
|
*/
|
|
@@ -835,7 +769,7 @@ globalOpts) {
|
|
|
835
769
|
return globalOpts.passphraseState;
|
|
836
770
|
}
|
|
837
771
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
838
|
-
//
|
|
772
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
839
773
|
// catch block, which renders them as structured `{ success: false,
|
|
840
774
|
// payload: { error, code } }` output instead of silently falling through
|
|
841
775
|
// to a confusing downstream error 112 / 114.
|
|
@@ -859,7 +793,7 @@ globalOpts) {
|
|
|
859
793
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
860
794
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
861
795
|
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
862
|
-
let deviceType =
|
|
796
|
+
let deviceType = selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? hd_shared_1.EDeviceType.Unknown;
|
|
863
797
|
let unlocked = selectedDevice.features?.unlocked;
|
|
864
798
|
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
865
799
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
@@ -896,7 +830,7 @@ globalOpts) {
|
|
|
896
830
|
if (passphraseProtection === false && deviceType !== hd_shared_1.EDeviceType.Pro2) {
|
|
897
831
|
return undefined;
|
|
898
832
|
}
|
|
899
|
-
// ── Step 5: Try keychain session reuse
|
|
833
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
900
834
|
// Only attempt if device was already unlocked — locking invalidates
|
|
901
835
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
902
836
|
if (!wasLocked && deviceId) {
|
|
@@ -906,38 +840,22 @@ globalOpts) {
|
|
|
906
840
|
return cached;
|
|
907
841
|
}
|
|
908
842
|
}
|
|
909
|
-
// ── Step 6: Keychain miss →
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
useEmptyPassphrase: false,
|
|
843
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
844
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
845
|
+
mode: 'select-hidden',
|
|
913
846
|
});
|
|
914
|
-
if (
|
|
915
|
-
|
|
916
|
-
if (!passphraseState) {
|
|
847
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
848
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
917
849
|
return undefined;
|
|
918
850
|
}
|
|
851
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
852
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
919
853
|
globalOpts.passphraseState = passphraseState;
|
|
920
|
-
// Save session to keychain for next invocation.
|
|
921
|
-
//
|
|
922
|
-
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
923
|
-
// Initialize would be re-run without passphrase_state, resetting the
|
|
924
|
-
// device to the standard wallet and returning a mismatched session_id.
|
|
925
|
-
// See the matching comment in `session connect`.
|
|
926
|
-
if (deviceId) {
|
|
927
|
-
const featAfter = await sdk.getFeatures(connectId, {
|
|
928
|
-
passphraseState,
|
|
929
|
-
skipPassphraseCheck: true,
|
|
930
|
-
});
|
|
931
|
-
const sessionId = passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
932
|
-
if (sessionId) {
|
|
933
|
-
await (0, session_1.saveSessionToKeychain)(deviceId, passphraseState, sessionId);
|
|
934
|
-
await (0, session_1.preloadSessionFromKeychain)(deviceId);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
854
|
return passphraseState;
|
|
938
855
|
}
|
|
939
856
|
return undefined;
|
|
940
857
|
}
|
|
858
|
+
exports.prepareSession = prepareSession;
|
|
941
859
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
942
860
|
function outputResult(_globalOpts, result) {
|
|
943
861
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -947,8 +865,8 @@ function outputResult(_globalOpts, result) {
|
|
|
947
865
|
!result.success) {
|
|
948
866
|
process.exitCode = 1;
|
|
949
867
|
}
|
|
950
|
-
// No process.exit here — runCommand()
|
|
951
|
-
//
|
|
868
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
869
|
+
// exit naturally so leaked USB handles remain observable.
|
|
952
870
|
}
|
|
953
871
|
async function runCommand(options, handler) {
|
|
954
872
|
const globalOpts = program.opts();
|
|
@@ -981,9 +899,7 @@ async function runCommand(options, handler) {
|
|
|
981
899
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
982
900
|
await (0, sdk_1.disposeSDK)();
|
|
983
901
|
}
|
|
984
|
-
//
|
|
985
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
986
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
902
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
987
903
|
}
|
|
988
904
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
989
905
|
function respondAndExit(result) {
|
|
@@ -1016,41 +932,28 @@ async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName
|
|
|
1016
932
|
throw new Error('Unable to scan BLE devices');
|
|
1017
933
|
}
|
|
1018
934
|
const devices = searchResult.payload;
|
|
1019
|
-
const
|
|
1020
|
-
const matches =
|
|
1021
|
-
? devices.filter(device => device.name
|
|
935
|
+
const requestedName = deviceName?.trim();
|
|
936
|
+
const matches = requestedName
|
|
937
|
+
? devices.filter(device => (0, hd_shared_1.isSameOnekeyBleName)(device.name, requestedName) ||
|
|
938
|
+
device.name?.trim().toLowerCase() === requestedName.toLowerCase())
|
|
1022
939
|
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1023
940
|
if (matches.length === 0) {
|
|
1024
|
-
throw new Error(
|
|
941
|
+
throw new Error(requestedName
|
|
942
|
+
? `BLE device not found by name: ${deviceName}`
|
|
943
|
+
: 'No Classic/Pure BLE device found');
|
|
1025
944
|
}
|
|
1026
945
|
if (matches.length > 1) {
|
|
1027
|
-
throw new Error(
|
|
946
|
+
throw new Error(requestedName
|
|
1028
947
|
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1029
948
|
: 'Multiple Classic/Pure BLE devices found; specify --device-name');
|
|
1030
949
|
}
|
|
1031
|
-
const connectId = matches
|
|
950
|
+
const [{ connectId, name }] = matches;
|
|
1032
951
|
if (!connectId)
|
|
1033
|
-
throw new Error(`BLE device has no connect ID: ${
|
|
952
|
+
throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1034
953
|
return connectId;
|
|
1035
954
|
}
|
|
1036
|
-
function parseResourceBundleParam(spec) {
|
|
1037
|
-
const sep = spec.indexOf(':');
|
|
1038
|
-
if (sep <= 0 || sep === spec.length - 1) {
|
|
1039
|
-
throw new Error(`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`);
|
|
1040
|
-
}
|
|
1041
|
-
const localPath = spec.slice(0, sep);
|
|
1042
|
-
const devicePath = spec.slice(sep + 1);
|
|
1043
|
-
if (!devicePath.startsWith('vol')) {
|
|
1044
|
-
throw new Error(`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`);
|
|
1045
|
-
}
|
|
1046
|
-
return {
|
|
1047
|
-
binary: readBinaryParam(localPath),
|
|
1048
|
-
devicePath,
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
955
|
function getFirmwareUpdateV4TotalBytes(params) {
|
|
1052
956
|
return [
|
|
1053
|
-
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1054
957
|
params.bootloaderBinary,
|
|
1055
958
|
params.applicationP1Binary,
|
|
1056
959
|
params.applicationP2Binary,
|
|
@@ -1100,9 +1003,7 @@ function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt,
|
|
|
1100
1003
|
totalBytes,
|
|
1101
1004
|
transferredBytes,
|
|
1102
1005
|
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1103
|
-
transferKiBPerSecond: elapsedMs > 0
|
|
1104
|
-
? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2))
|
|
1105
|
-
: null,
|
|
1006
|
+
transferKiBPerSecond: elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1106
1007
|
lastProgress,
|
|
1107
1008
|
};
|
|
1108
1009
|
}
|
|
@@ -1153,135 +1054,140 @@ async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries,
|
|
|
1153
1054
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1154
1055
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1155
1056
|
let currentSdk = sdk;
|
|
1156
|
-
let lastResult;
|
|
1157
1057
|
let retried = false;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
? undefined
|
|
1172
|
-
: globalOpts.connectId;
|
|
1173
|
-
const onUiEvent = (message) => {
|
|
1174
|
-
if (!message || typeof message !== 'object')
|
|
1175
|
-
return;
|
|
1176
|
-
const messageType = message.type;
|
|
1177
|
-
const payload = getFirmwareUpdatePayload(message);
|
|
1178
|
-
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1179
|
-
const tipMessage = payload?.data?.message;
|
|
1180
|
-
if (typeof tipMessage === 'string') {
|
|
1181
|
-
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1182
|
-
}
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
|
|
1186
|
-
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1187
|
-
process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
|
|
1191
|
-
return;
|
|
1192
|
-
const progress = Number(payload.progress);
|
|
1193
|
-
if (!Number.isFinite(progress))
|
|
1194
|
-
return;
|
|
1195
|
-
if (payload.progressType === 'transferData') {
|
|
1196
|
-
progressEvents += 1;
|
|
1197
|
-
lastProgress = Math.max(lastProgress, progress);
|
|
1198
|
-
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1199
|
-
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1200
|
-
progressType: 'transfer',
|
|
1201
|
-
progress,
|
|
1202
|
-
payload,
|
|
1203
|
-
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1204
|
-
});
|
|
1205
|
-
if (progress >= 100) {
|
|
1206
|
-
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
1207
|
-
}
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
if (payload.progressType === 'installingFirmware') {
|
|
1211
|
-
installProgressEvents += 1;
|
|
1212
|
-
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1213
|
-
installStartedAt ?? (installStartedAt = Date.now());
|
|
1214
|
-
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1215
|
-
progressType: 'install',
|
|
1216
|
-
progress,
|
|
1217
|
-
payload,
|
|
1218
|
-
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1219
|
-
});
|
|
1220
|
-
if (progress >= 100) {
|
|
1221
|
-
installEndedAt ?? (installEndedAt = Date.now());
|
|
1222
|
-
}
|
|
1058
|
+
let attempt = 1;
|
|
1059
|
+
let { connectId } = globalOpts;
|
|
1060
|
+
if (globalOpts.transport === 'usb') {
|
|
1061
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1062
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1063
|
+
scope: 'runtime',
|
|
1064
|
+
connectProtocol: 'V2',
|
|
1065
|
+
retryCount: 0,
|
|
1066
|
+
});
|
|
1067
|
+
if (isSuccessResult(probeResult))
|
|
1068
|
+
break;
|
|
1069
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1070
|
+
return probeResult;
|
|
1223
1071
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
currentSdk
|
|
1072
|
+
retried = true;
|
|
1073
|
+
process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`);
|
|
1074
|
+
await (0, sdk_1.disposeSDK)();
|
|
1075
|
+
await new Promise(resolve => {
|
|
1076
|
+
setTimeout(resolve, 3000);
|
|
1077
|
+
});
|
|
1078
|
+
currentSdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
1079
|
+
if (globalOpts.connectId)
|
|
1080
|
+
connectId = undefined;
|
|
1231
1081
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1082
|
+
}
|
|
1083
|
+
let progressEvents = 0;
|
|
1084
|
+
let lastProgress = -1;
|
|
1085
|
+
let transferStartedAt;
|
|
1086
|
+
let transferEndedAt;
|
|
1087
|
+
let installProgressEvents = 0;
|
|
1088
|
+
let lastInstallProgress = -1;
|
|
1089
|
+
let installStartedAt;
|
|
1090
|
+
let installEndedAt;
|
|
1091
|
+
let lastPrintedTransferProgress = -10;
|
|
1092
|
+
let lastPrintedInstallProgress = -10;
|
|
1093
|
+
const totalStartedAt = Date.now();
|
|
1094
|
+
const onUiEvent = (message) => {
|
|
1095
|
+
if (!message || typeof message !== 'object')
|
|
1096
|
+
return;
|
|
1097
|
+
const messageType = message.type;
|
|
1098
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1099
|
+
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1100
|
+
const tipMessage = payload?.data?.message;
|
|
1101
|
+
if (typeof tipMessage === 'string') {
|
|
1102
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1103
|
+
}
|
|
1104
|
+
return;
|
|
1234
1105
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
totalStartedAt,
|
|
1240
|
-
transferStartedAt,
|
|
1241
|
-
transferEndedAt,
|
|
1242
|
-
installStartedAt,
|
|
1243
|
-
installEndedAt,
|
|
1244
|
-
progressEvents,
|
|
1245
|
-
lastProgress,
|
|
1246
|
-
installProgressEvents,
|
|
1247
|
-
lastInstallProgress,
|
|
1248
|
-
retried,
|
|
1249
|
-
});
|
|
1250
|
-
if (lastResult && typeof lastResult === 'object') {
|
|
1251
|
-
const payload = (lastResult.payload ?? {});
|
|
1252
|
-
lastResult = {
|
|
1253
|
-
...lastResult,
|
|
1254
|
-
payload: {
|
|
1255
|
-
...payload,
|
|
1256
|
-
metrics,
|
|
1257
|
-
},
|
|
1258
|
-
};
|
|
1106
|
+
if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
|
|
1107
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1108
|
+
process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
|
|
1109
|
+
return;
|
|
1259
1110
|
}
|
|
1260
|
-
if (
|
|
1261
|
-
return
|
|
1111
|
+
if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
|
|
1112
|
+
return;
|
|
1113
|
+
const progress = Number(payload.progress);
|
|
1114
|
+
if (!Number.isFinite(progress))
|
|
1115
|
+
return;
|
|
1116
|
+
if (payload.progressType === 'transferData') {
|
|
1117
|
+
progressEvents += 1;
|
|
1118
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1119
|
+
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1120
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1121
|
+
progressType: 'transfer',
|
|
1122
|
+
progress,
|
|
1123
|
+
payload,
|
|
1124
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1125
|
+
});
|
|
1126
|
+
if (progress >= 100) {
|
|
1127
|
+
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
1128
|
+
}
|
|
1129
|
+
return;
|
|
1262
1130
|
}
|
|
1263
|
-
if (
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1131
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1132
|
+
installProgressEvents += 1;
|
|
1133
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1134
|
+
installStartedAt ?? (installStartedAt = Date.now());
|
|
1135
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1136
|
+
progressType: 'install',
|
|
1137
|
+
progress,
|
|
1138
|
+
payload,
|
|
1139
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1140
|
+
});
|
|
1141
|
+
if (progress >= 100) {
|
|
1142
|
+
installEndedAt ?? (installEndedAt = Date.now());
|
|
1143
|
+
}
|
|
1267
1144
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1145
|
+
};
|
|
1146
|
+
currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
|
|
1147
|
+
let result;
|
|
1148
|
+
try {
|
|
1149
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1150
|
+
}
|
|
1151
|
+
finally {
|
|
1152
|
+
currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
|
|
1153
|
+
}
|
|
1154
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1155
|
+
installEndedAt = Date.now();
|
|
1156
|
+
}
|
|
1157
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1158
|
+
attempt,
|
|
1159
|
+
maxAttempts,
|
|
1160
|
+
totalBytes,
|
|
1161
|
+
totalStartedAt,
|
|
1162
|
+
transferStartedAt,
|
|
1163
|
+
transferEndedAt,
|
|
1164
|
+
installStartedAt,
|
|
1165
|
+
installEndedAt,
|
|
1166
|
+
progressEvents,
|
|
1167
|
+
lastProgress,
|
|
1168
|
+
installProgressEvents,
|
|
1169
|
+
lastInstallProgress,
|
|
1170
|
+
retried,
|
|
1171
|
+
});
|
|
1172
|
+
if (result && typeof result === 'object') {
|
|
1173
|
+
const payload = (result.payload ?? {});
|
|
1174
|
+
return {
|
|
1175
|
+
...result,
|
|
1176
|
+
payload: {
|
|
1177
|
+
...payload,
|
|
1178
|
+
metrics,
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1275
1181
|
}
|
|
1276
|
-
return
|
|
1182
|
+
return result;
|
|
1277
1183
|
}
|
|
1184
|
+
exports.runFirmwareUpdateV4WithRetry = runFirmwareUpdateV4WithRetry;
|
|
1278
1185
|
function buildFirmwareUpdateV4Params(opts) {
|
|
1279
1186
|
const params = {
|
|
1280
1187
|
platform: 'desktop',
|
|
1281
1188
|
connectProtocol: 'V2',
|
|
1282
1189
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1283
1190
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1284
|
-
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1285
1191
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1286
1192
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1287
1193
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1291,9 +1197,9 @@ function buildFirmwareUpdateV4Params(opts) {
|
|
|
1291
1197
|
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1292
1198
|
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1293
1199
|
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1200
|
+
resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
|
|
1294
1201
|
};
|
|
1295
1202
|
const hasPayload = [
|
|
1296
|
-
params.resourceBundleFiles,
|
|
1297
1203
|
params.romloaderBinary,
|
|
1298
1204
|
params.bootloaderBinary,
|
|
1299
1205
|
params.applicationP1Binary,
|
|
@@ -1303,9 +1209,10 @@ function buildFirmwareUpdateV4Params(opts) {
|
|
|
1303
1209
|
params.se02Binary,
|
|
1304
1210
|
params.se03Binary,
|
|
1305
1211
|
params.se04Binary,
|
|
1212
|
+
params.resourceArchiveBinary,
|
|
1306
1213
|
].some(Boolean);
|
|
1307
1214
|
if (!hasPayload) {
|
|
1308
|
-
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1215
|
+
const err = new Error('firmware-update-v4 requires at least one firmware binary or resource archive path');
|
|
1309
1216
|
err.code = 'MISSING_FIRMWARE_BINARY';
|
|
1310
1217
|
throw err;
|
|
1311
1218
|
}
|