@onekeyfe/hardware-cli 1.2.0-alpha.13 → 1.2.0-alpha.130
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 +314 -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 +29 -41
- 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 +197 -1
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +47 -0
- package/src/cli.ts +395 -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 +37 -47
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,112 @@ 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('--jpeg <path>', '604x1024 JPEG 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 jpegBase64 = readFileSync(opts.jpeg).toString('base64');
|
|
108
|
+
|
|
109
|
+
let transferStartedAt: number | undefined;
|
|
110
|
+
let transferEndedAt: number | undefined;
|
|
111
|
+
let lastProgress = -1;
|
|
112
|
+
let lastPrintedProgress = -10;
|
|
113
|
+
let progressTotalBytes = 0;
|
|
114
|
+
let transferredBytes = 0;
|
|
115
|
+
const totalStartedAt = Date.now();
|
|
116
|
+
const onUiEvent = (message: unknown) => {
|
|
117
|
+
if (!message || typeof message !== 'object') return;
|
|
118
|
+
const event = message as {
|
|
119
|
+
type?: string;
|
|
120
|
+
payload?: {
|
|
121
|
+
progress?: number;
|
|
122
|
+
transferredBytes?: number;
|
|
123
|
+
totalBytes?: number;
|
|
124
|
+
rateBytesPerSecond?: number;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
if (event.type !== UI_REQUEST.DEVICE_PROGRESS || !event.payload) return;
|
|
128
|
+
const progress = Number(event.payload.progress);
|
|
129
|
+
if (!Number.isFinite(progress)) return;
|
|
130
|
+
transferStartedAt ??= Date.now();
|
|
131
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
132
|
+
const totalBytes = Number(event.payload.totalBytes);
|
|
133
|
+
if (Number.isFinite(totalBytes) && totalBytes > 0) progressTotalBytes = totalBytes;
|
|
134
|
+
const confirmedBytes = Number(event.payload.transferredBytes);
|
|
135
|
+
if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
|
|
136
|
+
transferredBytes = Math.max(transferredBytes, confirmedBytes);
|
|
137
|
+
}
|
|
138
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
139
|
+
if (printableProgress > lastPrintedProgress || progress >= 100) {
|
|
140
|
+
const rate = Number(event.payload.rateBytesPerSecond);
|
|
141
|
+
const rateText =
|
|
142
|
+
Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
|
|
143
|
+
process.stderr.write(
|
|
144
|
+
`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`
|
|
145
|
+
);
|
|
146
|
+
lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
|
|
147
|
+
}
|
|
148
|
+
if (progress >= 100) transferEndedAt ??= Date.now();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
sdk.on(UI_EVENT, onUiEvent);
|
|
152
|
+
let result: any;
|
|
153
|
+
try {
|
|
154
|
+
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
155
|
+
...params,
|
|
156
|
+
jpegBase64,
|
|
157
|
+
fileName: opts.fileName,
|
|
158
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
159
|
+
});
|
|
160
|
+
} finally {
|
|
161
|
+
sdk.off?.(UI_EVENT, onUiEvent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const endedAt = transferEndedAt ?? Date.now();
|
|
165
|
+
const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
|
|
166
|
+
outputResult(globalOpts, {
|
|
167
|
+
...result,
|
|
168
|
+
metrics: buildWallpaperUploadMetrics({
|
|
169
|
+
totalBytes,
|
|
170
|
+
transferredBytes: result?.success ? totalBytes : transferredBytes,
|
|
171
|
+
startedAt: transferStartedAt ?? totalStartedAt,
|
|
172
|
+
endedAt,
|
|
173
|
+
lastProgress,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
|
|
150
179
|
// ============================================================
|
|
151
180
|
// Signing Commands
|
|
152
181
|
// ============================================================
|
|
@@ -541,6 +570,38 @@ program
|
|
|
541
570
|
})
|
|
542
571
|
);
|
|
543
572
|
|
|
573
|
+
program
|
|
574
|
+
.command('firmware-update-legacy')
|
|
575
|
+
.description('Update Classic/Pure firmware through the legacy protocol')
|
|
576
|
+
.requiredOption('--binary <path>', 'Local firmware binary path')
|
|
577
|
+
.option('--device-name <name>', 'BLE advertising name, for example K1514')
|
|
578
|
+
.option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
|
|
579
|
+
.option('--no-reboot', 'Do not reboot the device after a successful update')
|
|
580
|
+
.action(opts =>
|
|
581
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
582
|
+
if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
|
|
583
|
+
throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const connectId = await resolveLegacyFirmwareConnectId(
|
|
587
|
+
sdk,
|
|
588
|
+
globalOpts.connectId,
|
|
589
|
+
opts.deviceName
|
|
590
|
+
);
|
|
591
|
+
const result = await sdk.firmwareUpdate(connectId, {
|
|
592
|
+
binary: readBinaryParam(opts.binary),
|
|
593
|
+
updateType: opts.updateType,
|
|
594
|
+
rebootOnSuccess: opts.reboot,
|
|
595
|
+
timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
|
|
596
|
+
});
|
|
597
|
+
outputResult(globalOpts, result);
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
export function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble') {
|
|
602
|
+
return transport === 'usb' ? 90_000 : undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
544
605
|
program
|
|
545
606
|
.command('firmware-update-ble')
|
|
546
607
|
.description('Run Protocol V2 firmware update over BLE')
|
|
@@ -559,10 +620,6 @@ program
|
|
|
559
620
|
.command('firmware-update-v4')
|
|
560
621
|
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
561
622
|
.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
623
|
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
567
624
|
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
568
625
|
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
@@ -572,6 +629,7 @@ program
|
|
|
572
629
|
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
573
630
|
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
574
631
|
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
632
|
+
.option('--resource-archive <path>', 'Complete signed Protocol V2 resource ZIP path')
|
|
575
633
|
.option('--forced-update-res', 'Force resource update')
|
|
576
634
|
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
577
635
|
.action(opts =>
|
|
@@ -727,7 +785,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
727
785
|
|
|
728
786
|
sessionCmd
|
|
729
787
|
.command('connect')
|
|
730
|
-
.description('Connect device and
|
|
788
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
731
789
|
.action(() =>
|
|
732
790
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
733
791
|
// 1. Search for device
|
|
@@ -739,7 +797,17 @@ sessionCmd
|
|
|
739
797
|
});
|
|
740
798
|
return;
|
|
741
799
|
}
|
|
742
|
-
const device =
|
|
800
|
+
const device = selectSearchDevice(
|
|
801
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
802
|
+
globalOpts.connectId
|
|
803
|
+
);
|
|
804
|
+
if (!device) {
|
|
805
|
+
outputResult(globalOpts, {
|
|
806
|
+
success: false,
|
|
807
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
743
811
|
const connectId = device.connectId || globalOpts.connectId;
|
|
744
812
|
|
|
745
813
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -750,62 +818,35 @@ sessionCmd
|
|
|
750
818
|
await unlockWithRetry(sdk, connectId);
|
|
751
819
|
}
|
|
752
820
|
|
|
753
|
-
// 3.
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
useEmptyPassphrase: false,
|
|
821
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
822
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
823
|
+
mode: 'select-hidden',
|
|
757
824
|
});
|
|
758
|
-
if (!
|
|
759
|
-
outputResult(globalOpts,
|
|
825
|
+
if (!sessionResult.success) {
|
|
826
|
+
outputResult(globalOpts, sessionResult);
|
|
760
827
|
return;
|
|
761
828
|
}
|
|
762
|
-
|
|
763
|
-
psResult.payload
|
|
764
|
-
);
|
|
765
|
-
if (!passphraseState) {
|
|
829
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
766
830
|
outputResult(globalOpts, {
|
|
767
831
|
success: false,
|
|
768
|
-
payload: { error: '
|
|
832
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
769
833
|
});
|
|
770
834
|
return;
|
|
771
835
|
}
|
|
836
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
772
837
|
|
|
773
838
|
// 4. Get address to verify + extract deviceId
|
|
774
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
839
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
775
840
|
path: "m/44'/60'/0'/0/0",
|
|
776
841
|
showOnOneKey: false,
|
|
777
842
|
passphraseState,
|
|
778
843
|
});
|
|
779
844
|
|
|
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
845
|
outputResult(globalOpts, {
|
|
804
846
|
success: true,
|
|
805
847
|
payload: {
|
|
806
848
|
passphraseState,
|
|
807
849
|
deviceId,
|
|
808
|
-
...(sessionId ? { sessionId } : {}),
|
|
809
850
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
810
851
|
},
|
|
811
852
|
});
|
|
@@ -818,8 +859,10 @@ sessionCmd
|
|
|
818
859
|
.action(() =>
|
|
819
860
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
820
861
|
const searchResult = await sdk.searchDevices();
|
|
821
|
-
const device =
|
|
822
|
-
(searchResult?.payload as
|
|
862
|
+
const device = selectSearchDevice(
|
|
863
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
864
|
+
globalOpts.connectId
|
|
865
|
+
);
|
|
823
866
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
824
867
|
if (deviceId) {
|
|
825
868
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -914,12 +957,12 @@ async function unlockWithRetry(
|
|
|
914
957
|
* Prepare passphrase session before SDK calls.
|
|
915
958
|
*
|
|
916
959
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
917
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
918
|
-
* 3. Keychain miss →
|
|
960
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
961
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
919
962
|
*
|
|
920
963
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
921
964
|
*/
|
|
922
|
-
async function prepareSession(
|
|
965
|
+
export async function prepareSession(
|
|
923
966
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
924
967
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
925
968
|
globalOpts: Record<string, any>
|
|
@@ -930,7 +973,7 @@ async function prepareSession(
|
|
|
930
973
|
}
|
|
931
974
|
|
|
932
975
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
933
|
-
//
|
|
976
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
934
977
|
// catch block, which renders them as structured `{ success: false,
|
|
935
978
|
// payload: { error, code } }` output instead of silently falling through
|
|
936
979
|
// to a confusing downstream error 112 / 114.
|
|
@@ -945,9 +988,30 @@ async function prepareSession(
|
|
|
945
988
|
return undefined;
|
|
946
989
|
}
|
|
947
990
|
|
|
948
|
-
const device =
|
|
991
|
+
const device = selectSearchDevice(
|
|
992
|
+
searchResult.payload as Array<{
|
|
993
|
+
connectId?: string;
|
|
994
|
+
deviceId?: string;
|
|
995
|
+
deviceType?: string;
|
|
996
|
+
features?: {
|
|
997
|
+
deviceId?: string | null;
|
|
998
|
+
deviceType?: string;
|
|
999
|
+
sessionId?: string | null;
|
|
1000
|
+
passphraseProtection?: boolean | null;
|
|
1001
|
+
unlocked?: boolean | null;
|
|
1002
|
+
};
|
|
1003
|
+
}>,
|
|
1004
|
+
globalOpts.connectId
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
if (!device) {
|
|
1008
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const selectedDevice = device as {
|
|
949
1012
|
connectId?: string;
|
|
950
1013
|
deviceId?: string;
|
|
1014
|
+
deviceType?: string;
|
|
951
1015
|
features?: {
|
|
952
1016
|
deviceId?: string | null;
|
|
953
1017
|
deviceType?: string;
|
|
@@ -956,7 +1020,7 @@ async function prepareSession(
|
|
|
956
1020
|
unlocked?: boolean | null;
|
|
957
1021
|
};
|
|
958
1022
|
};
|
|
959
|
-
const connectId =
|
|
1023
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
960
1024
|
if (!globalOpts.connectId && connectId) {
|
|
961
1025
|
globalOpts.connectId = connectId;
|
|
962
1026
|
}
|
|
@@ -964,10 +1028,11 @@ async function prepareSession(
|
|
|
964
1028
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
965
1029
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
966
1030
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
967
|
-
let deviceId =
|
|
968
|
-
let deviceType =
|
|
969
|
-
|
|
970
|
-
let
|
|
1031
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1032
|
+
let deviceType =
|
|
1033
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1034
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1035
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
971
1036
|
|
|
972
1037
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
973
1038
|
try {
|
|
@@ -1006,7 +1071,7 @@ async function prepareSession(
|
|
|
1006
1071
|
return undefined;
|
|
1007
1072
|
}
|
|
1008
1073
|
|
|
1009
|
-
// ── Step 5: Try keychain session reuse
|
|
1074
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
1010
1075
|
// Only attempt if device was already unlocked — locking invalidates
|
|
1011
1076
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
1012
1077
|
if (!wasLocked && deviceId) {
|
|
@@ -1017,40 +1082,19 @@ async function prepareSession(
|
|
|
1017
1082
|
}
|
|
1018
1083
|
}
|
|
1019
1084
|
|
|
1020
|
-
// ── Step 6: Keychain miss →
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
useEmptyPassphrase: false,
|
|
1085
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1086
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1087
|
+
mode: 'select-hidden',
|
|
1024
1088
|
});
|
|
1025
1089
|
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
psResult.payload
|
|
1029
|
-
);
|
|
1030
|
-
if (!passphraseState) {
|
|
1090
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1091
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
1031
1092
|
return undefined;
|
|
1032
1093
|
}
|
|
1094
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1095
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
1033
1096
|
globalOpts.passphraseState = passphraseState;
|
|
1034
1097
|
|
|
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
1098
|
return passphraseState;
|
|
1055
1099
|
}
|
|
1056
1100
|
return undefined;
|
|
@@ -1067,8 +1111,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1067
1111
|
) {
|
|
1068
1112
|
process.exitCode = 1;
|
|
1069
1113
|
}
|
|
1070
|
-
// No process.exit here — runCommand()
|
|
1071
|
-
//
|
|
1114
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1115
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1072
1116
|
}
|
|
1073
1117
|
|
|
1074
1118
|
/**
|
|
@@ -1080,7 +1124,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1080
1124
|
* 3. run the handler (which calls outputResult on success)
|
|
1081
1125
|
* 4. report uncaught errors as a structured failure result
|
|
1082
1126
|
* 5. dispose SDK
|
|
1083
|
-
* 6.
|
|
1127
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1084
1128
|
*
|
|
1085
1129
|
* This fixes three previous bugs:
|
|
1086
1130
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1132,9 +1176,7 @@ async function runCommand(
|
|
|
1132
1176
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1133
1177
|
await disposeSDK();
|
|
1134
1178
|
}
|
|
1135
|
-
//
|
|
1136
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1137
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1179
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1138
1180
|
}
|
|
1139
1181
|
|
|
1140
1182
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1162,29 +1204,46 @@ function readBinaryParam(path: string): ArrayBuffer {
|
|
|
1162
1204
|
return new Uint8Array(buffer).buffer;
|
|
1163
1205
|
}
|
|
1164
1206
|
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1207
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1208
|
+
sdk: AnySdk,
|
|
1209
|
+
explicitConnectId?: string,
|
|
1210
|
+
deviceName?: string
|
|
1211
|
+
): Promise<string> {
|
|
1212
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1213
|
+
|
|
1214
|
+
const searchResult = await sdk.searchDevices();
|
|
1215
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1216
|
+
throw new Error('Unable to scan BLE devices');
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1220
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1221
|
+
const matches = normalizedName
|
|
1222
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1223
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1224
|
+
|
|
1225
|
+
if (matches.length === 0) {
|
|
1168
1226
|
throw new Error(
|
|
1169
|
-
|
|
1227
|
+
normalizedName
|
|
1228
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1229
|
+
: 'No Classic/Pure BLE device found'
|
|
1170
1230
|
);
|
|
1171
1231
|
}
|
|
1172
|
-
|
|
1173
|
-
const devicePath = spec.slice(sep + 1);
|
|
1174
|
-
if (!devicePath.startsWith('vol')) {
|
|
1232
|
+
if (matches.length > 1) {
|
|
1175
1233
|
throw new Error(
|
|
1176
|
-
|
|
1234
|
+
normalizedName
|
|
1235
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1236
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1177
1237
|
);
|
|
1178
1238
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1239
|
+
|
|
1240
|
+
const [{ connectId, name }] = matches;
|
|
1241
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1242
|
+
return connectId;
|
|
1183
1243
|
}
|
|
1184
1244
|
|
|
1185
1245
|
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1186
1246
|
return [
|
|
1187
|
-
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1188
1247
|
params.bootloaderBinary,
|
|
1189
1248
|
params.applicationP1Binary,
|
|
1190
1249
|
params.applicationP2Binary,
|
|
@@ -1234,6 +1293,30 @@ function formatFirmwareBytes(bytes: number) {
|
|
|
1234
1293
|
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1235
1294
|
}
|
|
1236
1295
|
|
|
1296
|
+
export function buildWallpaperUploadMetrics({
|
|
1297
|
+
totalBytes,
|
|
1298
|
+
transferredBytes,
|
|
1299
|
+
startedAt,
|
|
1300
|
+
endedAt,
|
|
1301
|
+
lastProgress,
|
|
1302
|
+
}: {
|
|
1303
|
+
totalBytes: number;
|
|
1304
|
+
transferredBytes: number;
|
|
1305
|
+
startedAt: number;
|
|
1306
|
+
endedAt: number;
|
|
1307
|
+
lastProgress: number;
|
|
1308
|
+
}) {
|
|
1309
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1310
|
+
return {
|
|
1311
|
+
totalBytes,
|
|
1312
|
+
transferredBytes,
|
|
1313
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1314
|
+
transferKiBPerSecond:
|
|
1315
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1316
|
+
lastProgress,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1237
1320
|
function maybePrintFirmwareProgress({
|
|
1238
1321
|
progressType,
|
|
1239
1322
|
progress,
|
|
@@ -1330,7 +1413,7 @@ function buildFirmwareUpdateV4Metrics({
|
|
|
1330
1413
|
};
|
|
1331
1414
|
}
|
|
1332
1415
|
|
|
1333
|
-
async function runFirmwareUpdateV4WithRetry({
|
|
1416
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1334
1417
|
sdk,
|
|
1335
1418
|
globalOpts,
|
|
1336
1419
|
params,
|
|
@@ -1344,152 +1427,147 @@ async function runFirmwareUpdateV4WithRetry({
|
|
|
1344
1427
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1345
1428
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1346
1429
|
let currentSdk = sdk;
|
|
1347
|
-
let lastResult: unknown;
|
|
1348
1430
|
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;
|
|
1431
|
+
let attempt = 1;
|
|
1432
|
+
let { connectId } = globalOpts;
|
|
1433
|
+
|
|
1434
|
+
if (globalOpts.transport === 'usb') {
|
|
1435
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1436
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1437
|
+
scope: 'runtime',
|
|
1438
|
+
connectProtocol: 'V2',
|
|
1439
|
+
retryCount: 0,
|
|
1440
|
+
});
|
|
1441
|
+
if (isSuccessResult(probeResult)) break;
|
|
1442
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1443
|
+
return probeResult;
|
|
1386
1444
|
}
|
|
1387
1445
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1401
|
-
});
|
|
1402
|
-
if (progress >= 100) {
|
|
1403
|
-
transferEndedAt ??= Date.now();
|
|
1404
|
-
}
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1446
|
+
retried = true;
|
|
1447
|
+
process.stderr.write(
|
|
1448
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1449
|
+
);
|
|
1450
|
+
await disposeSDK();
|
|
1451
|
+
await new Promise(resolve => {
|
|
1452
|
+
setTimeout(resolve, 3000);
|
|
1453
|
+
});
|
|
1454
|
+
currentSdk = await createSDK(globalOpts);
|
|
1455
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1407
1458
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1459
|
+
let progressEvents = 0;
|
|
1460
|
+
let lastProgress = -1;
|
|
1461
|
+
let transferStartedAt: number | undefined;
|
|
1462
|
+
let transferEndedAt: number | undefined;
|
|
1463
|
+
let installProgressEvents = 0;
|
|
1464
|
+
let lastInstallProgress = -1;
|
|
1465
|
+
let installStartedAt: number | undefined;
|
|
1466
|
+
let installEndedAt: number | undefined;
|
|
1467
|
+
let lastPrintedTransferProgress = -10;
|
|
1468
|
+
let lastPrintedInstallProgress = -10;
|
|
1469
|
+
const totalStartedAt = Date.now();
|
|
1470
|
+
|
|
1471
|
+
const onUiEvent = (message: unknown) => {
|
|
1472
|
+
if (!message || typeof message !== 'object') return;
|
|
1473
|
+
const messageType = (message as { type?: string }).type;
|
|
1474
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1475
|
+
|
|
1476
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1477
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1478
|
+
if (typeof tipMessage === 'string') {
|
|
1479
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1421
1480
|
}
|
|
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();
|
|
1481
|
+
return;
|
|
1432
1482
|
}
|
|
1433
1483
|
|
|
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
|
-
};
|
|
1484
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1485
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1486
|
+
process.stderr.write(
|
|
1487
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1488
|
+
);
|
|
1489
|
+
return;
|
|
1462
1490
|
}
|
|
1463
1491
|
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1492
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1493
|
+
const progress = Number(payload.progress);
|
|
1494
|
+
if (!Number.isFinite(progress)) return;
|
|
1495
|
+
|
|
1496
|
+
if (payload.progressType === 'transferData') {
|
|
1497
|
+
progressEvents += 1;
|
|
1498
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1499
|
+
transferStartedAt ??= Date.now();
|
|
1500
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1501
|
+
progressType: 'transfer',
|
|
1502
|
+
progress,
|
|
1503
|
+
payload,
|
|
1504
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1505
|
+
});
|
|
1506
|
+
if (progress >= 100) {
|
|
1507
|
+
transferEndedAt ??= Date.now();
|
|
1508
|
+
}
|
|
1509
|
+
return;
|
|
1466
1510
|
}
|
|
1467
1511
|
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1512
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1513
|
+
installProgressEvents += 1;
|
|
1514
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1515
|
+
installStartedAt ??= Date.now();
|
|
1516
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1517
|
+
progressType: 'install',
|
|
1518
|
+
progress,
|
|
1519
|
+
payload,
|
|
1520
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1521
|
+
});
|
|
1522
|
+
if (progress >= 100) {
|
|
1523
|
+
installEndedAt ??= Date.now();
|
|
1524
|
+
}
|
|
1474
1525
|
}
|
|
1526
|
+
};
|
|
1475
1527
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1528
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1529
|
+
let result: unknown;
|
|
1530
|
+
try {
|
|
1531
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1532
|
+
} finally {
|
|
1533
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1534
|
+
}
|
|
1535
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1536
|
+
installEndedAt = Date.now();
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1540
|
+
attempt,
|
|
1541
|
+
maxAttempts,
|
|
1542
|
+
totalBytes,
|
|
1543
|
+
totalStartedAt,
|
|
1544
|
+
transferStartedAt,
|
|
1545
|
+
transferEndedAt,
|
|
1546
|
+
installStartedAt,
|
|
1547
|
+
installEndedAt,
|
|
1548
|
+
progressEvents,
|
|
1549
|
+
lastProgress,
|
|
1550
|
+
installProgressEvents,
|
|
1551
|
+
lastInstallProgress,
|
|
1552
|
+
retried,
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
if (result && typeof result === 'object') {
|
|
1556
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1557
|
+
return {
|
|
1558
|
+
...(result as Record<string, unknown>),
|
|
1559
|
+
payload: {
|
|
1560
|
+
...payload,
|
|
1561
|
+
metrics,
|
|
1562
|
+
},
|
|
1563
|
+
};
|
|
1485
1564
|
}
|
|
1486
1565
|
|
|
1487
|
-
return
|
|
1566
|
+
return result;
|
|
1488
1567
|
}
|
|
1489
1568
|
|
|
1490
1569
|
function buildFirmwareUpdateV4Params(opts: {
|
|
1491
1570
|
chunkSize?: string;
|
|
1492
|
-
resourceBundle?: string[];
|
|
1493
1571
|
romloader?: string;
|
|
1494
1572
|
bootloader?: string;
|
|
1495
1573
|
applicationP1?: string;
|
|
@@ -1499,6 +1577,7 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1499
1577
|
se02?: string;
|
|
1500
1578
|
se03?: string;
|
|
1501
1579
|
se04?: string;
|
|
1580
|
+
resourceArchive?: string;
|
|
1502
1581
|
forcedUpdateRes?: boolean;
|
|
1503
1582
|
}) {
|
|
1504
1583
|
const params = {
|
|
@@ -1506,7 +1585,6 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1506
1585
|
connectProtocol: 'V2' as const,
|
|
1507
1586
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1508
1587
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1509
|
-
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1510
1588
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1511
1589
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1512
1590
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1516,10 +1594,10 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1516
1594
|
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1517
1595
|
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1518
1596
|
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1597
|
+
resourceArchiveBinary: opts.resourceArchive ? readBinaryParam(opts.resourceArchive) : undefined,
|
|
1519
1598
|
};
|
|
1520
1599
|
|
|
1521
1600
|
const hasPayload = [
|
|
1522
|
-
params.resourceBundleFiles,
|
|
1523
1601
|
params.romloaderBinary,
|
|
1524
1602
|
params.bootloaderBinary,
|
|
1525
1603
|
params.applicationP1Binary,
|
|
@@ -1529,10 +1607,13 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1529
1607
|
params.se02Binary,
|
|
1530
1608
|
params.se03Binary,
|
|
1531
1609
|
params.se04Binary,
|
|
1610
|
+
params.resourceArchiveBinary,
|
|
1532
1611
|
].some(Boolean);
|
|
1533
1612
|
|
|
1534
1613
|
if (!hasPayload) {
|
|
1535
|
-
const err = new Error(
|
|
1614
|
+
const err = new Error(
|
|
1615
|
+
'firmware-update-v4 requires at least one firmware binary or resource archive path'
|
|
1616
|
+
);
|
|
1536
1617
|
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1537
1618
|
throw err;
|
|
1538
1619
|
}
|