@onekeyfe/hardware-cli 1.2.0-alpha.0 → 1.2.0-alpha.10
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 +3 -1
- package/dist/cli.js +325 -20
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.js +5 -3
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +387 -0
- package/package.json +7 -6
- package/src/__tests__/firmware-update-v4-command.test.ts +19 -0
- package/src/__tests__/noble-ble-plugin.test.ts +113 -0
- package/src/cli.ts +454 -29
- package/src/sdk.ts +11 -3
- package/src/transports/nobleBlePlugin.ts +498 -0
package/src/cli.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { Command } from 'commander';
|
|
3
|
+
import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
|
|
4
|
+
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
2
5
|
|
|
3
|
-
import { createSDK, disposeSDK } from './sdk';
|
|
4
|
-
import {
|
|
5
|
-
clearSessionFromKeychain,
|
|
6
|
-
preloadSessionFromKeychain,
|
|
7
|
-
saveSessionToKeychain,
|
|
8
|
-
} from './session';
|
|
9
6
|
import {
|
|
10
7
|
resolveBatchGetAddress,
|
|
11
8
|
resolveGetAddress,
|
|
@@ -13,9 +10,13 @@ import {
|
|
|
13
10
|
resolveSignMessage,
|
|
14
11
|
resolveSignTransaction,
|
|
15
12
|
} from './chains';
|
|
13
|
+
import { createSDK, disposeSDK } from './sdk';
|
|
14
|
+
import {
|
|
15
|
+
clearSessionFromKeychain,
|
|
16
|
+
preloadSessionFromKeychain,
|
|
17
|
+
saveSessionToKeychain,
|
|
18
|
+
} from './session';
|
|
16
19
|
|
|
17
|
-
import { EDeviceType } from '@onekeyfe/hd-shared';
|
|
18
|
-
import { getDeviceType } from '@onekeyfe/hd-core';
|
|
19
20
|
import type {
|
|
20
21
|
EthereumSignTypedDataMessage,
|
|
21
22
|
EthereumSignTypedDataTypes,
|
|
@@ -78,8 +79,10 @@ program.option(
|
|
|
78
79
|
'--device-id <id>',
|
|
79
80
|
'Persistent device ID from getFeatures (changes when seed changes)'
|
|
80
81
|
);
|
|
82
|
+
program.option('--transport <transport>', 'Transport to use: usb or ble', 'usb');
|
|
81
83
|
program.option('--passphrase-state <state>', 'Passphrase state for hidden wallet access');
|
|
82
84
|
program.option('--use-empty-passphrase', 'Use standard wallet (skip passphrase prompt)');
|
|
85
|
+
program.option('--debug', 'Enable SDK debug logs');
|
|
83
86
|
|
|
84
87
|
// ============================================================
|
|
85
88
|
// Device Commands
|
|
@@ -92,16 +95,16 @@ program
|
|
|
92
95
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
93
96
|
const result = await sdk.searchDevices();
|
|
94
97
|
|
|
95
|
-
//
|
|
96
|
-
if (result?.success && Array.isArray(result.payload)) {
|
|
98
|
+
// USB 下自动读取 features 成本低;BLE 搜索阶段只做枚举,避免批量连接导致超时。
|
|
99
|
+
if (globalOpts.transport !== 'ble' && result?.success && Array.isArray(result.payload)) {
|
|
97
100
|
for (const device of result.payload as EnrichedSearchDevice[]) {
|
|
98
101
|
if (device.connectId) {
|
|
99
102
|
try {
|
|
100
103
|
const features = await sdk.getFeatures(device.connectId);
|
|
101
104
|
if (features?.success && features.payload) {
|
|
102
105
|
device.features = features.payload;
|
|
103
|
-
device.name = features.payload.label || features.payload.
|
|
104
|
-
const devType = features.payload.
|
|
106
|
+
device.name = features.payload.label || features.payload.bleName || device.name;
|
|
107
|
+
const devType = features.payload.deviceType?.toLowerCase();
|
|
105
108
|
if (devType) {
|
|
106
109
|
device.deviceType = devType as IDeviceType;
|
|
107
110
|
}
|
|
@@ -540,18 +543,50 @@ program
|
|
|
540
543
|
|
|
541
544
|
program
|
|
542
545
|
.command('firmware-update-ble')
|
|
543
|
-
.description('
|
|
546
|
+
.description('Run Protocol V2 firmware update over BLE')
|
|
544
547
|
.action(() =>
|
|
545
548
|
respondAndExit({
|
|
546
549
|
success: false,
|
|
547
550
|
payload: {
|
|
548
551
|
error:
|
|
549
|
-
'
|
|
550
|
-
code: '
|
|
552
|
+
'Use `onekey-hw --transport ble firmware-update-v4` for BLE Protocol V2 firmware updates.',
|
|
553
|
+
code: 'USE_FIRMWARE_UPDATE_V4',
|
|
551
554
|
},
|
|
552
555
|
})
|
|
553
556
|
);
|
|
554
557
|
|
|
558
|
+
program
|
|
559
|
+
.command('firmware-update-v4')
|
|
560
|
+
.description('Run Protocol V2 firmware update through sdk.firmwareUpdateV4')
|
|
561
|
+
.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
|
+
.option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
|
|
567
|
+
.option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
|
|
568
|
+
.option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
|
|
569
|
+
.option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
|
|
570
|
+
.option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
|
|
571
|
+
.option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
|
|
572
|
+
.option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
|
|
573
|
+
.option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
|
|
574
|
+
.option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
|
|
575
|
+
.option('--forced-update-res', 'Force resource update')
|
|
576
|
+
.option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
|
|
577
|
+
.action(opts =>
|
|
578
|
+
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
579
|
+
const params = buildFirmwareUpdateV4Params(opts);
|
|
580
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
581
|
+
sdk,
|
|
582
|
+
globalOpts,
|
|
583
|
+
params,
|
|
584
|
+
retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
|
|
585
|
+
});
|
|
586
|
+
outputResult(globalOpts, result);
|
|
587
|
+
})
|
|
588
|
+
);
|
|
589
|
+
|
|
555
590
|
program
|
|
556
591
|
.command('bootloader-check')
|
|
557
592
|
.description('Check bootloader version and status')
|
|
@@ -757,8 +792,8 @@ sessionCmd
|
|
|
757
792
|
skipPassphraseCheck: true,
|
|
758
793
|
});
|
|
759
794
|
const featPayload = featResult?.success ? featResult.payload : undefined;
|
|
760
|
-
const deviceId = featPayload?.
|
|
761
|
-
const sessionId = passphraseSessionId || featPayload?.
|
|
795
|
+
const deviceId = featPayload?.deviceId || device.deviceId || '';
|
|
796
|
+
const sessionId = passphraseSessionId || featPayload?.sessionId || '';
|
|
762
797
|
|
|
763
798
|
// 6. Save to keychain
|
|
764
799
|
if (passphraseState && deviceId && sessionId) {
|
|
@@ -914,10 +949,10 @@ async function prepareSession(
|
|
|
914
949
|
connectId?: string;
|
|
915
950
|
deviceId?: string;
|
|
916
951
|
features?: {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
952
|
+
deviceId?: string | null;
|
|
953
|
+
deviceType?: string;
|
|
954
|
+
sessionId?: string | null;
|
|
955
|
+
passphraseProtection?: boolean | null;
|
|
921
956
|
unlocked?: boolean | null;
|
|
922
957
|
};
|
|
923
958
|
};
|
|
@@ -929,19 +964,19 @@ async function prepareSession(
|
|
|
929
964
|
// ── Step 2: Get features if searchDevices didn't populate them ──
|
|
930
965
|
// getFeatures failures here are non-fatal — we fall through to Step 3
|
|
931
966
|
// which will fail with a clearer error if the device is truly unreachable.
|
|
932
|
-
let deviceId = device.features?.
|
|
967
|
+
let deviceId = device.features?.deviceId || device.deviceId || '';
|
|
933
968
|
let deviceType = getDeviceType(device.features as Features | undefined);
|
|
934
969
|
let unlocked = device.features?.unlocked;
|
|
935
|
-
let passphraseProtection = device.features?.
|
|
970
|
+
let passphraseProtection = device.features?.passphraseProtection;
|
|
936
971
|
|
|
937
972
|
if (!deviceId || unlocked == null || passphraseProtection == null) {
|
|
938
973
|
try {
|
|
939
974
|
const featResult = await sdk.getFeatures(connectId);
|
|
940
975
|
if (featResult?.success && featResult.payload) {
|
|
941
|
-
deviceId = featResult.payload.
|
|
976
|
+
deviceId = featResult.payload.deviceId || deviceId;
|
|
942
977
|
deviceType = getDeviceType(featResult.payload) || deviceType;
|
|
943
978
|
unlocked = featResult.payload.unlocked;
|
|
944
|
-
passphraseProtection = featResult.payload.
|
|
979
|
+
passphraseProtection = featResult.payload.passphraseProtection;
|
|
945
980
|
}
|
|
946
981
|
} catch {
|
|
947
982
|
/* non-fatal — Step 3 will surface a clear error if device is gone */
|
|
@@ -957,9 +992,9 @@ async function prepareSession(
|
|
|
957
992
|
if (wasLocked) {
|
|
958
993
|
process.stderr.write('[onekey-hw] Device is locked. Unlocking (PIN required)...\n');
|
|
959
994
|
const { payload: feat } = await unlockWithRetry(sdk, connectId);
|
|
960
|
-
deviceId = feat.
|
|
995
|
+
deviceId = feat.deviceId || deviceId;
|
|
961
996
|
unlocked = feat.unlocked;
|
|
962
|
-
passphraseProtection = feat.
|
|
997
|
+
passphraseProtection = feat.passphraseProtection;
|
|
963
998
|
}
|
|
964
999
|
|
|
965
1000
|
if (!globalOpts.deviceId && deviceId) {
|
|
@@ -1009,7 +1044,7 @@ async function prepareSession(
|
|
|
1009
1044
|
skipPassphraseCheck: true,
|
|
1010
1045
|
});
|
|
1011
1046
|
const sessionId =
|
|
1012
|
-
passphraseSessionId || (featAfter?.success ? featAfter.payload?.
|
|
1047
|
+
passphraseSessionId || (featAfter?.success ? featAfter.payload?.sessionId : undefined);
|
|
1013
1048
|
if (sessionId) {
|
|
1014
1049
|
await saveSessionToKeychain(deviceId, passphraseState, sessionId);
|
|
1015
1050
|
await preloadSessionFromKeychain(deviceId);
|
|
@@ -1071,6 +1106,9 @@ async function runCommand(
|
|
|
1071
1106
|
): Promise<void> {
|
|
1072
1107
|
const globalOpts = program.opts();
|
|
1073
1108
|
try {
|
|
1109
|
+
if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
|
|
1110
|
+
throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
|
|
1111
|
+
}
|
|
1074
1112
|
const sdk = await createSDK(globalOpts);
|
|
1075
1113
|
if (options.needsSession) {
|
|
1076
1114
|
await prepareSession(sdk, globalOpts);
|
|
@@ -1119,6 +1157,389 @@ function safeJsonParse(input: string, label: string): unknown {
|
|
|
1119
1157
|
}
|
|
1120
1158
|
}
|
|
1121
1159
|
|
|
1160
|
+
function readBinaryParam(path: string): ArrayBuffer {
|
|
1161
|
+
const buffer = readFileSync(path);
|
|
1162
|
+
return new Uint8Array(buffer).buffer;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function parseResourceBundleParam(spec: string): { binary: ArrayBuffer; devicePath: string } {
|
|
1166
|
+
const sep = spec.indexOf(':');
|
|
1167
|
+
if (sep <= 0 || sep === spec.length - 1) {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
`Invalid --resource-bundle value: "${spec}". Expected <localPath>:<devicePath>`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
const localPath = spec.slice(0, sep);
|
|
1173
|
+
const devicePath = spec.slice(sep + 1);
|
|
1174
|
+
if (!devicePath.startsWith('vol')) {
|
|
1175
|
+
throw new Error(
|
|
1176
|
+
`Invalid --resource-bundle device path: "${devicePath}". Expected a vol*:/... path`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
return {
|
|
1180
|
+
binary: readBinaryParam(localPath),
|
|
1181
|
+
devicePath,
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function getFirmwareUpdateV4TotalBytes(params: ReturnType<typeof buildFirmwareUpdateV4Params>) {
|
|
1186
|
+
return [
|
|
1187
|
+
...(params.resourceBundleFiles?.map(item => item.binary) ?? []),
|
|
1188
|
+
params.bootloaderBinary,
|
|
1189
|
+
params.applicationP1Binary,
|
|
1190
|
+
params.applicationP2Binary,
|
|
1191
|
+
params.coprocessorBinary,
|
|
1192
|
+
params.se01Binary,
|
|
1193
|
+
params.se02Binary,
|
|
1194
|
+
params.se03Binary,
|
|
1195
|
+
params.se04Binary,
|
|
1196
|
+
].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function getFirmwareUpdateV4ErrorText(result: unknown) {
|
|
1200
|
+
if (!result || typeof result !== 'object') return '';
|
|
1201
|
+
const { payload } = result as { payload?: unknown };
|
|
1202
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
1203
|
+
const { error } = payload as { error?: unknown };
|
|
1204
|
+
return typeof error === 'string' ? error : '';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function isProtocolV2UsbProbeTransientResult(result: unknown) {
|
|
1208
|
+
const error = getFirmwareUpdateV4ErrorText(result);
|
|
1209
|
+
return (
|
|
1210
|
+
error.includes('Device protocol mismatch') &&
|
|
1211
|
+
error.includes('expected V2') &&
|
|
1212
|
+
error.includes('did not respond to expected protocol')
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function isSuccessResult(result: unknown) {
|
|
1217
|
+
return (
|
|
1218
|
+
!!result && typeof result === 'object' && (result as { success?: boolean }).success === true
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function getFirmwareUpdatePayload(message: unknown) {
|
|
1223
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
1224
|
+
return (message as { payload?: Record<string, unknown> }).payload;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function formatFirmwareProgress(progress: number) {
|
|
1228
|
+
if (!Number.isFinite(progress)) return '0%';
|
|
1229
|
+
return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function formatFirmwareBytes(bytes: number) {
|
|
1233
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '';
|
|
1234
|
+
return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function maybePrintFirmwareProgress({
|
|
1238
|
+
progressType,
|
|
1239
|
+
progress,
|
|
1240
|
+
payload,
|
|
1241
|
+
lastPrintedProgress,
|
|
1242
|
+
}: {
|
|
1243
|
+
progressType: string;
|
|
1244
|
+
progress: number;
|
|
1245
|
+
payload: Record<string, unknown>;
|
|
1246
|
+
lastPrintedProgress: number;
|
|
1247
|
+
}) {
|
|
1248
|
+
const printableProgress = Math.floor(progress / 10) * 10;
|
|
1249
|
+
if (printableProgress <= lastPrintedProgress && progress < 100) {
|
|
1250
|
+
return lastPrintedProgress;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const transferredBytes = Number(payload.transferredBytes);
|
|
1254
|
+
const totalBytes = Number(payload.totalBytes);
|
|
1255
|
+
const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
|
|
1256
|
+
const sizeText =
|
|
1257
|
+
Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
|
|
1258
|
+
? ` ${formatFirmwareBytes(transferredBytes)}/${formatFirmwareBytes(totalBytes)}`
|
|
1259
|
+
: '';
|
|
1260
|
+
const speedText =
|
|
1261
|
+
Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
|
|
1262
|
+
? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
|
|
1263
|
+
: '';
|
|
1264
|
+
|
|
1265
|
+
process.stderr.write(
|
|
1266
|
+
`[onekey-hw] Firmware ${progressType}: ${formatFirmwareProgress(
|
|
1267
|
+
progress
|
|
1268
|
+
)}${sizeText}${speedText}\n`
|
|
1269
|
+
);
|
|
1270
|
+
return progress >= 100 ? 100 : printableProgress;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function buildFirmwareUpdateV4Metrics({
|
|
1274
|
+
attempt,
|
|
1275
|
+
maxAttempts,
|
|
1276
|
+
totalBytes,
|
|
1277
|
+
totalStartedAt,
|
|
1278
|
+
transferStartedAt,
|
|
1279
|
+
transferEndedAt,
|
|
1280
|
+
installStartedAt,
|
|
1281
|
+
installEndedAt,
|
|
1282
|
+
progressEvents,
|
|
1283
|
+
lastProgress,
|
|
1284
|
+
installProgressEvents,
|
|
1285
|
+
lastInstallProgress,
|
|
1286
|
+
retried,
|
|
1287
|
+
}: {
|
|
1288
|
+
attempt: number;
|
|
1289
|
+
maxAttempts: number;
|
|
1290
|
+
totalBytes: number;
|
|
1291
|
+
totalStartedAt: number;
|
|
1292
|
+
transferStartedAt?: number;
|
|
1293
|
+
transferEndedAt?: number;
|
|
1294
|
+
installStartedAt?: number;
|
|
1295
|
+
installEndedAt?: number;
|
|
1296
|
+
progressEvents: number;
|
|
1297
|
+
lastProgress: number;
|
|
1298
|
+
installProgressEvents: number;
|
|
1299
|
+
lastInstallProgress: number;
|
|
1300
|
+
retried: boolean;
|
|
1301
|
+
}) {
|
|
1302
|
+
const totalElapsedMs = Date.now() - totalStartedAt;
|
|
1303
|
+
const transferElapsedMs =
|
|
1304
|
+
transferStartedAt !== undefined && transferEndedAt !== undefined
|
|
1305
|
+
? transferEndedAt - transferStartedAt
|
|
1306
|
+
: undefined;
|
|
1307
|
+
const installElapsedMs =
|
|
1308
|
+
installStartedAt !== undefined && installEndedAt !== undefined
|
|
1309
|
+
? installEndedAt - installStartedAt
|
|
1310
|
+
: undefined;
|
|
1311
|
+
|
|
1312
|
+
return {
|
|
1313
|
+
attempt,
|
|
1314
|
+
maxAttempts,
|
|
1315
|
+
retried,
|
|
1316
|
+
totalBytes,
|
|
1317
|
+
progressEvents,
|
|
1318
|
+
lastProgress,
|
|
1319
|
+
installProgressEvents,
|
|
1320
|
+
lastInstallProgress,
|
|
1321
|
+
transferSeconds:
|
|
1322
|
+
transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
|
|
1323
|
+
transferKiBPerSecond:
|
|
1324
|
+
transferElapsedMs !== undefined && transferElapsedMs > 0
|
|
1325
|
+
? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
|
|
1326
|
+
: null,
|
|
1327
|
+
installSeconds:
|
|
1328
|
+
installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
|
|
1329
|
+
totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
async function runFirmwareUpdateV4WithRetry({
|
|
1334
|
+
sdk,
|
|
1335
|
+
globalOpts,
|
|
1336
|
+
params,
|
|
1337
|
+
retries,
|
|
1338
|
+
}: {
|
|
1339
|
+
sdk: AnySdk;
|
|
1340
|
+
globalOpts: Record<string, any>;
|
|
1341
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
1342
|
+
retries?: number;
|
|
1343
|
+
}) {
|
|
1344
|
+
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1345
|
+
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1346
|
+
let currentSdk = sdk;
|
|
1347
|
+
let lastResult: unknown;
|
|
1348
|
+
let retried = false;
|
|
1349
|
+
|
|
1350
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1351
|
+
let progressEvents = 0;
|
|
1352
|
+
let lastProgress = -1;
|
|
1353
|
+
let transferStartedAt: number | undefined;
|
|
1354
|
+
let transferEndedAt: number | undefined;
|
|
1355
|
+
let installProgressEvents = 0;
|
|
1356
|
+
let lastInstallProgress = -1;
|
|
1357
|
+
let installStartedAt: number | undefined;
|
|
1358
|
+
let installEndedAt: number | undefined;
|
|
1359
|
+
let lastPrintedTransferProgress = -10;
|
|
1360
|
+
let lastPrintedInstallProgress = -10;
|
|
1361
|
+
const totalStartedAt = Date.now();
|
|
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;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1389
|
+
const progress = Number(payload.progress);
|
|
1390
|
+
if (!Number.isFinite(progress)) return;
|
|
1391
|
+
|
|
1392
|
+
if (payload.progressType === 'transferData') {
|
|
1393
|
+
progressEvents += 1;
|
|
1394
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1395
|
+
transferStartedAt ??= Date.now();
|
|
1396
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1397
|
+
progressType: 'transfer',
|
|
1398
|
+
progress,
|
|
1399
|
+
payload,
|
|
1400
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1401
|
+
});
|
|
1402
|
+
if (progress >= 100) {
|
|
1403
|
+
transferEndedAt ??= Date.now();
|
|
1404
|
+
}
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
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();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1435
|
+
attempt,
|
|
1436
|
+
maxAttempts,
|
|
1437
|
+
totalBytes,
|
|
1438
|
+
totalStartedAt,
|
|
1439
|
+
transferStartedAt,
|
|
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
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (isSuccessResult(lastResult)) {
|
|
1465
|
+
return lastResult;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (
|
|
1469
|
+
attempt >= maxAttempts ||
|
|
1470
|
+
globalOpts.transport !== 'usb' ||
|
|
1471
|
+
!isProtocolV2UsbProbeTransientResult(lastResult)
|
|
1472
|
+
) {
|
|
1473
|
+
return lastResult;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
retried = true;
|
|
1477
|
+
process.stderr.write(
|
|
1478
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
|
|
1479
|
+
);
|
|
1480
|
+
await disposeSDK();
|
|
1481
|
+
await new Promise(resolve => {
|
|
1482
|
+
setTimeout(resolve, 3000);
|
|
1483
|
+
});
|
|
1484
|
+
currentSdk = await createSDK(globalOpts);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return lastResult;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function buildFirmwareUpdateV4Params(opts: {
|
|
1491
|
+
chunkSize?: string;
|
|
1492
|
+
resourceBundle?: string[];
|
|
1493
|
+
romloader?: string;
|
|
1494
|
+
bootloader?: string;
|
|
1495
|
+
applicationP1?: string;
|
|
1496
|
+
applicationP2?: string;
|
|
1497
|
+
coprocessor?: string;
|
|
1498
|
+
se01?: string;
|
|
1499
|
+
se02?: string;
|
|
1500
|
+
se03?: string;
|
|
1501
|
+
se04?: string;
|
|
1502
|
+
forcedUpdateRes?: boolean;
|
|
1503
|
+
}) {
|
|
1504
|
+
const params = {
|
|
1505
|
+
platform: 'desktop' as const,
|
|
1506
|
+
connectProtocol: 'V2' as const,
|
|
1507
|
+
chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
|
|
1508
|
+
forcedUpdateRes: opts.forcedUpdateRes,
|
|
1509
|
+
resourceBundleFiles: opts.resourceBundle?.map(parseResourceBundleParam),
|
|
1510
|
+
romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
|
|
1511
|
+
bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
|
|
1512
|
+
applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
|
|
1513
|
+
applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
|
|
1514
|
+
coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
|
|
1515
|
+
se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
|
|
1516
|
+
se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
|
|
1517
|
+
se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
|
|
1518
|
+
se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
const hasPayload = [
|
|
1522
|
+
params.resourceBundleFiles,
|
|
1523
|
+
params.romloaderBinary,
|
|
1524
|
+
params.bootloaderBinary,
|
|
1525
|
+
params.applicationP1Binary,
|
|
1526
|
+
params.applicationP2Binary,
|
|
1527
|
+
params.coprocessorBinary,
|
|
1528
|
+
params.se01Binary,
|
|
1529
|
+
params.se02Binary,
|
|
1530
|
+
params.se03Binary,
|
|
1531
|
+
params.se04Binary,
|
|
1532
|
+
].some(Boolean);
|
|
1533
|
+
|
|
1534
|
+
if (!hasPayload) {
|
|
1535
|
+
const err = new Error('firmware-update-v4 requires at least one binary path');
|
|
1536
|
+
(err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
|
|
1537
|
+
throw err;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
return params;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1122
1543
|
/**
|
|
1123
1544
|
* #9 FIX: Safe parseInt with NaN check
|
|
1124
1545
|
*/
|
|
@@ -1130,4 +1551,8 @@ function safeParseInt(input: string, label: string): number {
|
|
|
1130
1551
|
return num;
|
|
1131
1552
|
}
|
|
1132
1553
|
|
|
1133
|
-
program
|
|
1554
|
+
export { program };
|
|
1555
|
+
|
|
1556
|
+
if (require.main === module) {
|
|
1557
|
+
program.parse();
|
|
1558
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -14,6 +14,7 @@ import HardwareSDK from '@onekeyfe/hd-common-connect-sdk';
|
|
|
14
14
|
import { DEVICE, UI_EVENT, UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core';
|
|
15
15
|
|
|
16
16
|
import { promptPassphraseViaPinentry } from './pinentry';
|
|
17
|
+
import { createNobleBlePlugin } from './transports/nobleBlePlugin';
|
|
17
18
|
|
|
18
19
|
import type { ConnectSettings } from '@onekeyfe/hd-core';
|
|
19
20
|
import type { PinentryResult } from './pinentry';
|
|
@@ -22,6 +23,8 @@ export interface SDKOptions {
|
|
|
22
23
|
connectId?: string;
|
|
23
24
|
passphraseState?: string;
|
|
24
25
|
useEmptyPassphrase?: boolean;
|
|
26
|
+
debug?: boolean;
|
|
27
|
+
transport?: 'usb' | 'ble';
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -182,12 +185,17 @@ function registerEventHandlers(sdk: typeof HardwareSDK): void {
|
|
|
182
185
|
// ---------------------------------------------------------------------------
|
|
183
186
|
|
|
184
187
|
async function initSDK(): Promise<typeof HardwareSDK> {
|
|
188
|
+
const transport = currentOpts.transport ?? 'usb';
|
|
185
189
|
const settings: Partial<ConnectSettings> = {
|
|
186
|
-
debug: false,
|
|
190
|
+
debug: currentOpts.debug ?? false,
|
|
187
191
|
fetchConfig: true,
|
|
188
|
-
env: 'node-usb',
|
|
192
|
+
env: transport === 'ble' ? 'lowlevel' : 'node-usb',
|
|
189
193
|
};
|
|
190
|
-
await HardwareSDK.init(
|
|
194
|
+
await HardwareSDK.init(
|
|
195
|
+
settings,
|
|
196
|
+
undefined,
|
|
197
|
+
transport === 'ble' ? createNobleBlePlugin() : undefined
|
|
198
|
+
);
|
|
191
199
|
|
|
192
200
|
// Defensive: strip any stale listeners (e.g. left over from a previous
|
|
193
201
|
// dispose/init cycle in a long-running process) before wiring ours.
|