@onekeyfe/hardware-cli 1.2.0-alpha.3 → 1.2.0-alpha.4

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/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,8 +95,8 @@ program
92
95
  runCommand({}, async ({ sdk, globalOpts }) => {
93
96
  const result = await sdk.searchDevices();
94
97
 
95
- // Auto-fetch features for each discovered device (doesn't require PIN)
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 {
@@ -540,18 +543,47 @@ program
540
543
 
541
544
  program
542
545
  .command('firmware-update-ble')
543
- .description('BLE firmware update is not supported via CLI')
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
- 'BLE firmware update via CLI is not supported. Please use the OneKey App or https://firmware.onekey.so/ to update firmware.',
550
- code: 'FIRMWARE_UPDATE_NOT_SUPPORTED',
552
+ 'Use `onekey-hw --transport ble firmware-update-v4-debug` for BLE Protocol V2 firmware update debugging.',
553
+ code: 'USE_FIRMWARE_UPDATE_V4_DEBUG',
551
554
  },
552
555
  })
553
556
  );
554
557
 
558
+ program
559
+ .command('firmware-update-v4-debug')
560
+ .description('Debug Protocol V2 firmware update through sdk.firmwareUpdateV4')
561
+ .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
562
+ .option('--resource <path>', 'FW_MGMT_TARGET_CRATE resource package path')
563
+ .option('--romloader <path>', 'FW_MGMT_TARGET_ROMLOADER binary path')
564
+ .option('--bootloader <path>', 'FW_MGMT_TARGET_BOOTLOADER binary path')
565
+ .option('--application-p1 <path>', 'FW_MGMT_TARGET_APPLICATION_P1 binary path')
566
+ .option('--application-p2 <path>', 'FW_MGMT_TARGET_APPLICATION_P2 binary path')
567
+ .option('--coprocessor <path>', 'FW_MGMT_TARGET_COPROCESSOR binary path')
568
+ .option('--se01 <path>', 'FW_MGMT_TARGET_SE01 binary path')
569
+ .option('--se02 <path>', 'FW_MGMT_TARGET_SE02 binary path')
570
+ .option('--se03 <path>', 'FW_MGMT_TARGET_SE03 binary path')
571
+ .option('--se04 <path>', 'FW_MGMT_TARGET_SE04 binary path')
572
+ .option('--forced-update-res', 'Force resource update')
573
+ .option('--retries <count>', 'Retry count for transient Protocol V2 USB probe failures')
574
+ .action(opts =>
575
+ runCommand({}, async ({ sdk, globalOpts }) => {
576
+ const params = buildFirmwareUpdateV4DebugParams(opts);
577
+ const result = await runFirmwareUpdateV4DebugWithRetry({
578
+ sdk,
579
+ globalOpts,
580
+ params,
581
+ retries: opts.retries ? safeParseInt(opts.retries, '--retries') : undefined,
582
+ });
583
+ outputResult(globalOpts, result);
584
+ })
585
+ );
586
+
555
587
  program
556
588
  .command('bootloader-check')
557
589
  .description('Check bootloader version and status')
@@ -1071,6 +1103,9 @@ async function runCommand(
1071
1103
  ): Promise<void> {
1072
1104
  const globalOpts = program.opts();
1073
1105
  try {
1106
+ if (globalOpts.transport !== 'usb' && globalOpts.transport !== 'ble') {
1107
+ throw new Error(`Unsupported transport: ${globalOpts.transport}. Use "usb" or "ble".`);
1108
+ }
1074
1109
  const sdk = await createSDK(globalOpts);
1075
1110
  if (options.needsSession) {
1076
1111
  await prepareSession(sdk, globalOpts);
@@ -1119,6 +1154,369 @@ function safeJsonParse(input: string, label: string): unknown {
1119
1154
  }
1120
1155
  }
1121
1156
 
1157
+ function readBinaryParam(path: string): ArrayBuffer {
1158
+ const buffer = readFileSync(path);
1159
+ return new Uint8Array(buffer).buffer;
1160
+ }
1161
+
1162
+ function getFirmwareUpdateV4DebugTotalBytes(
1163
+ params: ReturnType<typeof buildFirmwareUpdateV4DebugParams>
1164
+ ) {
1165
+ return [
1166
+ params.resourceBinary,
1167
+ params.bootloaderBinary,
1168
+ params.applicationP1Binary,
1169
+ params.applicationP2Binary,
1170
+ params.coprocessorBinary,
1171
+ params.se01Binary,
1172
+ params.se02Binary,
1173
+ params.se03Binary,
1174
+ params.se04Binary,
1175
+ ].reduce((total, binary) => total + (binary?.byteLength ?? 0), 0);
1176
+ }
1177
+
1178
+ function getFirmwareUpdateV4DebugErrorText(result: unknown) {
1179
+ if (!result || typeof result !== 'object') return '';
1180
+ const payload = (result as { payload?: unknown }).payload;
1181
+ if (!payload || typeof payload !== 'object') return '';
1182
+ const error = (payload as { error?: unknown }).error;
1183
+ return typeof error === 'string' ? error : '';
1184
+ }
1185
+
1186
+ function isProtocolV2UsbProbeTransientResult(result: unknown) {
1187
+ const error = getFirmwareUpdateV4DebugErrorText(result);
1188
+ return (
1189
+ error.includes('Device protocol mismatch') &&
1190
+ error.includes('expected V2') &&
1191
+ error.includes('did not respond to expected protocol')
1192
+ );
1193
+ }
1194
+
1195
+ function isSuccessResult(result: unknown) {
1196
+ return (
1197
+ !!result && typeof result === 'object' && (result as { success?: boolean }).success === true
1198
+ );
1199
+ }
1200
+
1201
+ function getFirmwareDebugPayload(message: unknown) {
1202
+ if (!message || typeof message !== 'object') return undefined;
1203
+ return (message as { payload?: Record<string, unknown> }).payload;
1204
+ }
1205
+
1206
+ function formatFirmwareDebugProgress(progress: number) {
1207
+ if (!Number.isFinite(progress)) return '0%';
1208
+ return `${Math.min(Math.max(Math.round(progress), 0), 100)}%`;
1209
+ }
1210
+
1211
+ function formatFirmwareDebugBytes(bytes: number) {
1212
+ if (!Number.isFinite(bytes) || bytes <= 0) return '';
1213
+ return `${(bytes / 1024).toFixed(1)} KiB`;
1214
+ }
1215
+
1216
+ function maybePrintFirmwareDebugProgress({
1217
+ progressType,
1218
+ progress,
1219
+ payload,
1220
+ lastPrintedProgress,
1221
+ }: {
1222
+ progressType: string;
1223
+ progress: number;
1224
+ payload: Record<string, unknown>;
1225
+ lastPrintedProgress: number;
1226
+ }) {
1227
+ const printableProgress = Math.floor(progress / 10) * 10;
1228
+ if (printableProgress <= lastPrintedProgress && progress < 100) {
1229
+ return lastPrintedProgress;
1230
+ }
1231
+
1232
+ const transferredBytes = Number(payload.transferredBytes);
1233
+ const totalBytes = Number(payload.totalBytes);
1234
+ const rateBytesPerSecond = Number(payload.rateBytesPerSecond);
1235
+ const sizeText =
1236
+ Number.isFinite(transferredBytes) && Number.isFinite(totalBytes) && totalBytes > 0
1237
+ ? ` ${formatFirmwareDebugBytes(transferredBytes)}/${formatFirmwareDebugBytes(totalBytes)}`
1238
+ : '';
1239
+ const speedText =
1240
+ Number.isFinite(rateBytesPerSecond) && rateBytesPerSecond > 0
1241
+ ? ` ${(rateBytesPerSecond / 1024).toFixed(2)} KiB/s`
1242
+ : '';
1243
+
1244
+ process.stderr.write(
1245
+ `[onekey-hw] Firmware ${progressType}: ${formatFirmwareDebugProgress(
1246
+ progress
1247
+ )}${sizeText}${speedText}\n`
1248
+ );
1249
+ return progress >= 100 ? 100 : printableProgress;
1250
+ }
1251
+
1252
+ function buildFirmwareUpdateV4DebugMetrics({
1253
+ attempt,
1254
+ maxAttempts,
1255
+ totalBytes,
1256
+ totalStartedAt,
1257
+ transferStartedAt,
1258
+ transferEndedAt,
1259
+ installStartedAt,
1260
+ installEndedAt,
1261
+ progressEvents,
1262
+ lastProgress,
1263
+ installProgressEvents,
1264
+ lastInstallProgress,
1265
+ retried,
1266
+ }: {
1267
+ attempt: number;
1268
+ maxAttempts: number;
1269
+ totalBytes: number;
1270
+ totalStartedAt: number;
1271
+ transferStartedAt?: number;
1272
+ transferEndedAt?: number;
1273
+ installStartedAt?: number;
1274
+ installEndedAt?: number;
1275
+ progressEvents: number;
1276
+ lastProgress: number;
1277
+ installProgressEvents: number;
1278
+ lastInstallProgress: number;
1279
+ retried: boolean;
1280
+ }) {
1281
+ const totalElapsedMs = Date.now() - totalStartedAt;
1282
+ const transferElapsedMs =
1283
+ transferStartedAt !== undefined && transferEndedAt !== undefined
1284
+ ? transferEndedAt - transferStartedAt
1285
+ : undefined;
1286
+ const installElapsedMs =
1287
+ installStartedAt !== undefined && installEndedAt !== undefined
1288
+ ? installEndedAt - installStartedAt
1289
+ : undefined;
1290
+
1291
+ return {
1292
+ attempt,
1293
+ maxAttempts,
1294
+ retried,
1295
+ totalBytes,
1296
+ progressEvents,
1297
+ lastProgress,
1298
+ installProgressEvents,
1299
+ lastInstallProgress,
1300
+ transferSeconds:
1301
+ transferElapsedMs !== undefined ? Number((transferElapsedMs / 1000).toFixed(2)) : null,
1302
+ transferKiBPerSecond:
1303
+ transferElapsedMs !== undefined && transferElapsedMs > 0
1304
+ ? Number((totalBytes / 1024 / (transferElapsedMs / 1000)).toFixed(2))
1305
+ : null,
1306
+ installSeconds:
1307
+ installElapsedMs !== undefined ? Number((installElapsedMs / 1000).toFixed(2)) : null,
1308
+ totalSeconds: Number((totalElapsedMs / 1000).toFixed(2)),
1309
+ };
1310
+ }
1311
+
1312
+ async function runFirmwareUpdateV4DebugWithRetry({
1313
+ sdk,
1314
+ globalOpts,
1315
+ params,
1316
+ retries,
1317
+ }: {
1318
+ sdk: AnySdk;
1319
+ globalOpts: Record<string, any>;
1320
+ params: ReturnType<typeof buildFirmwareUpdateV4DebugParams>;
1321
+ retries?: number;
1322
+ }) {
1323
+ const totalBytes = getFirmwareUpdateV4DebugTotalBytes(params);
1324
+ const maxAttempts = Math.max((retries ?? 2) + 1, 1);
1325
+ let currentSdk = sdk;
1326
+ let lastResult: unknown;
1327
+ let retried = false;
1328
+
1329
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1330
+ let progressEvents = 0;
1331
+ let lastProgress = -1;
1332
+ let transferStartedAt: number | undefined;
1333
+ let transferEndedAt: number | undefined;
1334
+ let installProgressEvents = 0;
1335
+ let lastInstallProgress = -1;
1336
+ let installStartedAt: number | undefined;
1337
+ let installEndedAt: number | undefined;
1338
+ let lastPrintedTransferProgress = -10;
1339
+ let lastPrintedInstallProgress = -10;
1340
+ const totalStartedAt = Date.now();
1341
+ const connectId =
1342
+ retried && globalOpts.transport === 'usb' && globalOpts.connectId
1343
+ ? undefined
1344
+ : globalOpts.connectId;
1345
+
1346
+ const onUiEvent = (message: unknown) => {
1347
+ if (!message || typeof message !== 'object') return;
1348
+ const messageType = (message as { type?: string }).type;
1349
+ const payload = getFirmwareDebugPayload(message);
1350
+
1351
+ if (messageType === UI_REQUEST.FIRMWARE_TIP) {
1352
+ const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
1353
+ if (typeof tipMessage === 'string') {
1354
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1355
+ }
1356
+ return;
1357
+ }
1358
+
1359
+ if (messageType === UI_REQUEST.REQUEST_BUTTON) {
1360
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1361
+ process.stderr.write(
1362
+ `[onekey-hw] Please confirm the firmware update on your device${code}.\n`
1363
+ );
1364
+ return;
1365
+ }
1366
+
1367
+ if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
1368
+ const progress = Number(payload.progress);
1369
+ if (!Number.isFinite(progress)) return;
1370
+
1371
+ if (payload.progressType === 'transferData') {
1372
+ progressEvents += 1;
1373
+ lastProgress = Math.max(lastProgress, progress);
1374
+ transferStartedAt ??= Date.now();
1375
+ lastPrintedTransferProgress = maybePrintFirmwareDebugProgress({
1376
+ progressType: 'transfer',
1377
+ progress,
1378
+ payload,
1379
+ lastPrintedProgress: lastPrintedTransferProgress,
1380
+ });
1381
+ if (progress >= 100) {
1382
+ transferEndedAt ??= Date.now();
1383
+ }
1384
+ return;
1385
+ }
1386
+
1387
+ if (payload.progressType === 'installingFirmware') {
1388
+ installProgressEvents += 1;
1389
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1390
+ installStartedAt ??= Date.now();
1391
+ lastPrintedInstallProgress = maybePrintFirmwareDebugProgress({
1392
+ progressType: 'install',
1393
+ progress,
1394
+ payload,
1395
+ lastPrintedProgress: lastPrintedInstallProgress,
1396
+ });
1397
+ if (progress >= 100) {
1398
+ installEndedAt ??= Date.now();
1399
+ }
1400
+ }
1401
+ };
1402
+
1403
+ currentSdk.on(UI_EVENT, onUiEvent);
1404
+ try {
1405
+ lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1406
+ } finally {
1407
+ currentSdk.off?.(UI_EVENT, onUiEvent);
1408
+ }
1409
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1410
+ installEndedAt = Date.now();
1411
+ }
1412
+
1413
+ const debugMetrics = buildFirmwareUpdateV4DebugMetrics({
1414
+ attempt,
1415
+ maxAttempts,
1416
+ totalBytes,
1417
+ totalStartedAt,
1418
+ transferStartedAt,
1419
+ transferEndedAt,
1420
+ installStartedAt,
1421
+ installEndedAt,
1422
+ progressEvents,
1423
+ lastProgress,
1424
+ installProgressEvents,
1425
+ lastInstallProgress,
1426
+ retried,
1427
+ });
1428
+
1429
+ if (lastResult && typeof lastResult === 'object') {
1430
+ const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
1431
+ string,
1432
+ unknown
1433
+ >;
1434
+ lastResult = {
1435
+ ...(lastResult as Record<string, unknown>),
1436
+ payload: {
1437
+ ...payload,
1438
+ _debug: debugMetrics,
1439
+ },
1440
+ };
1441
+ }
1442
+
1443
+ if (isSuccessResult(lastResult)) {
1444
+ return lastResult;
1445
+ }
1446
+
1447
+ if (
1448
+ attempt >= maxAttempts ||
1449
+ globalOpts.transport !== 'usb' ||
1450
+ !isProtocolV2UsbProbeTransientResult(lastResult)
1451
+ ) {
1452
+ return lastResult;
1453
+ }
1454
+
1455
+ retried = true;
1456
+ process.stderr.write(
1457
+ `[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
1458
+ );
1459
+ await disposeSDK();
1460
+ await new Promise(resolve => setTimeout(resolve, 3000));
1461
+ currentSdk = await createSDK(globalOpts);
1462
+ }
1463
+
1464
+ return lastResult;
1465
+ }
1466
+
1467
+ function buildFirmwareUpdateV4DebugParams(opts: {
1468
+ chunkSize?: string;
1469
+ resource?: string;
1470
+ romloader?: string;
1471
+ bootloader?: string;
1472
+ applicationP1?: string;
1473
+ applicationP2?: string;
1474
+ coprocessor?: string;
1475
+ se01?: string;
1476
+ se02?: string;
1477
+ se03?: string;
1478
+ se04?: string;
1479
+ forcedUpdateRes?: boolean;
1480
+ }) {
1481
+ const params = {
1482
+ platform: 'desktop' as const,
1483
+ connectProtocol: 'V2' as const,
1484
+ chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
1485
+ forcedUpdateRes: opts.forcedUpdateRes,
1486
+ resourceBinary: opts.resource ? readBinaryParam(opts.resource) : undefined,
1487
+ romloaderBinary: opts.romloader ? readBinaryParam(opts.romloader) : undefined,
1488
+ bootloaderBinary: opts.bootloader ? readBinaryParam(opts.bootloader) : undefined,
1489
+ applicationP1Binary: opts.applicationP1 ? readBinaryParam(opts.applicationP1) : undefined,
1490
+ applicationP2Binary: opts.applicationP2 ? readBinaryParam(opts.applicationP2) : undefined,
1491
+ coprocessorBinary: opts.coprocessor ? readBinaryParam(opts.coprocessor) : undefined,
1492
+ se01Binary: opts.se01 ? readBinaryParam(opts.se01) : undefined,
1493
+ se02Binary: opts.se02 ? readBinaryParam(opts.se02) : undefined,
1494
+ se03Binary: opts.se03 ? readBinaryParam(opts.se03) : undefined,
1495
+ se04Binary: opts.se04 ? readBinaryParam(opts.se04) : undefined,
1496
+ };
1497
+
1498
+ const hasPayload = [
1499
+ params.resourceBinary,
1500
+ params.romloaderBinary,
1501
+ params.bootloaderBinary,
1502
+ params.applicationP1Binary,
1503
+ params.applicationP2Binary,
1504
+ params.coprocessorBinary,
1505
+ params.se01Binary,
1506
+ params.se02Binary,
1507
+ params.se03Binary,
1508
+ params.se04Binary,
1509
+ ].some(Boolean);
1510
+
1511
+ if (!hasPayload) {
1512
+ const err = new Error('firmware-update-v4-debug requires at least one binary path');
1513
+ (err as Error & { code?: string }).code = 'MISSING_FIRMWARE_BINARY';
1514
+ throw err;
1515
+ }
1516
+
1517
+ return params;
1518
+ }
1519
+
1122
1520
  /**
1123
1521
  * #9 FIX: Safe parseInt with NaN check
1124
1522
  */
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(settings);
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.