@onekeyfe/hd-transport-electron 1.2.0-alpha.56 → 1.2.0-alpha.58

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-e98d7b00.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-c1642d47.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-c1642d47.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 = {
@@ -101,9 +104,7 @@ const ONEKEY_WRITE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ON
101
104
  const ONEKEY_NOTIFY_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
102
105
  const BLUETOOTH_INIT_TIMEOUT = 10000;
103
106
  const DEVICE_SCAN_TIMEOUT = 5000;
104
- const FAST_SCAN_TIMEOUT = 1500;
105
107
  const DEVICE_CHECK_INTERVAL = 500;
106
- const CONNECTION_TIMEOUT = 3000;
107
108
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
108
109
  const BLE_CLEANUP_TIMEOUT = 250;
109
110
  const BLE_PACKET_SIZE = 192;
@@ -513,6 +514,14 @@ function ensureDiscoverListener() {
513
514
  logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
514
515
  }
515
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
+ }
516
525
  function performTargetedScan(targetDeviceId) {
517
526
  return index.__awaiter(this, void 0, void 0, function* () {
518
527
  if (!noble) {
@@ -521,6 +530,25 @@ function performTargetedScan(targetDeviceId) {
521
530
  const nobleInstance = noble;
522
531
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
523
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
+ });
524
552
  const onDiscover = (peripheral) => {
525
553
  var _a;
526
554
  if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
@@ -528,26 +556,17 @@ function performTargetedScan(targetDeviceId) {
528
556
  id: peripheral.id,
529
557
  name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
530
558
  });
531
- clearTimeout(timeoutId);
532
- nobleInstance.removeListener('discover', onDiscover);
533
- nobleInstance.stopScanning();
534
- discoveredDevices.set(peripheral.id, peripheral);
535
- resolve(peripheral);
559
+ finish(peripheral).catch(reject);
536
560
  }
537
561
  };
538
562
  const timeoutId = setTimeout(() => {
539
- nobleInstance.removeListener('discover', onDiscover);
540
- nobleInstance.stopScanning();
541
563
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
542
- resolve(null);
543
- }, FAST_SCAN_TIMEOUT);
564
+ finish(null).catch(reject);
565
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
544
566
  nobleInstance.on('discover', onDiscover);
545
567
  nobleInstance.startScanning([], false, (error) => {
546
568
  if (error) {
547
- clearTimeout(timeoutId);
548
- nobleInstance.removeListener('discover', onDiscover);
549
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
550
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
569
+ finish(null, error).catch(reject);
551
570
  return;
552
571
  }
553
572
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -570,12 +589,12 @@ function enumerateDevices() {
570
589
  return new Promise((resolve, reject) => {
571
590
  const devices = [];
572
591
  let intervalId;
573
- const cleanup = () => {
592
+ const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
574
593
  clearTimeout(timeoutId);
575
594
  if (intervalId)
576
595
  clearInterval(intervalId);
577
- nobleInstance.stopScanning();
578
- };
596
+ yield waitForNobleScanStop(nobleInstance);
597
+ });
579
598
  const checkDevices = () => {
580
599
  discoveredDevices.forEach((peripheral, id) => {
581
600
  var _a;
@@ -591,23 +610,23 @@ function enumerateDevices() {
591
610
  }
592
611
  });
593
612
  };
594
- const timeoutId = setTimeout(() => {
613
+ const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
595
614
  checkDevices();
596
- cleanup();
615
+ yield cleanup();
597
616
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
598
617
  resolve(devices);
599
- }, DEVICE_SCAN_TIMEOUT);
618
+ }), DEVICE_SCAN_TIMEOUT);
600
619
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
601
- nobleInstance.startScanning([], false, (error) => {
620
+ nobleInstance.startScanning([], false, (error) => index.__awaiter(this, void 0, void 0, function* () {
602
621
  if (error) {
603
- cleanup();
622
+ yield cleanup();
604
623
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
605
624
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
606
625
  return;
607
626
  }
608
627
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning started for OneKey devices');
609
628
  intervalId = setInterval(checkDevices, DEVICE_CHECK_INTERVAL);
610
- });
629
+ }));
611
630
  });
612
631
  });
613
632
  }
@@ -616,10 +635,7 @@ function stopScanning() {
616
635
  if (!noble)
617
636
  return;
618
637
  const nobleInstance = noble;
619
- yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
620
- timeoutMs: BLE_CLEANUP_TIMEOUT,
621
- timeoutBehavior: 'resolve',
622
- });
638
+ yield waitForNobleScanStop(nobleInstance);
623
639
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
624
640
  });
625
641
  }
@@ -774,7 +790,7 @@ function forceReconnectPeripheral(peripheral, deviceId) {
774
790
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
775
791
  }
776
792
  yield runBleCallbackOperation(callback => peripheral.connect(callback), {
777
- timeoutMs: CONNECTION_TIMEOUT,
793
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
778
794
  timeoutBehavior: 'reject',
779
795
  });
780
796
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
@@ -944,12 +960,24 @@ function connectDevice(deviceId, webContents) {
944
960
  return;
945
961
  }
946
962
  return new Promise((resolve, reject) => {
963
+ let connectionTimedOut = false;
947
964
  const timeout = setTimeout(() => {
965
+ connectionTimedOut = true;
948
966
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
949
- }, CONNECTION_TIMEOUT);
967
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
950
968
  const connectedPeripheral = peripheral;
951
969
  connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
952
970
  clearTimeout(timeout);
971
+ if (connectionTimedOut) {
972
+ if (!error) {
973
+ try {
974
+ connectedPeripheral.disconnect(() => undefined);
975
+ }
976
+ catch (_a) {
977
+ }
978
+ }
979
+ return;
980
+ }
953
981
  if (error) {
954
982
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
955
983
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
@@ -1068,7 +1096,7 @@ function subscribeNotifications(deviceId, callback) {
1068
1096
  timeoutBehavior: 'resolve',
1069
1097
  });
1070
1098
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1071
- timeoutMs: CONNECTION_TIMEOUT,
1099
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1072
1100
  timeoutBehavior: 'reject',
1073
1101
  });
1074
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":"AA0BA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AAo7ChE,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.56",
3
+ "version": "1.2.0-alpha.58",
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.56",
29
- "@onekeyfe/hd-shared": "1.2.0-alpha.56",
30
- "@onekeyfe/hd-transport": "1.2.0-alpha.56",
28
+ "@onekeyfe/hd-core": "1.2.0-alpha.58",
29
+ "@onekeyfe/hd-shared": "1.2.0-alpha.58",
30
+ "@onekeyfe/hd-transport": "1.2.0-alpha.58",
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": "2c472748d67072307c21f8d33bc021cf18fe2ad7"
39
+ "gitHead": "43c11efeb7001741d4258c855b287993bc0a798e"
40
40
  }
@@ -1,6 +1,11 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
3
3
 
4
+ import {
5
+ NOBLE_BLE_CONNECTION_TIMEOUT_MS,
6
+ NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
7
+ } from '../noble-ble-timeouts';
8
+
4
9
  import type { WebContents } from 'electron';
5
10
 
6
11
  type IpcHandler = (...args: unknown[]) => Promise<unknown> | unknown;
@@ -21,6 +26,213 @@ describe('Electron Noble BLE device discovery', () => {
21
26
  jest.clearAllMocks();
22
27
  });
23
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
+
24
236
  test('does not enumerate a Pro2 Find My advertisement with the communication service', async () => {
25
237
  jest.useFakeTimers({ doNotFake: ['performance'] });
26
238
 
@@ -23,6 +23,10 @@ import pRetry from 'p-retry';
23
23
 
24
24
  import { safeLog } from './types/noble-extended';
25
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';
26
30
 
27
31
  import type { IpcMainInvokeEvent, WebContents } from 'electron';
28
32
  import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
@@ -77,9 +81,7 @@ const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARA
77
81
  // Timeout and interval constants
78
82
  const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
79
83
  const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
80
- const FAST_SCAN_TIMEOUT = 1500; // 1.5 seconds for targeted scanning
81
84
  const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
82
- const CONNECTION_TIMEOUT = 3000; // 3 seconds for device connection
83
85
  const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
84
86
  const BLE_CLEANUP_TIMEOUT = 250;
85
87
 
@@ -650,6 +652,13 @@ function ensureDiscoverListener(): void {
650
652
  }
651
653
  }
652
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
+
653
662
  // Perform targeted scan for a specific device ID
654
663
  // Uses self-contained local listener pattern - no global state needed
655
664
  async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
@@ -663,6 +672,26 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
663
672
  logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
664
673
 
665
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
+
666
695
  // Local discover listener - only matches target device
667
696
  const onDiscover = (peripheral: Peripheral) => {
668
697
  if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
@@ -670,21 +699,14 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
670
699
  id: peripheral.id,
671
700
  name: peripheral.advertisement?.localName,
672
701
  });
673
- clearTimeout(timeoutId);
674
- nobleInstance.removeListener('discover', onDiscover);
675
- nobleInstance.stopScanning();
676
- discoveredDevices.set(peripheral.id, peripheral);
677
- resolve(peripheral);
702
+ finish(peripheral).catch(reject);
678
703
  }
679
704
  };
680
705
 
681
- // Timeout handler - must be after onDiscover so it can reference it
682
706
  const timeoutId = setTimeout(() => {
683
- nobleInstance.removeListener('discover', onDiscover);
684
- nobleInstance.stopScanning();
685
707
  logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
686
- resolve(null);
687
- }, FAST_SCAN_TIMEOUT);
708
+ finish(null).catch(reject);
709
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
688
710
 
689
711
  // Add local listener for this scan
690
712
  nobleInstance.on('discover', onDiscover);
@@ -692,10 +714,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
692
714
  // Start scanning — no service UUID filter (Pro2 may use different service UUID)
693
715
  nobleInstance.startScanning([], false, (error?: Error) => {
694
716
  if (error) {
695
- clearTimeout(timeoutId);
696
- nobleInstance.removeListener('discover', onDiscover);
697
- logger?.error('[NobleBLE] Failed to start targeted scan:', error);
698
- reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
717
+ finish(null, error).catch(reject);
699
718
  return;
700
719
  }
701
720
  logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -729,11 +748,13 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
729
748
  const devices: DeviceInfo[] = [];
730
749
  let intervalId: ReturnType<typeof setInterval> | undefined;
731
750
 
732
- // Cleanup function: clears both timeout and interval
733
- 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 () => {
734
755
  clearTimeout(timeoutId);
735
756
  if (intervalId) clearInterval(intervalId);
736
- nobleInstance.stopScanning();
757
+ await waitForNobleScanStop(nobleInstance);
737
758
  };
738
759
 
739
760
  // Collect discovered devices into the devices array
@@ -753,10 +774,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
753
774
  };
754
775
 
755
776
  // Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
756
- const timeoutId = setTimeout(() => {
777
+ const timeoutId = setTimeout(async () => {
757
778
  // Final collection before resolving — catches devices discovered near the deadline
758
779
  checkDevices();
759
- cleanup();
780
+ await cleanup();
760
781
  logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
761
782
  resolve(devices);
762
783
  }, DEVICE_SCAN_TIMEOUT);
@@ -764,9 +785,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
764
785
  // Start scanning without a service UUID filter so Pro2 advertisements with
765
786
  // short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
766
787
  logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
767
- nobleInstance.startScanning([], false, (error?: Error) => {
788
+ nobleInstance.startScanning([], false, async (error?: Error) => {
768
789
  if (error) {
769
- cleanup();
790
+ await cleanup();
770
791
  logger?.error('[NobleBLE] Failed to start scanning:', error);
771
792
  reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
772
793
  return;
@@ -784,10 +805,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
784
805
  async function stopScanning(): Promise<void> {
785
806
  if (!noble) return;
786
807
  const nobleInstance = noble;
787
- await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
788
- timeoutMs: BLE_CLEANUP_TIMEOUT,
789
- timeoutBehavior: 'resolve',
790
- });
808
+ await waitForNobleScanStop(nobleInstance);
791
809
  logger?.info('[NobleBLE] Scanning stopped');
792
810
  }
793
811
 
@@ -1005,7 +1023,7 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
1005
1023
 
1006
1024
  // Step 3: Re-establish connection
1007
1025
  await runBleCallbackOperation(callback => peripheral.connect(callback), {
1008
- timeoutMs: CONNECTION_TIMEOUT,
1026
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1009
1027
  timeoutBehavior: 'reject',
1010
1028
  });
1011
1029
  logger?.info('[NobleBLE] Force reconnect successful');
@@ -1277,15 +1295,30 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1277
1295
  }
1278
1296
 
1279
1297
  return new Promise((resolve, reject) => {
1298
+ let connectionTimedOut = false;
1280
1299
  const timeout = setTimeout(() => {
1300
+ connectionTimedOut = true;
1281
1301
  reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
1282
- }, CONNECTION_TIMEOUT);
1302
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
1283
1303
 
1284
1304
  // TypeScript type assertion - peripheral is guaranteed to be defined at this point
1285
1305
  const connectedPeripheral = peripheral as Peripheral;
1286
1306
  connectedPeripheral.connect(async (error: Error | undefined) => {
1287
1307
  clearTimeout(timeout);
1288
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
+
1289
1322
  if (error) {
1290
1323
  logger?.error('[NobleBLE] Connection failed:', error);
1291
1324
  reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
@@ -1459,7 +1492,7 @@ async function subscribeNotifications(
1459
1492
  timeoutBehavior: 'resolve',
1460
1493
  });
1461
1494
  await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1462
- timeoutMs: CONNECTION_TIMEOUT,
1495
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1463
1496
  timeoutBehavior: 'reject',
1464
1497
  });
1465
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;