@onekeyfe/hd-transport-electron 1.2.0-alpha.64 → 1.2.0-alpha.66
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/{index-5f728113.js → index-84fea839.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-d58af248.js → noble-ble-handler-a0b2f06d.js} +66 -34
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/noble-ble-timeouts.d.ts +3 -0
- package/dist/noble-ble-timeouts.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +354 -0
- package/src/noble-ble-handler.ts +74 -32
- package/src/noble-ble-timeouts.ts +2 -0
|
@@ -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-
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
539
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
},
|
|
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:
|
|
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":"
|
|
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 @@
|
|
|
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.
|
|
3
|
+
"version": "1.2.0-alpha.66",
|
|
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.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.66",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.66",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.66",
|
|
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": "
|
|
39
|
+
"gitHead": "81629c086665bb92ce4d52087ae47f084976da79"
|
|
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
|
+
});
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
},
|
|
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
|
-
|
|
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
|
|
724
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
},
|
|
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:
|
|
1495
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1454
1496
|
timeoutBehavior: 'reject',
|
|
1455
1497
|
});
|
|
1456
1498
|
|