@onekeyfe/hd-transport-electron 1.2.0-alpha.67 → 1.2.0-alpha.68

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.
@@ -32,7 +32,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
32
32
 
33
33
  function initNobleBleSupport(webContents) {
34
34
  return __awaiter(this, void 0, void 0, function* () {
35
- const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-d58af248.js'); });
35
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-a0b2f06d.js'); });
36
36
  setupNobleBleHandlers(webContents);
37
37
  });
38
38
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('./index-5f728113.js');
5
+ var index = require('./index-84fea839.js');
6
6
 
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-5f728113.js');
3
+ var index = require('./index-84fea839.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -78,6 +78,9 @@ function softRefreshSubscription(params) {
78
78
  });
79
79
  }
80
80
 
81
+ const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
82
+ const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
83
+
81
84
  let noble = null;
82
85
  let logger = null;
83
86
  const bluetoothState = {
@@ -100,10 +103,8 @@ const ONEKEY_SERVICE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.
100
103
  const ONEKEY_WRITE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_WRITE_CHARACTERISTIC_UUID);
101
104
  const ONEKEY_NOTIFY_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
102
105
  const BLUETOOTH_INIT_TIMEOUT = 10000;
103
- const DEVICE_SCAN_TIMEOUT = 8000;
104
- const FAST_SCAN_TIMEOUT = 8000;
106
+ const DEVICE_SCAN_TIMEOUT = 5000;
105
107
  const DEVICE_CHECK_INTERVAL = 500;
106
- const CONNECTION_TIMEOUT = 8000;
107
108
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
108
109
  const BLE_CLEANUP_TIMEOUT = 250;
109
110
  const BLE_PACKET_SIZE = 192;
@@ -116,10 +117,14 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
116
117
  function isOneKeyPeripheral(peripheral) {
117
118
  var _a, _b;
118
119
  const serviceUuids = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids;
120
+ const localName = (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName;
121
+ if (!(localName === null || localName === void 0 ? void 0 : localName.trim()) || hdShared.isPro2FindMyAdvertisementName(localName)) {
122
+ return false;
123
+ }
119
124
  return (hdShared.hasOnekeyCommunicationService(serviceUuids) &&
120
125
  hdShared.isOnekeyBluetoothDevice({
121
126
  id: peripheral.id,
122
- localName: (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName,
127
+ localName,
123
128
  serviceUuids,
124
129
  }));
125
130
  }
@@ -509,6 +514,14 @@ function ensureDiscoverListener() {
509
514
  logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
510
515
  }
511
516
  }
517
+ function waitForNobleScanStop(nobleInstance) {
518
+ return index.__awaiter(this, void 0, void 0, function* () {
519
+ yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
520
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
521
+ timeoutBehavior: 'resolve',
522
+ });
523
+ });
524
+ }
512
525
  function performTargetedScan(targetDeviceId) {
513
526
  return index.__awaiter(this, void 0, void 0, function* () {
514
527
  if (!noble) {
@@ -517,6 +530,25 @@ function performTargetedScan(targetDeviceId) {
517
530
  const nobleInstance = noble;
518
531
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
519
532
  return new Promise((resolve, reject) => {
533
+ let settled = false;
534
+ const finish = (peripheral, error) => index.__awaiter(this, void 0, void 0, function* () {
535
+ if (settled)
536
+ return;
537
+ settled = true;
538
+ if (timeoutId)
539
+ clearTimeout(timeoutId);
540
+ nobleInstance.removeListener('discover', onDiscover);
541
+ yield waitForNobleScanStop(nobleInstance);
542
+ if (error) {
543
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
544
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
545
+ return;
546
+ }
547
+ if (peripheral) {
548
+ discoveredDevices.set(peripheral.id, peripheral);
549
+ }
550
+ resolve(peripheral);
551
+ });
520
552
  const onDiscover = (peripheral) => {
521
553
  var _a;
522
554
  if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
@@ -524,26 +556,17 @@ function performTargetedScan(targetDeviceId) {
524
556
  id: peripheral.id,
525
557
  name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
526
558
  });
527
- clearTimeout(timeoutId);
528
- nobleInstance.removeListener('discover', onDiscover);
529
- nobleInstance.stopScanning();
530
- discoveredDevices.set(peripheral.id, peripheral);
531
- resolve(peripheral);
559
+ finish(peripheral).catch(reject);
532
560
  }
533
561
  };
534
562
  const timeoutId = setTimeout(() => {
535
- nobleInstance.removeListener('discover', onDiscover);
536
- nobleInstance.stopScanning();
537
563
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
538
- resolve(null);
539
- }, FAST_SCAN_TIMEOUT);
564
+ finish(null).catch(reject);
565
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
540
566
  nobleInstance.on('discover', onDiscover);
541
567
  nobleInstance.startScanning([], false, (error) => {
542
568
  if (error) {
543
- clearTimeout(timeoutId);
544
- nobleInstance.removeListener('discover', onDiscover);
545
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
546
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
569
+ finish(null, error).catch(reject);
547
570
  return;
548
571
  }
549
572
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -566,12 +589,12 @@ function enumerateDevices() {
566
589
  return new Promise((resolve, reject) => {
567
590
  const devices = [];
568
591
  let intervalId;
569
- const cleanup = () => {
592
+ const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
570
593
  clearTimeout(timeoutId);
571
594
  if (intervalId)
572
595
  clearInterval(intervalId);
573
- nobleInstance.stopScanning();
574
- };
596
+ yield waitForNobleScanStop(nobleInstance);
597
+ });
575
598
  const checkDevices = () => {
576
599
  discoveredDevices.forEach((peripheral, id) => {
577
600
  var _a;
@@ -587,23 +610,23 @@ function enumerateDevices() {
587
610
  }
588
611
  });
589
612
  };
590
- const timeoutId = setTimeout(() => {
613
+ const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
591
614
  checkDevices();
592
- cleanup();
615
+ yield cleanup();
593
616
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
594
617
  resolve(devices);
595
- }, DEVICE_SCAN_TIMEOUT);
618
+ }), DEVICE_SCAN_TIMEOUT);
596
619
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
597
- nobleInstance.startScanning([], false, (error) => {
620
+ nobleInstance.startScanning([], false, (error) => index.__awaiter(this, void 0, void 0, function* () {
598
621
  if (error) {
599
- cleanup();
622
+ yield cleanup();
600
623
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
601
624
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
602
625
  return;
603
626
  }
604
627
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning started for OneKey devices');
605
628
  intervalId = setInterval(checkDevices, DEVICE_CHECK_INTERVAL);
606
- });
629
+ }));
607
630
  });
608
631
  });
609
632
  }
@@ -612,10 +635,7 @@ function stopScanning() {
612
635
  if (!noble)
613
636
  return;
614
637
  const nobleInstance = noble;
615
- yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
616
- timeoutMs: BLE_CLEANUP_TIMEOUT,
617
- timeoutBehavior: 'resolve',
618
- });
638
+ yield waitForNobleScanStop(nobleInstance);
619
639
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
620
640
  });
621
641
  }
@@ -770,7 +790,7 @@ function forceReconnectPeripheral(peripheral, deviceId) {
770
790
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
771
791
  }
772
792
  yield runBleCallbackOperation(callback => peripheral.connect(callback), {
773
- timeoutMs: CONNECTION_TIMEOUT,
793
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
774
794
  timeoutBehavior: 'reject',
775
795
  });
776
796
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
@@ -940,12 +960,24 @@ function connectDevice(deviceId, webContents) {
940
960
  return;
941
961
  }
942
962
  return new Promise((resolve, reject) => {
963
+ let connectionTimedOut = false;
943
964
  const timeout = setTimeout(() => {
965
+ connectionTimedOut = true;
944
966
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
945
- }, CONNECTION_TIMEOUT);
967
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
946
968
  const connectedPeripheral = peripheral;
947
969
  connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
948
970
  clearTimeout(timeout);
971
+ if (connectionTimedOut) {
972
+ if (!error) {
973
+ try {
974
+ connectedPeripheral.disconnect(() => undefined);
975
+ }
976
+ catch (_a) {
977
+ }
978
+ }
979
+ return;
980
+ }
949
981
  if (error) {
950
982
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
951
983
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
@@ -1064,7 +1096,7 @@ function subscribeNotifications(deviceId, callback) {
1064
1096
  timeoutBehavior: 'resolve',
1065
1097
  });
1066
1098
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1067
- timeoutMs: CONNECTION_TIMEOUT,
1099
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1068
1100
  timeoutBehavior: 'reject',
1069
1101
  });
1070
1102
  notifyCharacteristic.on('data', (data) => {
@@ -1 +1 @@
1
- {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AA46ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAyJpE"}
1
+ {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AAi9ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAyJpE"}
@@ -0,0 +1,3 @@
1
+ export declare const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
2
+ export declare const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
3
+ //# sourceMappingURL=noble-ble-timeouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noble-ble-timeouts.d.ts","sourceRoot":"","sources":["../src/noble-ble-timeouts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kCAAkC,OAAQ,CAAC;AACxD,eAAO,MAAM,+BAA+B,QAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-electron",
3
- "version": "1.2.0-alpha.67",
3
+ "version": "1.2.0-alpha.68",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -25,9 +25,9 @@
25
25
  "electron-log": ">=4.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-core": "1.2.0-alpha.67",
29
- "@onekeyfe/hd-shared": "1.2.0-alpha.67",
30
- "@onekeyfe/hd-transport": "1.2.0-alpha.67",
28
+ "@onekeyfe/hd-core": "1.2.0-alpha.68",
29
+ "@onekeyfe/hd-shared": "1.2.0-alpha.68",
30
+ "@onekeyfe/hd-transport": "1.2.0-alpha.68",
31
31
  "@stoprocent/noble": "2.3.16",
32
32
  "p-retry": "^4.6.2"
33
33
  },
@@ -36,5 +36,5 @@
36
36
  "electron": "^25.0.0",
37
37
  "typescript": "^5.3.3"
38
38
  },
39
- "gitHead": "5fea66e8c65a1fab2ccc099b2811c8749b2487e0"
39
+ "gitHead": "6e8a931d08768980cfe7fa918bba447ed73e2724"
40
40
  }
@@ -0,0 +1,354 @@
1
+ import { EventEmitter } from 'events';
2
+ import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
3
+
4
+ import {
5
+ NOBLE_BLE_CONNECTION_TIMEOUT_MS,
6
+ NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
7
+ } from '../noble-ble-timeouts';
8
+
9
+ import type { WebContents } from 'electron';
10
+
11
+ type IpcHandler = (...args: unknown[]) => Promise<unknown> | unknown;
12
+
13
+ const createPeripheral = (id: string, localName?: string) => ({
14
+ id,
15
+ state: 'disconnected',
16
+ advertisement: {
17
+ localName,
18
+ serviceUuids: ['0001', 'fffd'],
19
+ },
20
+ });
21
+
22
+ describe('Electron Noble BLE device discovery', () => {
23
+ afterEach(() => {
24
+ jest.useRealTimers();
25
+ jest.resetModules();
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ test('allows enough time for a slow targeted scan and connection', () => {
30
+ expect(NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS).toBe(5_000);
31
+ expect(NOBLE_BLE_CONNECTION_TIMEOUT_MS).toBe(10_000);
32
+ });
33
+
34
+ test('waits for Noble to stop scanning before enumeration resolves', async () => {
35
+ jest.useFakeTimers({ doNotFake: ['performance'] });
36
+
37
+ const handlers = new Map<string, IpcHandler>();
38
+ const ipcMain = {
39
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
40
+ handlers.set(channel, handler);
41
+ }),
42
+ };
43
+ const noble = new EventEmitter() as EventEmitter & {
44
+ state: string;
45
+ startScanning: jest.Mock;
46
+ stopScanning: jest.Mock;
47
+ };
48
+ let resolveScanStarted = () => undefined;
49
+ const scanStarted = new Promise<void>(resolve => {
50
+ resolveScanStarted = resolve;
51
+ });
52
+ let stopScanningCallback: (() => void) | undefined;
53
+ noble.state = 'poweredOn';
54
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
55
+ callback?.();
56
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
57
+ resolveScanStarted();
58
+ });
59
+ noble.stopScanning = jest.fn(callback => {
60
+ stopScanningCallback = callback;
61
+ });
62
+
63
+ jest.doMock('@stoprocent/noble', () => noble);
64
+ jest.doMock('electron', () => ({ ipcMain }));
65
+ jest.doMock('electron-log', () => ({
66
+ info: jest.fn(),
67
+ debug: jest.fn(),
68
+ error: jest.fn(),
69
+ }));
70
+
71
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
72
+ setupNobleBleHandlers({
73
+ on: jest.fn(),
74
+ send: jest.fn(),
75
+ } as unknown as WebContents);
76
+
77
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
78
+ if (!enumerate) {
79
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
80
+ }
81
+
82
+ let enumerationResolved = false;
83
+ const devicesPromise = Promise.resolve(enumerate()).then(devices => {
84
+ enumerationResolved = true;
85
+ return devices;
86
+ });
87
+ await scanStarted;
88
+ jest.advanceTimersByTime(5_000);
89
+ await Promise.resolve();
90
+
91
+ expect(noble.stopScanning).toHaveBeenCalledTimes(1);
92
+ expect(enumerationResolved).toBe(false);
93
+
94
+ stopScanningCallback?.();
95
+ await expect(devicesPromise).resolves.toEqual([
96
+ expect.objectContaining({ id: 'onekey-device' }),
97
+ ]);
98
+ });
99
+
100
+ test('waits for a targeted scan to stop before connecting', async () => {
101
+ jest.useFakeTimers({ doNotFake: ['performance'] });
102
+
103
+ const handlers = new Map<string, IpcHandler>();
104
+ const ipcMain = {
105
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
106
+ handlers.set(channel, handler);
107
+ }),
108
+ };
109
+ const noble = new EventEmitter() as EventEmitter & {
110
+ state: string;
111
+ startScanning: jest.Mock;
112
+ stopScanning: jest.Mock;
113
+ };
114
+ let resolveScanStarted = () => undefined;
115
+ const scanStarted = new Promise<void>(resolve => {
116
+ resolveScanStarted = resolve;
117
+ });
118
+ let stopScanningCallback: (() => void) | undefined;
119
+ const peripheral = Object.assign(
120
+ new EventEmitter(),
121
+ createPeripheral('target-device', 'Pro2 A1B2'),
122
+ {
123
+ connect: jest.fn((callback: (error?: Error) => void) => {
124
+ callback(new Error('expected test connection failure'));
125
+ }),
126
+ }
127
+ );
128
+ noble.state = 'poweredOn';
129
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
130
+ callback?.();
131
+ noble.emit('discover', peripheral);
132
+ resolveScanStarted();
133
+ });
134
+ noble.stopScanning = jest.fn(callback => {
135
+ stopScanningCallback = callback;
136
+ });
137
+
138
+ jest.doMock('@stoprocent/noble', () => noble);
139
+ jest.doMock('electron', () => ({ ipcMain }));
140
+ jest.doMock('electron-log', () => ({
141
+ info: jest.fn(),
142
+ debug: jest.fn(),
143
+ error: jest.fn(),
144
+ }));
145
+
146
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
147
+ setupNobleBleHandlers({
148
+ on: jest.fn(),
149
+ send: jest.fn(),
150
+ } as unknown as WebContents);
151
+
152
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
153
+ if (!connect) {
154
+ throw new Error('Electron Noble BLE connect handler was not registered');
155
+ }
156
+
157
+ const connectPromise = Promise.resolve(connect(undefined, 'target-device'));
158
+ await scanStarted;
159
+
160
+ expect(noble.stopScanning).toHaveBeenCalledTimes(1);
161
+ expect(peripheral.connect).not.toHaveBeenCalled();
162
+
163
+ stopScanningCallback?.();
164
+ await expect(connectPromise).rejects.toThrow('expected test connection failure');
165
+ expect(peripheral.connect).toHaveBeenCalledTimes(1);
166
+ });
167
+
168
+ test('disconnects a connection callback that arrives after timeout', async () => {
169
+ jest.useFakeTimers({ doNotFake: ['performance'] });
170
+
171
+ const handlers = new Map<string, IpcHandler>();
172
+ const ipcMain = {
173
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
174
+ handlers.set(channel, handler);
175
+ }),
176
+ };
177
+ const noble = new EventEmitter() as EventEmitter & {
178
+ state: string;
179
+ startScanning: jest.Mock;
180
+ stopScanning: jest.Mock;
181
+ };
182
+ let connectCallback: ((error?: Error) => void) | undefined;
183
+ let resolveConnectStarted = () => undefined;
184
+ const connectStarted = new Promise<void>(resolve => {
185
+ resolveConnectStarted = resolve;
186
+ });
187
+ const peripheral = Object.assign(
188
+ new EventEmitter(),
189
+ createPeripheral('slow-device', 'Pro2 A1B2'),
190
+ {
191
+ connect: jest.fn((callback: (error?: Error) => void) => {
192
+ connectCallback = callback;
193
+ resolveConnectStarted();
194
+ }),
195
+ disconnect: jest.fn((callback: () => void) => callback()),
196
+ }
197
+ );
198
+ noble.state = 'poweredOn';
199
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
200
+ callback?.();
201
+ noble.emit('discover', peripheral);
202
+ });
203
+ noble.stopScanning = jest.fn(callback => callback?.());
204
+
205
+ jest.doMock('@stoprocent/noble', () => noble);
206
+ jest.doMock('electron', () => ({ ipcMain }));
207
+ jest.doMock('electron-log', () => ({
208
+ info: jest.fn(),
209
+ debug: jest.fn(),
210
+ error: jest.fn(),
211
+ }));
212
+
213
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
214
+ setupNobleBleHandlers({
215
+ on: jest.fn(),
216
+ send: jest.fn(),
217
+ } as unknown as WebContents);
218
+
219
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
220
+ if (!connect) {
221
+ throw new Error('Electron Noble BLE connect handler was not registered');
222
+ }
223
+
224
+ const connectPromise = Promise.resolve(connect(undefined, 'slow-device'));
225
+ await connectStarted;
226
+ expect(peripheral.connect).toHaveBeenCalledTimes(1);
227
+
228
+ jest.advanceTimersByTime(NOBLE_BLE_CONNECTION_TIMEOUT_MS);
229
+ await expect(connectPromise).rejects.toThrow('Connection timeout');
230
+
231
+ connectCallback?.();
232
+ await Promise.resolve();
233
+ expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
234
+ });
235
+
236
+ test('does not enumerate a Pro2 Find My advertisement with the communication service', async () => {
237
+ jest.useFakeTimers({ doNotFake: ['performance'] });
238
+
239
+ const handlers = new Map<string, IpcHandler>();
240
+ const ipcMain = {
241
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
242
+ handlers.set(channel, handler);
243
+ }),
244
+ };
245
+ const noble = new EventEmitter() as EventEmitter & {
246
+ state: string;
247
+ startScanning: jest.Mock;
248
+ stopScanning: jest.Mock;
249
+ };
250
+ let resolveScanStarted = () => undefined;
251
+ const scanStarted = new Promise<void>(resolve => {
252
+ resolveScanStarted = resolve;
253
+ });
254
+ noble.state = 'poweredOn';
255
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
256
+ callback?.();
257
+ noble.emit('discover', createPeripheral('find-my-device', 'Pro2 A1B2 - Find My'));
258
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
259
+ resolveScanStarted();
260
+ });
261
+ noble.stopScanning = jest.fn(callback => callback?.());
262
+
263
+ jest.doMock('@stoprocent/noble', () => noble);
264
+ jest.doMock('electron', () => ({ ipcMain }));
265
+ jest.doMock('electron-log', () => ({
266
+ info: jest.fn(),
267
+ debug: jest.fn(),
268
+ error: jest.fn(),
269
+ }));
270
+
271
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
272
+ setupNobleBleHandlers({
273
+ on: jest.fn(),
274
+ send: jest.fn(),
275
+ } as unknown as WebContents);
276
+
277
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
278
+ if (!enumerate) {
279
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
280
+ }
281
+
282
+ const devicesPromise = Promise.resolve(enumerate());
283
+ await scanStarted;
284
+ jest.advanceTimersByTime(5000);
285
+
286
+ await expect(devicesPromise).resolves.toEqual([
287
+ expect.objectContaining({
288
+ id: 'onekey-device',
289
+ name: 'Pro2 A1B2',
290
+ }),
291
+ ]);
292
+ });
293
+
294
+ test('does not enumerate a Find My peripheral first discovered without a name', async () => {
295
+ jest.useFakeTimers({ doNotFake: ['performance'] });
296
+
297
+ const handlers = new Map<string, IpcHandler>();
298
+ const ipcMain = {
299
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
300
+ handlers.set(channel, handler);
301
+ }),
302
+ };
303
+ const noble = new EventEmitter() as EventEmitter & {
304
+ state: string;
305
+ startScanning: jest.Mock;
306
+ stopScanning: jest.Mock;
307
+ };
308
+ let resolveScanStarted = () => undefined;
309
+ const scanStarted = new Promise<void>(resolve => {
310
+ resolveScanStarted = resolve;
311
+ });
312
+ const findMyPeripheral = createPeripheral('find-my-device');
313
+ noble.state = 'poweredOn';
314
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
315
+ callback?.();
316
+ noble.emit('discover', findMyPeripheral);
317
+ findMyPeripheral.advertisement.localName = 'Pro2 A1B2 - Find My';
318
+ noble.emit('discover', findMyPeripheral);
319
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
320
+ resolveScanStarted();
321
+ });
322
+ noble.stopScanning = jest.fn(callback => callback?.());
323
+
324
+ jest.doMock('@stoprocent/noble', () => noble);
325
+ jest.doMock('electron', () => ({ ipcMain }));
326
+ jest.doMock('electron-log', () => ({
327
+ info: jest.fn(),
328
+ debug: jest.fn(),
329
+ error: jest.fn(),
330
+ }));
331
+
332
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
333
+ setupNobleBleHandlers({
334
+ on: jest.fn(),
335
+ send: jest.fn(),
336
+ } as unknown as WebContents);
337
+
338
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
339
+ if (!enumerate) {
340
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
341
+ }
342
+
343
+ const devicesPromise = Promise.resolve(enumerate());
344
+ await scanStarted;
345
+ jest.advanceTimersByTime(5000);
346
+
347
+ await expect(devicesPromise).resolves.toEqual([
348
+ expect.objectContaining({
349
+ id: 'onekey-device',
350
+ name: 'Pro2 A1B2',
351
+ }),
352
+ ]);
353
+ });
354
+ });
@@ -15,6 +15,7 @@ import {
15
15
  createKnownBleUuidAliases,
16
16
  hasOnekeyCommunicationService,
17
17
  isOnekeyBluetoothDevice,
18
+ isPro2FindMyAdvertisementName,
18
19
  matchesKnownBleUuid,
19
20
  wait,
20
21
  } from '@onekeyfe/hd-shared';
@@ -22,6 +23,10 @@ import pRetry from 'p-retry';
22
23
 
23
24
  import { safeLog } from './types/noble-extended';
24
25
  import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
26
+ import {
27
+ NOBLE_BLE_CONNECTION_TIMEOUT_MS,
28
+ NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
29
+ } from './noble-ble-timeouts';
25
30
 
26
31
  import type { IpcMainInvokeEvent, WebContents } from 'electron';
27
32
  import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
@@ -75,10 +80,8 @@ const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARA
75
80
 
76
81
  // Timeout and interval constants
77
82
  const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
78
- const DEVICE_SCAN_TIMEOUT = 8000; // 8 seconds for device scanning (Pro2 has longer advertising interval)
79
- const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has longer advertising interval)
83
+ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
80
84
  const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
81
- const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow)
82
85
  const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
83
86
  const BLE_CLEANUP_TIMEOUT = 250;
84
87
 
@@ -93,11 +96,19 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
93
96
 
94
97
  function isOneKeyPeripheral(peripheral: Peripheral) {
95
98
  const serviceUuids = peripheral.advertisement?.serviceUuids;
99
+ const localName = peripheral.advertisement?.localName;
100
+
101
+ // Noble localName is the current advertisement name, so reject the Pro2
102
+ // Find My endpoint before the communication-service fast path accepts it.
103
+ if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
104
+ return false;
105
+ }
106
+
96
107
  return (
97
108
  hasOnekeyCommunicationService(serviceUuids) &&
98
109
  isOnekeyBluetoothDevice({
99
110
  id: peripheral.id,
100
- localName: peripheral.advertisement?.localName,
111
+ localName,
101
112
  serviceUuids,
102
113
  })
103
114
  );
@@ -641,6 +652,13 @@ function ensureDiscoverListener(): void {
641
652
  }
642
653
  }
643
654
 
655
+ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
656
+ await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
657
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
658
+ timeoutBehavior: 'resolve',
659
+ });
660
+ }
661
+
644
662
  // Perform targeted scan for a specific device ID
645
663
  // Uses self-contained local listener pattern - no global state needed
646
664
  async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
@@ -654,6 +672,26 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
654
672
  logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
655
673
 
656
674
  return new Promise((resolve, reject) => {
675
+ let settled = false;
676
+
677
+ const finish = async (peripheral: Peripheral | null, error?: Error) => {
678
+ if (settled) return;
679
+ settled = true;
680
+ if (timeoutId) clearTimeout(timeoutId);
681
+ nobleInstance.removeListener('discover', onDiscover);
682
+ await waitForNobleScanStop(nobleInstance);
683
+
684
+ if (error) {
685
+ logger?.error('[NobleBLE] Failed to start targeted scan:', error);
686
+ reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
687
+ return;
688
+ }
689
+ if (peripheral) {
690
+ discoveredDevices.set(peripheral.id, peripheral);
691
+ }
692
+ resolve(peripheral);
693
+ };
694
+
657
695
  // Local discover listener - only matches target device
658
696
  const onDiscover = (peripheral: Peripheral) => {
659
697
  if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
@@ -661,21 +699,14 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
661
699
  id: peripheral.id,
662
700
  name: peripheral.advertisement?.localName,
663
701
  });
664
- clearTimeout(timeoutId);
665
- nobleInstance.removeListener('discover', onDiscover);
666
- nobleInstance.stopScanning();
667
- discoveredDevices.set(peripheral.id, peripheral);
668
- resolve(peripheral);
702
+ finish(peripheral).catch(reject);
669
703
  }
670
704
  };
671
705
 
672
- // Timeout handler - must be after onDiscover so it can reference it
673
706
  const timeoutId = setTimeout(() => {
674
- nobleInstance.removeListener('discover', onDiscover);
675
- nobleInstance.stopScanning();
676
707
  logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
677
- resolve(null);
678
- }, FAST_SCAN_TIMEOUT);
708
+ finish(null).catch(reject);
709
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
679
710
 
680
711
  // Add local listener for this scan
681
712
  nobleInstance.on('discover', onDiscover);
@@ -683,10 +714,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
683
714
  // Start scanning — no service UUID filter (Pro2 may use different service UUID)
684
715
  nobleInstance.startScanning([], false, (error?: Error) => {
685
716
  if (error) {
686
- clearTimeout(timeoutId);
687
- nobleInstance.removeListener('discover', onDiscover);
688
- logger?.error('[NobleBLE] Failed to start targeted scan:', error);
689
- reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
717
+ finish(null, error).catch(reject);
690
718
  return;
691
719
  }
692
720
  logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -720,11 +748,13 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
720
748
  const devices: DeviceInfo[] = [];
721
749
  let intervalId: ReturnType<typeof setInterval> | undefined;
722
750
 
723
- // Cleanup function: clears both timeout and interval
724
- const cleanup = () => {
751
+ // Cleanup function: clears timers and waits until Noble confirms scanning
752
+ // has stopped. Resolving enumerate before this callback creates a race with
753
+ // an immediately-following connection attempt.
754
+ const cleanup = async () => {
725
755
  clearTimeout(timeoutId);
726
756
  if (intervalId) clearInterval(intervalId);
727
- nobleInstance.stopScanning();
757
+ await waitForNobleScanStop(nobleInstance);
728
758
  };
729
759
 
730
760
  // Collect discovered devices into the devices array
@@ -744,10 +774,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
744
774
  };
745
775
 
746
776
  // Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
747
- const timeoutId = setTimeout(() => {
777
+ const timeoutId = setTimeout(async () => {
748
778
  // Final collection before resolving — catches devices discovered near the deadline
749
779
  checkDevices();
750
- cleanup();
780
+ await cleanup();
751
781
  logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
752
782
  resolve(devices);
753
783
  }, DEVICE_SCAN_TIMEOUT);
@@ -755,9 +785,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
755
785
  // Start scanning without a service UUID filter so Pro2 advertisements with
756
786
  // short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
757
787
  logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
758
- nobleInstance.startScanning([], false, (error?: Error) => {
788
+ nobleInstance.startScanning([], false, async (error?: Error) => {
759
789
  if (error) {
760
- cleanup();
790
+ await cleanup();
761
791
  logger?.error('[NobleBLE] Failed to start scanning:', error);
762
792
  reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
763
793
  return;
@@ -775,10 +805,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
775
805
  async function stopScanning(): Promise<void> {
776
806
  if (!noble) return;
777
807
  const nobleInstance = noble;
778
- await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
779
- timeoutMs: BLE_CLEANUP_TIMEOUT,
780
- timeoutBehavior: 'resolve',
781
- });
808
+ await waitForNobleScanStop(nobleInstance);
782
809
  logger?.info('[NobleBLE] Scanning stopped');
783
810
  }
784
811
 
@@ -996,7 +1023,7 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
996
1023
 
997
1024
  // Step 3: Re-establish connection
998
1025
  await runBleCallbackOperation(callback => peripheral.connect(callback), {
999
- timeoutMs: CONNECTION_TIMEOUT,
1026
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1000
1027
  timeoutBehavior: 'reject',
1001
1028
  });
1002
1029
  logger?.info('[NobleBLE] Force reconnect successful');
@@ -1268,15 +1295,30 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1268
1295
  }
1269
1296
 
1270
1297
  return new Promise((resolve, reject) => {
1298
+ let connectionTimedOut = false;
1271
1299
  const timeout = setTimeout(() => {
1300
+ connectionTimedOut = true;
1272
1301
  reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
1273
- }, CONNECTION_TIMEOUT);
1302
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
1274
1303
 
1275
1304
  // TypeScript type assertion - peripheral is guaranteed to be defined at this point
1276
1305
  const connectedPeripheral = peripheral as Peripheral;
1277
1306
  connectedPeripheral.connect(async (error: Error | undefined) => {
1278
1307
  clearTimeout(timeout);
1279
1308
 
1309
+ // Noble may invoke the callback after the SDK timed out and released the request.
1310
+ // Ignore it to avoid initializing disposed commands or leaving an orphaned connection.
1311
+ if (connectionTimedOut) {
1312
+ if (!error) {
1313
+ try {
1314
+ connectedPeripheral.disconnect(() => undefined);
1315
+ } catch {
1316
+ // Best-effort cleanup only.
1317
+ }
1318
+ }
1319
+ return;
1320
+ }
1321
+
1280
1322
  if (error) {
1281
1323
  logger?.error('[NobleBLE] Connection failed:', error);
1282
1324
  reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
@@ -1450,7 +1492,7 @@ async function subscribeNotifications(
1450
1492
  timeoutBehavior: 'resolve',
1451
1493
  });
1452
1494
  await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1453
- timeoutMs: CONNECTION_TIMEOUT,
1495
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1454
1496
  timeoutBehavior: 'reject',
1455
1497
  });
1456
1498
 
@@ -0,0 +1,2 @@
1
+ export const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5_000;
2
+ export const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10_000;