@onekeyfe/hardware-cli 1.2.0-alpha.2 → 1.2.0-alpha.21
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 +17 -1
- package/dist/cli.js +480 -54
- 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/sdk.d.ts +2 -0
- package/dist/sdk.js +8 -6
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +384 -0
- package/package.json +8 -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 +20 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +19 -0
- package/src/__tests__/noble-ble-plugin.test.ts +225 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +691 -66
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/sdk.ts +15 -7
- package/src/transports/nobleBlePlugin.ts +498 -0
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import { Command } from 'commander';
|
|
4
|
+
import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
|
|
5
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
6
|
|
|
3
|
-
import { createSDK, disposeSDK } from './sdk';
|
|
4
|
-
import {
|
|
5
|
-
clearSessionFromKeychain,
|
|
6
|
-
preloadSessionFromKeychain,
|
|
7
|
-
saveSessionToKeychain,
|
|
8
|
-
} from './session';
|
|
9
7
|
import {
|
|
10
8
|
resolveBatchGetAddress,
|
|
11
9
|
resolveGetAddress,
|
|
@@ -13,14 +11,20 @@ import {
|
|
|
13
11
|
resolveSignMessage,
|
|
14
12
|
resolveSignTransaction,
|
|
15
13
|
} from './chains';
|
|
14
|
+
import { selectSearchDevice } from './deviceSelection';
|
|
15
|
+
import { getCanonicalDeviceState, getCompatibleFeatures } from './deviceStateCommands';
|
|
16
|
+
import { createSDK, disposeSDK } from './sdk';
|
|
17
|
+
import {
|
|
18
|
+
clearSessionFromKeychain,
|
|
19
|
+
preloadSessionFromKeychain,
|
|
20
|
+
saveSessionToKeychain,
|
|
21
|
+
} from './session';
|
|
16
22
|
|
|
17
|
-
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
18
|
-
import { getDeviceType } from '@onekeyfe/hd-core';
|
|
19
23
|
import type {
|
|
24
|
+
DeviceStateScope,
|
|
20
25
|
EthereumSignTypedDataMessage,
|
|
21
26
|
EthereumSignTypedDataTypes,
|
|
22
27
|
Features,
|
|
23
|
-
IDeviceType,
|
|
24
28
|
SearchDevice,
|
|
25
29
|
} from '@onekeyfe/hd-core';
|
|
26
30
|
|
|
@@ -63,11 +67,14 @@ function extractPassphraseSession(payload: unknown): {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
const program = new Command();
|
|
70
|
+
const { version: cliVersion } = JSON.parse(
|
|
71
|
+
readFileSync(resolve(__dirname, '../package.json'), 'utf8')
|
|
72
|
+
) as { version: string };
|
|
66
73
|
|
|
67
74
|
program
|
|
68
75
|
.name('onekey-hw')
|
|
69
76
|
.description('OneKey hardware wallet CLI for AI agent integration')
|
|
70
|
-
.version(
|
|
77
|
+
.version(cliVersion);
|
|
71
78
|
|
|
72
79
|
// ============================================================
|
|
73
80
|
// Global Options
|
|
@@ -78,8 +85,10 @@ program.option(
|
|
|
78
85
|
'--device-id <id>',
|
|
79
86
|
'Persistent device ID from getFeatures (changes when seed changes)'
|
|
80
87
|
);
|
|
88
|
+
program.option('--transport <transport>', 'Transport to use: usb or ble', 'usb');
|
|
81
89
|
program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
|
|
82
90
|
program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
|
|
91
|
+
program.option('--debug', 'Enable SDK debug logs');
|
|
83
92
|
|
|
84
93
|
// ============================================================
|
|
85
94
|
// Device Commands
|
|
@@ -91,28 +100,6 @@ program
|
|
|
91
100
|
.action(() =>
|
|
92
101
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
93
102
|
const result = await sdk.searchDevices();
|
|
94
|
-
|
|
95
|
-
// Auto-fetch features for each discovered device (doesn't require PIN)
|
|
96
|
-
if (result?.success && Array.isArray(result.payload)) {
|
|
97
|
-
for (const device of result.payload as EnrichedSearchDevice[]) {
|
|
98
|
-
if (device.connectId) {
|
|
99
|
-
try {
|
|
100
|
-
const features = await sdk.getFeatures(device.connectId);
|
|
101
|
-
if (features?.success && features.payload) {
|
|
102
|
-
device.features = features.payload;
|
|
103
|
-
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
104
|
-
const devType = features.payload.deviceType;
|
|
105
|
-
if (devType) {
|
|
106
|
-
device.deviceType = devType as IDeviceType;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
// Features fetch failed — device may need PIN, continue with basic info
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
103
|
outputResult(globalOpts, result);
|
|
117
104
|
})
|
|
118
105
|
);
|
|
@@ -122,28 +109,120 @@ program
|
|
|
122
109
|
.description('Get device features (firmware, unlock state, passphrase protection, etc.)')
|
|
123
110
|
.action(() =>
|
|
124
111
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
112
|
+
const result = await getCompatibleFeatures(sdk, globalOpts.connectId);
|
|
113
|
+
outputResult(globalOpts, result);
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
program
|
|
118
|
+
.command('get-state')
|
|
119
|
+
.description('Get canonical device state for Protocol V1 and Protocol V2 devices')
|
|
120
|
+
.option('--scope <scope>', 'State refresh scope: runtime, settings, or firmware', 'runtime')
|
|
121
|
+
.action((opts: { scope: string }) =>
|
|
122
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
123
|
+
const supportedScopes: DeviceStateScope[] = ['runtime', 'settings', 'firmware'];
|
|
124
|
+
if (!supportedScopes.includes(opts.scope as DeviceStateScope)) {
|
|
125
|
+
const error = new Error(`Unsupported device state scope: ${opts.scope}`);
|
|
126
|
+
(error as Error & { code?: string }).code = 'INVALID_DEVICE_STATE_SCOPE';
|
|
127
|
+
throw error;
|
|
141
128
|
}
|
|
142
|
-
const result = await
|
|
129
|
+
const result = await getCanonicalDeviceState(
|
|
130
|
+
sdk,
|
|
131
|
+
globalOpts.connectId,
|
|
132
|
+
opts.scope as DeviceStateScope
|
|
133
|
+
);
|
|
143
134
|
outputResult(globalOpts, result);
|
|
144
135
|
})
|
|
145
136
|
);
|
|
146
137
|
|
|
138
|
+
program
|
|
139
|
+
.command('upload-wallpaper')
|
|
140
|
+
.description('Upload and activate a Pro2 wallpaper')
|
|
141
|
+
.requiredOption('--rgba <path>', '604x1024 raw RGBA file')
|
|
142
|
+
.option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
|
|
143
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
144
|
+
.action(opts =>
|
|
145
|
+
runCommand({}, async ({ sdk, globalOpts, params }) => {
|
|
146
|
+
const rgba = readBinaryParam(opts.rgba);
|
|
147
|
+
const expectedBytes = 604 * 1024 * 4;
|
|
148
|
+
if (rgba.byteLength !== expectedBytes) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let transferStartedAt: number | undefined;
|
|
155
|
+
let transferEndedAt: number | undefined;
|
|
156
|
+
let lastProgress = -1;
|
|
157
|
+
let lastPrintedProgress = -10;
|
|
158
|
+
let progressTotalBytes = 0;
|
|
159
|
+
let transferredBytes = 0;
|
|
160
|
+
const totalStartedAt = Date.now();
|
|
161
|
+
const onUiEvent = (message: unknown) => {
|
|
162
|
+
if (!message || typeof message !== 'object') return;
|
|
163
|
+
const event = message as {
|
|
164
|
+
type?: string;
|
|
165
|
+
payload?: {
|
|
166
|
+
progress?: number;
|
|
167
|
+
transferredBytes?: number;
|
|
168
|
+
totalBytes?: number;
|
|
169
|
+
rateBytesPerSecond?: number;
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
if (event.type !== UI_REQUEST.DEVICE_PROGRESS || !event.payload) return;
|
|
173
|
+
const progress = Number(event.payload.progress);
|
|
174
|
+
if (!Number.isFinite(progress)) return;
|
|
175
|
+
transferStartedAt ??= Date.now();
|
|
176
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
177
|
+
const totalBytes = Number(event.payload.totalBytes);
|
|
178
|
+
if (Number.isFinite(totalBytes) && totalBytes > 0) progressTotalBytes = totalBytes;
|
|
179
|
+
const confirmedBytes = Number(event.payload.transferredBytes);
|
|
180
|
+
if (Number.isFinite(confirmedBytes) && confirmedBytes >= 0) {
|
|
181
|
+
transferredBytes = Math.max(transferredBytes, confirmedBytes);
|
|
182
|
+
}
|
|
183
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
184
|
+
if (printableProgress > lastPrintedProgress || progress >= 100) {
|
|
185
|
+
const rate = Number(event.payload.rateBytesPerSecond);
|
|
186
|
+
const rateText =
|
|
187
|
+
Number.isFinite(rate) && rate > 0 ? ` ${(rate / 1024).toFixed(2)} KiB/s` : '';
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`[onekey-hw] Wallpaper transfer: ${Math.round(progress)}%${rateText}\n`
|
|
190
|
+
);
|
|
191
|
+
lastPrintedProgress = progress >= 100 ? 100 : printableProgress;
|
|
192
|
+
}
|
|
193
|
+
if (progress >= 100) transferEndedAt ??= Date.now();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
sdk.on(UI_EVENT, onUiEvent);
|
|
197
|
+
let result: any;
|
|
198
|
+
try {
|
|
199
|
+
result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
|
|
200
|
+
...params,
|
|
201
|
+
width: 604,
|
|
202
|
+
height: 1024,
|
|
203
|
+
rgba,
|
|
204
|
+
fileName: opts.fileName,
|
|
205
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
206
|
+
});
|
|
207
|
+
} finally {
|
|
208
|
+
sdk.off?.(UI_EVENT, onUiEvent);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const endedAt = transferEndedAt ?? Date.now();
|
|
212
|
+
const totalBytes = Number(result?.payload?.size) || progressTotalBytes;
|
|
213
|
+
outputResult(globalOpts, {
|
|
214
|
+
...result,
|
|
215
|
+
metrics: buildWallpaperUploadMetrics({
|
|
216
|
+
totalBytes,
|
|
217
|
+
transferredBytes: result?.success ? totalBytes : transferredBytes,
|
|
218
|
+
startedAt: transferStartedAt ?? totalStartedAt,
|
|
219
|
+
endedAt,
|
|
220
|
+
lastProgress,
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
223
|
+
})
|
|
224
|
+
);
|
|
225
|
+
|
|
147
226
|
// ============================================================
|
|
148
227
|
// Signing Commands
|
|
149
228
|
// ============================================================
|
|
@@ -538,20 +617,84 @@ program
|
|
|
538
617
|
})
|
|
539
618
|
);
|
|
540
619
|
|
|
620
|
+
program
|
|
621
|
+
.command('firmware-update-legacy')
|
|
622
|
+
.description('Update Classic/Pure firmware through the legacy protocol')
|
|
623
|
+
.requiredOption('--binary <path>', 'Local firmware binary path')
|
|
624
|
+
.option('--device-name <name>', 'BLE advertising name, for example K1514')
|
|
625
|
+
.option('--update-type <type>', 'Firmware component: firmware or ble', 'firmware')
|
|
626
|
+
.option('--no-reboot', 'Do not reboot the device after a successful update')
|
|
627
|
+
.action(opts =>
|
|
628
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
629
|
+
if (opts.updateType !== 'firmware' && opts.updateType !== 'ble') {
|
|
630
|
+
throw new Error(`Unsupported --update-type: ${opts.updateType}. Use "firmware" or "ble".`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const connectId = await resolveLegacyFirmwareConnectId(
|
|
634
|
+
sdk,
|
|
635
|
+
globalOpts.connectId,
|
|
636
|
+
opts.deviceName
|
|
637
|
+
);
|
|
638
|
+
const result = await sdk.firmwareUpdate(connectId, {
|
|
639
|
+
binary: readBinaryParam(opts.binary),
|
|
640
|
+
updateType: opts.updateType,
|
|
641
|
+
rebootOnSuccess: opts.reboot,
|
|
642
|
+
timeout: getLegacyFirmwareConnectTimeout(globalOpts.transport),
|
|
643
|
+
});
|
|
644
|
+
outputResult(globalOpts, result);
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
export function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble') {
|
|
649
|
+
return transport === 'usb' ? 90_000 : undefined;
|
|
650
|
+
}
|
|
651
|
+
|
|
541
652
|
program
|
|
542
653
|
.command('firmware-update-ble')
|
|
543
|
-
.description('
|
|
654
|
+
.description('Run Protocol V2 firmware update over BLE')
|
|
544
655
|
.action(() =>
|
|
545
656
|
respondAndExit({
|
|
546
657
|
success: false,
|
|
547
658
|
payload: {
|
|
548
659
|
error:
|
|
549
|
-
'
|
|
550
|
-
code: '
|
|
660
|
+
'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
661
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
551
662
|
},
|
|
552
663
|
})
|
|
553
664
|
);
|
|
554
665
|
|
|
666
|
+
program
|
|
667
|
+
.command('firmware-update-v4')
|
|
668
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
669
|
+
.option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
|
|
670
|
+
.option(
|
|
671
|
+
'--resource-bundle <spec...>',
|
|
672
|
+
'RESC bundle direct-write spec: <localPath>:<devicePath>, e.g. wallpaper.okpkg:vol0:/bundles/images/wallpaper.okpkg'
|
|
673
|
+
)
|
|
674
|
+
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
675
|
+
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
676
|
+
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
677
|
+
.option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
|
|
678
|
+
.option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
|
|
679
|
+
.option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
|
|
680
|
+
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
681
|
+
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
682
|
+
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
683
|
+
.option('--forced-update-res', 'Force resource update')
|
|
684
|
+
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
685
|
+
.action(opts =>
|
|
686
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
687
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
688
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
689
|
+
sdk,
|
|
690
|
+
globalOpts,
|
|
691
|
+
params,
|
|
692
|
+
retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
|
|
693
|
+
});
|
|
694
|
+
outputResult(globalOpts, result);
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
|
|
555
698
|
program
|
|
556
699
|
.command('bootloader-check')
|
|
557
700
|
.description('Check bootloader version and status')
|
|
@@ -704,7 +847,7 @@ sessionCmd
|
|
|
704
847
|
});
|
|
705
848
|
return;
|
|
706
849
|
}
|
|
707
|
-
const device = searchResult.payload[0]
|
|
850
|
+
const device: EnrichedSearchDevice = searchResult.payload[0];
|
|
708
851
|
const connectId = device.connectId || globalOpts.connectId;
|
|
709
852
|
|
|
710
853
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -910,8 +1053,39 @@ async function prepareSession(
|
|
|
910
1053
|
return undefined;
|
|
911
1054
|
}
|
|
912
1055
|
|
|
913
|
-
const device =
|
|
914
|
-
|
|
1056
|
+
const device = selectSearchDevice(
|
|
1057
|
+
searchResult.payload as Array<{
|
|
1058
|
+
connectId?: string;
|
|
1059
|
+
deviceId?: string;
|
|
1060
|
+
deviceType?: string;
|
|
1061
|
+
features?: {
|
|
1062
|
+
deviceId?: string | null;
|
|
1063
|
+
deviceType?: string;
|
|
1064
|
+
sessionId?: string | null;
|
|
1065
|
+
passphraseProtection?: boolean | null;
|
|
1066
|
+
unlocked?: boolean | null;
|
|
1067
|
+
};
|
|
1068
|
+
}>,
|
|
1069
|
+
globalOpts.connectId
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
if (!device) {
|
|
1073
|
+
throw new Error(`未找到指定的 BLE 设备: ${globalOpts.connectId}`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const selectedDevice = device as {
|
|
1077
|
+
connectId?: string;
|
|
1078
|
+
deviceId?: string;
|
|
1079
|
+
deviceType?: string;
|
|
1080
|
+
features?: {
|
|
1081
|
+
deviceId?: string | null;
|
|
1082
|
+
deviceType?: string;
|
|
1083
|
+
sessionId?: string | null;
|
|
1084
|
+
passphraseProtection?: boolean | null;
|
|
1085
|
+
unlocked?: boolean | null;
|
|
1086
|
+
};
|
|
1087
|
+
};
|
|
1088
|
+
const connectId = selectedDevice.connectId || globalOpts.connectId || '';
|
|
915
1089
|
if (!globalOpts.connectId && connectId) {
|
|
916
1090
|
globalOpts.connectId = connectId;
|
|
917
1091
|
}
|
|
@@ -919,10 +1093,11 @@ async function prepareSession(
|
|
|
919
1093
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
920
1094
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
921
1095
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
922
|
-
let deviceId =
|
|
923
|
-
let deviceType =
|
|
924
|
-
|
|
925
|
-
let
|
|
1096
|
+
let deviceId = selectedDevice.features?.deviceId || selectedDevice.deviceId || '';
|
|
1097
|
+
let deviceType =
|
|
1098
|
+
selectedDevice.features?.deviceType ?? selectedDevice.deviceType ?? EDeviceType.Unknown;
|
|
1099
|
+
let unlocked = selectedDevice.features?.unlocked;
|
|
1100
|
+
let passphraseProtection = selectedDevice.features?.passphraseProtection;
|
|
926
1101
|
|
|
927
1102
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
928
1103
|
try {
|
|
@@ -1022,8 +1197,8 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1022
1197
|
) {
|
|
1023
1198
|
process.exitCode = 1;
|
|
1024
1199
|
}
|
|
1025
|
-
// No process.exit here — runCommand()
|
|
1026
|
-
//
|
|
1200
|
+
// No process.exit here — runCommand() waits for SDK cleanup, then lets Node
|
|
1201
|
+
// exit naturally so leaked USB handles remain observable.
|
|
1027
1202
|
}
|
|
1028
1203
|
|
|
1029
1204
|
/**
|
|
@@ -1035,7 +1210,7 @@ function outputResult(_globalOpts: Record<string, any>, result: unknown): void {
|
|
|
1035
1210
|
* 3. run the handler (which calls outputResult on success)
|
|
1036
1211
|
* 4. report uncaught errors as a structured failure result
|
|
1037
1212
|
* 5. dispose SDK
|
|
1038
|
-
* 6.
|
|
1213
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
1039
1214
|
*
|
|
1040
1215
|
* This fixes three previous bugs:
|
|
1041
1216
|
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
@@ -1061,6 +1236,9 @@ async function runCommand(
|
|
|
1061
1236
|
): Promise<void> {
|
|
1062
1237
|
const globalOpts = program.opts();
|
|
1063
1238
|
try {
|
|
1239
|
+
if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
|
|
1240
|
+
throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
|
|
1241
|
+
}
|
|
1064
1242
|
const sdk = await createSDK(globalOpts);
|
|
1065
1243
|
if (options.needsSession) {
|
|
1066
1244
|
await prepareSession(sdk, globalOpts);
|
|
@@ -1084,9 +1262,7 @@ async function runCommand(
|
|
|
1084
1262
|
// promise reference. Idempotent, safe to call even if init failed.
|
|
1085
1263
|
await disposeSDK();
|
|
1086
1264
|
}
|
|
1087
|
-
//
|
|
1088
|
-
// setImmediate lets any trailing stdout/stderr writes flush first.
|
|
1089
|
-
setImmediate(() => process.exit(process.exitCode ?? 0));
|
|
1265
|
+
// disposeSDK awaits transport cleanup; let Node exit naturally so leaks remain visible.
|
|
1090
1266
|
}
|
|
1091
1267
|
|
|
1092
1268
|
/** For commands that don't touch the SDK at all (e.g. firmware-update stubs). */
|
|
@@ -1109,6 +1285,451 @@ function safeJsonParse(input: string, label: string): unknown {
|
|
|
1109
1285
|
}
|
|
1110
1286
|
}
|
|
1111
1287
|
|
|
1288
|
+
function readBinaryParam(path: string): ArrayBuffer {
|
|
1289
|
+
const buffer = readFileSync(path);
|
|
1290
|
+
return new Uint8Array(buffer).buffer;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async function resolveLegacyFirmwareConnectId(
|
|
1294
|
+
sdk: AnySdk,
|
|
1295
|
+
explicitConnectId?: string,
|
|
1296
|
+
deviceName?: string
|
|
1297
|
+
): Promise<string> {
|
|
1298
|
+
if (explicitConnectId && !deviceName) return explicitConnectId;
|
|
1299
|
+
|
|
1300
|
+
const searchResult = await sdk.searchDevices();
|
|
1301
|
+
if (!searchResult?.success || !Array.isArray(searchResult.payload)) {
|
|
1302
|
+
throw new Error('Unable to scan BLE devices');
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const devices = searchResult.payload as EnrichedSearchDevice[];
|
|
1306
|
+
const normalizedName = deviceName?.trim().toLowerCase();
|
|
1307
|
+
const matches = normalizedName
|
|
1308
|
+
? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
|
|
1309
|
+
: devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
|
|
1310
|
+
|
|
1311
|
+
if (matches.length === 0) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
normalizedName
|
|
1314
|
+
? `BLE device not found by name: ${deviceName}`
|
|
1315
|
+
: 'No Classic/Pure BLE device found'
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
if (matches.length > 1) {
|
|
1319
|
+
throw new Error(
|
|
1320
|
+
normalizedName
|
|
1321
|
+
? `Multiple BLE devices found by name: ${deviceName}`
|
|
1322
|
+
: 'Multiple Classic/Pure BLE devices found; specify --device-name'
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const [{ connectId, name }] = matches;
|
|
1327
|
+
if (!connectId) throw new Error(`BLE device has no connect ID: ${name}`);
|
|
1328
|
+
return connectId;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function parseResourceBundleParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
|
|
1332
|
+
const sep = spec.indexOf(':');
|
|
1333
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
1334
|
+
throw new Error(
|
|
1335
|
+
`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
const localPath = spec.slice(0, sep);
|
|
1339
|
+
const devicePath = spec.slice(sep + 1);
|
|
1340
|
+
if (!devicePath.startsWith('vol')) {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
binary: readBinaryParam(localPath),
|
|
1347
|
+
devicePath,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1352
|
+
return [
|
|
1353
|
+
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1354
|
+
params.bootloaderBinary,
|
|
1355
|
+
params.applicationP1Binary,
|
|
1356
|
+
params.applicationP2Binary,
|
|
1357
|
+
params.coprocessorBinary,
|
|
1358
|
+
params.se01Binary,
|
|
1359
|
+
params.se02Binary,
|
|
1360
|
+
params.se03Binary,
|
|
1361
|
+
params.se04Binary,
|
|
1362
|
+
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function getFirmwareUpdateV4ErrorText(result: unknown) {
|
|
1366
|
+
if (!result || typeof result !== 'object') return '';
|
|
1367
|
+
const { payload } = result as { payload?: unknown };
|
|
1368
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
1369
|
+
const { error } = payload as { error?: unknown };
|
|
1370
|
+
return typeof error === 'string' ? error : '';
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function isProtocolV2UsbProbeTransientResult(result: unknown) {
|
|
1374
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1375
|
+
return (
|
|
1376
|
+
error.includes('Device protocol mismatch') &&
|
|
1377
|
+
error.includes('expected V2') &&
|
|
1378
|
+
error.includes('did not respond to expected protocol')
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function isSuccessResult(result: unknown) {
|
|
1383
|
+
return (
|
|
1384
|
+
!!result && typeof result === 'object' && (result as { success?: boolean }).success === true
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function getFirmwareUpdatePayload(message: unknown) {
|
|
1389
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
1390
|
+
return (message as { payload?: Record<string, unknown> }).payload;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function formatFirmwareProgress(progress: number) {
|
|
1394
|
+
if (!Number.isFinite(progress)) return '0%';
|
|
1395
|
+
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function formatFirmwareBytes(bytes: number) {
|
|
1399
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
1400
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
export function buildWallpaperUploadMetrics({
|
|
1404
|
+
totalBytes,
|
|
1405
|
+
transferredBytes,
|
|
1406
|
+
startedAt,
|
|
1407
|
+
endedAt,
|
|
1408
|
+
lastProgress,
|
|
1409
|
+
}: {
|
|
1410
|
+
totalBytes: number;
|
|
1411
|
+
transferredBytes: number;
|
|
1412
|
+
startedAt: number;
|
|
1413
|
+
endedAt: number;
|
|
1414
|
+
lastProgress: number;
|
|
1415
|
+
}) {
|
|
1416
|
+
const elapsedMs = Math.max(endedAt - startedAt, 0);
|
|
1417
|
+
return {
|
|
1418
|
+
totalBytes,
|
|
1419
|
+
transferredBytes,
|
|
1420
|
+
totalSeconds: Number((elapsedMs / 1000).toFixed(2)),
|
|
1421
|
+
transferKiBPerSecond:
|
|
1422
|
+
elapsedMs > 0 ? Number((transferredBytes / 1024 / (elapsedMs / 1000)).toFixed(2)) : null,
|
|
1423
|
+
lastProgress,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function maybePrintFirmwareProgress({
|
|
1428
|
+
progressType,
|
|
1429
|
+
progress,
|
|
1430
|
+
payload,
|
|
1431
|
+
lastPrintedProgress,
|
|
1432
|
+
}: {
|
|
1433
|
+
progressType: string;
|
|
1434
|
+
progress: number;
|
|
1435
|
+
payload: Record<string, unknown>;
|
|
1436
|
+
lastPrintedProgress: number;
|
|
1437
|
+
}) {
|
|
1438
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
1439
|
+
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
1440
|
+
return lastPrintedProgress;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const transferredBytes = Number(payload.transferredBytes);
|
|
1444
|
+
const totalBytes = Number(payload.totalBytes);
|
|
1445
|
+
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1446
|
+
const sizeText =
|
|
1447
|
+
Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1448
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1449
|
+
: '';
|
|
1450
|
+
const speedText =
|
|
1451
|
+
Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
1452
|
+
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
1453
|
+
: '';
|
|
1454
|
+
|
|
1455
|
+
process.stderr.write(
|
|
1456
|
+
`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(
|
|
1457
|
+
progress
|
|
1458
|
+
)}${sizeText}${speedText}\n`
|
|
1459
|
+
);
|
|
1460
|
+
return progress >= 100 ? 100 : printableProgress;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function buildFirmwareUpdateV4Metrics({
|
|
1464
|
+
attempt,
|
|
1465
|
+
maxAttempts,
|
|
1466
|
+
totalBytes,
|
|
1467
|
+
totalStartedAt,
|
|
1468
|
+
transferStartedAt,
|
|
1469
|
+
transferEndedAt,
|
|
1470
|
+
installStartedAt,
|
|
1471
|
+
installEndedAt,
|
|
1472
|
+
progressEvents,
|
|
1473
|
+
lastProgress,
|
|
1474
|
+
installProgressEvents,
|
|
1475
|
+
lastInstallProgress,
|
|
1476
|
+
retried,
|
|
1477
|
+
}: {
|
|
1478
|
+
attempt: number;
|
|
1479
|
+
maxAttempts: number;
|
|
1480
|
+
totalBytes: number;
|
|
1481
|
+
totalStartedAt: number;
|
|
1482
|
+
transferStartedAt?: number;
|
|
1483
|
+
transferEndedAt?: number;
|
|
1484
|
+
installStartedAt?: number;
|
|
1485
|
+
installEndedAt?: number;
|
|
1486
|
+
progressEvents: number;
|
|
1487
|
+
lastProgress: number;
|
|
1488
|
+
installProgressEvents: number;
|
|
1489
|
+
lastInstallProgress: number;
|
|
1490
|
+
retried: boolean;
|
|
1491
|
+
}) {
|
|
1492
|
+
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
1493
|
+
const transferElapsedMs =
|
|
1494
|
+
transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
1495
|
+
? transferEndedAt - transferStartedAt
|
|
1496
|
+
: undefined;
|
|
1497
|
+
const installElapsedMs =
|
|
1498
|
+
installStartedAt !== undefined && installEndedAt !== undefined
|
|
1499
|
+
? installEndedAt - installStartedAt
|
|
1500
|
+
: undefined;
|
|
1501
|
+
|
|
1502
|
+
return {
|
|
1503
|
+
attempt,
|
|
1504
|
+
maxAttempts,
|
|
1505
|
+
retried,
|
|
1506
|
+
totalBytes,
|
|
1507
|
+
progressEvents,
|
|
1508
|
+
lastProgress,
|
|
1509
|
+
installProgressEvents,
|
|
1510
|
+
lastInstallProgress,
|
|
1511
|
+
transferSeconds:
|
|
1512
|
+
transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
|
|
1513
|
+
transferKiBPerSecond:
|
|
1514
|
+
transferElapsedMs !== undefined && transferElapsedMs > 0
|
|
1515
|
+
? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
|
|
1516
|
+
: null,
|
|
1517
|
+
installSeconds:
|
|
1518
|
+
installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
|
|
1519
|
+
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
async function runFirmwareUpdateV4WithRetry({
|
|
1524
|
+
sdk,
|
|
1525
|
+
globalOpts,
|
|
1526
|
+
params,
|
|
1527
|
+
retries,
|
|
1528
|
+
}: {
|
|
1529
|
+
sdk: AnySdk;
|
|
1530
|
+
globalOpts: Record<string, any>;
|
|
1531
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
1532
|
+
retries?: number;
|
|
1533
|
+
}) {
|
|
1534
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1535
|
+
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1536
|
+
let currentSdk = sdk;
|
|
1537
|
+
let lastResult: unknown;
|
|
1538
|
+
let retried = false;
|
|
1539
|
+
|
|
1540
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1541
|
+
let progressEvents = 0;
|
|
1542
|
+
let lastProgress = -1;
|
|
1543
|
+
let transferStartedAt: number | undefined;
|
|
1544
|
+
let transferEndedAt: number | undefined;
|
|
1545
|
+
let installProgressEvents = 0;
|
|
1546
|
+
let lastInstallProgress = -1;
|
|
1547
|
+
let installStartedAt: number | undefined;
|
|
1548
|
+
let installEndedAt: number | undefined;
|
|
1549
|
+
let lastPrintedTransferProgress = -10;
|
|
1550
|
+
let lastPrintedInstallProgress = -10;
|
|
1551
|
+
const totalStartedAt = Date.now();
|
|
1552
|
+
const connectId =
|
|
1553
|
+
retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1554
|
+
? undefined
|
|
1555
|
+
: globalOpts.connectId;
|
|
1556
|
+
|
|
1557
|
+
const onUiEvent = (message: unknown) => {
|
|
1558
|
+
if (!message || typeof message !== 'object') return;
|
|
1559
|
+
const messageType = (message as { type?: string }).type;
|
|
1560
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1561
|
+
|
|
1562
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1563
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1564
|
+
if (typeof tipMessage === 'string') {
|
|
1565
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1571
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1572
|
+
process.stderr.write(
|
|
1573
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1574
|
+
);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1579
|
+
const progress = Number(payload.progress);
|
|
1580
|
+
if (!Number.isFinite(progress)) return;
|
|
1581
|
+
|
|
1582
|
+
if (payload.progressType === 'transferData') {
|
|
1583
|
+
progressEvents += 1;
|
|
1584
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1585
|
+
transferStartedAt ??= Date.now();
|
|
1586
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1587
|
+
progressType: 'transfer',
|
|
1588
|
+
progress,
|
|
1589
|
+
payload,
|
|
1590
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1591
|
+
});
|
|
1592
|
+
if (progress >= 100) {
|
|
1593
|
+
transferEndedAt ??= Date.now();
|
|
1594
|
+
}
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1599
|
+
installProgressEvents += 1;
|
|
1600
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1601
|
+
installStartedAt ??= Date.now();
|
|
1602
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1603
|
+
progressType: 'install',
|
|
1604
|
+
progress,
|
|
1605
|
+
payload,
|
|
1606
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1607
|
+
});
|
|
1608
|
+
if (progress >= 100) {
|
|
1609
|
+
installEndedAt ??= Date.now();
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1615
|
+
try {
|
|
1616
|
+
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1617
|
+
} finally {
|
|
1618
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1619
|
+
}
|
|
1620
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1621
|
+
installEndedAt = Date.now();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1625
|
+
attempt,
|
|
1626
|
+
maxAttempts,
|
|
1627
|
+
totalBytes,
|
|
1628
|
+
totalStartedAt,
|
|
1629
|
+
transferStartedAt,
|
|
1630
|
+
transferEndedAt,
|
|
1631
|
+
installStartedAt,
|
|
1632
|
+
installEndedAt,
|
|
1633
|
+
progressEvents,
|
|
1634
|
+
lastProgress,
|
|
1635
|
+
installProgressEvents,
|
|
1636
|
+
lastInstallProgress,
|
|
1637
|
+
retried,
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
if (lastResult && typeof lastResult === 'object') {
|
|
1641
|
+
const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
|
|
1642
|
+
string,
|
|
1643
|
+
unknown
|
|
1644
|
+
>;
|
|
1645
|
+
lastResult = {
|
|
1646
|
+
...(lastResult as Record<string, unknown>),
|
|
1647
|
+
payload: {
|
|
1648
|
+
...payload,
|
|
1649
|
+
metrics,
|
|
1650
|
+
},
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (isSuccessResult(lastResult)) {
|
|
1655
|
+
return lastResult;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (
|
|
1659
|
+
attempt >= maxAttempts ||
|
|
1660
|
+
globalOpts.transport !== 'usb' ||
|
|
1661
|
+
!isProtocolV2UsbProbeTransientResult(lastResult)
|
|
1662
|
+
) {
|
|
1663
|
+
return lastResult;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
retried = true;
|
|
1667
|
+
process.stderr.write(
|
|
1668
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
|
|
1669
|
+
);
|
|
1670
|
+
await disposeSDK();
|
|
1671
|
+
await new Promise(resolve => {
|
|
1672
|
+
setTimeout(resolve, 3000);
|
|
1673
|
+
});
|
|
1674
|
+
currentSdk = await createSDK(globalOpts);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
return lastResult;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function buildFirmwareUpdateV4Params(opts: {
|
|
1681
|
+
chunkSize?: string;
|
|
1682
|
+
resourceBundle?: string[];
|
|
1683
|
+
romloader?: string;
|
|
1684
|
+
bootloader?: string;
|
|
1685
|
+
applicationP1?: string;
|
|
1686
|
+
applicationP2?: string;
|
|
1687
|
+
coprocessor?: string;
|
|
1688
|
+
se01?: string;
|
|
1689
|
+
se02?: string;
|
|
1690
|
+
se03?: string;
|
|
1691
|
+
se04?: string;
|
|
1692
|
+
forcedUpdateRes?: boolean;
|
|
1693
|
+
}) {
|
|
1694
|
+
const params = {
|
|
1695
|
+
platform: 'desktop' as const,
|
|
1696
|
+
connectProtocol: 'V2' as const,
|
|
1697
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1698
|
+
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1699
|
+
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1700
|
+
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1701
|
+
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1702
|
+
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
1703
|
+
applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
|
|
1704
|
+
coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
|
|
1705
|
+
se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
|
|
1706
|
+
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1707
|
+
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1708
|
+
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
const hasPayload = [
|
|
1712
|
+
params.resourceBundleFiles,
|
|
1713
|
+
params.romloaderBinary,
|
|
1714
|
+
params.bootloaderBinary,
|
|
1715
|
+
params.applicationP1Binary,
|
|
1716
|
+
params.applicationP2Binary,
|
|
1717
|
+
params.coprocessorBinary,
|
|
1718
|
+
params.se01Binary,
|
|
1719
|
+
params.se02Binary,
|
|
1720
|
+
params.se03Binary,
|
|
1721
|
+
params.se04Binary,
|
|
1722
|
+
].some(Boolean);
|
|
1723
|
+
|
|
1724
|
+
if (!hasPayload) {
|
|
1725
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1726
|
+
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1727
|
+
throw err;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return params;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1112
1733
|
/**
|
|
1113
1734
|
* #9 FIX: Safe parseInt with NaN check
|
|
1114
1735
|
*/
|
|
@@ -1120,4 +1741,8 @@ function safeParseInt(input: string, label: string): number {
|
|
|
1120
1741
|
return num;
|
|
1121
1742
|
}
|
|
1122
1743
|
|
|
1123
|
-
program
|
|
1744
|
+
export { program };
|
|
1745
|
+
|
|
1746
|
+
if (require.main === module) {
|
|
1747
|
+
program.parse();
|
|
1748
|
+
}
|