@onekeyfe/hd-transport-electron 1.2.0-alpha.32 → 1.2.0-alpha.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ble-ops.d.ts +6 -0
- package/dist/ble-ops.d.ts.map +1 -1
- package/dist/{index-55699e30.js → index-5f728113.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-16791965.js → noble-ble-handler-d58af248.js} +151 -144
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/ble-ops.test.ts +23 -0
- package/src/ble-ops.ts +40 -10
- package/src/noble-ble-handler.ts +133 -164
package/dist/ble-ops.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import type { Characteristic } from '@stoprocent/noble';
|
|
3
3
|
import type { Logger } from './types/noble-extended';
|
|
4
|
+
type BleCallback = (error?: Error) => void;
|
|
5
|
+
export declare function runBleCallbackOperation(operation: (callback: BleCallback) => void, options: {
|
|
6
|
+
timeoutMs: number;
|
|
7
|
+
timeoutBehavior: 'resolve' | 'reject';
|
|
8
|
+
}): Promise<void>;
|
|
4
9
|
export interface SoftRefreshParams {
|
|
5
10
|
deviceId: string;
|
|
6
11
|
notifyCharacteristic: Characteristic | null | undefined;
|
|
@@ -11,4 +16,5 @@ export interface SoftRefreshParams {
|
|
|
11
16
|
logger: Logger | null;
|
|
12
17
|
}
|
|
13
18
|
export declare function softRefreshSubscription(params: SoftRefreshParams): Promise<void>;
|
|
19
|
+
export {};
|
|
14
20
|
//# sourceMappingURL=ble-ops.d.ts.map
|
package/dist/ble-ops.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ble-ops.d.ts","sourceRoot":"","sources":["../src/ble-ops.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;IACxD,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG,MAAM,CAAC,CAAC;IAC9E,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"ble-ops.d.ts","sourceRoot":"","sources":["../src/ble-ops.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,KAAK,WAAW,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;AAE3C,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,EAC1C,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;IACxD,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG,MAAM,CAAC,CAAC;IAC9E,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CtF"}
|
|
@@ -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-d58af248.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-5f728113.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -17,6 +17,37 @@ function safeLog(logger, level, message, ...args) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function runBleCallbackOperation(operation, options) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let settled = false;
|
|
23
|
+
const settle = (error) => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
if (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
resolve();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
if (options.timeoutBehavior === 'reject') {
|
|
37
|
+
settle(new Error(`BLE operation timed out after ${options.timeoutMs}ms`));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
settle();
|
|
41
|
+
}
|
|
42
|
+
}, options.timeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
operation(settle);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
settle(error instanceof Error ? error : new Error(String(error)));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
20
51
|
function softRefreshSubscription(params) {
|
|
21
52
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
22
53
|
const { deviceId, notifyCharacteristic, subscriptionOperations, subscribedDevices, pairedDevices, onNotificationData, logger, } = params;
|
|
@@ -25,17 +56,13 @@ function softRefreshSubscription(params) {
|
|
|
25
56
|
}
|
|
26
57
|
logger === null || logger === void 0 ? void 0 : logger.info('[BLE-OPS] Starting subscription refresh', { deviceId });
|
|
27
58
|
subscriptionOperations.set(deviceId, 'subscribing');
|
|
28
|
-
yield
|
|
29
|
-
|
|
59
|
+
yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
60
|
+
timeoutMs: 250,
|
|
61
|
+
timeoutBehavior: 'resolve',
|
|
30
62
|
});
|
|
31
|
-
yield
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
reject(error);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
resolve();
|
|
38
|
-
});
|
|
63
|
+
yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
64
|
+
timeoutMs: 8000,
|
|
65
|
+
timeoutBehavior: 'reject',
|
|
39
66
|
});
|
|
40
67
|
notifyCharacteristic.removeAllListeners('data');
|
|
41
68
|
notifyCharacteristic.on('data', (data) => {
|
|
@@ -59,6 +86,7 @@ const bluetoothState = {
|
|
|
59
86
|
initialized: false,
|
|
60
87
|
};
|
|
61
88
|
let persistentStateListener = null;
|
|
89
|
+
let persistentDiscoverListener = null;
|
|
62
90
|
const discoveredDevices = new Map();
|
|
63
91
|
const connectedDevices = new Map();
|
|
64
92
|
const pairedDevices = new Set();
|
|
@@ -66,16 +94,18 @@ const deviceCharacteristics = new Map();
|
|
|
66
94
|
const notificationCallbacks = new Map();
|
|
67
95
|
const subscribedDevices = new Map();
|
|
68
96
|
const subscriptionOperations = new Map();
|
|
97
|
+
const deviceDisconnectListeners = new Map();
|
|
69
98
|
const ONEKEY_SERVICE_UUIDS = [hdShared.ONEKEY_SERVICE_UUID];
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
99
|
+
const ONEKEY_SERVICE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_SERVICE_UUID);
|
|
100
|
+
const ONEKEY_WRITE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
101
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
73
102
|
const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
74
103
|
const DEVICE_SCAN_TIMEOUT = 8000;
|
|
75
104
|
const FAST_SCAN_TIMEOUT = 8000;
|
|
76
105
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
77
106
|
const CONNECTION_TIMEOUT = 8000;
|
|
78
107
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
108
|
+
const BLE_CLEANUP_TIMEOUT = 250;
|
|
79
109
|
const BLE_PACKET_SIZE = 192;
|
|
80
110
|
const UNIFIED_WRITE_DELAY = 5;
|
|
81
111
|
const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 };
|
|
@@ -83,30 +113,15 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
83
113
|
const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
84
114
|
/status:\s*3/i,
|
|
85
115
|
];
|
|
86
|
-
function getBleUuidKey(uuid) {
|
|
87
|
-
const normalized = (uuid !== null && uuid !== void 0 ? uuid : '').replace(/-/g, '').toLowerCase();
|
|
88
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
89
|
-
}
|
|
90
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
91
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
92
|
-
'0001',
|
|
93
|
-
]);
|
|
94
|
-
function isGenericBleService(uuid) {
|
|
95
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
96
|
-
}
|
|
97
|
-
function hasOneKeyAdvertisementService(peripheral) {
|
|
98
|
-
var _a, _b;
|
|
99
|
-
const serviceUuids = (_b = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids) !== null && _b !== void 0 ? _b : [];
|
|
100
|
-
return serviceUuids.some(uuid => {
|
|
101
|
-
const uuidKey = getBleUuidKey(uuid);
|
|
102
|
-
return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
103
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
116
|
function isOneKeyPeripheral(peripheral) {
|
|
107
|
-
var _a;
|
|
108
|
-
const
|
|
109
|
-
return hdShared.
|
|
117
|
+
var _a, _b;
|
|
118
|
+
const serviceUuids = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids;
|
|
119
|
+
return (hdShared.hasOnekeyCommunicationService(serviceUuids) &&
|
|
120
|
+
hdShared.isOnekeyBluetoothDevice({
|
|
121
|
+
id: peripheral.id,
|
|
122
|
+
localName: (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName,
|
|
123
|
+
serviceUuids,
|
|
124
|
+
}));
|
|
110
125
|
}
|
|
111
126
|
function emitRawNotification(deviceId, data) {
|
|
112
127
|
const appCb = notificationCallbacks.get(deviceId);
|
|
@@ -245,9 +260,12 @@ function initializeNoble() {
|
|
|
245
260
|
};
|
|
246
261
|
noble.on('stateChange', onStateChange);
|
|
247
262
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
if (!persistentDiscoverListener) {
|
|
264
|
+
persistentDiscoverListener = (peripheral) => {
|
|
265
|
+
handleDeviceDiscovered(peripheral);
|
|
266
|
+
};
|
|
267
|
+
noble.on('discover', persistentDiscoverListener);
|
|
268
|
+
}
|
|
251
269
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble initialized successfully');
|
|
252
270
|
}
|
|
253
271
|
catch (error) {
|
|
@@ -272,6 +290,11 @@ function cleanupDevice(deviceId, webContents, options = {}) {
|
|
|
272
290
|
const peripheral = connectedDevices.get(deviceId);
|
|
273
291
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
274
292
|
if (cleanupConnection) {
|
|
293
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
294
|
+
if (disconnectEntry) {
|
|
295
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
296
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
297
|
+
}
|
|
275
298
|
connectedDevices.delete(deviceId);
|
|
276
299
|
deviceCharacteristics.delete(deviceId);
|
|
277
300
|
notificationCallbacks.delete(deviceId);
|
|
@@ -306,10 +329,15 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
306
329
|
});
|
|
307
330
|
}
|
|
308
331
|
function setupDisconnectListener(peripheral, deviceId, webContents) {
|
|
309
|
-
|
|
310
|
-
|
|
332
|
+
const existing = deviceDisconnectListeners.get(deviceId);
|
|
333
|
+
if (existing) {
|
|
334
|
+
existing.peripheral.removeListener('disconnect', existing.listener);
|
|
335
|
+
}
|
|
336
|
+
const listener = () => {
|
|
311
337
|
handleDeviceDisconnect(deviceId, webContents);
|
|
312
|
-
}
|
|
338
|
+
};
|
|
339
|
+
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
340
|
+
peripheral.on('disconnect', listener);
|
|
313
341
|
}
|
|
314
342
|
function writeCharacteristicWithAck(deviceId, writeCharacteristic, buffer) {
|
|
315
343
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
@@ -470,15 +498,15 @@ function handleDeviceDiscovered(peripheral) {
|
|
|
470
498
|
function ensureDiscoverListener() {
|
|
471
499
|
if (!noble)
|
|
472
500
|
return;
|
|
473
|
-
|
|
474
|
-
if (listenerCount === 0) {
|
|
501
|
+
if (!persistentDiscoverListener) {
|
|
475
502
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Discover listener missing, re-adding it');
|
|
476
|
-
|
|
503
|
+
persistentDiscoverListener = (peripheral) => {
|
|
477
504
|
handleDeviceDiscovered(peripheral);
|
|
478
|
-
}
|
|
505
|
+
};
|
|
506
|
+
noble.on('discover', persistentDiscoverListener);
|
|
479
507
|
}
|
|
480
508
|
else {
|
|
481
|
-
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already
|
|
509
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
|
|
482
510
|
}
|
|
483
511
|
}
|
|
484
512
|
function performTargetedScan(targetDeviceId) {
|
|
@@ -491,7 +519,7 @@ function performTargetedScan(targetDeviceId) {
|
|
|
491
519
|
return new Promise((resolve, reject) => {
|
|
492
520
|
const onDiscover = (peripheral) => {
|
|
493
521
|
var _a;
|
|
494
|
-
if (peripheral.id === targetDeviceId) {
|
|
522
|
+
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
495
523
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Target device found during targeted scan:', {
|
|
496
524
|
id: peripheral.id,
|
|
497
525
|
name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
|
|
@@ -583,25 +611,23 @@ function stopScanning() {
|
|
|
583
611
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
584
612
|
if (!noble)
|
|
585
613
|
return;
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
noble.stopScanning(() => {
|
|
592
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
|
|
593
|
-
resolve();
|
|
594
|
-
});
|
|
614
|
+
const nobleInstance = noble;
|
|
615
|
+
yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
616
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
617
|
+
timeoutBehavior: 'resolve',
|
|
595
618
|
});
|
|
619
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
|
|
596
620
|
});
|
|
597
621
|
}
|
|
598
622
|
function cleanupNobleListeners() {
|
|
599
623
|
if (!noble)
|
|
600
624
|
return;
|
|
601
625
|
try {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
626
|
+
if (persistentDiscoverListener) {
|
|
627
|
+
noble.removeListener('discover', persistentDiscoverListener);
|
|
628
|
+
persistentDiscoverListener = null;
|
|
629
|
+
}
|
|
630
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Owned Noble listeners cleaned up');
|
|
605
631
|
}
|
|
606
632
|
catch (error) {
|
|
607
633
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to clean up some listeners:', error);
|
|
@@ -678,14 +704,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
678
704
|
deviceId: peripheral.id,
|
|
679
705
|
serviceUUIDs: services.map(service => service.uuid),
|
|
680
706
|
});
|
|
681
|
-
|
|
682
|
-
if (!service) {
|
|
683
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Known OneKey service UUID not found, trying first vendor service');
|
|
684
|
-
service =
|
|
685
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
686
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
687
|
-
services[0];
|
|
688
|
-
}
|
|
707
|
+
const service = services.find(s => hdShared.matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
689
708
|
if (!service) {
|
|
690
709
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound);
|
|
691
710
|
}
|
|
@@ -708,12 +727,10 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
708
727
|
let writeCharacteristic = null;
|
|
709
728
|
let notifyCharacteristic = null;
|
|
710
729
|
for (const characteristic of characteristics) {
|
|
711
|
-
|
|
712
|
-
const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
|
|
713
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
730
|
+
if (hdShared.matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
714
731
|
writeCharacteristic = characteristic;
|
|
715
732
|
}
|
|
716
|
-
else if (
|
|
733
|
+
else if (hdShared.matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
717
734
|
notifyCharacteristic = characteristic;
|
|
718
735
|
}
|
|
719
736
|
}
|
|
@@ -747,27 +764,17 @@ function forceReconnectPeripheral(peripheral, deviceId) {
|
|
|
747
764
|
reason: 'force-reconnect',
|
|
748
765
|
});
|
|
749
766
|
if (peripheral.state === 'connected') {
|
|
750
|
-
peripheral.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
resolve();
|
|
755
|
-
});
|
|
756
|
-
});
|
|
767
|
+
yield runBleCallbackOperation(callback => peripheral.disconnect(() => {
|
|
768
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force disconnect completed');
|
|
769
|
+
callback();
|
|
770
|
+
}), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
|
|
757
771
|
}
|
|
758
|
-
yield
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Force reconnect failed:', error);
|
|
762
|
-
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
|
|
766
|
-
connectedDevices.set(deviceId, peripheral);
|
|
767
|
-
resolve();
|
|
768
|
-
}
|
|
769
|
-
});
|
|
772
|
+
yield runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
773
|
+
timeoutMs: CONNECTION_TIMEOUT,
|
|
774
|
+
timeoutBehavior: 'reject',
|
|
770
775
|
});
|
|
776
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
|
|
777
|
+
connectedDevices.set(deviceId, peripheral);
|
|
771
778
|
yield hdShared.wait(500);
|
|
772
779
|
});
|
|
773
780
|
}
|
|
@@ -844,13 +851,23 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
844
851
|
}
|
|
845
852
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
846
853
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
854
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
855
|
+
try {
|
|
856
|
+
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
857
|
+
}
|
|
858
|
+
catch (initialError) {
|
|
859
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
860
|
+
deviceId,
|
|
861
|
+
error: initialError,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
847
864
|
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
848
865
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
849
866
|
try {
|
|
850
867
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
851
868
|
}
|
|
852
|
-
catch (
|
|
853
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...',
|
|
869
|
+
catch (reconnectError) {
|
|
870
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
|
|
854
871
|
return freshScanAndDiscover(deviceId, webContents);
|
|
855
872
|
}
|
|
856
873
|
});
|
|
@@ -958,17 +975,20 @@ function disconnectDevice(deviceId) {
|
|
|
958
975
|
if (!peripheral) {
|
|
959
976
|
return;
|
|
960
977
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
peripheral.disconnect
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
978
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
979
|
+
if (disconnectEntry) {
|
|
980
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
981
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
982
|
+
}
|
|
983
|
+
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
984
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
985
|
+
timeoutBehavior: 'resolve',
|
|
986
|
+
});
|
|
987
|
+
cleanupDevice(deviceId, undefined, {
|
|
988
|
+
cleanupConnection: true,
|
|
989
|
+
sendDisconnectEvent: false,
|
|
990
|
+
cancelOperations: true,
|
|
991
|
+
reason: 'manual-disconnect',
|
|
972
992
|
});
|
|
973
993
|
});
|
|
974
994
|
}
|
|
@@ -983,18 +1003,11 @@ function unsubscribeNotifications(deviceId) {
|
|
|
983
1003
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Unsubscribing from notifications for device:', deviceId);
|
|
984
1004
|
subscriptionOperations.set(deviceId, 'unsubscribing');
|
|
985
1005
|
try {
|
|
986
|
-
yield
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Notification unsubscription failed:', error);
|
|
990
|
-
reject(error);
|
|
991
|
-
}
|
|
992
|
-
else {
|
|
993
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification unsubscription successful');
|
|
994
|
-
resolve();
|
|
995
|
-
}
|
|
996
|
-
});
|
|
1006
|
+
yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1007
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1008
|
+
timeoutBehavior: 'resolve',
|
|
997
1009
|
});
|
|
1010
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification unsubscription completed');
|
|
998
1011
|
notifyCharacteristic.removeAllListeners('data');
|
|
999
1012
|
notificationCallbacks.delete(deviceId);
|
|
1000
1013
|
subscribedDevices.delete(deviceId);
|
|
@@ -1046,19 +1059,13 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1046
1059
|
notificationCallbacks.set(deviceId, callback);
|
|
1047
1060
|
function rebuildAppSubscription(deviceId, notifyCharacteristic) {
|
|
1048
1061
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1049
|
-
yield
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
});
|
|
1062
|
+
yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1063
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1064
|
+
timeoutBehavior: 'resolve',
|
|
1053
1065
|
});
|
|
1054
|
-
yield
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
reject(error);
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
resolve();
|
|
1061
|
-
});
|
|
1066
|
+
yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1067
|
+
timeoutMs: CONNECTION_TIMEOUT,
|
|
1068
|
+
timeoutBehavior: 'reject',
|
|
1062
1069
|
});
|
|
1063
1070
|
notifyCharacteristic.on('data', (data) => {
|
|
1064
1071
|
if (!pairedDevices.has(deviceId)) {
|
|
@@ -1158,23 +1165,23 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1158
1165
|
}));
|
|
1159
1166
|
webContents.on('destroyed', () => {
|
|
1160
1167
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1168
|
+
(() => index.__awaiter(this, void 0, void 0, function* () {
|
|
1169
|
+
const deviceIds = Array.from(connectedDevices.keys());
|
|
1170
|
+
for (const deviceId of deviceIds) {
|
|
1171
|
+
yield unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1172
|
+
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1173
|
+
}
|
|
1174
|
+
yield stopScanning().catch(() => undefined);
|
|
1175
|
+
if (noble && persistentStateListener) {
|
|
1176
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1177
|
+
persistentStateListener = null;
|
|
1178
|
+
}
|
|
1179
|
+
cleanupNobleListeners();
|
|
1180
|
+
discoveredDevices.clear();
|
|
1181
|
+
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1182
|
+
}))().catch(error => {
|
|
1183
|
+
safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
|
|
1169
1184
|
});
|
|
1170
|
-
stopScanning();
|
|
1171
|
-
if (noble && persistentStateListener) {
|
|
1172
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1173
|
-
persistentStateListener = null;
|
|
1174
|
-
}
|
|
1175
|
-
cleanupNobleListeners();
|
|
1176
|
-
discoveredDevices.clear();
|
|
1177
|
-
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1178
1185
|
});
|
|
1179
1186
|
safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
|
|
1180
1187
|
}
|
|
@@ -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":"AAyBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AA46ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAyJpE"}
|
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.34",
|
|
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.34",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.34",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.34",
|
|
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": "07f10b4c9fab177da7c36d976ec18b075492b57f"
|
|
40
40
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { runBleCallbackOperation } from '../ble-ops';
|
|
2
|
+
|
|
3
|
+
describe('Electron Noble callback operations', () => {
|
|
4
|
+
test('settles when Noble never invokes the callback', async () => {
|
|
5
|
+
const result = await runBleCallbackOperation(
|
|
6
|
+
callback => {
|
|
7
|
+
void callback;
|
|
8
|
+
},
|
|
9
|
+
{ timeoutMs: 5, timeoutBehavior: 'resolve' }
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
expect(result).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('rejects callback errors before the timeout', async () => {
|
|
16
|
+
await expect(
|
|
17
|
+
runBleCallbackOperation(callback => callback(new Error('operation failed')), {
|
|
18
|
+
timeoutMs: 50,
|
|
19
|
+
timeoutBehavior: 'reject',
|
|
20
|
+
})
|
|
21
|
+
).rejects.toThrow('operation failed');
|
|
22
|
+
});
|
|
23
|
+
});
|
package/src/ble-ops.ts
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import type { Characteristic } from '@stoprocent/noble';
|
|
2
2
|
import type { Logger } from './types/noble-extended';
|
|
3
3
|
|
|
4
|
+
type BleCallback = (error?: Error) => void;
|
|
5
|
+
|
|
6
|
+
export function runBleCallbackOperation(
|
|
7
|
+
operation: (callback: BleCallback) => void,
|
|
8
|
+
options: { timeoutMs: number; timeoutBehavior: 'resolve' | 'reject' }
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
return new Promise<void>((resolve, reject) => {
|
|
11
|
+
let settled = false;
|
|
12
|
+
const settle = (error?: Error) => {
|
|
13
|
+
if (settled) return;
|
|
14
|
+
settled = true;
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
if (error) {
|
|
17
|
+
reject(error);
|
|
18
|
+
} else {
|
|
19
|
+
resolve();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const timeout = setTimeout(() => {
|
|
23
|
+
if (options.timeoutBehavior === 'reject') {
|
|
24
|
+
settle(new Error(`BLE operation timed out after ${options.timeoutMs}ms`));
|
|
25
|
+
} else {
|
|
26
|
+
settle();
|
|
27
|
+
}
|
|
28
|
+
}, options.timeoutMs);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
operation(settle);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
settle(error instanceof Error ? error : new Error(String(error)));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
4
38
|
export interface SoftRefreshParams {
|
|
5
39
|
deviceId: string;
|
|
6
40
|
notifyCharacteristic: Characteristic | null | undefined;
|
|
@@ -30,18 +64,14 @@ export async function softRefreshSubscription(params: SoftRefreshParams): Promis
|
|
|
30
64
|
|
|
31
65
|
subscriptionOperations.set(deviceId, 'subscribing');
|
|
32
66
|
|
|
33
|
-
await
|
|
34
|
-
|
|
67
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
68
|
+
timeoutMs: 250,
|
|
69
|
+
timeoutBehavior: 'resolve',
|
|
35
70
|
});
|
|
36
71
|
|
|
37
|
-
await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
reject(error);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
resolve();
|
|
44
|
-
});
|
|
72
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
73
|
+
timeoutMs: 8_000,
|
|
74
|
+
timeoutBehavior: 'reject',
|
|
45
75
|
});
|
|
46
76
|
|
|
47
77
|
notifyCharacteristic.removeAllListeners('data');
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -9,14 +9,19 @@ import {
|
|
|
9
9
|
EOneKeyBleMessageKeys,
|
|
10
10
|
ERRORS,
|
|
11
11
|
HardwareErrorCode,
|
|
12
|
+
ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
|
|
12
13
|
ONEKEY_SERVICE_UUID,
|
|
13
|
-
|
|
14
|
+
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
15
|
+
createKnownBleUuidAliases,
|
|
16
|
+
hasOnekeyCommunicationService,
|
|
17
|
+
isOnekeyBluetoothDevice,
|
|
18
|
+
matchesKnownBleUuid,
|
|
14
19
|
wait,
|
|
15
20
|
} from '@onekeyfe/hd-shared';
|
|
16
21
|
import pRetry from 'p-retry';
|
|
17
22
|
|
|
18
23
|
import { safeLog } from './types/noble-extended';
|
|
19
|
-
import { softRefreshSubscription } from './ble-ops';
|
|
24
|
+
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
20
25
|
|
|
21
26
|
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
22
27
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
@@ -39,6 +44,7 @@ const bluetoothState: {
|
|
|
39
44
|
|
|
40
45
|
// Global persistent state listener for app layer
|
|
41
46
|
let persistentStateListener: ((state: string) => void) | null = null;
|
|
47
|
+
let persistentDiscoverListener: ((peripheral: Peripheral) => void) | null = null;
|
|
42
48
|
|
|
43
49
|
// Device cache and connection state
|
|
44
50
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
@@ -50,6 +56,10 @@ const subscribedDevices = new Map<string, boolean>(); // Track subscription stat
|
|
|
50
56
|
|
|
51
57
|
// 🔒 Subscription operation state tracking to prevent race conditions
|
|
52
58
|
const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' | 'idle'>();
|
|
59
|
+
const deviceDisconnectListeners = new Map<
|
|
60
|
+
string,
|
|
61
|
+
{ peripheral: Peripheral; listener: () => void }
|
|
62
|
+
>();
|
|
53
63
|
|
|
54
64
|
// Windows-only response watchdog state moved to utils/windows-ble-recovery
|
|
55
65
|
|
|
@@ -59,11 +69,9 @@ const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' |
|
|
|
59
69
|
|
|
60
70
|
// Service UUIDs to scan for - using constants from hd-shared
|
|
61
71
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const NORMALIZED_WRITE_UUID = '0002';
|
|
66
|
-
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
72
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
73
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
74
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
67
75
|
|
|
68
76
|
// Timeout and interval constants
|
|
69
77
|
const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
|
|
@@ -72,6 +80,7 @@ const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has lon
|
|
|
72
80
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
73
81
|
const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow)
|
|
74
82
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
83
|
+
const BLE_CLEANUP_TIMEOUT = 250;
|
|
75
84
|
|
|
76
85
|
// Write-related constants
|
|
77
86
|
const BLE_PACKET_SIZE = 192;
|
|
@@ -82,34 +91,16 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
|
82
91
|
/status:\s*3/i, // Windows pairing cancelled / GATT write failed
|
|
83
92
|
];
|
|
84
93
|
|
|
85
|
-
function getBleUuidKey(uuid?: string | null) {
|
|
86
|
-
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
87
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
91
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
92
|
-
'0001',
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
function isGenericBleService(uuid?: string | null) {
|
|
96
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function hasOneKeyAdvertisementService(peripheral: Peripheral) {
|
|
100
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
|
|
101
|
-
return serviceUuids.some(uuid => {
|
|
102
|
-
const uuidKey = getBleUuidKey(uuid);
|
|
103
|
-
return (
|
|
104
|
-
NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
105
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
94
|
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
111
|
-
const
|
|
112
|
-
return
|
|
95
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
96
|
+
return (
|
|
97
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
98
|
+
isOnekeyBluetoothDevice({
|
|
99
|
+
id: peripheral.id,
|
|
100
|
+
localName: peripheral.advertisement?.localName,
|
|
101
|
+
serviceUuids,
|
|
102
|
+
})
|
|
103
|
+
);
|
|
113
104
|
}
|
|
114
105
|
|
|
115
106
|
/**
|
|
@@ -290,9 +281,12 @@ async function initializeNoble(): Promise<void> {
|
|
|
290
281
|
});
|
|
291
282
|
|
|
292
283
|
// Set up device discovery
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
284
|
+
if (!persistentDiscoverListener) {
|
|
285
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
286
|
+
handleDeviceDiscovered(peripheral);
|
|
287
|
+
};
|
|
288
|
+
noble.on('discover', persistentDiscoverListener);
|
|
289
|
+
}
|
|
296
290
|
|
|
297
291
|
logger?.info('[NobleBLE] Noble initialized successfully');
|
|
298
292
|
} catch (error) {
|
|
@@ -354,6 +348,11 @@ function cleanupDevice(
|
|
|
354
348
|
|
|
355
349
|
// 1. Clean up connection state
|
|
356
350
|
if (cleanupConnection) {
|
|
351
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
352
|
+
if (disconnectEntry) {
|
|
353
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
354
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
355
|
+
}
|
|
357
356
|
connectedDevices.delete(deviceId);
|
|
358
357
|
deviceCharacteristics.delete(deviceId);
|
|
359
358
|
notificationCallbacks.delete(deviceId);
|
|
@@ -403,13 +402,16 @@ function setupDisconnectListener(
|
|
|
403
402
|
deviceId: string,
|
|
404
403
|
webContents: WebContents
|
|
405
404
|
): void {
|
|
406
|
-
|
|
407
|
-
|
|
405
|
+
const existing = deviceDisconnectListeners.get(deviceId);
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.peripheral.removeListener('disconnect', existing.listener);
|
|
408
|
+
}
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
peripheral.on('disconnect', () => {
|
|
410
|
+
const listener = () => {
|
|
411
411
|
handleDeviceDisconnect(deviceId, webContents);
|
|
412
|
-
}
|
|
412
|
+
};
|
|
413
|
+
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
414
|
+
peripheral.on('disconnect', listener);
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
// ===== Write helpers (inline) =====
|
|
@@ -628,16 +630,14 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
628
630
|
function ensureDiscoverListener(): void {
|
|
629
631
|
if (!noble) return;
|
|
630
632
|
|
|
631
|
-
|
|
632
|
-
const listenerCount = (noble as any).listenerCount('discover');
|
|
633
|
-
|
|
634
|
-
if (listenerCount === 0) {
|
|
633
|
+
if (!persistentDiscoverListener) {
|
|
635
634
|
logger?.info('[NobleBLE] Discover listener missing, re-adding it');
|
|
636
|
-
|
|
635
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
637
636
|
handleDeviceDiscovered(peripheral);
|
|
638
|
-
}
|
|
637
|
+
};
|
|
638
|
+
noble.on('discover', persistentDiscoverListener);
|
|
639
639
|
} else {
|
|
640
|
-
logger?.debug('[NobleBLE] Discover listener already
|
|
640
|
+
logger?.debug('[NobleBLE] Discover listener already registered');
|
|
641
641
|
}
|
|
642
642
|
}
|
|
643
643
|
|
|
@@ -656,7 +656,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
656
656
|
return new Promise((resolve, reject) => {
|
|
657
657
|
// Local discover listener - only matches target device
|
|
658
658
|
const onDiscover = (peripheral: Peripheral) => {
|
|
659
|
-
if (peripheral.id === targetDeviceId) {
|
|
659
|
+
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
660
660
|
logger?.info('[NobleBLE] Target device found during targeted scan:', {
|
|
661
661
|
id: peripheral.id,
|
|
662
662
|
name: peripheral.advertisement?.localName,
|
|
@@ -774,30 +774,24 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
774
774
|
// Stop scanning
|
|
775
775
|
async function stopScanning(): Promise<void> {
|
|
776
776
|
if (!noble) return;
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
noble.stopScanning(() => {
|
|
785
|
-
logger?.info('[NobleBLE] Scanning stopped');
|
|
786
|
-
resolve();
|
|
787
|
-
});
|
|
777
|
+
const nobleInstance = noble;
|
|
778
|
+
await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
779
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
780
|
+
timeoutBehavior: 'resolve',
|
|
788
781
|
});
|
|
782
|
+
logger?.info('[NobleBLE] Scanning stopped');
|
|
789
783
|
}
|
|
790
784
|
|
|
791
785
|
// Clean up all Noble listeners (for app exit)
|
|
792
786
|
function cleanupNobleListeners(): void {
|
|
793
787
|
if (!noble) return;
|
|
794
788
|
|
|
795
|
-
// Remove all listeners to prevent memory leaks
|
|
796
|
-
// Noble uses EventEmitter, need to use removeAllListeners
|
|
797
789
|
try {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
790
|
+
if (persistentDiscoverListener) {
|
|
791
|
+
noble.removeListener('discover', persistentDiscoverListener);
|
|
792
|
+
persistentDiscoverListener = null;
|
|
793
|
+
}
|
|
794
|
+
logger?.info('[NobleBLE] Owned Noble listeners cleaned up');
|
|
801
795
|
} catch (error) {
|
|
802
796
|
logger?.error('[NobleBLE] Failed to clean up some listeners:', error);
|
|
803
797
|
}
|
|
@@ -905,14 +899,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
905
899
|
});
|
|
906
900
|
|
|
907
901
|
// Find OneKey service — Noble may expose 128-bit UUIDs as short UUID keys.
|
|
908
|
-
|
|
909
|
-
if (!service) {
|
|
910
|
-
logger?.info('[NobleBLE] Known OneKey service UUID not found, trying first vendor service');
|
|
911
|
-
service =
|
|
912
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
913
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
914
|
-
services[0];
|
|
915
|
-
}
|
|
902
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
916
903
|
if (!service) {
|
|
917
904
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
918
905
|
}
|
|
@@ -938,12 +925,9 @@ async function discoverServicesAndCharacteristics(
|
|
|
938
925
|
let notifyCharacteristic: Characteristic | null = null;
|
|
939
926
|
|
|
940
927
|
for (const characteristic of characteristics) {
|
|
941
|
-
|
|
942
|
-
const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
|
|
943
|
-
|
|
944
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
928
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
945
929
|
writeCharacteristic = characteristic;
|
|
946
|
-
} else if (
|
|
930
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
947
931
|
notifyCharacteristic = characteristic;
|
|
948
932
|
}
|
|
949
933
|
}
|
|
@@ -1000,29 +984,23 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
1000
984
|
|
|
1001
985
|
// Step 2: Force disconnect if connected
|
|
1002
986
|
if (peripheral.state === 'connected') {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
}
|
|
1010
|
-
|
|
987
|
+
await runBleCallbackOperation(
|
|
988
|
+
callback =>
|
|
989
|
+
peripheral.disconnect(() => {
|
|
990
|
+
logger?.info('[NobleBLE] Force disconnect completed');
|
|
991
|
+
callback();
|
|
992
|
+
}),
|
|
993
|
+
{ timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' }
|
|
994
|
+
);
|
|
1011
995
|
}
|
|
1012
996
|
|
|
1013
997
|
// Step 3: Re-establish connection
|
|
1014
|
-
await
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
logger?.error('[NobleBLE] Force reconnect failed:', error);
|
|
1018
|
-
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
1019
|
-
} else {
|
|
1020
|
-
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1021
|
-
connectedDevices.set(deviceId, peripheral);
|
|
1022
|
-
resolve();
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
998
|
+
await runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
999
|
+
timeoutMs: CONNECTION_TIMEOUT,
|
|
1000
|
+
timeoutBehavior: 'reject',
|
|
1025
1001
|
});
|
|
1002
|
+
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1003
|
+
connectedDevices.set(deviceId, peripheral);
|
|
1026
1004
|
|
|
1027
1005
|
// Wait for connection to stabilize
|
|
1028
1006
|
await wait(500);
|
|
@@ -1153,16 +1131,26 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1153
1131
|
deviceId: string,
|
|
1154
1132
|
webContents: WebContents
|
|
1155
1133
|
): Promise<CharacteristicPair> {
|
|
1156
|
-
// Force reconnect to clear GATT cache
|
|
1157
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1158
1134
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1159
1135
|
|
|
1160
|
-
// Discover services and characteristics with retry
|
|
1161
1136
|
try {
|
|
1162
1137
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1163
|
-
} catch (
|
|
1164
|
-
|
|
1165
|
-
|
|
1138
|
+
} catch (initialError) {
|
|
1139
|
+
logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
1140
|
+
deviceId,
|
|
1141
|
+
error: initialError,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1146
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1147
|
+
try {
|
|
1148
|
+
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1149
|
+
} catch (reconnectError) {
|
|
1150
|
+
logger?.error(
|
|
1151
|
+
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1152
|
+
reconnectError
|
|
1153
|
+
);
|
|
1166
1154
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1167
1155
|
}
|
|
1168
1156
|
}
|
|
@@ -1325,20 +1313,21 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1325
1313
|
return;
|
|
1326
1314
|
}
|
|
1327
1315
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
peripheral.
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1316
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
1317
|
+
if (disconnectEntry) {
|
|
1318
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
1319
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1323
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1324
|
+
timeoutBehavior: 'resolve',
|
|
1325
|
+
});
|
|
1326
|
+
cleanupDevice(deviceId, undefined, {
|
|
1327
|
+
cleanupConnection: true,
|
|
1328
|
+
sendDisconnectEvent: false,
|
|
1329
|
+
cancelOperations: true,
|
|
1330
|
+
reason: 'manual-disconnect',
|
|
1342
1331
|
});
|
|
1343
1332
|
}
|
|
1344
1333
|
|
|
@@ -1359,17 +1348,11 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
|
|
|
1359
1348
|
subscriptionOperations.set(deviceId, 'unsubscribing');
|
|
1360
1349
|
|
|
1361
1350
|
try {
|
|
1362
|
-
await
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
logger?.error('[NobleBLE] Notification unsubscription failed:', error);
|
|
1366
|
-
reject(error);
|
|
1367
|
-
} else {
|
|
1368
|
-
logger?.info('[NobleBLE] Notification unsubscription successful');
|
|
1369
|
-
resolve();
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1351
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1352
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1353
|
+
timeoutBehavior: 'resolve',
|
|
1372
1354
|
});
|
|
1355
|
+
logger?.info('[NobleBLE] Notification unsubscription completed');
|
|
1373
1356
|
|
|
1374
1357
|
// Remove all listeners and clear subscription status
|
|
1375
1358
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -1462,19 +1445,13 @@ async function subscribeNotifications(
|
|
|
1462
1445
|
notifyCharacteristic: Characteristic
|
|
1463
1446
|
): Promise<void> {
|
|
1464
1447
|
// Listeners already cleaned up above, no need to repeat
|
|
1465
|
-
await
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
});
|
|
1448
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1449
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1450
|
+
timeoutBehavior: 'resolve',
|
|
1469
1451
|
});
|
|
1470
|
-
await
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
reject(error);
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
resolve();
|
|
1477
|
-
});
|
|
1452
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1453
|
+
timeoutMs: CONNECTION_TIMEOUT,
|
|
1454
|
+
timeoutBehavior: 'reject',
|
|
1478
1455
|
});
|
|
1479
1456
|
|
|
1480
1457
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
@@ -1626,32 +1603,24 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1626
1603
|
// Cleanup on app quit
|
|
1627
1604
|
webContents.on('destroyed', () => {
|
|
1628
1605
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1606
|
+
(async () => {
|
|
1607
|
+
const deviceIds = Array.from(connectedDevices.keys());
|
|
1608
|
+
for (const deviceId of deviceIds) {
|
|
1609
|
+
await unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1610
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1611
|
+
}
|
|
1629
1612
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1613
|
+
await stopScanning().catch(() => undefined);
|
|
1614
|
+
if (noble && persistentStateListener) {
|
|
1615
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1616
|
+
persistentStateListener = null;
|
|
1617
|
+
}
|
|
1618
|
+
cleanupNobleListeners();
|
|
1619
|
+
discoveredDevices.clear();
|
|
1620
|
+
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1621
|
+
})().catch(error => {
|
|
1622
|
+
safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
|
|
1639
1623
|
});
|
|
1640
|
-
|
|
1641
|
-
// 2. Stop scanning
|
|
1642
|
-
stopScanning();
|
|
1643
|
-
|
|
1644
|
-
// 3. Clean up Noble listeners
|
|
1645
|
-
if (noble && persistentStateListener) {
|
|
1646
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1647
|
-
persistentStateListener = null;
|
|
1648
|
-
}
|
|
1649
|
-
cleanupNobleListeners();
|
|
1650
|
-
|
|
1651
|
-
// 4. Clear discovered devices cache
|
|
1652
|
-
discoveredDevices.clear();
|
|
1653
|
-
|
|
1654
|
-
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1655
1624
|
});
|
|
1656
1625
|
|
|
1657
1626
|
safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
|