@onekeyfe/hardware-cli 1.2.0-alpha.33 → 1.2.0-alpha.34

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import { createSDK } from './sdk';
2
3
  declare const program: Command;
3
4
  export declare function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble'): 90000 | undefined;
4
5
  /**
@@ -11,6 +12,26 @@ export declare function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble'
11
12
  * After this, globalOpts.passphraseState is set and getCommonParams will include it.
12
13
  */
13
14
  export declare function prepareSession(sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default, globalOpts: Record<string, any>): Promise<string | undefined>;
15
+ /**
16
+ * Unified command runner. All `.action(...)` handlers should wrap their body
17
+ * in this helper so every command goes through the same lifecycle:
18
+ *
19
+ * 1. (optional) create SDK
20
+ * 2. (optional) prepareSession — unlock + keychain preload + passphraseState
21
+ * 3. run the handler (which calls outputResult on success)
22
+ * 4. report uncaught errors as a structured failure result
23
+ * 5. dispose SDK
24
+ * 6. let Node exit naturally after all SDK resources are released
25
+ *
26
+ * This fixes three previous bugs:
27
+ * - Most signing commands skipped prepareSession, so keychain sessions
28
+ * were never actually reused.
29
+ * - outputResult scheduled process.exit(200ms) before dispose completed,
30
+ * racing USB release on slower transports.
31
+ * - Errors thrown from SDK calls leaked as stack traces instead of the
32
+ * CLI's structured `{ success: false, payload: { error } }` shape.
33
+ */
34
+ type AnySdk = Awaited<ReturnType<typeof createSDK>>;
14
35
  export declare function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }: {
15
36
  totalBytes: number;
16
37
  transferredBytes: number;
@@ -24,4 +45,42 @@ export declare function buildWallpaperUploadMetrics({ totalBytes, transferredByt
24
45
  transferKiBPerSecond: number | null;
25
46
  lastProgress: number;
26
47
  };
48
+ export declare function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }: {
49
+ sdk: AnySdk;
50
+ globalOpts: Record<string, any>;
51
+ params: ReturnType<typeof buildFirmwareUpdateV4Params>;
52
+ retries?: number;
53
+ }): Promise<unknown>;
54
+ declare function buildFirmwareUpdateV4Params(opts: {
55
+ chunkSize?: string;
56
+ resourceBundle?: string[];
57
+ romloader?: string;
58
+ bootloader?: string;
59
+ applicationP1?: string;
60
+ applicationP2?: string;
61
+ coprocessor?: string;
62
+ se01?: string;
63
+ se02?: string;
64
+ se03?: string;
65
+ se04?: string;
66
+ forcedUpdateRes?: boolean;
67
+ }): {
68
+ platform: "desktop";
69
+ connectProtocol: "V2";
70
+ chunkSize: number | undefined;
71
+ forcedUpdateRes: boolean | undefined;
72
+ resourceBundleFiles: {
73
+ binary: ArrayBuffer;
74
+ devicePath: string;
75
+ }[] | undefined;
76
+ romloaderBinary: ArrayBuffer | undefined;
77
+ bootloaderBinary: ArrayBuffer | undefined;
78
+ applicationP1Binary: ArrayBuffer | undefined;
79
+ applicationP2Binary: ArrayBuffer | undefined;
80
+ coprocessorBinary: ArrayBuffer | undefined;
81
+ se01Binary: ArrayBuffer | undefined;
82
+ se02Binary: ArrayBuffer | undefined;
83
+ se03Binary: ArrayBuffer | undefined;
84
+ se04Binary: ArrayBuffer | undefined;
85
+ };
27
86
  export { program };
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.program = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
4
+ exports.program = exports.runFirmwareUpdateV4WithRetry = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
5
5
  const node_fs_1 = require("node:fs");
6
6
  const node_path_1 = require("node:path");
7
7
  const commander_1 = require("commander");
@@ -1069,128 +1069,134 @@ async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries,
1069
1069
  const totalBytes = getFirmwareUpdateV4TotalBytes(params);
1070
1070
  const maxAttempts = Math.max((retries ?? 2) + 1, 1);
1071
1071
  let currentSdk = sdk;
1072
- let lastResult;
1073
1072
  let retried = false;
1074
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1075
- let progressEvents = 0;
1076
- let lastProgress = -1;
1077
- let transferStartedAt;
1078
- let transferEndedAt;
1079
- let installProgressEvents = 0;
1080
- let lastInstallProgress = -1;
1081
- let installStartedAt;
1082
- let installEndedAt;
1083
- let lastPrintedTransferProgress = -10;
1084
- let lastPrintedInstallProgress = -10;
1085
- const totalStartedAt = Date.now();
1086
- const connectId = retried && globalOpts.transport === 'usb' && globalOpts.connectId
1087
- ? undefined
1088
- : globalOpts.connectId;
1089
- const onUiEvent = (message) => {
1090
- if (!message || typeof message !== 'object')
1091
- return;
1092
- const messageType = message.type;
1093
- const payload = getFirmwareUpdatePayload(message);
1094
- if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1095
- const tipMessage = payload?.data?.message;
1096
- if (typeof tipMessage === 'string') {
1097
- process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1098
- }
1099
- return;
1100
- }
1101
- if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1102
- const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1103
- process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1104
- return;
1105
- }
1106
- if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1107
- return;
1108
- const progress = Number(payload.progress);
1109
- if (!Number.isFinite(progress))
1110
- return;
1111
- if (payload.progressType === 'transferData') {
1112
- progressEvents += 1;
1113
- lastProgress = Math.max(lastProgress, progress);
1114
- transferStartedAt ?? (transferStartedAt = Date.now());
1115
- lastPrintedTransferProgress = maybePrintFirmwareProgress({
1116
- progressType: 'transfer',
1117
- progress,
1118
- payload,
1119
- lastPrintedProgress: lastPrintedTransferProgress,
1120
- });
1121
- if (progress >= 100) {
1122
- transferEndedAt ?? (transferEndedAt = Date.now());
1123
- }
1124
- return;
1125
- }
1126
- if (payload.progressType === 'installingFirmware') {
1127
- installProgressEvents += 1;
1128
- lastInstallProgress = Math.max(lastInstallProgress, progress);
1129
- installStartedAt ?? (installStartedAt = Date.now());
1130
- lastPrintedInstallProgress = maybePrintFirmwareProgress({
1131
- progressType: 'install',
1132
- progress,
1133
- payload,
1134
- lastPrintedProgress: lastPrintedInstallProgress,
1135
- });
1136
- if (progress >= 100) {
1137
- installEndedAt ?? (installEndedAt = Date.now());
1138
- }
1073
+ let attempt = 1;
1074
+ let { connectId } = globalOpts;
1075
+ if (globalOpts.transport === 'usb') {
1076
+ for (; attempt <= maxAttempts; attempt += 1) {
1077
+ const probeResult = await currentSdk.getDeviceState(connectId, {
1078
+ scope: 'runtime',
1079
+ connectProtocol: 'V2',
1080
+ retryCount: 0,
1081
+ });
1082
+ if (isSuccessResult(probeResult))
1083
+ break;
1084
+ if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
1085
+ return probeResult;
1139
1086
  }
1140
- };
1141
- currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1142
- try {
1143
- lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1144
- }
1145
- finally {
1146
- currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1087
+ retried = true;
1088
+ process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`);
1089
+ await (0, sdk_1.disposeSDK)();
1090
+ await new Promise(resolve => {
1091
+ setTimeout(resolve, 3000);
1092
+ });
1093
+ currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1094
+ if (globalOpts.connectId)
1095
+ connectId = undefined;
1147
1096
  }
1148
- if (installStartedAt !== undefined && installEndedAt === undefined) {
1149
- installEndedAt = Date.now();
1097
+ }
1098
+ let progressEvents = 0;
1099
+ let lastProgress = -1;
1100
+ let transferStartedAt;
1101
+ let transferEndedAt;
1102
+ let installProgressEvents = 0;
1103
+ let lastInstallProgress = -1;
1104
+ let installStartedAt;
1105
+ let installEndedAt;
1106
+ let lastPrintedTransferProgress = -10;
1107
+ let lastPrintedInstallProgress = -10;
1108
+ const totalStartedAt = Date.now();
1109
+ const onUiEvent = (message) => {
1110
+ if (!message || typeof message !== 'object')
1111
+ return;
1112
+ const messageType = message.type;
1113
+ const payload = getFirmwareUpdatePayload(message);
1114
+ if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
1115
+ const tipMessage = payload?.data?.message;
1116
+ if (typeof tipMessage === 'string') {
1117
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1118
+ }
1119
+ return;
1150
1120
  }
1151
- const metrics = buildFirmwareUpdateV4Metrics({
1152
- attempt,
1153
- maxAttempts,
1154
- totalBytes,
1155
- totalStartedAt,
1156
- transferStartedAt,
1157
- transferEndedAt,
1158
- installStartedAt,
1159
- installEndedAt,
1160
- progressEvents,
1161
- lastProgress,
1162
- installProgressEvents,
1163
- lastInstallProgress,
1164
- retried,
1165
- });
1166
- if (lastResult && typeof lastResult === 'object') {
1167
- const payload = (lastResult.payload ?? {});
1168
- lastResult = {
1169
- ...lastResult,
1170
- payload: {
1171
- ...payload,
1172
- metrics,
1173
- },
1174
- };
1121
+ if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
1122
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1123
+ process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
1124
+ return;
1175
1125
  }
1176
- if (isSuccessResult(lastResult)) {
1177
- return lastResult;
1126
+ if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
1127
+ return;
1128
+ const progress = Number(payload.progress);
1129
+ if (!Number.isFinite(progress))
1130
+ return;
1131
+ if (payload.progressType === 'transferData') {
1132
+ progressEvents += 1;
1133
+ lastProgress = Math.max(lastProgress, progress);
1134
+ transferStartedAt ?? (transferStartedAt = Date.now());
1135
+ lastPrintedTransferProgress = maybePrintFirmwareProgress({
1136
+ progressType: 'transfer',
1137
+ progress,
1138
+ payload,
1139
+ lastPrintedProgress: lastPrintedTransferProgress,
1140
+ });
1141
+ if (progress >= 100) {
1142
+ transferEndedAt ?? (transferEndedAt = Date.now());
1143
+ }
1144
+ return;
1178
1145
  }
1179
- if (attempt >= maxAttempts ||
1180
- globalOpts.transport !== 'usb' ||
1181
- !isProtocolV2UsbProbeTransientResult(lastResult)) {
1182
- return lastResult;
1146
+ if (payload.progressType === 'installingFirmware') {
1147
+ installProgressEvents += 1;
1148
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1149
+ installStartedAt ?? (installStartedAt = Date.now());
1150
+ lastPrintedInstallProgress = maybePrintFirmwareProgress({
1151
+ progressType: 'install',
1152
+ progress,
1153
+ payload,
1154
+ lastPrintedProgress: lastPrintedInstallProgress,
1155
+ });
1156
+ if (progress >= 100) {
1157
+ installEndedAt ?? (installEndedAt = Date.now());
1158
+ }
1183
1159
  }
1184
- retried = true;
1185
- process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`);
1186
- await (0, sdk_1.disposeSDK)();
1187
- await new Promise(resolve => {
1188
- setTimeout(resolve, 3000);
1189
- });
1190
- currentSdk = await (0, sdk_1.createSDK)(globalOpts);
1160
+ };
1161
+ currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
1162
+ let result;
1163
+ try {
1164
+ result = await currentSdk.firmwareUpdateV4(connectId, params);
1165
+ }
1166
+ finally {
1167
+ currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
1168
+ }
1169
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1170
+ installEndedAt = Date.now();
1171
+ }
1172
+ const metrics = buildFirmwareUpdateV4Metrics({
1173
+ attempt,
1174
+ maxAttempts,
1175
+ totalBytes,
1176
+ totalStartedAt,
1177
+ transferStartedAt,
1178
+ transferEndedAt,
1179
+ installStartedAt,
1180
+ installEndedAt,
1181
+ progressEvents,
1182
+ lastProgress,
1183
+ installProgressEvents,
1184
+ lastInstallProgress,
1185
+ retried,
1186
+ });
1187
+ if (result && typeof result === 'object') {
1188
+ const payload = (result.payload ?? {});
1189
+ return {
1190
+ ...result,
1191
+ payload: {
1192
+ ...payload,
1193
+ metrics,
1194
+ },
1195
+ };
1191
1196
  }
1192
- return lastResult;
1197
+ return result;
1193
1198
  }
1199
+ exports.runFirmwareUpdateV4WithRetry = runFirmwareUpdateV4WithRetry;
1194
1200
  function buildFirmwareUpdateV4Params(opts) {
1195
1201
  const params = {
1196
1202
  platform: 'desktop',
@@ -3,13 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createNobleBlePlugin = void 0;
4
4
  const hd_shared_1 = require("@onekeyfe/hd-shared");
5
5
  const ONEKEY_SERVICE_UUIDS = [hd_shared_1.ONEKEY_SERVICE_UUID];
6
- const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']);
7
- const NORMALIZED_WRITE_UUID = '0002';
8
- const NORMALIZED_NOTIFY_UUID = '0003';
9
- const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
10
- ...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
11
- '0001',
12
- ]);
6
+ const ONEKEY_SERVICE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_SERVICE_UUID);
7
+ const ONEKEY_WRITE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_WRITE_CHARACTERISTIC_UUID);
8
+ const ONEKEY_NOTIFY_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
13
9
  const BLUETOOTH_INIT_TIMEOUT = 10000;
14
10
  const DEVICE_SCAN_TIMEOUT = 8000;
15
11
  const CONNECTION_TIMEOUT = 8000;
@@ -24,24 +20,14 @@ const connectedDevices = new Map();
24
20
  const deviceCharacteristics = new Map();
25
21
  const notificationStates = new Map();
26
22
  const notificationGenerations = new Map();
27
- function getBleUuidKey(uuid) {
28
- const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
29
- return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
30
- }
31
- function isGenericBleService(uuid) {
32
- return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
33
- }
34
- function hasOneKeyAdvertisementService(peripheral) {
35
- const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
36
- return serviceUuids.some(uuid => {
37
- const uuidKey = getBleUuidKey(uuid);
38
- return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
39
- PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
40
- });
41
- }
42
23
  function isOneKeyPeripheral(peripheral) {
43
- const deviceName = peripheral.advertisement?.localName || null;
44
- return (0, hd_shared_1.isOnekeyDevice)(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
24
+ const serviceUuids = peripheral.advertisement?.serviceUuids;
25
+ return ((0, hd_shared_1.hasOnekeyCommunicationService)(serviceUuids) &&
26
+ (0, hd_shared_1.isOnekeyBluetoothDevice)({
27
+ id: peripheral.id,
28
+ localName: peripheral.advertisement?.localName,
29
+ serviceUuids,
30
+ }));
45
31
  }
46
32
  function enqueueNotification(deviceId, generation, data) {
47
33
  const state = notificationStates.get(deviceId);
@@ -171,7 +157,7 @@ async function scanDevices(targetDeviceId) {
171
157
  const onDiscover = (peripheral) => {
172
158
  if (targetDeviceId && peripheral.id !== targetDeviceId)
173
159
  return;
174
- if (!targetDeviceId && !isOneKeyPeripheral(peripheral))
160
+ if (!isOneKeyPeripheral(peripheral))
175
161
  return;
176
162
  discoveredDevices.set(peripheral.id, peripheral);
177
163
  found.set(peripheral.id, peripheral);
@@ -220,13 +206,7 @@ async function discoverCharacteristics(peripheral) {
220
206
  resolve(discoveredServices);
221
207
  });
222
208
  });
223
- let service = services.find(s => NORMALIZED_ONEKEY_SERVICE_UUIDS.has(getBleUuidKey(s.uuid)));
224
- if (!service) {
225
- service =
226
- services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
227
- services.find(s => !isGenericBleService(s.uuid)) ||
228
- services[0];
229
- }
209
+ const service = services.find(s => (0, hd_shared_1.matchesKnownBleUuid)(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
230
210
  if (!service) {
231
211
  throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
232
212
  }
@@ -243,11 +223,10 @@ async function discoverCharacteristics(peripheral) {
243
223
  let writeCharacteristic;
244
224
  let notifyCharacteristic;
245
225
  for (const characteristic of characteristics) {
246
- const uuidKey = getBleUuidKey(characteristic.uuid);
247
- if (uuidKey === NORMALIZED_WRITE_UUID) {
226
+ if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
248
227
  writeCharacteristic = characteristic;
249
228
  }
250
- else if (uuidKey === NORMALIZED_NOTIFY_UUID) {
229
+ else if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
251
230
  notifyCharacteristic = characteristic;
252
231
  }
253
232
  }
@@ -335,17 +314,25 @@ function createNobleBlePlugin() {
335
314
  throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.DeviceNotFound, `BLE device not found: ${uuid}`);
336
315
  }
337
316
  await connectPeripheral(peripheral);
338
- const characteristics = await discoverCharacteristics(peripheral);
339
- const notificationState = createNotificationState(uuid);
317
+ let characteristics;
340
318
  try {
319
+ characteristics = await discoverCharacteristics(peripheral);
320
+ const notificationState = createNotificationState(uuid);
341
321
  await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
322
+ connectedDevices.set(uuid, peripheral);
323
+ deviceCharacteristics.set(uuid, characteristics);
342
324
  }
343
325
  catch (error) {
344
326
  clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
327
+ if (characteristics) {
328
+ characteristics.notify.removeAllListeners('data');
329
+ await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
330
+ }
331
+ if (peripheral.state !== 'disconnected') {
332
+ await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
333
+ }
345
334
  throw error;
346
335
  }
347
- connectedDevices.set(uuid, peripheral);
348
- deviceCharacteristics.set(uuid, characteristics);
349
336
  },
350
337
  async disconnect(uuid) {
351
338
  await disconnectDevice(uuid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hardware-cli",
3
- "version": "1.2.0-alpha.33",
3
+ "version": "1.2.0-alpha.34",
4
4
  "description": "OneKey hardware wallet CLI for testing device communication",
5
5
  "author": "OneKey",
6
6
  "license": "Apache-2.0",
@@ -31,12 +31,12 @@
31
31
  "test": "jest"
32
32
  },
33
33
  "dependencies": {
34
- "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.33",
35
- "@onekeyfe/hd-core": "1.2.0-alpha.33",
36
- "@onekeyfe/hd-shared": "1.2.0-alpha.33",
37
- "@onekeyfe/hd-transport-usb": "1.2.0-alpha.33",
34
+ "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.34",
35
+ "@onekeyfe/hd-core": "1.2.0-alpha.34",
36
+ "@onekeyfe/hd-shared": "1.2.0-alpha.34",
37
+ "@onekeyfe/hd-transport-usb": "1.2.0-alpha.34",
38
38
  "@stoprocent/noble": "2.3.16",
39
39
  "commander": "^12.0.0"
40
40
  },
41
- "gitHead": "965e837c93fc67225ccfac28d22c3403ff96ad25"
41
+ "gitHead": "07f10b4c9fab177da7c36d976ec18b075492b57f"
42
42
  }
@@ -1,6 +1,34 @@
1
- import { program } from '../cli';
1
+ import { createSDK, disposeSDK } from '../sdk';
2
+ import { program, runFirmwareUpdateV4WithRetry } from '../cli';
3
+
4
+ jest.mock('../sdk', () => ({
5
+ createSDK: jest.fn(),
6
+ disposeSDK: jest.fn(),
7
+ }));
8
+
9
+ const transientProbeFailure = {
10
+ success: false,
11
+ payload: {
12
+ error: 'Device protocol mismatch: expected V2; device did not respond to expected protocol',
13
+ },
14
+ };
15
+
16
+ const createSdkMock = () => ({
17
+ getDeviceState: jest.fn(),
18
+ firmwareUpdateV4: jest.fn(),
19
+ on: jest.fn(),
20
+ off: jest.fn(),
21
+ });
2
22
 
3
23
  describe('firmware-update-v4 CLI command', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ afterEach(() => {
29
+ jest.restoreAllMocks();
30
+ });
31
+
4
32
  test('exposes firmware-update-v4 as the formal command', () => {
5
33
  const command = program.commands.find(item => item.name() === 'firmware-update-v4');
6
34
 
@@ -16,4 +44,57 @@ describe('firmware-update-v4 CLI command', () => {
16
44
  false
17
45
  );
18
46
  });
47
+
48
+ test('retries only the read-only USB probe before starting the firmware update', async () => {
49
+ const firstSdk = createSdkMock();
50
+ const retrySdk = createSdkMock();
51
+ firstSdk.getDeviceState.mockResolvedValue(transientProbeFailure);
52
+ retrySdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
53
+ retrySdk.firmwareUpdateV4.mockResolvedValue({ success: true, payload: {} });
54
+ jest.mocked(createSDK).mockResolvedValue(retrySdk as never);
55
+ jest.mocked(disposeSDK).mockResolvedValue(undefined);
56
+ jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
57
+ callback();
58
+ return 0 as never;
59
+ });
60
+
61
+ const result = await runFirmwareUpdateV4WithRetry({
62
+ sdk: firstSdk as never,
63
+ globalOpts: { transport: 'usb', connectId: 'stale-connect-id' },
64
+ params: { applicationP1Binary: new ArrayBuffer(1) } as never,
65
+ retries: 1,
66
+ });
67
+
68
+ expect(firstSdk.firmwareUpdateV4).not.toHaveBeenCalled();
69
+ expect(disposeSDK).toHaveBeenCalledTimes(1);
70
+ expect(retrySdk.getDeviceState).toHaveBeenCalledWith(undefined, {
71
+ scope: 'runtime',
72
+ connectProtocol: 'V2',
73
+ retryCount: 0,
74
+ });
75
+ expect(retrySdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
76
+ expect(result).toMatchObject({ success: true, payload: { metrics: { attempt: 2 } } });
77
+ });
78
+
79
+ test('does not replay firmwareUpdateV4 after the read-only probe succeeds', async () => {
80
+ const sdk = createSdkMock();
81
+ sdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
82
+ sdk.firmwareUpdateV4.mockResolvedValue(transientProbeFailure);
83
+
84
+ const result = await runFirmwareUpdateV4WithRetry({
85
+ sdk: sdk as never,
86
+ globalOpts: { transport: 'usb', connectId: 'pro2-connect-id' },
87
+ params: { applicationP1Binary: new ArrayBuffer(1) } as never,
88
+ retries: 2,
89
+ });
90
+
91
+ expect(sdk.getDeviceState).toHaveBeenCalledTimes(1);
92
+ expect(sdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
93
+ expect(disposeSDK).not.toHaveBeenCalled();
94
+ expect(createSDK).not.toHaveBeenCalled();
95
+ expect(result).toMatchObject({
96
+ success: false,
97
+ payload: { error: transientProbeFailure.payload.error, metrics: { attempt: 1 } },
98
+ });
99
+ });
19
100
  });
@@ -33,12 +33,13 @@ const createPeripheral = (id: string) => {
33
33
  state: 'connected',
34
34
  advertisement: {
35
35
  localName: `OneKey Pro 2 ${id}`,
36
- serviceUuids: ['fffd'],
36
+ serviceUuids: ['0001'],
37
37
  },
38
38
  discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
39
39
  connect: jest.fn(callback => callback()),
40
40
  disconnect: jest.fn(callback => callback()),
41
41
  },
42
+ service,
42
43
  write,
43
44
  notify,
44
45
  };
@@ -46,10 +47,74 @@ const createPeripheral = (id: string) => {
46
47
 
47
48
  describe('Noble BLE plugin notification routing', () => {
48
49
  afterEach(() => {
50
+ jest.useRealTimers();
49
51
  jest.resetModules();
50
52
  jest.clearAllMocks();
51
53
  });
52
54
 
55
+ test('does not enumerate Find My advertisements that expose FFFD', async () => {
56
+ jest.useFakeTimers({ doNotFake: ['performance'] });
57
+ const oneKey = createPeripheral('onekey-device');
58
+ const findMy = createPeripheral('find-my-device');
59
+ findMy.peripheral.advertisement.localName = 'Find My';
60
+ findMy.peripheral.advertisement.serviceUuids = ['fffd'];
61
+ const noble = new EventEmitter() as EventEmitter & {
62
+ state: string;
63
+ startScanning: jest.Mock;
64
+ stopScanning: jest.Mock;
65
+ };
66
+ noble.state = 'poweredOn';
67
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
68
+ callback?.();
69
+ noble.emit('discover', findMy.peripheral);
70
+ noble.emit('discover', oneKey.peripheral);
71
+ });
72
+ noble.stopScanning = jest.fn(callback => callback?.());
73
+ jest.doMock('@stoprocent/noble', () => noble);
74
+
75
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
76
+ const plugin = createNobleBlePlugin();
77
+ await plugin.init();
78
+ const devicesPromise = plugin.enumerate();
79
+ await Promise.resolve();
80
+ jest.runAllTimers();
81
+ await Promise.resolve();
82
+
83
+ await expect(devicesPromise).resolves.toEqual([
84
+ expect.objectContaining({ id: 'onekey-device' }),
85
+ ]);
86
+ jest.useRealTimers();
87
+ });
88
+
89
+ test('does not enumerate a name-only candidate without the communication service', async () => {
90
+ jest.useFakeTimers({ doNotFake: ['performance'] });
91
+ const nameOnly = createPeripheral('name-only-device');
92
+ nameOnly.peripheral.advertisement.serviceUuids = [];
93
+ const noble = new EventEmitter() as EventEmitter & {
94
+ state: string;
95
+ startScanning: jest.Mock;
96
+ stopScanning: jest.Mock;
97
+ };
98
+ noble.state = 'poweredOn';
99
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
100
+ callback?.();
101
+ noble.emit('discover', nameOnly.peripheral);
102
+ });
103
+ noble.stopScanning = jest.fn(callback => callback?.());
104
+ jest.doMock('@stoprocent/noble', () => noble);
105
+
106
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
107
+ const plugin = createNobleBlePlugin();
108
+ await plugin.init();
109
+ const devicesPromise = plugin.enumerate();
110
+ await Promise.resolve();
111
+ jest.runAllTimers();
112
+ await Promise.resolve();
113
+
114
+ await expect(devicesPromise).resolves.toEqual([]);
115
+ jest.useRealTimers();
116
+ });
117
+
53
118
  test('routes notifications to the receiver waiting for the same device', async () => {
54
119
  const deviceA = createPeripheral('device-a');
55
120
  const deviceB = createPeripheral('device-b');
@@ -112,6 +177,82 @@ describe('Noble BLE plugin notification routing', () => {
112
177
  expect(result).toBe('completed');
113
178
  });
114
179
 
180
+ test('disconnects an untracked peripheral when service discovery fails', async () => {
181
+ const device = createPeripheral('device-a');
182
+ device.peripheral.discoverServices.mockImplementation((_uuids, callback) =>
183
+ callback(new Error('service discovery failed'))
184
+ );
185
+ const noble = new EventEmitter() as EventEmitter & {
186
+ state: string;
187
+ startScanning: jest.Mock;
188
+ stopScanning: jest.Mock;
189
+ };
190
+ noble.state = 'poweredOn';
191
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
192
+ callback?.();
193
+ noble.emit('discover', device.peripheral);
194
+ });
195
+ noble.stopScanning = jest.fn(callback => callback?.());
196
+ jest.doMock('@stoprocent/noble', () => noble);
197
+
198
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
199
+ const plugin = createNobleBlePlugin();
200
+ await plugin.init();
201
+
202
+ await expect(plugin.connect('device-a')).rejects.toThrow('service discovery failed');
203
+ expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
204
+ });
205
+
206
+ test('rejects a vendor-specific service containing the OneKey short UUID', async () => {
207
+ const device = createPeripheral('device-a');
208
+ device.service.uuid = 'abcd0001-1234-5678-9012-abcdefabcdef';
209
+ const noble = new EventEmitter() as EventEmitter & {
210
+ state: string;
211
+ startScanning: jest.Mock;
212
+ stopScanning: jest.Mock;
213
+ };
214
+ noble.state = 'poweredOn';
215
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
216
+ callback?.();
217
+ noble.emit('discover', device.peripheral);
218
+ });
219
+ noble.stopScanning = jest.fn(callback => callback?.());
220
+ jest.doMock('@stoprocent/noble', () => noble);
221
+
222
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
223
+ const plugin = createNobleBlePlugin();
224
+ await plugin.init();
225
+
226
+ await expect(plugin.connect('device-a')).rejects.toThrow('No BLE service found');
227
+ });
228
+
229
+ test('unsubscribes and disconnects an untracked peripheral when notification setup fails', async () => {
230
+ const device = createPeripheral('device-a');
231
+ device.notify.subscribe.mockImplementation(callback =>
232
+ callback(new Error('notification setup failed'))
233
+ );
234
+ const noble = new EventEmitter() as EventEmitter & {
235
+ state: string;
236
+ startScanning: jest.Mock;
237
+ stopScanning: jest.Mock;
238
+ };
239
+ noble.state = 'poweredOn';
240
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
241
+ callback?.();
242
+ noble.emit('discover', device.peripheral);
243
+ });
244
+ noble.stopScanning = jest.fn(callback => callback?.());
245
+ jest.doMock('@stoprocent/noble', () => noble);
246
+
247
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
248
+ const plugin = createNobleBlePlugin();
249
+ await plugin.init();
250
+
251
+ await expect(plugin.connect('device-a')).rejects.toThrow('notification setup failed');
252
+ expect(device.notify.unsubscribe).toHaveBeenCalled();
253
+ expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
254
+ });
255
+
115
256
  test('uses withoutResponse for normal and high-volume writes', async () => {
116
257
  const device = createPeripheral('device-a');
117
258
  const noble = new EventEmitter() as EventEmitter & {
package/src/cli.ts CHANGED
@@ -1433,7 +1433,7 @@ function buildFirmwareUpdateV4Metrics({
1433
1433
  };
1434
1434
  }
1435
1435
 
1436
- async function runFirmwareUpdateV4WithRetry({
1436
+ export async function runFirmwareUpdateV4WithRetry({
1437
1437
  sdk,
1438
1438
  globalOpts,
1439
1439
  params,
@@ -1447,147 +1447,143 @@ async function runFirmwareUpdateV4WithRetry({
1447
1447
  const totalBytes = getFirmwareUpdateV4TotalBytes(params);
1448
1448
  const maxAttempts = Math.max((retries ?? 2) + 1, 1);
1449
1449
  let currentSdk = sdk;
1450
- let lastResult: unknown;
1451
1450
  let retried = false;
1452
-
1453
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1454
- let progressEvents = 0;
1455
- let lastProgress = -1;
1456
- let transferStartedAt: number | undefined;
1457
- let transferEndedAt: number | undefined;
1458
- let installProgressEvents = 0;
1459
- let lastInstallProgress = -1;
1460
- let installStartedAt: number | undefined;
1461
- let installEndedAt: number | undefined;
1462
- let lastPrintedTransferProgress = -10;
1463
- let lastPrintedInstallProgress = -10;
1464
- const totalStartedAt = Date.now();
1465
- const connectId =
1466
- retried && globalOpts.transport === 'usb' && globalOpts.connectId
1467
- ? undefined
1468
- : globalOpts.connectId;
1469
-
1470
- const onUiEvent = (message: unknown) => {
1471
- if (!message || typeof message !== 'object') return;
1472
- const messageType = (message as { type?: string }).type;
1473
- const payload = getFirmwareUpdatePayload(message);
1474
-
1475
- if (messageType === UI_REQUEST.FIRMWARE_TIP) {
1476
- const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
1477
- if (typeof tipMessage === 'string') {
1478
- process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1479
- }
1480
- return;
1481
- }
1482
-
1483
- if (messageType === UI_REQUEST.REQUEST_BUTTON) {
1484
- const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1485
- process.stderr.write(
1486
- `[onekey-hw] Please confirm the firmware update on your device${code}.\n`
1487
- );
1488
- return;
1451
+ let attempt = 1;
1452
+ let { connectId } = globalOpts;
1453
+
1454
+ if (globalOpts.transport === 'usb') {
1455
+ for (; attempt <= maxAttempts; attempt += 1) {
1456
+ const probeResult = await currentSdk.getDeviceState(connectId, {
1457
+ scope: 'runtime',
1458
+ connectProtocol: 'V2',
1459
+ retryCount: 0,
1460
+ });
1461
+ if (isSuccessResult(probeResult)) break;
1462
+ if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
1463
+ return probeResult;
1489
1464
  }
1490
1465
 
1491
- if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
1492
- const progress = Number(payload.progress);
1493
- if (!Number.isFinite(progress)) return;
1494
-
1495
- if (payload.progressType === 'transferData') {
1496
- progressEvents += 1;
1497
- lastProgress = Math.max(lastProgress, progress);
1498
- transferStartedAt ??= Date.now();
1499
- lastPrintedTransferProgress = maybePrintFirmwareProgress({
1500
- progressType: 'transfer',
1501
- progress,
1502
- payload,
1503
- lastPrintedProgress: lastPrintedTransferProgress,
1504
- });
1505
- if (progress >= 100) {
1506
- transferEndedAt ??= Date.now();
1507
- }
1508
- return;
1509
- }
1466
+ retried = true;
1467
+ process.stderr.write(
1468
+ `[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
1469
+ );
1470
+ await disposeSDK();
1471
+ await new Promise(resolve => {
1472
+ setTimeout(resolve, 3000);
1473
+ });
1474
+ currentSdk = await createSDK(globalOpts);
1475
+ if (globalOpts.connectId) connectId = undefined;
1476
+ }
1477
+ }
1510
1478
 
1511
- if (payload.progressType === 'installingFirmware') {
1512
- installProgressEvents += 1;
1513
- lastInstallProgress = Math.max(lastInstallProgress, progress);
1514
- installStartedAt ??= Date.now();
1515
- lastPrintedInstallProgress = maybePrintFirmwareProgress({
1516
- progressType: 'install',
1517
- progress,
1518
- payload,
1519
- lastPrintedProgress: lastPrintedInstallProgress,
1520
- });
1521
- if (progress >= 100) {
1522
- installEndedAt ??= Date.now();
1523
- }
1479
+ let progressEvents = 0;
1480
+ let lastProgress = -1;
1481
+ let transferStartedAt: number | undefined;
1482
+ let transferEndedAt: number | undefined;
1483
+ let installProgressEvents = 0;
1484
+ let lastInstallProgress = -1;
1485
+ let installStartedAt: number | undefined;
1486
+ let installEndedAt: number | undefined;
1487
+ let lastPrintedTransferProgress = -10;
1488
+ let lastPrintedInstallProgress = -10;
1489
+ const totalStartedAt = Date.now();
1490
+
1491
+ const onUiEvent = (message: unknown) => {
1492
+ if (!message || typeof message !== 'object') return;
1493
+ const messageType = (message as { type?: string }).type;
1494
+ const payload = getFirmwareUpdatePayload(message);
1495
+
1496
+ if (messageType === UI_REQUEST.FIRMWARE_TIP) {
1497
+ const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
1498
+ if (typeof tipMessage === 'string') {
1499
+ process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
1524
1500
  }
1525
- };
1526
-
1527
- currentSdk.on(UI_EVENT, onUiEvent);
1528
- try {
1529
- lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
1530
- } finally {
1531
- currentSdk.off?.(UI_EVENT, onUiEvent);
1532
- }
1533
- if (installStartedAt !== undefined && installEndedAt === undefined) {
1534
- installEndedAt = Date.now();
1501
+ return;
1535
1502
  }
1536
1503
 
1537
- const metrics = buildFirmwareUpdateV4Metrics({
1538
- attempt,
1539
- maxAttempts,
1540
- totalBytes,
1541
- totalStartedAt,
1542
- transferStartedAt,
1543
- transferEndedAt,
1544
- installStartedAt,
1545
- installEndedAt,
1546
- progressEvents,
1547
- lastProgress,
1548
- installProgressEvents,
1549
- lastInstallProgress,
1550
- retried,
1551
- });
1552
-
1553
- if (lastResult && typeof lastResult === 'object') {
1554
- const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
1555
- string,
1556
- unknown
1557
- >;
1558
- lastResult = {
1559
- ...(lastResult as Record<string, unknown>),
1560
- payload: {
1561
- ...payload,
1562
- metrics,
1563
- },
1564
- };
1504
+ if (messageType === UI_REQUEST.REQUEST_BUTTON) {
1505
+ const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
1506
+ process.stderr.write(
1507
+ `[onekey-hw] Please confirm the firmware update on your device${code}.\n`
1508
+ );
1509
+ return;
1565
1510
  }
1566
1511
 
1567
- if (isSuccessResult(lastResult)) {
1568
- return lastResult;
1512
+ if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
1513
+ const progress = Number(payload.progress);
1514
+ if (!Number.isFinite(progress)) return;
1515
+
1516
+ if (payload.progressType === 'transferData') {
1517
+ progressEvents += 1;
1518
+ lastProgress = Math.max(lastProgress, progress);
1519
+ transferStartedAt ??= Date.now();
1520
+ lastPrintedTransferProgress = maybePrintFirmwareProgress({
1521
+ progressType: 'transfer',
1522
+ progress,
1523
+ payload,
1524
+ lastPrintedProgress: lastPrintedTransferProgress,
1525
+ });
1526
+ if (progress >= 100) {
1527
+ transferEndedAt ??= Date.now();
1528
+ }
1529
+ return;
1569
1530
  }
1570
1531
 
1571
- if (
1572
- attempt >= maxAttempts ||
1573
- globalOpts.transport !== 'usb' ||
1574
- !isProtocolV2UsbProbeTransientResult(lastResult)
1575
- ) {
1576
- return lastResult;
1532
+ if (payload.progressType === 'installingFirmware') {
1533
+ installProgressEvents += 1;
1534
+ lastInstallProgress = Math.max(lastInstallProgress, progress);
1535
+ installStartedAt ??= Date.now();
1536
+ lastPrintedInstallProgress = maybePrintFirmwareProgress({
1537
+ progressType: 'install',
1538
+ progress,
1539
+ payload,
1540
+ lastPrintedProgress: lastPrintedInstallProgress,
1541
+ });
1542
+ if (progress >= 100) {
1543
+ installEndedAt ??= Date.now();
1544
+ }
1577
1545
  }
1546
+ };
1578
1547
 
1579
- retried = true;
1580
- process.stderr.write(
1581
- `[onekey-hw] Protocol V2 USB probe was transient; retrying firmwareUpdateV4 (${attempt}/${maxAttempts})...\n`
1582
- );
1583
- await disposeSDK();
1584
- await new Promise(resolve => {
1585
- setTimeout(resolve, 3000);
1586
- });
1587
- currentSdk = await createSDK(globalOpts);
1548
+ currentSdk.on(UI_EVENT, onUiEvent);
1549
+ let result: unknown;
1550
+ try {
1551
+ result = await currentSdk.firmwareUpdateV4(connectId, params);
1552
+ } finally {
1553
+ currentSdk.off?.(UI_EVENT, onUiEvent);
1554
+ }
1555
+ if (installStartedAt !== undefined && installEndedAt === undefined) {
1556
+ installEndedAt = Date.now();
1557
+ }
1558
+
1559
+ const metrics = buildFirmwareUpdateV4Metrics({
1560
+ attempt,
1561
+ maxAttempts,
1562
+ totalBytes,
1563
+ totalStartedAt,
1564
+ transferStartedAt,
1565
+ transferEndedAt,
1566
+ installStartedAt,
1567
+ installEndedAt,
1568
+ progressEvents,
1569
+ lastProgress,
1570
+ installProgressEvents,
1571
+ lastInstallProgress,
1572
+ retried,
1573
+ });
1574
+
1575
+ if (result && typeof result === 'object') {
1576
+ const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
1577
+ return {
1578
+ ...(result as Record<string, unknown>),
1579
+ payload: {
1580
+ ...payload,
1581
+ metrics,
1582
+ },
1583
+ };
1588
1584
  }
1589
1585
 
1590
- return lastResult;
1586
+ return result;
1591
1587
  }
1592
1588
 
1593
1589
  function buildFirmwareUpdateV4Params(opts: {
@@ -1,8 +1,13 @@
1
1
  import {
2
2
  ERRORS,
3
3
  HardwareErrorCode,
4
+ ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
4
5
  ONEKEY_SERVICE_UUID,
5
- isOnekeyDevice,
6
+ ONEKEY_WRITE_CHARACTERISTIC_UUID,
7
+ createKnownBleUuidAliases,
8
+ hasOnekeyCommunicationService,
9
+ isOnekeyBluetoothDevice,
10
+ matchesKnownBleUuid,
6
11
  } from '@onekeyfe/hd-shared';
7
12
 
8
13
  import type { LowLevelDevice, LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
@@ -39,13 +44,9 @@ type NobleNotificationState = {
39
44
  };
40
45
 
41
46
  const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
42
- const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']);
43
- const NORMALIZED_WRITE_UUID = '0002';
44
- const NORMALIZED_NOTIFY_UUID = '0003';
45
- const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
46
- ...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
47
- '0001',
48
- ]);
47
+ const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
48
+ const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
49
+ const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
49
50
 
50
51
  const BLUETOOTH_INIT_TIMEOUT = 10_000;
51
52
  const DEVICE_SCAN_TIMEOUT = 8_000;
@@ -63,29 +64,16 @@ const deviceCharacteristics = new Map<string, CharacteristicPair>();
63
64
  const notificationStates = new Map<string, NobleNotificationState>();
64
65
  const notificationGenerations = new Map<string, number>();
65
66
 
66
- function getBleUuidKey(uuid?: string | null) {
67
- const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
68
- return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
69
- }
70
-
71
- function isGenericBleService(uuid?: string | null) {
72
- return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
73
- }
74
-
75
- function hasOneKeyAdvertisementService(peripheral: Peripheral) {
76
- const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
77
- return serviceUuids.some(uuid => {
78
- const uuidKey = getBleUuidKey(uuid);
79
- return (
80
- NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
81
- PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
82
- );
83
- });
84
- }
85
-
86
67
  function isOneKeyPeripheral(peripheral: Peripheral) {
87
- const deviceName = peripheral.advertisement?.localName || null;
88
- return isOnekeyDevice(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
68
+ const serviceUuids = peripheral.advertisement?.serviceUuids;
69
+ return (
70
+ hasOnekeyCommunicationService(serviceUuids) &&
71
+ isOnekeyBluetoothDevice({
72
+ id: peripheral.id,
73
+ localName: peripheral.advertisement?.localName,
74
+ serviceUuids,
75
+ })
76
+ );
89
77
  }
90
78
 
91
79
  function enqueueNotification(deviceId: string, generation: number, data: Buffer) {
@@ -230,7 +218,7 @@ async function scanDevices(targetDeviceId?: string) {
230
218
 
231
219
  const onDiscover = (peripheral: Peripheral) => {
232
220
  if (targetDeviceId && peripheral.id !== targetDeviceId) return;
233
- if (!targetDeviceId && !isOneKeyPeripheral(peripheral)) return;
221
+ if (!isOneKeyPeripheral(peripheral)) return;
234
222
 
235
223
  discoveredDevices.set(peripheral.id, peripheral);
236
224
  found.set(peripheral.id, peripheral);
@@ -285,13 +273,7 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
285
273
  });
286
274
  });
287
275
 
288
- let service = services.find(s => NORMALIZED_ONEKEY_SERVICE_UUIDS.has(getBleUuidKey(s.uuid)));
289
- if (!service) {
290
- service =
291
- services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
292
- services.find(s => !isGenericBleService(s.uuid)) ||
293
- services[0];
294
- }
276
+ const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
295
277
  if (!service) {
296
278
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
297
279
  }
@@ -310,10 +292,9 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
310
292
  let writeCharacteristic: Characteristic | undefined;
311
293
  let notifyCharacteristic: Characteristic | undefined;
312
294
  for (const characteristic of characteristics) {
313
- const uuidKey = getBleUuidKey(characteristic.uuid);
314
- if (uuidKey === NORMALIZED_WRITE_UUID) {
295
+ if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
315
296
  writeCharacteristic = characteristic;
316
- } else if (uuidKey === NORMALIZED_NOTIFY_UUID) {
297
+ } else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
317
298
  notifyCharacteristic = characteristic;
318
299
  }
319
300
  }
@@ -437,16 +418,24 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
437
418
  }
438
419
 
439
420
  await connectPeripheral(peripheral);
440
- const characteristics = await discoverCharacteristics(peripheral);
441
- const notificationState = createNotificationState(uuid);
421
+ let characteristics: CharacteristicPair | undefined;
442
422
  try {
423
+ characteristics = await discoverCharacteristics(peripheral);
424
+ const notificationState = createNotificationState(uuid);
443
425
  await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
426
+ connectedDevices.set(uuid, peripheral);
427
+ deviceCharacteristics.set(uuid, characteristics);
444
428
  } catch (error) {
445
429
  clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
430
+ if (characteristics) {
431
+ characteristics.notify.removeAllListeners('data');
432
+ await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
433
+ }
434
+ if (peripheral.state !== 'disconnected') {
435
+ await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
436
+ }
446
437
  throw error;
447
438
  }
448
- connectedDevices.set(uuid, peripheral);
449
- deviceCharacteristics.set(uuid, characteristics);
450
439
  },
451
440
 
452
441
  async disconnect(uuid: string) {