@onekeyfe/hardware-cli 1.2.0-alpha.9 → 1.2.0-alpha.91
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 +80 -0
- package/dist/cli.js +320 -266
- 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 +31 -47
- 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 +83 -1
- package/src/__tests__/noble-ble-plugin.test.ts +258 -1
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +403 -314
- 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 +43 -54
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')
|
|
@@ -559,10 +628,6 @@ program
|
|
|
559
628
|
.command('firmware-update-v4')
|
|
560
629
|
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
561
630
|
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
562
|
-
.option(
|
|
563
|
-
'--resource-bundle <spec...>',
|
|
564
|
-
'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
|
|
565
|
-
)
|
|
566
631
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
567
632
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
568
633
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -572,6 +637,7 @@ program
|
|
|
572
637
|
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
573
638
|
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
574
639
|
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
640
|
+
.option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
|
|
575
641
|
.option('--forced-update-res', 'Force resource update')
|
|
576
642
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
577
643
|
.action(opts =>
|
|
@@ -727,7 +793,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
727
793
|
|
|
728
794
|
sessionCmd
|
|
729
795
|
.command('connect')
|
|
730
|
-
.description('Connect device and
|
|
796
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
731
797
|
.action(() =>
|
|
732
798
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
733
799
|
// 1. Search for device
|
|
@@ -739,7 +805,17 @@ sessionCmd
|
|
|
739
805
|
});
|
|
740
806
|
return;
|
|
741
807
|
}
|
|
742
|
-
const device =
|
|
808
|
+
const device = selectSearchDevice(
|
|
809
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
810
|
+
globalOpts.connectId
|
|
811
|
+
);
|
|
812
|
+
if (!device) {
|
|
813
|
+
outputResult(globalOpts, {
|
|
814
|
+
success: false,
|
|
815
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
816
|
+
});
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
743
819
|
const connectId = device.connectId || globalOpts.connectId;
|
|
744
820
|
|
|
745
821
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -750,62 +826,35 @@ sessionCmd
|
|
|
750
826
|
await unlockWithRetry(sdk, connectId);
|
|
751
827
|
}
|
|
752
828
|
|
|
753
|
-
// 3.
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
useEmptyPassphrase: false,
|
|
829
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
830
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
831
|
+
mode: 'select-hidden',
|
|
757
832
|
});
|
|
758
|
-
if (!
|
|
759
|
-
outputResult(globalOpts,
|
|
833
|
+
if (!sessionResult.success) {
|
|
834
|
+
outputResult(globalOpts, sessionResult);
|
|
760
835
|
return;
|
|
761
836
|
}
|
|
762
|
-
|
|
763
|
-
psResult.payload
|
|
764
|
-
);
|
|
765
|
-
if (!passphraseState) {
|
|
837
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
766
838
|
outputResult(globalOpts, {
|
|
767
839
|
success: false,
|
|
768
|
-
payload: { error: '
|
|
840
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
769
841
|
});
|
|
770
842
|
return;
|
|
771
843
|
}
|
|
844
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
772
845
|
|
|
773
846
|
// 4. Get address to verify + extract deviceId
|
|
774
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
847
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
775
848
|
path: "m/44'/60'/0'/0/0",
|
|
776
849
|
showOnOneKey: false,
|
|
777
850
|
passphraseState,
|
|
778
851
|
});
|
|
779
852
|
|
|
780
|
-
// 5. Fetch the now-active session_id via getFeatures.
|
|
781
|
-
//
|
|
782
|
-
// IMPORTANT: pass `passphraseState` here. Without it, the SDK's
|
|
783
|
-
// connectStateChange guard (core/index.ts) would see the payload's
|
|
784
|
-
// passphraseState flip from mnNy → undefined, clear the cached Device,
|
|
785
|
-
// and call Initialize again with no passphrase_state / no session_id.
|
|
786
|
-
// That Initialize resets the device to the standard wallet and returns
|
|
787
|
-
// a *standard-wallet* session_id — which we'd then save in the keychain
|
|
788
|
-
// paired with the hidden-wallet passphraseState. On the next CLI run
|
|
789
|
-
// the mismatch would trigger PassphraseRequest (1/2/3 again).
|
|
790
|
-
const featResult = await sdk.getFeatures(connectId, {
|
|
791
|
-
passphraseState,
|
|
792
|
-
skipPassphraseCheck: true,
|
|
793
|
-
});
|
|
794
|
-
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
795
|
-
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
796
|
-
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
797
|
-
|
|
798
|
-
// 6. Save to keychain
|
|
799
|
-
if (passphraseState && deviceId && sessionId) {
|
|
800
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
853
|
outputResult(globalOpts, {
|
|
804
854
|
success: true,
|
|
805
855
|
payload: {
|
|
806
856
|
passphraseState,
|
|
807
857
|
deviceId,
|
|
808
|
-
...(sessionId ? { sessionId } : {}),
|
|
809
858
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
810
859
|
},
|
|
811
860
|
});
|
|
@@ -818,8 +867,10 @@ sessionCmd
|
|
|
818
867
|
.action(() =>
|
|
819
868
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
820
869
|
const searchResult = await sdk.searchDevices();
|
|
821
|
-
const device =
|
|
822
|
-
(searchResult?.payload as
|
|
870
|
+
const device = selectSearchDevice(
|
|
871
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
872
|
+
globalOpts.connectId
|
|
873
|
+
);
|
|
823
874
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
824
875
|
if (deviceId) {
|
|
825
876
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -914,12 +965,12 @@ async function unlockWithRetry(
|
|
|
914
965
|
* Prepare passphrase session before SDK calls.
|
|
915
966
|
*
|
|
916
967
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
917
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
918
|
-
* 3. Keychain miss →
|
|
968
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
969
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
919
970
|
*
|
|
920
971
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
921
972
|
*/
|
|
922
|
-
async function prepareSession(
|
|
973
|
+
export async function prepareSession(
|
|
923
974
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
924
975
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
925
976
|
globalOpts: Record<string, any>
|
|
@@ -930,7 +981,7 @@ async function prepareSession(
|
|
|
930
981
|
}
|
|
931
982
|
|
|
932
983
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
933
|
-
//
|
|
984
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
934
985
|
// catch block, which renders them as structured `{ success: false,
|
|
935
986
|
// payload: { error, code } }` output instead of silently falling through
|
|
936
987
|
// to a confusing downstream error 112 / 114.
|
|
@@ -945,9 +996,30 @@ async function prepareSession(
|
|
|
945
996
|
return undefined;
|
|
946
997
|
}
|
|
947
998
|
|
|
948
|
-
const device =
|
|
999
|
+
const device = selectSearchDevice(
|
|
1000
|
+
searchResult.payload as Array<{
|
|
1001
|
+
connectId?: string;
|
|
1002
|
+
deviceId?: string;
|
|
1003
|
+
deviceType?: string;
|
|
1004
|
+
features?: {
|
|
1005
|
+
deviceId?: string | null;
|
|
1006
|
+
deviceType?: string;
|
|
1007
|
+
sessionId?: string | null;
|
|
1008
|
+
passphraseProtection?: boolean | null;
|
|
1009
|
+
unlocked?: boolean | null;
|
|
1010
|
+
};
|
|
1011
|
+
}>,
|
|
1012
|
+
globalOpts.connectId
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
if (!device) {
|
|
1016
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const selectedDevice = device as {
|
|
949
1020
|
connectId?: string;
|
|
950
1021
|
deviceId?: string;
|
|
1022
|
+
deviceType?: string;
|
|
951
1023
|
features?: {
|
|
952
1024
|
deviceId?: string | null;
|
|
953
1025
|
deviceType?: string;
|
|
@@ -956,7 +1028,7 @@ async function prepareSession(
|
|
|
956
1028
|
unlocked?: boolean | null;
|
|
957
1029
|
};
|
|
958
1030
|
};
|
|
959
|
-
const connectId =
|
|
1031
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
960
1032
|
if (!globalOpts.connectId && connectId) {
|
|
961
1033
|
globalOpts.connectId = connectId;
|
|
962
1034
|
}
|
|
@@ -964,10 +1036,11 @@ async function prepareSession(
|
|
|
964
1036
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
965
1037
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
966
1038
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
967
|
-
let deviceId =
|
|
968
|
-
let deviceType =
|
|
969
|
-
|
|
970
|
-
let
|
|
1039
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1040
|
+
let deviceType =
|
|
1041
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1042
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1043
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
971
1044
|
|
|
972
1045
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
973
1046
|
try {
|
|
@@ -1006,7 +1079,7 @@ async function prepareSession(
|
|
|
1006
1079
|
return undefined;
|
|
1007
1080
|
}
|
|
1008
1081
|
|
|
1009
|
-
// ── Step 5: Try keychain session reuse
|
|
1082
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
1010
1083
|
// Only attempt if device was already unlocked — locking invalidates
|
|
1011
1084
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
1012
1085
|
if (!wasLocked && deviceId) {
|
|
@@ -1017,40 +1090,19 @@ async function prepareSession(
|
|
|
1017
1090
|
}
|
|
1018
1091
|
}
|
|
1019
1092
|
|
|
1020
|
-
// ── Step 6: Keychain miss →
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
useEmptyPassphrase: false,
|
|
1093
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1094
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1095
|
+
mode: 'select-hidden',
|
|
1024
1096
|
});
|
|
1025
1097
|
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
psResult.payload
|
|
1029
|
-
);
|
|
1030
|
-
if (!passphraseState) {
|
|
1098
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1099
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
1031
1100
|
return undefined;
|
|
1032
1101
|
}
|
|
1102
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1103
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
1033
1104
|
globalOpts.passphraseState = passphraseState;
|
|
1034
1105
|
|
|
1035
|
-
// Save session to keychain for next invocation.
|
|
1036
|
-
//
|
|
1037
|
-
// Pass passphraseState to keep connectStateChange=false — otherwise
|
|
1038
|
-
// Initialize would be re-run without passphrase_state, resetting the
|
|
1039
|
-
// device to the standard wallet and returning a mismatched session_id.
|
|
1040
|
-
// See the matching comment in `session connect`.
|
|
1041
|
-
if (deviceId) {
|
|
1042
|
-
const featAfter = await sdk.getFeatures(connectId, {
|
|
1043
|
-
passphraseState,
|
|
1044
|
-
skipPassphraseCheck: true,
|
|
1045
|
-
});
|
|
1046
|
-
const sessionId =
|
|
1047
|
-
passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
1048
|
-
if (sessionId) {
|
|
1049
|
-
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
1050
|
-
await preloadSessionFromKeychain(deviceId);
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
1106
|
return passphraseState;
|
|
1055
1107
|
}
|
|
1056
1108
|
return undefined;
|
|
@@ -1067,8 +1119,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1067
1119
|
) {
|
|
1068
1120
|
process.exitCode = 1;
|
|
1069
1121
|
}
|
|
1070
|
-
// No process.exit here — runCommand()
|
|
1071
|
-
//
|
|
1122
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1123
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1072
1124
|
}
|
|
1073
1125
|
|
|
1074
1126
|
/**
|
|
@@ -1080,7 +1132,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1080
1132
|
* 3. run the handler (which calls outputResult on success)
|
|
1081
1133
|
* 4. report uncaught errors as a structured failure result
|
|
1082
1134
|
* 5. dispose SDK
|
|
1083
|
-
* 6.
|
|
1135
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1084
1136
|
*
|
|
1085
1137
|
* This fixes three previous bugs:
|
|
1086
1138
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1132,9 +1184,7 @@ async function runCommand(
|
|
|
1132
1184
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1133
1185
|
await disposeSDK();
|
|
1134
1186
|
}
|
|
1135
|
-
//
|
|
1136
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1137
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1187
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1138
1188
|
}
|
|
1139
1189
|
|
|
1140
1190
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1162,29 +1212,46 @@ function readBinaryParam(path: string): ArrayBuffer {
|
|
|
1162
1212
|
return new Uint8Array(buffer).buffer;
|
|
1163
1213
|
}
|
|
1164
1214
|
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1215
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1216
|
+
sdk: AnySdk,
|
|
1217
|
+
explicitConnectId?: string,
|
|
1218
|
+
deviceName?: string
|
|
1219
|
+
): Promise<string> {
|
|
1220
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1221
|
+
|
|
1222
|
+
const searchResult = await sdk.searchDevices();
|
|
1223
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1224
|
+
throw new Error('Unable to scan BLE devices');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1228
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1229
|
+
const matches = normalizedName
|
|
1230
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1231
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1232
|
+
|
|
1233
|
+
if (matches.length === 0) {
|
|
1168
1234
|
throw new Error(
|
|
1169
|
-
|
|
1235
|
+
normalizedName
|
|
1236
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1237
|
+
: 'No Classic/Pure BLE device found'
|
|
1170
1238
|
);
|
|
1171
1239
|
}
|
|
1172
|
-
|
|
1173
|
-
const devicePath = spec.slice(sep + 1);
|
|
1174
|
-
if (!devicePath.startsWith('vol')) {
|
|
1240
|
+
if (matches.length > 1) {
|
|
1175
1241
|
throw new Error(
|
|
1176
|
-
|
|
1242
|
+
normalizedName
|
|
1243
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1244
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1177
1245
|
);
|
|
1178
1246
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1247
|
+
|
|
1248
|
+
const [{ connectId, name }] = matches;
|
|
1249
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1250
|
+
return connectId;
|
|
1183
1251
|
}
|
|
1184
1252
|
|
|
1185
1253
|
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1186
1254
|
return [
|
|
1187
|
-
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1188
1255
|
params.bootloaderBinary,
|
|
1189
1256
|
params.applicationP1Binary,
|
|
1190
1257
|
params.applicationP2Binary,
|
|
@@ -1234,6 +1301,30 @@ function formatFirmwareBytes(bytes: number) {
|
|
|
1234
1301
|
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1235
1302
|
}
|
|
1236
1303
|
|
|
1304
|
+
export function buildWallpaperUploadMetrics({
|
|
1305
|
+
totalBytes,
|
|
1306
|
+
transferredBytes,
|
|
1307
|
+
startedAt,
|
|
1308
|
+
endedAt,
|
|
1309
|
+
lastProgress,
|
|
1310
|
+
}: {
|
|
1311
|
+
totalBytes: number;
|
|
1312
|
+
transferredBytes: number;
|
|
1313
|
+
startedAt: number;
|
|
1314
|
+
endedAt: number;
|
|
1315
|
+
lastProgress: number;
|
|
1316
|
+
}) {
|
|
1317
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1318
|
+
return {
|
|
1319
|
+
totalBytes,
|
|
1320
|
+
transferredBytes,
|
|
1321
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1322
|
+
transferKiBPerSecond:
|
|
1323
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1324
|
+
lastProgress,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1237
1328
|
function maybePrintFirmwareProgress({
|
|
1238
1329
|
progressType,
|
|
1239
1330
|
progress,
|
|
@@ -1330,7 +1421,7 @@ function buildFirmwareUpdateV4Metrics({
|
|
|
1330
1421
|
};
|
|
1331
1422
|
}
|
|
1332
1423
|
|
|
1333
|
-
async function runFirmwareUpdateV4WithRetry({
|
|
1424
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1334
1425
|
sdk,
|
|
1335
1426
|
globalOpts,
|
|
1336
1427
|
params,
|
|
@@ -1344,152 +1435,147 @@ async function runFirmwareUpdateV4WithRetry({
|
|
|
1344
1435
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1345
1436
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1346
1437
|
let currentSdk = sdk;
|
|
1347
|
-
let lastResult: unknown;
|
|
1348
1438
|
let retried = false;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
const connectId =
|
|
1363
|
-
retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1364
|
-
? undefined
|
|
1365
|
-
: globalOpts.connectId;
|
|
1366
|
-
|
|
1367
|
-
const onUiEvent = (message: unknown) => {
|
|
1368
|
-
if (!message || typeof message !== 'object') return;
|
|
1369
|
-
const messageType = (message as { type?: string }).type;
|
|
1370
|
-
const payload = getFirmwareUpdatePayload(message);
|
|
1371
|
-
|
|
1372
|
-
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1373
|
-
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1374
|
-
if (typeof tipMessage === 'string') {
|
|
1375
|
-
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1376
|
-
}
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1381
|
-
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1382
|
-
process.stderr.write(
|
|
1383
|
-
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1384
|
-
);
|
|
1385
|
-
return;
|
|
1439
|
+
let attempt = 1;
|
|
1440
|
+
let { connectId } = globalOpts;
|
|
1441
|
+
|
|
1442
|
+
if (globalOpts.transport === 'usb') {
|
|
1443
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1444
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1445
|
+
scope: 'runtime',
|
|
1446
|
+
connectProtocol: 'V2',
|
|
1447
|
+
retryCount: 0,
|
|
1448
|
+
});
|
|
1449
|
+
if (isSuccessResult(probeResult)) break;
|
|
1450
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1451
|
+
return probeResult;
|
|
1386
1452
|
}
|
|
1387
1453
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1454
|
+
retried = true;
|
|
1455
|
+
process.stderr.write(
|
|
1456
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1457
|
+
);
|
|
1458
|
+
await disposeSDK();
|
|
1459
|
+
await new Promise(resolve => {
|
|
1460
|
+
setTimeout(resolve, 3000);
|
|
1461
|
+
});
|
|
1462
|
+
currentSdk = await createSDK(globalOpts);
|
|
1463
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1391
1466
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1467
|
+
let progressEvents = 0;
|
|
1468
|
+
let lastProgress = -1;
|
|
1469
|
+
let transferStartedAt: number | undefined;
|
|
1470
|
+
let transferEndedAt: number | undefined;
|
|
1471
|
+
let installProgressEvents = 0;
|
|
1472
|
+
let lastInstallProgress = -1;
|
|
1473
|
+
let installStartedAt: number | undefined;
|
|
1474
|
+
let installEndedAt: number | undefined;
|
|
1475
|
+
let lastPrintedTransferProgress = -10;
|
|
1476
|
+
let lastPrintedInstallProgress = -10;
|
|
1477
|
+
const totalStartedAt = Date.now();
|
|
1478
|
+
|
|
1479
|
+
const onUiEvent = (message: unknown) => {
|
|
1480
|
+
if (!message || typeof message !== 'object') return;
|
|
1481
|
+
const messageType = (message as { type?: string }).type;
|
|
1482
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1483
|
+
|
|
1484
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1485
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1486
|
+
if (typeof tipMessage === 'string') {
|
|
1487
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1406
1488
|
}
|
|
1407
|
-
|
|
1408
|
-
if (payload.progressType === 'installingFirmware') {
|
|
1409
|
-
installProgressEvents += 1;
|
|
1410
|
-
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1411
|
-
installStartedAt ??= Date.now();
|
|
1412
|
-
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1413
|
-
progressType: 'install',
|
|
1414
|
-
progress,
|
|
1415
|
-
payload,
|
|
1416
|
-
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1417
|
-
});
|
|
1418
|
-
if (progress >= 100) {
|
|
1419
|
-
installEndedAt ??= Date.now();
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
|
-
|
|
1424
|
-
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1425
|
-
try {
|
|
1426
|
-
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1427
|
-
} finally {
|
|
1428
|
-
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1429
|
-
}
|
|
1430
|
-
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1431
|
-
installEndedAt = Date.now();
|
|
1489
|
+
return;
|
|
1432
1490
|
}
|
|
1433
1491
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
transferEndedAt,
|
|
1441
|
-
installStartedAt,
|
|
1442
|
-
installEndedAt,
|
|
1443
|
-
progressEvents,
|
|
1444
|
-
lastProgress,
|
|
1445
|
-
installProgressEvents,
|
|
1446
|
-
lastInstallProgress,
|
|
1447
|
-
retried,
|
|
1448
|
-
});
|
|
1449
|
-
|
|
1450
|
-
if (lastResult && typeof lastResult === 'object') {
|
|
1451
|
-
const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
|
|
1452
|
-
string,
|
|
1453
|
-
unknown
|
|
1454
|
-
>;
|
|
1455
|
-
lastResult = {
|
|
1456
|
-
...(lastResult as Record<string, unknown>),
|
|
1457
|
-
payload: {
|
|
1458
|
-
...payload,
|
|
1459
|
-
metrics,
|
|
1460
|
-
},
|
|
1461
|
-
};
|
|
1492
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1493
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1494
|
+
process.stderr.write(
|
|
1495
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1496
|
+
);
|
|
1497
|
+
return;
|
|
1462
1498
|
}
|
|
1463
1499
|
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1500
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1501
|
+
const progress = Number(payload.progress);
|
|
1502
|
+
if (!Number.isFinite(progress)) return;
|
|
1503
|
+
|
|
1504
|
+
if (payload.progressType === 'transferData') {
|
|
1505
|
+
progressEvents += 1;
|
|
1506
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1507
|
+
transferStartedAt ??= Date.now();
|
|
1508
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1509
|
+
progressType: 'transfer',
|
|
1510
|
+
progress,
|
|
1511
|
+
payload,
|
|
1512
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1513
|
+
});
|
|
1514
|
+
if (progress >= 100) {
|
|
1515
|
+
transferEndedAt ??= Date.now();
|
|
1516
|
+
}
|
|
1517
|
+
return;
|
|
1466
1518
|
}
|
|
1467
1519
|
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1520
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1521
|
+
installProgressEvents += 1;
|
|
1522
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1523
|
+
installStartedAt ??= Date.now();
|
|
1524
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1525
|
+
progressType: 'install',
|
|
1526
|
+
progress,
|
|
1527
|
+
payload,
|
|
1528
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1529
|
+
});
|
|
1530
|
+
if (progress >= 100) {
|
|
1531
|
+
installEndedAt ??= Date.now();
|
|
1532
|
+
}
|
|
1474
1533
|
}
|
|
1534
|
+
};
|
|
1475
1535
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1536
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1537
|
+
let result: unknown;
|
|
1538
|
+
try {
|
|
1539
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1540
|
+
} finally {
|
|
1541
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1542
|
+
}
|
|
1543
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1544
|
+
installEndedAt = Date.now();
|
|
1485
1545
|
}
|
|
1486
1546
|
|
|
1487
|
-
|
|
1547
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1548
|
+
attempt,
|
|
1549
|
+
maxAttempts,
|
|
1550
|
+
totalBytes,
|
|
1551
|
+
totalStartedAt,
|
|
1552
|
+
transferStartedAt,
|
|
1553
|
+
transferEndedAt,
|
|
1554
|
+
installStartedAt,
|
|
1555
|
+
installEndedAt,
|
|
1556
|
+
progressEvents,
|
|
1557
|
+
lastProgress,
|
|
1558
|
+
installProgressEvents,
|
|
1559
|
+
lastInstallProgress,
|
|
1560
|
+
retried,
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
if (result && typeof result === 'object') {
|
|
1564
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1565
|
+
return {
|
|
1566
|
+
...(result as Record<string, unknown>),
|
|
1567
|
+
payload: {
|
|
1568
|
+
...payload,
|
|
1569
|
+
metrics,
|
|
1570
|
+
},
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
return result;
|
|
1488
1575
|
}
|
|
1489
1576
|
|
|
1490
1577
|
function buildFirmwareUpdateV4Params(opts: {
|
|
1491
1578
|
chunkSize?: string;
|
|
1492
|
-
resourceBundle?: string[];
|
|
1493
1579
|
romloader?: string;
|
|
1494
1580
|
bootloader?: string;
|
|
1495
1581
|
applicationP1?: string;
|
|
@@ -1499,6 +1585,7 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1499
1585
|
se02?: string;
|
|
1500
1586
|
se03?: string;
|
|
1501
1587
|
se04?: string;
|
|
1588
|
+
resourceArchive?: string;
|
|
1502
1589
|
forcedUpdateRes?: boolean;
|
|
1503
1590
|
}) {
|
|
1504
1591
|
const params = {
|
|
@@ -1506,7 +1593,6 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1506
1593
|
connectProtocol: 'V2' as const,
|
|
1507
1594
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1508
1595
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1509
|
-
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1510
1596
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1511
1597
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1512
1598
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1516,10 +1602,10 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1516
1602
|
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1517
1603
|
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1518
1604
|
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1605
|
+
resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
|
|
1519
1606
|
};
|
|
1520
1607
|
|
|
1521
1608
|
const hasPayload = [
|
|
1522
|
-
params.resourceBundleFiles,
|
|
1523
1609
|
params.romloaderBinary,
|
|
1524
1610
|
params.bootloaderBinary,
|
|
1525
1611
|
params.applicationP1Binary,
|
|
@@ -1529,10 +1615,13 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1529
1615
|
params.se02Binary,
|
|
1530
1616
|
params.se03Binary,
|
|
1531
1617
|
params.se04Binary,
|
|
1618
|
+
params.resourceArchiveBinary,
|
|
1532
1619
|
].some(Boolean);
|
|
1533
1620
|
|
|
1534
1621
|
if (!hasPayload) {
|
|
1535
|
-
const err = new Error(
|
|
1622
|
+
const err = new Error(
|
|
1623
|
+
'firmware-update-v4 requires at least one firmware binary or resource archive path'
|
|
1624
|
+
);
|
|
1536
1625
|
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1537
1626
|
throw err;
|
|
1538
1627
|
}
|