@onekeyfe/hardware-cli 1.2.0-alpha.9 → 1.2.0-alpha.90
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 +78 -0
- package/dist/cli.js +316 -265
- 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 +82 -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 +396 -313
- 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')
|
|
@@ -727,7 +792,7 @@ const sessionCmd = program.command('session').description('Manage device passphr
|
|
|
727
792
|
|
|
728
793
|
sessionCmd
|
|
729
794
|
.command('connect')
|
|
730
|
-
.description('Connect device and
|
|
795
|
+
.description('Connect device and select a hidden wallet for this invocation')
|
|
731
796
|
.action(() =>
|
|
732
797
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
733
798
|
// 1. Search for device
|
|
@@ -739,7 +804,17 @@ sessionCmd
|
|
|
739
804
|
});
|
|
740
805
|
return;
|
|
741
806
|
}
|
|
742
|
-
const device =
|
|
807
|
+
const device = selectSearchDevice(
|
|
808
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
809
|
+
globalOpts.connectId
|
|
810
|
+
);
|
|
811
|
+
if (!device) {
|
|
812
|
+
outputResult(globalOpts, {
|
|
813
|
+
success: false,
|
|
814
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
815
|
+
});
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
743
818
|
const connectId = device.connectId || globalOpts.connectId;
|
|
744
819
|
|
|
745
820
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -750,62 +825,35 @@ sessionCmd
|
|
|
750
825
|
await unlockWithRetry(sdk, connectId);
|
|
751
826
|
}
|
|
752
827
|
|
|
753
|
-
// 3.
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
useEmptyPassphrase: false,
|
|
828
|
+
// 3. Open a hidden wallet session (triggers 1/2/3 selection).
|
|
829
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
830
|
+
mode: 'select-hidden',
|
|
757
831
|
});
|
|
758
|
-
if (!
|
|
759
|
-
outputResult(globalOpts,
|
|
832
|
+
if (!sessionResult.success) {
|
|
833
|
+
outputResult(globalOpts, sessionResult);
|
|
760
834
|
return;
|
|
761
835
|
}
|
|
762
|
-
|
|
763
|
-
psResult.payload
|
|
764
|
-
);
|
|
765
|
-
if (!passphraseState) {
|
|
836
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
766
837
|
outputResult(globalOpts, {
|
|
767
838
|
success: false,
|
|
768
|
-
payload: { error: '
|
|
839
|
+
payload: { error: 'Hidden wallet selection did not return a hidden wallet session' },
|
|
769
840
|
});
|
|
770
841
|
return;
|
|
771
842
|
}
|
|
843
|
+
const { deviceId, passphraseState } = sessionResult.payload;
|
|
772
844
|
|
|
773
845
|
// 4. Get address to verify + extract deviceId
|
|
774
|
-
const addrResult = await sdk.evmGetAddress(connectId,
|
|
846
|
+
const addrResult = await sdk.evmGetAddress(connectId, deviceId, {
|
|
775
847
|
path: "m/44'/60'/0'/0/0",
|
|
776
848
|
showOnOneKey: false,
|
|
777
849
|
passphraseState,
|
|
778
850
|
});
|
|
779
851
|
|
|
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
852
|
outputResult(globalOpts, {
|
|
804
853
|
success: true,
|
|
805
854
|
payload: {
|
|
806
855
|
passphraseState,
|
|
807
856
|
deviceId,
|
|
808
|
-
...(sessionId ? { sessionId } : {}),
|
|
809
857
|
...(addrResult?.success ? { address: addrResult.payload.address } : {}),
|
|
810
858
|
},
|
|
811
859
|
});
|
|
@@ -818,8 +866,10 @@ sessionCmd
|
|
|
818
866
|
.action(() =>
|
|
819
867
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
820
868
|
const searchResult = await sdk.searchDevices();
|
|
821
|
-
const device =
|
|
822
|
-
(searchResult?.payload as
|
|
869
|
+
const device = selectSearchDevice(
|
|
870
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
871
|
+
globalOpts.connectId
|
|
872
|
+
);
|
|
823
873
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
824
874
|
if (deviceId) {
|
|
825
875
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -914,12 +964,12 @@ async function unlockWithRetry(
|
|
|
914
964
|
* Prepare passphrase session before SDK calls.
|
|
915
965
|
*
|
|
916
966
|
* 1. If --use-empty-passphrase or --passphrase-state provided → use as-is
|
|
917
|
-
* 2. Try keychain → preloadSessionCache → use cached session
|
|
918
|
-
* 3. Keychain miss →
|
|
967
|
+
* 2. Try a legacy keychain entry → preloadSessionCache → use cached session
|
|
968
|
+
* 3. Keychain miss → openWalletSession (triggers 1/2/3 prompt)
|
|
919
969
|
*
|
|
920
970
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
921
971
|
*/
|
|
922
|
-
async function prepareSession(
|
|
972
|
+
export async function prepareSession(
|
|
923
973
|
sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default,
|
|
924
974
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
925
975
|
globalOpts: Record<string, any>
|
|
@@ -930,7 +980,7 @@ async function prepareSession(
|
|
|
930
980
|
}
|
|
931
981
|
|
|
932
982
|
// Errors from the SDK calls below (PIN cancelled, transport broken,
|
|
933
|
-
//
|
|
983
|
+
// openWalletSession rejection) intentionally propagate to runCommand's
|
|
934
984
|
// catch block, which renders them as structured `{ success: false,
|
|
935
985
|
// payload: { error, code } }` output instead of silently falling through
|
|
936
986
|
// to a confusing downstream error 112 / 114.
|
|
@@ -945,9 +995,30 @@ async function prepareSession(
|
|
|
945
995
|
return undefined;
|
|
946
996
|
}
|
|
947
997
|
|
|
948
|
-
const device =
|
|
998
|
+
const device = selectSearchDevice(
|
|
999
|
+
searchResult.payload as Array<{
|
|
1000
|
+
connectId?: string;
|
|
1001
|
+
deviceId?: string;
|
|
1002
|
+
deviceType?: string;
|
|
1003
|
+
features?: {
|
|
1004
|
+
deviceId?: string | null;
|
|
1005
|
+
deviceType?: string;
|
|
1006
|
+
sessionId?: string | null;
|
|
1007
|
+
passphraseProtection?: boolean | null;
|
|
1008
|
+
unlocked?: boolean | null;
|
|
1009
|
+
};
|
|
1010
|
+
}>,
|
|
1011
|
+
globalOpts.connectId
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
if (!device) {
|
|
1015
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const selectedDevice = device as {
|
|
949
1019
|
connectId?: string;
|
|
950
1020
|
deviceId?: string;
|
|
1021
|
+
deviceType?: string;
|
|
951
1022
|
features?: {
|
|
952
1023
|
deviceId?: string | null;
|
|
953
1024
|
deviceType?: string;
|
|
@@ -956,7 +1027,7 @@ async function prepareSession(
|
|
|
956
1027
|
unlocked?: boolean | null;
|
|
957
1028
|
};
|
|
958
1029
|
};
|
|
959
|
-
const connectId =
|
|
1030
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
960
1031
|
if (!globalOpts.connectId && connectId) {
|
|
961
1032
|
globalOpts.connectId = connectId;
|
|
962
1033
|
}
|
|
@@ -964,10 +1035,11 @@ async function prepareSession(
|
|
|
964
1035
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
965
1036
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
966
1037
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
967
|
-
let deviceId =
|
|
968
|
-
let deviceType =
|
|
969
|
-
|
|
970
|
-
let
|
|
1038
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1039
|
+
let deviceType =
|
|
1040
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1041
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1042
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
971
1043
|
|
|
972
1044
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
973
1045
|
try {
|
|
@@ -1006,7 +1078,7 @@ async function prepareSession(
|
|
|
1006
1078
|
return undefined;
|
|
1007
1079
|
}
|
|
1008
1080
|
|
|
1009
|
-
// ── Step 5: Try keychain session reuse
|
|
1081
|
+
// ── Step 5: Try legacy keychain session reuse ────────────────────
|
|
1010
1082
|
// Only attempt if device was already unlocked — locking invalidates
|
|
1011
1083
|
// all passphrase sessions, so cached session_id is useless after unlock.
|
|
1012
1084
|
if (!wasLocked && deviceId) {
|
|
@@ -1017,40 +1089,19 @@ async function prepareSession(
|
|
|
1017
1089
|
}
|
|
1018
1090
|
}
|
|
1019
1091
|
|
|
1020
|
-
// ── Step 6: Keychain miss →
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
useEmptyPassphrase: false,
|
|
1092
|
+
// ── Step 6: Keychain miss → openWalletSession (triggers 1/2/3 prompt) ──
|
|
1093
|
+
const sessionResult = await sdk.openWalletSession(connectId, {
|
|
1094
|
+
mode: 'select-hidden',
|
|
1024
1095
|
});
|
|
1025
1096
|
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
psResult.payload
|
|
1029
|
-
);
|
|
1030
|
-
if (!passphraseState) {
|
|
1097
|
+
if (sessionResult.success && sessionResult.payload) {
|
|
1098
|
+
if (sessionResult.payload.walletType !== 'hidden') {
|
|
1031
1099
|
return undefined;
|
|
1032
1100
|
}
|
|
1101
|
+
const { deviceId: sessionDeviceId, passphraseState } = sessionResult.payload;
|
|
1102
|
+
globalOpts.deviceId = sessionDeviceId;
|
|
1033
1103
|
globalOpts.passphraseState = passphraseState;
|
|
1034
1104
|
|
|
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
1105
|
return passphraseState;
|
|
1055
1106
|
}
|
|
1056
1107
|
return undefined;
|
|
@@ -1067,8 +1118,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1067
1118
|
) {
|
|
1068
1119
|
process.exitCode = 1;
|
|
1069
1120
|
}
|
|
1070
|
-
// No process.exit here — runCommand()
|
|
1071
|
-
//
|
|
1121
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1122
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1072
1123
|
}
|
|
1073
1124
|
|
|
1074
1125
|
/**
|
|
@@ -1080,7 +1131,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1080
1131
|
* 3. run the handler (which calls outputResult on success)
|
|
1081
1132
|
* 4. report uncaught errors as a structured failure result
|
|
1082
1133
|
* 5. dispose SDK
|
|
1083
|
-
* 6.
|
|
1134
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1084
1135
|
*
|
|
1085
1136
|
* This fixes three previous bugs:
|
|
1086
1137
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1132,9 +1183,7 @@ async function runCommand(
|
|
|
1132
1183
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1133
1184
|
await disposeSDK();
|
|
1134
1185
|
}
|
|
1135
|
-
//
|
|
1136
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1137
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1186
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1138
1187
|
}
|
|
1139
1188
|
|
|
1140
1189
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1162,29 +1211,46 @@ function readBinaryParam(path: string): ArrayBuffer {
|
|
|
1162
1211
|
return new Uint8Array(buffer).buffer;
|
|
1163
1212
|
}
|
|
1164
1213
|
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1214
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1215
|
+
sdk: AnySdk,
|
|
1216
|
+
explicitConnectId?: string,
|
|
1217
|
+
deviceName?: string
|
|
1218
|
+
): Promise<string> {
|
|
1219
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1220
|
+
|
|
1221
|
+
const searchResult = await sdk.searchDevices();
|
|
1222
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1223
|
+
throw new Error('Unable to scan BLE devices');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1227
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1228
|
+
const matches = normalizedName
|
|
1229
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1230
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1231
|
+
|
|
1232
|
+
if (matches.length === 0) {
|
|
1168
1233
|
throw new Error(
|
|
1169
|
-
|
|
1234
|
+
normalizedName
|
|
1235
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1236
|
+
: 'No Classic/Pure BLE device found'
|
|
1170
1237
|
);
|
|
1171
1238
|
}
|
|
1172
|
-
|
|
1173
|
-
const devicePath = spec.slice(sep + 1);
|
|
1174
|
-
if (!devicePath.startsWith('vol')) {
|
|
1239
|
+
if (matches.length > 1) {
|
|
1175
1240
|
throw new Error(
|
|
1176
|
-
|
|
1241
|
+
normalizedName
|
|
1242
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1243
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1177
1244
|
);
|
|
1178
1245
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1246
|
+
|
|
1247
|
+
const [{ connectId, name }] = matches;
|
|
1248
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1249
|
+
return connectId;
|
|
1183
1250
|
}
|
|
1184
1251
|
|
|
1185
1252
|
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1186
1253
|
return [
|
|
1187
|
-
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1188
1254
|
params.bootloaderBinary,
|
|
1189
1255
|
params.applicationP1Binary,
|
|
1190
1256
|
params.applicationP2Binary,
|
|
@@ -1234,6 +1300,30 @@ function formatFirmwareBytes(bytes: number) {
|
|
|
1234
1300
|
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1235
1301
|
}
|
|
1236
1302
|
|
|
1303
|
+
export function buildWallpaperUploadMetrics({
|
|
1304
|
+
totalBytes,
|
|
1305
|
+
transferredBytes,
|
|
1306
|
+
startedAt,
|
|
1307
|
+
endedAt,
|
|
1308
|
+
lastProgress,
|
|
1309
|
+
}: {
|
|
1310
|
+
totalBytes: number;
|
|
1311
|
+
transferredBytes: number;
|
|
1312
|
+
startedAt: number;
|
|
1313
|
+
endedAt: number;
|
|
1314
|
+
lastProgress: number;
|
|
1315
|
+
}) {
|
|
1316
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1317
|
+
return {
|
|
1318
|
+
totalBytes,
|
|
1319
|
+
transferredBytes,
|
|
1320
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1321
|
+
transferKiBPerSecond:
|
|
1322
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1323
|
+
lastProgress,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1237
1327
|
function maybePrintFirmwareProgress({
|
|
1238
1328
|
progressType,
|
|
1239
1329
|
progress,
|
|
@@ -1330,7 +1420,7 @@ function buildFirmwareUpdateV4Metrics({
|
|
|
1330
1420
|
};
|
|
1331
1421
|
}
|
|
1332
1422
|
|
|
1333
|
-
async function runFirmwareUpdateV4WithRetry({
|
|
1423
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1334
1424
|
sdk,
|
|
1335
1425
|
globalOpts,
|
|
1336
1426
|
params,
|
|
@@ -1344,152 +1434,147 @@ async function runFirmwareUpdateV4WithRetry({
|
|
|
1344
1434
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1345
1435
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1346
1436
|
let currentSdk = sdk;
|
|
1347
|
-
let lastResult: unknown;
|
|
1348
1437
|
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;
|
|
1438
|
+
let attempt = 1;
|
|
1439
|
+
let { connectId } = globalOpts;
|
|
1440
|
+
|
|
1441
|
+
if (globalOpts.transport === 'usb') {
|
|
1442
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1443
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1444
|
+
scope: 'runtime',
|
|
1445
|
+
connectProtocol: 'V2',
|
|
1446
|
+
retryCount: 0,
|
|
1447
|
+
});
|
|
1448
|
+
if (isSuccessResult(probeResult)) break;
|
|
1449
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1450
|
+
return probeResult;
|
|
1386
1451
|
}
|
|
1387
1452
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1453
|
+
retried = true;
|
|
1454
|
+
process.stderr.write(
|
|
1455
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1456
|
+
);
|
|
1457
|
+
await disposeSDK();
|
|
1458
|
+
await new Promise(resolve => {
|
|
1459
|
+
setTimeout(resolve, 3000);
|
|
1460
|
+
});
|
|
1461
|
+
currentSdk = await createSDK(globalOpts);
|
|
1462
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1391
1465
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1466
|
+
let progressEvents = 0;
|
|
1467
|
+
let lastProgress = -1;
|
|
1468
|
+
let transferStartedAt: number | undefined;
|
|
1469
|
+
let transferEndedAt: number | undefined;
|
|
1470
|
+
let installProgressEvents = 0;
|
|
1471
|
+
let lastInstallProgress = -1;
|
|
1472
|
+
let installStartedAt: number | undefined;
|
|
1473
|
+
let installEndedAt: number | undefined;
|
|
1474
|
+
let lastPrintedTransferProgress = -10;
|
|
1475
|
+
let lastPrintedInstallProgress = -10;
|
|
1476
|
+
const totalStartedAt = Date.now();
|
|
1477
|
+
|
|
1478
|
+
const onUiEvent = (message: unknown) => {
|
|
1479
|
+
if (!message || typeof message !== 'object') return;
|
|
1480
|
+
const messageType = (message as { type?: string }).type;
|
|
1481
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1482
|
+
|
|
1483
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1484
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1485
|
+
if (typeof tipMessage === 'string') {
|
|
1486
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1406
1487
|
}
|
|
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();
|
|
1488
|
+
return;
|
|
1432
1489
|
}
|
|
1433
1490
|
|
|
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
|
-
};
|
|
1491
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1492
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1493
|
+
process.stderr.write(
|
|
1494
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1495
|
+
);
|
|
1496
|
+
return;
|
|
1462
1497
|
}
|
|
1463
1498
|
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1499
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1500
|
+
const progress = Number(payload.progress);
|
|
1501
|
+
if (!Number.isFinite(progress)) return;
|
|
1502
|
+
|
|
1503
|
+
if (payload.progressType === 'transferData') {
|
|
1504
|
+
progressEvents += 1;
|
|
1505
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1506
|
+
transferStartedAt ??= Date.now();
|
|
1507
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1508
|
+
progressType: 'transfer',
|
|
1509
|
+
progress,
|
|
1510
|
+
payload,
|
|
1511
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1512
|
+
});
|
|
1513
|
+
if (progress >= 100) {
|
|
1514
|
+
transferEndedAt ??= Date.now();
|
|
1515
|
+
}
|
|
1516
|
+
return;
|
|
1466
1517
|
}
|
|
1467
1518
|
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1519
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1520
|
+
installProgressEvents += 1;
|
|
1521
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1522
|
+
installStartedAt ??= Date.now();
|
|
1523
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1524
|
+
progressType: 'install',
|
|
1525
|
+
progress,
|
|
1526
|
+
payload,
|
|
1527
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1528
|
+
});
|
|
1529
|
+
if (progress >= 100) {
|
|
1530
|
+
installEndedAt ??= Date.now();
|
|
1531
|
+
}
|
|
1474
1532
|
}
|
|
1533
|
+
};
|
|
1475
1534
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1535
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1536
|
+
let result: unknown;
|
|
1537
|
+
try {
|
|
1538
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1539
|
+
} finally {
|
|
1540
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1541
|
+
}
|
|
1542
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1543
|
+
installEndedAt = Date.now();
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1547
|
+
attempt,
|
|
1548
|
+
maxAttempts,
|
|
1549
|
+
totalBytes,
|
|
1550
|
+
totalStartedAt,
|
|
1551
|
+
transferStartedAt,
|
|
1552
|
+
transferEndedAt,
|
|
1553
|
+
installStartedAt,
|
|
1554
|
+
installEndedAt,
|
|
1555
|
+
progressEvents,
|
|
1556
|
+
lastProgress,
|
|
1557
|
+
installProgressEvents,
|
|
1558
|
+
lastInstallProgress,
|
|
1559
|
+
retried,
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
if (result && typeof result === 'object') {
|
|
1563
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1564
|
+
return {
|
|
1565
|
+
...(result as Record<string, unknown>),
|
|
1566
|
+
payload: {
|
|
1567
|
+
...payload,
|
|
1568
|
+
metrics,
|
|
1569
|
+
},
|
|
1570
|
+
};
|
|
1485
1571
|
}
|
|
1486
1572
|
|
|
1487
|
-
return
|
|
1573
|
+
return result;
|
|
1488
1574
|
}
|
|
1489
1575
|
|
|
1490
1576
|
function buildFirmwareUpdateV4Params(opts: {
|
|
1491
1577
|
chunkSize?: string;
|
|
1492
|
-
resourceBundle?: string[];
|
|
1493
1578
|
romloader?: string;
|
|
1494
1579
|
bootloader?: string;
|
|
1495
1580
|
applicationP1?: string;
|
|
@@ -1506,7 +1591,6 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1506
1591
|
connectProtocol: 'V2' as const,
|
|
1507
1592
|
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1508
1593
|
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1509
|
-
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1510
1594
|
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1511
1595
|
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1512
1596
|
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
@@ -1519,7 +1603,6 @@ function buildFirmwareUpdateV4Params(opts: {
|
|
|
1519
1603
|
};
|
|
1520
1604
|
|
|
1521
1605
|
const hasPayload = [
|
|
1522
|
-
params.resourceBundleFiles,
|
|
1523
1606
|
params.romloaderBinary,
|
|
1524
1607
|
params.bootloaderBinary,
|
|
1525
1608
|
params.applicationP1Binary,
|