@onekeyfe/hd-transport-electron 1.2.0-alpha.57 → 1.2.0-alpha.59
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-c1642d47.js → index-84fea839.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-e98d7b00.js → noble-ble-handler-a0b2f06d.js} +60 -32
- 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 +212 -0
- package/src/noble-ble-handler.ts +63 -30
- 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 = {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
543
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
},
|
|
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:
|
|
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":"
|
|
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.59",
|
|
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.59",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.59",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.59",
|
|
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": "98794c3032aab55e13fb2afedc645d8281141d5f"
|
|
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
|
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
687
|
-
},
|
|
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
|
-
|
|
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
|
|
733
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
},
|
|
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:
|
|
1495
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1463
1496
|
timeoutBehavior: 'reject',
|
|
1464
1497
|
});
|
|
1465
1498
|
|