@onekeyfe/hardware-cli 1.2.0-alpha.7 → 1.2.0-alpha.71
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 +86 -1
- package/dist/cli.js +364 -273
- 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 +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 +113 -70
- package/package.json +7 -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 +100 -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 +457 -321
- package/src/deviceSelection.ts +13 -0
- 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 +148 -76
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
|
|
4
5
|
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
@@ -10,65 +11,31 @@ import {
|
|
|
10
11
|
resolveSignMessage,
|
|
11
12
|
resolveSignTransaction,
|
|
12
13
|
} from './chains';
|
|
14
|
+
import { selectSearchDevice } from './deviceSelection';
|
|
15
|
+
import { getCanonicalDeviceState, getCompatibleFeatures } from './deviceStateCommands';
|
|
13
16
|
import { createSDK, disposeSDK } from './sdk';
|
|
14
|
-
import {
|
|
15
|
-
clearSessionFromKeychain,
|
|
16
|
-
preloadSessionFromKeychain,
|
|
17
|
-
saveSessionToKeychain,
|
|
18
|
-
} from './session';
|
|
17
|
+
import { clearSessionFromKeychain, preloadSessionFromKeychain } from './session';
|
|
19
18
|
|
|
20
19
|
import type {
|
|
20
|
+
DeviceStateScope,
|
|
21
21
|
EthereumSignTypedDataMessage,
|
|
22
22
|
EthereumSignTypedDataTypes,
|
|
23
23
|
Features,
|
|
24
|
-
IDeviceType,
|
|
25
24
|
SearchDevice,
|
|
26
25
|
} from '@onekeyfe/hd-core';
|
|
27
26
|
|
|
28
27
|
/** SearchDevice enriched with features fetched after discovery */
|
|
29
28
|
type EnrichedSearchDevice = SearchDevice & { features?: Features };
|
|
30
29
|
|
|
31
|
-
function extractPassphraseSession(payload: unknown): {
|
|
32
|
-
passphraseState?: string;
|
|
33
|
-
sessionId?: string;
|
|
34
|
-
} {
|
|
35
|
-
if (typeof payload === 'string') {
|
|
36
|
-
return { passphraseState: payload };
|
|
37
|
-
}
|
|
38
|
-
if (!payload || typeof payload !== 'object') {
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const statePayload = payload as {
|
|
43
|
-
passphrase_state?: unknown;
|
|
44
|
-
passphraseState?: unknown;
|
|
45
|
-
session_id?: unknown;
|
|
46
|
-
sessionId?: unknown;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
let passphraseState: string | undefined;
|
|
50
|
-
if (typeof statePayload.passphrase_state === 'string') {
|
|
51
|
-
passphraseState = statePayload.passphrase_state;
|
|
52
|
-
} else if (typeof statePayload.passphraseState === 'string') {
|
|
53
|
-
passphraseState = statePayload.passphraseState;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let sessionId: string | undefined;
|
|
57
|
-
if (typeof statePayload.session_id === 'string') {
|
|
58
|
-
sessionId = statePayload.session_id;
|
|
59
|
-
} else if (typeof statePayload.sessionId === 'string') {
|
|
60
|
-
sessionId = statePayload.sessionId;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { passphraseState, sessionId };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
30
|
const program = new Command();
|
|
31
|
+
const { version: cliVersion } = JSON.parse(
|
|
32
|
+
readFileSync(resolve(__dirname, '../package.json'), 'utf8')
|
|
33
|
+
) as { version: string };
|
|
67
34
|
|
|
68
35
|
program
|
|
69
36
|
.name('onekey-hw')
|
|
70
37
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
71
|
-
.version(
|
|
38
|
+
.version(cliVersion);
|
|
72
39
|
|
|
73
40
|
// ============================================================
|
|
74
41
|
// Global Options
|
|
@@ -94,28 +61,6 @@ program
|
|
|
94
61
|
.action(() =>
|
|
95
62
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
96
63
|
const result = await sdk.searchDevices();
|
|
97
|
-
|
|
98
|
-
// USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
|
|
99
|
-
if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
|
|
100
|
-
for (const device of result.payload as EnrichedSearchDevice[]) {
|
|
101
|
-
if (device.connectId) {
|
|
102
|
-
try {
|
|
103
|
-
const features = await sdk.getFeatures(device.connectId);
|
|
104
|
-
if (features?.success && features.payload) {
|
|
105
|
-
device.features = features.payload;
|
|
106
|
-
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
107
|
-
const devType = features.payload.deviceType?.toLowerCase();
|
|
108
|
-
if (devType) {
|
|
109
|
-
device.deviceType = devType as IDeviceType;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
// Features fetch failed — device may need PIN, continue with basic info
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
64
|
outputResult(globalOpts, result);
|
|
120
65
|
})
|
|
121
66
|
);
|
|
@@ -125,28 +70,120 @@ program
|
|
|
125
70
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
126
71
|
.action(() =>
|
|
127
72
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
73
|
+
const result = await getCompatibleFeatures(sdk, globalOpts.connectId);
|
|
74
|
+
outputResult(globalOpts, result);
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('get-state')
|
|
80
|
+
.description('Get canonical device state for Protocol V1 and Protocol V2 devices')
|
|
81
|
+
.option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
|
|
82
|
+
.action((opts: { scope: string }) =>
|
|
83
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
84
|
+
const supportedScopes: DeviceStateScope[] = ['runtime', 'settings', 'firmware'];
|
|
85
|
+
if (!supportedScopes.includes(opts.scope as DeviceStateScope)) {
|
|
86
|
+
const error = new Error(`Unsupported device state scope: ${opts.scope}`);
|
|
87
|
+
(error as Error & { code?: string }).code = 'INVALID_DEVICE_STATE_SCOPE';
|
|
88
|
+
throw error;
|
|
144
89
|
}
|
|
145
|
-
const result = await
|
|
90
|
+
const result = await getCanonicalDeviceState(
|
|
91
|
+
sdk,
|
|
92
|
+
globalOpts.connectId,
|
|
93
|
+
opts.scope as DeviceStateScope
|
|
94
|
+
);
|
|
146
95
|
outputResult(globalOpts, result);
|
|
147
96
|
})
|
|
148
97
|
);
|
|
149
98
|
|
|
99
|
+
program
|
|
100
|
+
.command('upload-wallpaper')
|
|
101
|
+
.description('Upload and activate a Pro2 wallpaper')
|
|
102
|
+
.requiredOption('--rgba <path>', '604x1024 raw RGBA file')
|
|
103
|
+
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
104
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
105
|
+
.action(opts =>
|
|
106
|
+
runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
107
|
+
const rgba = readBinaryParam(opts.rgba);
|
|
108
|
+
const expectedBytes = 604 * 1024 * 4;
|
|
109
|
+
if (rgba.byteLength !== expectedBytes) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let transferStartedAt: number | undefined;
|
|
116
|
+
let transferEndedAt: number | undefined;
|
|
117
|
+
let lastProgress = -1;
|
|
118
|
+
let lastPrintedProgress = -10;
|
|
119
|
+
let progressTotalBytes = 0;
|
|
120
|
+
let transferredBytes = 0;
|
|
121
|
+
const totalStartedAt = Date.now();
|
|
122
|
+
const onUiEvent = (message: unknown) => {
|
|
123
|
+
if (!message || typeof message !== 'object') return;
|
|
124
|
+
const event = message as {
|
|
125
|
+
type?: string;
|
|
126
|
+
payload?: {
|
|
127
|
+
progress?: number;
|
|
128
|
+
transferredBytes?: number;
|
|
129
|
+
totalBytes?: number;
|
|
130
|
+
rateBytesPerSecond?: number;
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
if (event.type !== UI_REQUEST.DEVICE_PROGRESS || !event.payload) return;
|
|
134
|
+
const progress = Number(event.payload.progress);
|
|
135
|
+
if (!Number.isFinite(progress)) return;
|
|
136
|
+
transferStartedAt ??= Date.now();
|
|
137
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
138
|
+
const totalBytes = Number(event.payload.totalBytes);
|
|
139
|
+
if (Number.isFinite(totalBytes) && totalBytes > 0) progressTotalBytes = totalBytes;
|
|
140
|
+
const confirmedBytes = Number(event.payload.transferredBytes);
|
|
141
|
+
if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
|
|
142
|
+
transferredBytes = Math.max(transferredBytes, confirmedBytes);
|
|
143
|
+
}
|
|
144
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
145
|
+
if (printableProgress > lastPrintedProgress || progress >= 100) {
|
|
146
|
+
const rate = Number(event.payload.rateBytesPerSecond);
|
|
147
|
+
const rateText =
|
|
148
|
+
Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`
|
|
151
|
+
);
|
|
152
|
+
lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
|
|
153
|
+
}
|
|
154
|
+
if (progress >= 100) transferEndedAt ??= Date.now();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
sdk.on(UI_EVENT, onUiEvent);
|
|
158
|
+
let result: any;
|
|
159
|
+
try {
|
|
160
|
+
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
161
|
+
...params,
|
|
162
|
+
width: 604,
|
|
163
|
+
height: 1024,
|
|
164
|
+
rgba,
|
|
165
|
+
fileName: opts.fileName,
|
|
166
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
167
|
+
});
|
|
168
|
+
} finally {
|
|
169
|
+
sdk.off?.(UI_EVENT, onUiEvent);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const endedAt = transferEndedAt ?? Date.now();
|
|
173
|
+
const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
|
|
174
|
+
outputResult(globalOpts, {
|
|
175
|
+
...result,
|
|
176
|
+
metrics: buildWallpaperUploadMetrics({
|
|
177
|
+
totalBytes,
|
|
178
|
+
transferredBytes: result?.success ? totalBytes : transferredBytes,
|
|
179
|
+
startedAt: transferStartedAt ?? totalStartedAt,
|
|
180
|
+
endedAt,
|
|
181
|
+
lastProgress,
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
|
|
150
187
|
// ============================================================
|
|
151
188
|
// Signing Commands
|
|
152
189
|
// ============================================================
|
|
@@ -541,6 +578,38 @@ program
|
|
|
541
578
|
})
|
|
542
579
|
);
|
|
543
580
|
|
|
581
|
+
program
|
|
582
|
+
.command('firmware-update-legacy')
|
|
583
|
+
.description('Update Classic/Pure firmware through the legacy protocol')
|
|
584
|
+
.requiredOption('--binary <path>', 'Local firmware binary path')
|
|
585
|
+
.option('--device-name <name>', 'BLE advertising name, for example K1514')
|
|
586
|
+
.option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
|
|
587
|
+
.option('--no-reboot', 'Do not reboot the device after a successful update')
|
|
588
|
+
.action(opts =>
|
|
589
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
590
|
+
if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
|
|
591
|
+
throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const connectId = await resolveLegacyFirmwareConnectId(
|
|
595
|
+
sdk,
|
|
596
|
+
globalOpts.connectId,
|
|
597
|
+
opts.deviceName
|
|
598
|
+
);
|
|
599
|
+
const result = await sdk.firmwareUpdate(connectId, {
|
|
600
|
+
binary: readBinaryParam(opts.binary),
|
|
601
|
+
updateType: opts.updateType,
|
|
602
|
+
rebootOnSuccess: opts.reboot,
|
|
603
|
+
timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
|
|
604
|
+
});
|
|
605
|
+
outputResult(globalOpts, result);
|
|
606
|
+
})
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
export function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble') {
|
|
610
|
+
return transport === 'usb' ? 90_000 : undefined;
|
|
611
|
+
}
|
|
612
|
+
|
|
544
613
|
program
|
|
545
614
|
.command('firmware-update-ble')
|
|
546
615
|
.description('Run Protocol V2 firmware update over BLE')
|
|
@@ -549,17 +618,20 @@ program
|
|
|
549
618
|
success: false,
|
|
550
619
|
payload: {
|
|
551
620
|
error:
|
|
552
|
-
'Use `onekey-hw --transport ble firmware-update-v4
|
|
553
|
-
code: '
|
|
621
|
+
'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
622
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
554
623
|
},
|
|
555
624
|
})
|
|
556
625
|
);
|
|
557
626
|
|
|
558
627
|
program
|
|
559
|
-
.command('firmware-update-v4
|
|
560
|
-
.description('
|
|
628
|
+
.command('firmware-update-v4')
|
|
629
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
561
630
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
562
|
-
.option(
|
|
631
|
+
.option(
|
|
632
|
+
'--resource-file <spec...>',
|
|
633
|
+
'Resource direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
|
|
634
|
+
)
|
|
563
635
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
564
636
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
565
637
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -573,8 +645,8 @@ program
|
|
|
573
645
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
574
646
|
.action(opts =>
|
|
575
647
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
576
|
-
const params =
|
|
577
|
-
const result = await
|
|
648
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
649
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
578
650
|
sdk,
|
|
579
651
|
globalOpts,
|
|
580
652
|
params,
|
|
@@ -724,7 +796,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
724
796
|
|
|
725
797
|
sessionCmd
|
|
726
798
|
.command('connect')
|
|
727
|
-
.description('Connect device and
|
|
799
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
728
800
|
.action(() =>
|
|
729
801
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
730
802
|
// 1. Search for device
|
|
@@ -736,7 +808,17 @@ sessionCmd
|
|
|
736
808
|
});
|
|
737
809
|
return;
|
|
738
810
|
}
|
|
739
|
-
const device =
|
|
811
|
+
const device = selectSearchDevice(
|
|
812
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
813
|
+
globalOpts.connectId
|
|
814
|
+
);
|
|
815
|
+
if (!device) {
|
|
816
|
+
outputResult(globalOpts, {
|
|
817
|
+
success: false,
|
|
818
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
740
822
|
const connectId = device.connectId || globalOpts.connectId;
|
|
741
823
|
|
|
742
824
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -747,62 +829,35 @@ sessionCmd
|
|
|
747
829
|
await unlockWithRetry(sdk, connectId);
|
|
748
830
|
}
|
|
749
831
|
|
|
750
|
-
// 3.
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
useEmptyPassphrase: false,
|
|
832
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
833
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
834
|
+
mode: 'select-hidden',
|
|
754
835
|
});
|
|
755
|
-
if (!
|
|
756
|
-
outputResult(globalOpts,
|
|
836
|
+
if (!sessionResult.success) {
|
|
837
|
+
outputResult(globalOpts, sessionResult);
|
|
757
838
|
return;
|
|
758
839
|
}
|
|
759
|
-
|
|
760
|
-
psResult.payload
|
|
761
|
-
);
|
|
762
|
-
if (!passphraseState) {
|
|
840
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
763
841
|
outputResult(globalOpts, {
|
|
764
842
|
success: false,
|
|
765
|
-
payload: { error: '
|
|
843
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
766
844
|
});
|
|
767
845
|
return;
|
|
768
846
|
}
|
|
847
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
769
848
|
|
|
770
849
|
// 4. Get address to verify + extract deviceId
|
|
771
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
850
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
772
851
|
path: "m/44'/60'/0'/0/0",
|
|
773
852
|
showOnOneKey: false,
|
|
774
853
|
passphraseState,
|
|
775
854
|
});
|
|
776
855
|
|
|
777
|
-
// 5. Fetch the now-active session_id via getFeatures.
|
|
778
|
-
//
|
|
779
|
-
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
780
|
-
// connectStateChange guard (core/index.ts) would see the payload's
|
|
781
|
-
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
782
|
-
// and call Initialize again with no passphrase_state / no session_id.
|
|
783
|
-
// That Initialize resets the device to the standard wallet and returns
|
|
784
|
-
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
785
|
-
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
786
|
-
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
787
|
-
const featResult = await sdk.getFeatures(connectId, {
|
|
788
|
-
passphraseState,
|
|
789
|
-
skipPassphraseCheck: true,
|
|
790
|
-
});
|
|
791
|
-
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
792
|
-
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
793
|
-
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
794
|
-
|
|
795
|
-
// 6. Save to keychain
|
|
796
|
-
if (passphraseState && deviceId && sessionId) {
|
|
797
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
856
|
outputResult(globalOpts, {
|
|
801
857
|
success: true,
|
|
802
858
|
payload: {
|
|
803
859
|
passphraseState,
|
|
804
860
|
deviceId,
|
|
805
|
-
...(sessionId ? { sessionId } : {}),
|
|
806
861
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
807
862
|
},
|
|
808
863
|
});
|
|
@@ -815,8 +870,10 @@ sessionCmd
|
|
|
815
870
|
.action(() =>
|
|
816
871
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
817
872
|
const searchResult = await sdk.searchDevices();
|
|
818
|
-
const device =
|
|
819
|
-
(searchResult?.payload as
|
|
873
|
+
const device = selectSearchDevice(
|
|
874
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
875
|
+
globalOpts.connectId
|
|
876
|
+
);
|
|
820
877
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
821
878
|
if (deviceId) {
|
|
822
879
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -911,12 +968,12 @@ async function unlockWithRetry(
|
|
|
911
968
|
* Prepare passphrase session before SDK calls.
|
|
912
969
|
*
|
|
913
970
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
914
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
915
|
-
* 3. Keychain miss →
|
|
971
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
972
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
916
973
|
*
|
|
917
974
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
918
975
|
*/
|
|
919
|
-
async function prepareSession(
|
|
976
|
+
export async function prepareSession(
|
|
920
977
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
921
978
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
922
979
|
globalOpts: Record<string, any>
|
|
@@ -927,7 +984,7 @@ async function prepareSession(
|
|
|
927
984
|
}
|
|
928
985
|
|
|
929
986
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
930
|
-
//
|
|
987
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
931
988
|
// catch block, which renders them as structured `{ success: false,
|
|
932
989
|
// payload: { error, code } }` output instead of silently falling through
|
|
933
990
|
// to a confusing downstream error 112 / 114.
|
|
@@ -942,9 +999,30 @@ async function prepareSession(
|
|
|
942
999
|
return undefined;
|
|
943
1000
|
}
|
|
944
1001
|
|
|
945
|
-
const device =
|
|
1002
|
+
const device = selectSearchDevice(
|
|
1003
|
+
searchResult.payload as Array<{
|
|
1004
|
+
connectId?: string;
|
|
1005
|
+
deviceId?: string;
|
|
1006
|
+
deviceType?: string;
|
|
1007
|
+
features?: {
|
|
1008
|
+
deviceId?: string | null;
|
|
1009
|
+
deviceType?: string;
|
|
1010
|
+
sessionId?: string | null;
|
|
1011
|
+
passphraseProtection?: boolean | null;
|
|
1012
|
+
unlocked?: boolean | null;
|
|
1013
|
+
};
|
|
1014
|
+
}>,
|
|
1015
|
+
globalOpts.connectId
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
if (!device) {
|
|
1019
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const selectedDevice = device as {
|
|
946
1023
|
connectId?: string;
|
|
947
1024
|
deviceId?: string;
|
|
1025
|
+
deviceType?: string;
|
|
948
1026
|
features?: {
|
|
949
1027
|
deviceId?: string | null;
|
|
950
1028
|
deviceType?: string;
|
|
@@ -953,7 +1031,7 @@ async function prepareSession(
|
|
|
953
1031
|
unlocked?: boolean | null;
|
|
954
1032
|
};
|
|
955
1033
|
};
|
|
956
|
-
const connectId =
|
|
1034
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
957
1035
|
if (!globalOpts.connectId && connectId) {
|
|
958
1036
|
globalOpts.connectId = connectId;
|
|
959
1037
|
}
|
|
@@ -961,10 +1039,11 @@ async function prepareSession(
|
|
|
961
1039
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
962
1040
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
963
1041
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
964
|
-
let deviceId =
|
|
965
|
-
let deviceType =
|
|
966
|
-
|
|
967
|
-
let
|
|
1042
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1043
|
+
let deviceType =
|
|
1044
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1045
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1046
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
968
1047
|
|
|
969
1048
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
970
1049
|
try {
|
|
@@ -1003,7 +1082,7 @@ async function prepareSession(
|
|
|
1003
1082
|
return undefined;
|
|
1004
1083
|
}
|
|
1005
1084
|
|
|
1006
|
-
// ── Step 5: Try keychain session reuse
|
|
1085
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
1007
1086
|
// Only attempt if device was already unlocked — locking invalidates
|
|
1008
1087
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
1009
1088
|
if (!wasLocked && deviceId) {
|
|
@@ -1014,40 +1093,19 @@ async function prepareSession(
|
|
|
1014
1093
|
}
|
|
1015
1094
|
}
|
|
1016
1095
|
|
|
1017
|
-
// ── Step 6: Keychain miss →
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
useEmptyPassphrase: false,
|
|
1096
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1097
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1098
|
+
mode: 'select-hidden',
|
|
1021
1099
|
});
|
|
1022
1100
|
|
|
1023
|
-
if (
|
|
1024
|
-
|
|
1025
|
-
psResult.payload
|
|
1026
|
-
);
|
|
1027
|
-
if (!passphraseState) {
|
|
1101
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1102
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
1028
1103
|
return undefined;
|
|
1029
1104
|
}
|
|
1105
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1106
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
1030
1107
|
globalOpts.passphraseState = passphraseState;
|
|
1031
1108
|
|
|
1032
|
-
// Save session to keychain for next invocation.
|
|
1033
|
-
//
|
|
1034
|
-
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
1035
|
-
// Initialize would be re-run without passphrase_state, resetting the
|
|
1036
|
-
// device to the standard wallet and returning a mismatched session_id.
|
|
1037
|
-
// See the matching comment in `session connect`.
|
|
1038
|
-
if (deviceId) {
|
|
1039
|
-
const featAfter = await sdk.getFeatures(connectId, {
|
|
1040
|
-
passphraseState,
|
|
1041
|
-
skipPassphraseCheck: true,
|
|
1042
|
-
});
|
|
1043
|
-
const sessionId =
|
|
1044
|
-
passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
1045
|
-
if (sessionId) {
|
|
1046
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
1047
|
-
await preloadSessionFromKeychain(deviceId);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
1109
|
return passphraseState;
|
|
1052
1110
|
}
|
|
1053
1111
|
return undefined;
|
|
@@ -1064,8 +1122,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1064
1122
|
) {
|
|
1065
1123
|
process.exitCode = 1;
|
|
1066
1124
|
}
|
|
1067
|
-
// No process.exit here — runCommand()
|
|
1068
|
-
//
|
|
1125
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1126
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1069
1127
|
}
|
|
1070
1128
|
|
|
1071
1129
|
/**
|
|
@@ -1077,7 +1135,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1077
1135
|
* 3. run the handler (which calls outputResult on success)
|
|
1078
1136
|
* 4. report uncaught errors as a structured failure result
|
|
1079
1137
|
* 5. dispose SDK
|
|
1080
|
-
* 6.
|
|
1138
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1081
1139
|
*
|
|
1082
1140
|
* This fixes three previous bugs:
|
|
1083
1141
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1129,9 +1187,7 @@ async function runCommand(
|
|
|
1129
1187
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1130
1188
|
await disposeSDK();
|
|
1131
1189
|
}
|
|
1132
|
-
//
|
|
1133
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1134
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1190
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1135
1191
|
}
|
|
1136
1192
|
|
|
1137
1193
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1159,11 +1215,65 @@ function readBinaryParam(path: string): ArrayBuffer {
|
|
|
1159
1215
|
return new Uint8Array(buffer).buffer;
|
|
1160
1216
|
}
|
|
1161
1217
|
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1164
|
-
|
|
1218
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1219
|
+
sdk: AnySdk,
|
|
1220
|
+
explicitConnectId?: string,
|
|
1221
|
+
deviceName?: string
|
|
1222
|
+
): Promise<string> {
|
|
1223
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1224
|
+
|
|
1225
|
+
const searchResult = await sdk.searchDevices();
|
|
1226
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1227
|
+
throw new Error('Unable to scan BLE devices');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1231
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1232
|
+
const matches = normalizedName
|
|
1233
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1234
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1235
|
+
|
|
1236
|
+
if (matches.length === 0) {
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
normalizedName
|
|
1239
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1240
|
+
: 'No Classic/Pure BLE device found'
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
if (matches.length > 1) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
normalizedName
|
|
1246
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1247
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const [{ connectId, name }] = matches;
|
|
1252
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1253
|
+
return connectId;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function parseResourceFileParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
|
|
1257
|
+
const sep = spec.indexOf(':');
|
|
1258
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
1259
|
+
throw new Error(`Invalid --resource-file value: "${spec}". Expected <localPath>:<devicePath>`);
|
|
1260
|
+
}
|
|
1261
|
+
const localPath = spec.slice(0, sep);
|
|
1262
|
+
const devicePath = spec.slice(sep + 1);
|
|
1263
|
+
if (!devicePath.startsWith('vol')) {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`Invalid --resource-file device path: "${devicePath}". Expected a vol*:/... path`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
binary: readBinaryParam(localPath),
|
|
1270
|
+
devicePath,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1165
1275
|
return [
|
|
1166
|
-
...(params.
|
|
1276
|
+
...(params.resourceFiles?.map(item => item.binary) ?? []),
|
|
1167
1277
|
params.bootloaderBinary,
|
|
1168
1278
|
params.applicationP1Binary,
|
|
1169
1279
|
params.applicationP2Binary,
|
|
@@ -1175,16 +1285,16 @@ function getFirmwareUpdateV4DebugTotalBytes(
|
|
|
1175
1285
|
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1176
1286
|
}
|
|
1177
1287
|
|
|
1178
|
-
function
|
|
1288
|
+
function getFirmwareUpdateV4ErrorText(result: unknown) {
|
|
1179
1289
|
if (!result || typeof result !== 'object') return '';
|
|
1180
|
-
const payload =
|
|
1290
|
+
const { payload } = result as { payload?: unknown };
|
|
1181
1291
|
if (!payload || typeof payload !== 'object') return '';
|
|
1182
|
-
const error =
|
|
1292
|
+
const { error } = payload as { error?: unknown };
|
|
1183
1293
|
return typeof error === 'string' ? error : '';
|
|
1184
1294
|
}
|
|
1185
1295
|
|
|
1186
1296
|
function isProtocolV2UsbProbeTransientResult(result: unknown) {
|
|
1187
|
-
const error =
|
|
1297
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1188
1298
|
return (
|
|
1189
1299
|
error.includes('Device protocol mismatch') &&
|
|
1190
1300
|
error.includes('expected V2') &&
|
|
@@ -1198,22 +1308,46 @@ function isSuccessResult(result: unknown) {
|
|
|
1198
1308
|
);
|
|
1199
1309
|
}
|
|
1200
1310
|
|
|
1201
|
-
function
|
|
1311
|
+
function getFirmwareUpdatePayload(message: unknown) {
|
|
1202
1312
|
if (!message || typeof message !== 'object') return undefined;
|
|
1203
1313
|
return (message as { payload?: Record<string, unknown> }).payload;
|
|
1204
1314
|
}
|
|
1205
1315
|
|
|
1206
|
-
function
|
|
1316
|
+
function formatFirmwareProgress(progress: number) {
|
|
1207
1317
|
if (!Number.isFinite(progress)) return '0%';
|
|
1208
1318
|
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1209
1319
|
}
|
|
1210
1320
|
|
|
1211
|
-
function
|
|
1321
|
+
function formatFirmwareBytes(bytes: number) {
|
|
1212
1322
|
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
1213
1323
|
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1214
1324
|
}
|
|
1215
1325
|
|
|
1216
|
-
function
|
|
1326
|
+
export function buildWallpaperUploadMetrics({
|
|
1327
|
+
totalBytes,
|
|
1328
|
+
transferredBytes,
|
|
1329
|
+
startedAt,
|
|
1330
|
+
endedAt,
|
|
1331
|
+
lastProgress,
|
|
1332
|
+
}: {
|
|
1333
|
+
totalBytes: number;
|
|
1334
|
+
transferredBytes: number;
|
|
1335
|
+
startedAt: number;
|
|
1336
|
+
endedAt: number;
|
|
1337
|
+
lastProgress: number;
|
|
1338
|
+
}) {
|
|
1339
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1340
|
+
return {
|
|
1341
|
+
totalBytes,
|
|
1342
|
+
transferredBytes,
|
|
1343
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1344
|
+
transferKiBPerSecond:
|
|
1345
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1346
|
+
lastProgress,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function maybePrintFirmwareProgress({
|
|
1217
1351
|
progressType,
|
|
1218
1352
|
progress,
|
|
1219
1353
|
payload,
|
|
@@ -1234,7 +1368,7 @@ function maybePrintFirmwareDebugProgress({
|
|
|
1234
1368
|
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1235
1369
|
const sizeText =
|
|
1236
1370
|
Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1237
|
-
? ` ${
|
|
1371
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1238
1372
|
: '';
|
|
1239
1373
|
const speedText =
|
|
1240
1374
|
Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
@@ -1242,14 +1376,14 @@ function maybePrintFirmwareDebugProgress({
|
|
|
1242
1376
|
: '';
|
|
1243
1377
|
|
|
1244
1378
|
process.stderr.write(
|
|
1245
|
-
`[onekey-hw] Firmware ${progressType}: ${
|
|
1379
|
+
`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(
|
|
1246
1380
|
progress
|
|
1247
1381
|
)}${sizeText}${speedText}\n`
|
|
1248
1382
|
);
|
|
1249
1383
|
return progress >= 100 ? 100 : printableProgress;
|
|
1250
1384
|
}
|
|
1251
1385
|
|
|
1252
|
-
function
|
|
1386
|
+
function buildFirmwareUpdateV4Metrics({
|
|
1253
1387
|
attempt,
|
|
1254
1388
|
maxAttempts,
|
|
1255
1389
|
totalBytes,
|
|
@@ -1309,7 +1443,7 @@ function buildFirmwareUpdateV4DebugMetrics({
|
|
|
1309
1443
|
};
|
|
1310
1444
|
}
|
|
1311
1445
|
|
|
1312
|
-
async function
|
|
1446
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1313
1447
|
sdk,
|
|
1314
1448
|
globalOpts,
|
|
1315
1449
|
params,
|
|
@@ -1317,156 +1451,154 @@ async function runFirmwareUpdateV4DebugWithRetry({
|
|
|
1317
1451
|
}: {
|
|
1318
1452
|
sdk: AnySdk;
|
|
1319
1453
|
globalOpts: Record<string, any>;
|
|
1320
|
-
params: ReturnType<typeof
|
|
1454
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
1321
1455
|
retries?: number;
|
|
1322
1456
|
}) {
|
|
1323
|
-
const totalBytes =
|
|
1457
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1324
1458
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1325
1459
|
let currentSdk = sdk;
|
|
1326
|
-
let lastResult: unknown;
|
|
1327
1460
|
let retried = false;
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const connectId =
|
|
1342
|
-
retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1343
|
-
? undefined
|
|
1344
|
-
: globalOpts.connectId;
|
|
1345
|
-
|
|
1346
|
-
const onUiEvent = (message: unknown) => {
|
|
1347
|
-
if (!message || typeof message !== 'object') return;
|
|
1348
|
-
const messageType = (message as { type?: string }).type;
|
|
1349
|
-
const payload = getFirmwareDebugPayload(message);
|
|
1350
|
-
|
|
1351
|
-
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1352
|
-
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1353
|
-
if (typeof tipMessage === 'string') {
|
|
1354
|
-
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1355
|
-
}
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1360
|
-
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1361
|
-
process.stderr.write(
|
|
1362
|
-
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1363
|
-
);
|
|
1364
|
-
return;
|
|
1461
|
+
let attempt = 1;
|
|
1462
|
+
let { connectId } = globalOpts;
|
|
1463
|
+
|
|
1464
|
+
if (globalOpts.transport === 'usb') {
|
|
1465
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1466
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1467
|
+
scope: 'runtime',
|
|
1468
|
+
connectProtocol: 'V2',
|
|
1469
|
+
retryCount: 0,
|
|
1470
|
+
});
|
|
1471
|
+
if (isSuccessResult(probeResult)) break;
|
|
1472
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1473
|
+
return probeResult;
|
|
1365
1474
|
}
|
|
1366
1475
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1380
|
-
});
|
|
1381
|
-
if (progress >= 100) {
|
|
1382
|
-
transferEndedAt ??= Date.now();
|
|
1383
|
-
}
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1476
|
+
retried = true;
|
|
1477
|
+
process.stderr.write(
|
|
1478
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1479
|
+
);
|
|
1480
|
+
await disposeSDK();
|
|
1481
|
+
await new Promise(resolve => {
|
|
1482
|
+
setTimeout(resolve, 3000);
|
|
1483
|
+
});
|
|
1484
|
+
currentSdk = await createSDK(globalOpts);
|
|
1485
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1386
1488
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1489
|
+
let progressEvents = 0;
|
|
1490
|
+
let lastProgress = -1;
|
|
1491
|
+
let transferStartedAt: number | undefined;
|
|
1492
|
+
let transferEndedAt: number | undefined;
|
|
1493
|
+
let installProgressEvents = 0;
|
|
1494
|
+
let lastInstallProgress = -1;
|
|
1495
|
+
let installStartedAt: number | undefined;
|
|
1496
|
+
let installEndedAt: number | undefined;
|
|
1497
|
+
let lastPrintedTransferProgress = -10;
|
|
1498
|
+
let lastPrintedInstallProgress = -10;
|
|
1499
|
+
const totalStartedAt = Date.now();
|
|
1500
|
+
|
|
1501
|
+
const onUiEvent = (message: unknown) => {
|
|
1502
|
+
if (!message || typeof message !== 'object') return;
|
|
1503
|
+
const messageType = (message as { type?: string }).type;
|
|
1504
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1505
|
+
|
|
1506
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1507
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1508
|
+
if (typeof tipMessage === 'string') {
|
|
1509
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1400
1510
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1404
|
-
try {
|
|
1405
|
-
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1406
|
-
} finally {
|
|
1407
|
-
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1408
|
-
}
|
|
1409
|
-
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1410
|
-
installEndedAt = Date.now();
|
|
1511
|
+
return;
|
|
1411
1512
|
}
|
|
1412
1513
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
transferEndedAt,
|
|
1420
|
-
installStartedAt,
|
|
1421
|
-
installEndedAt,
|
|
1422
|
-
progressEvents,
|
|
1423
|
-
lastProgress,
|
|
1424
|
-
installProgressEvents,
|
|
1425
|
-
lastInstallProgress,
|
|
1426
|
-
retried,
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
if (lastResult && typeof lastResult === 'object') {
|
|
1430
|
-
const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
|
|
1431
|
-
string,
|
|
1432
|
-
unknown
|
|
1433
|
-
>;
|
|
1434
|
-
lastResult = {
|
|
1435
|
-
...(lastResult as Record<string, unknown>),
|
|
1436
|
-
payload: {
|
|
1437
|
-
...payload,
|
|
1438
|
-
_debug: debugMetrics,
|
|
1439
|
-
},
|
|
1440
|
-
};
|
|
1514
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1515
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1516
|
+
process.stderr.write(
|
|
1517
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1518
|
+
);
|
|
1519
|
+
return;
|
|
1441
1520
|
}
|
|
1442
1521
|
|
|
1443
|
-
if (
|
|
1444
|
-
|
|
1522
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1523
|
+
const progress = Number(payload.progress);
|
|
1524
|
+
if (!Number.isFinite(progress)) return;
|
|
1525
|
+
|
|
1526
|
+
if (payload.progressType === 'transferData') {
|
|
1527
|
+
progressEvents += 1;
|
|
1528
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1529
|
+
transferStartedAt ??= Date.now();
|
|
1530
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1531
|
+
progressType: 'transfer',
|
|
1532
|
+
progress,
|
|
1533
|
+
payload,
|
|
1534
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1535
|
+
});
|
|
1536
|
+
if (progress >= 100) {
|
|
1537
|
+
transferEndedAt ??= Date.now();
|
|
1538
|
+
}
|
|
1539
|
+
return;
|
|
1445
1540
|
}
|
|
1446
1541
|
|
|
1447
|
-
if (
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1542
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1543
|
+
installProgressEvents += 1;
|
|
1544
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1545
|
+
installStartedAt ??= Date.now();
|
|
1546
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1547
|
+
progressType: 'install',
|
|
1548
|
+
progress,
|
|
1549
|
+
payload,
|
|
1550
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1551
|
+
});
|
|
1552
|
+
if (progress >= 100) {
|
|
1553
|
+
installEndedAt ??= Date.now();
|
|
1554
|
+
}
|
|
1453
1555
|
}
|
|
1556
|
+
};
|
|
1454
1557
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
);
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1558
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1559
|
+
let result: unknown;
|
|
1560
|
+
try {
|
|
1561
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1562
|
+
} finally {
|
|
1563
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1564
|
+
}
|
|
1565
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1566
|
+
installEndedAt = Date.now();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1570
|
+
attempt,
|
|
1571
|
+
maxAttempts,
|
|
1572
|
+
totalBytes,
|
|
1573
|
+
totalStartedAt,
|
|
1574
|
+
transferStartedAt,
|
|
1575
|
+
transferEndedAt,
|
|
1576
|
+
installStartedAt,
|
|
1577
|
+
installEndedAt,
|
|
1578
|
+
progressEvents,
|
|
1579
|
+
lastProgress,
|
|
1580
|
+
installProgressEvents,
|
|
1581
|
+
lastInstallProgress,
|
|
1582
|
+
retried,
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
if (result && typeof result === 'object') {
|
|
1586
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1587
|
+
return {
|
|
1588
|
+
...(result as Record<string, unknown>),
|
|
1589
|
+
payload: {
|
|
1590
|
+
...payload,
|
|
1591
|
+
metrics,
|
|
1592
|
+
},
|
|
1593
|
+
};
|
|
1462
1594
|
}
|
|
1463
1595
|
|
|
1464
|
-
return
|
|
1596
|
+
return result;
|
|
1465
1597
|
}
|
|
1466
1598
|
|
|
1467
|
-
function
|
|
1599
|
+
function buildFirmwareUpdateV4Params(opts: {
|
|
1468
1600
|
chunkSize?: string;
|
|
1469
|
-
|
|
1601
|
+
resourceFile?: string[];
|
|
1470
1602
|
romloader?: string;
|
|
1471
1603
|
bootloader?: string;
|
|
1472
1604
|
applicationP1?: string;
|
|
@@ -1483,7 +1615,7 @@ function buildFirmwareUpdateV4DebugParams(opts: {
|
|
|
1483
1615
|
connectProtocol: 'V2' as const,
|
|
1484
1616
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1485
1617
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1486
|
-
|
|
1618
|
+
resourceFiles: opts.resourceFile?.map(parseResourceFileParam),
|
|
1487
1619
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1488
1620
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1489
1621
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1496,7 +1628,7 @@ function buildFirmwareUpdateV4DebugParams(opts: {
|
|
|
1496
1628
|
};
|
|
1497
1629
|
|
|
1498
1630
|
const hasPayload = [
|
|
1499
|
-
params.
|
|
1631
|
+
params.resourceFiles,
|
|
1500
1632
|
params.romloaderBinary,
|
|
1501
1633
|
params.bootloaderBinary,
|
|
1502
1634
|
params.applicationP1Binary,
|
|
@@ -1509,7 +1641,7 @@ function buildFirmwareUpdateV4DebugParams(opts: {
|
|
|
1509
1641
|
].some(Boolean);
|
|
1510
1642
|
|
|
1511
1643
|
if (!hasPayload) {
|
|
1512
|
-
const err = new Error('firmware-update-v4
|
|
1644
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1513
1645
|
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1514
1646
|
throw err;
|
|
1515
1647
|
}
|
|
@@ -1528,4 +1660,8 @@ function safeParseInt(input: string, label: string): number {
|
|
|
1528
1660
|
return num;
|
|
1529
1661
|
}
|
|
1530
1662
|
|
|
1531
|
-
program
|
|
1663
|
+
export { program };
|
|
1664
|
+
|
|
1665
|
+
if (require.main === module) {
|
|
1666
|
+
program.parse();
|
|
1667
|
+
}
|