@onekeyfe/hd-transport-electron 1.2.0-alpha.98 → 1.2.0
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-6da32f94.js → index-a518d449.js} +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-ce551d9a.js → noble-ble-handler-56d2eab3.js} +106 -57
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/types/desktop-api.d.ts +2 -0
- package/dist/types/desktop-api.d.ts.map +1 -1
- package/dist/types/noble-extended.d.ts +1 -0
- package/dist/types/noble-extended.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +13 -7
- package/src/noble-ble-handler.ts +237 -72
- package/src/types/desktop-api.ts +11 -1
- package/src/types/noble-extended.ts +1 -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-56d2eab3.js'); });
|
|
36
36
|
setupNobleBleHandlers(webContents);
|
|
37
37
|
});
|
|
38
38
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WebContents } from 'electron';
|
|
2
2
|
import { Characteristic, Peripheral } from '@stoprocent/noble';
|
|
3
3
|
import { OneKeyDeviceInfoBase } from '@onekeyfe/hd-transport';
|
|
4
|
+
import { EBleDisconnectReason } from '@onekeyfe/hd-shared';
|
|
4
5
|
|
|
5
6
|
interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
6
7
|
id: string;
|
|
@@ -24,6 +25,7 @@ interface NobleModule {
|
|
|
24
25
|
interface Logger {
|
|
25
26
|
info(message: string, ...args: any[]): void;
|
|
26
27
|
debug(message: string, ...args: any[]): void;
|
|
28
|
+
warn(message: string, ...args: any[]): void;
|
|
27
29
|
error(message: string, ...args: any[]): void;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -31,6 +33,7 @@ interface Logger {
|
|
|
31
33
|
* Desktop API types for Electron preload script
|
|
32
34
|
* These types define the core interface for Noble BLE communication
|
|
33
35
|
*/
|
|
36
|
+
|
|
34
37
|
interface NobleBleWriteOptions {
|
|
35
38
|
pacingDelayMs?: number;
|
|
36
39
|
}
|
|
@@ -55,9 +58,16 @@ interface NobleBleAPI {
|
|
|
55
58
|
id: string;
|
|
56
59
|
mtu: number;
|
|
57
60
|
}) => void) => () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Fires whenever a BLE link drops. `reason` distinguishes a real peripheral
|
|
63
|
+
* drop from the main process freeing an idle link on its keep-alive timer;
|
|
64
|
+
* it is optional so an older host bridge that omits it still type-checks
|
|
65
|
+
* (absent is treated as a real drop by consumers).
|
|
66
|
+
*/
|
|
58
67
|
onDeviceDisconnected: (callback: (device: {
|
|
59
68
|
id: string;
|
|
60
69
|
name: string;
|
|
70
|
+
reason?: EBleDisconnectReason;
|
|
61
71
|
}) => void) => () => void;
|
|
62
72
|
checkAvailability: () => Promise<{
|
|
63
73
|
available: boolean;
|
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-a518d449.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -116,7 +116,8 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
116
116
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
117
117
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
118
118
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
119
|
-
const
|
|
119
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
120
|
+
const BLE_IDLE_DISCONNECT_MS = 20000;
|
|
120
121
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
121
122
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
122
123
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
@@ -135,7 +136,7 @@ function isOneKeyPeripheral(peripheral) {
|
|
|
135
136
|
var _a, _b;
|
|
136
137
|
const serviceUuids = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids;
|
|
137
138
|
const localName = (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName;
|
|
138
|
-
if (!(localName === null || localName === void 0 ? void 0 : localName.trim())
|
|
139
|
+
if (!(localName === null || localName === void 0 ? void 0 : localName.trim())) {
|
|
139
140
|
return false;
|
|
140
141
|
}
|
|
141
142
|
return (hdShared.hasOnekeyCommunicationService(serviceUuids) &&
|
|
@@ -242,6 +243,7 @@ function initializeNoble() {
|
|
|
242
243
|
try {
|
|
243
244
|
noble = require('@stoprocent/noble');
|
|
244
245
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
|
|
246
|
+
setupPersistentStateListener();
|
|
245
247
|
yield new Promise((resolve, reject) => {
|
|
246
248
|
if (!noble) {
|
|
247
249
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
|
|
@@ -251,7 +253,6 @@ function initializeNoble() {
|
|
|
251
253
|
resolve();
|
|
252
254
|
return;
|
|
253
255
|
}
|
|
254
|
-
setupPersistentStateListener();
|
|
255
256
|
const timeout = setTimeout(() => {
|
|
256
257
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
|
|
257
258
|
}, BLUETOOTH_INIT_TIMEOUT);
|
|
@@ -336,11 +337,19 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
336
337
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
337
338
|
const peripheral = connectedDevices.get(deviceId);
|
|
338
339
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
339
|
-
const pending =
|
|
340
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
341
|
+
.catch(error => {
|
|
342
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
343
|
+
deviceId,
|
|
344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
345
|
+
});
|
|
346
|
+
})
|
|
347
|
+
.then(() => disconnectDevice(deviceId))
|
|
340
348
|
.then(() => {
|
|
341
349
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
342
350
|
id: deviceId,
|
|
343
351
|
name: deviceName,
|
|
352
|
+
reason: hdShared.EBleDisconnectReason.IdleKeepAlive,
|
|
344
353
|
});
|
|
345
354
|
})
|
|
346
355
|
.catch(error => {
|
|
@@ -356,7 +365,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
356
365
|
}
|
|
357
366
|
function cleanupDevice(deviceId, webContents, options = {}) {
|
|
358
367
|
var _a;
|
|
359
|
-
const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', } = options;
|
|
368
|
+
const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', disconnectReason = hdShared.EBleDisconnectReason.DeviceDisconnected, } = options;
|
|
360
369
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device cleanup', {
|
|
361
370
|
deviceId,
|
|
362
371
|
reason,
|
|
@@ -393,6 +402,7 @@ function cleanupDevice(deviceId, webContents, options = {}) {
|
|
|
393
402
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
394
403
|
id: deviceId,
|
|
395
404
|
name: deviceName,
|
|
405
|
+
reason: disconnectReason,
|
|
396
406
|
});
|
|
397
407
|
}
|
|
398
408
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device cleanup completed', { deviceId, reason });
|
|
@@ -407,6 +417,7 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
407
417
|
});
|
|
408
418
|
cleanupDevice(deviceId, webContents, {
|
|
409
419
|
cleanupConnection: true,
|
|
420
|
+
cleanupDiscoveredCache: true,
|
|
410
421
|
sendDisconnectEvent: true,
|
|
411
422
|
cancelOperations: true,
|
|
412
423
|
reason: 'auto-disconnect',
|
|
@@ -588,18 +599,37 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
588
599
|
}
|
|
589
600
|
});
|
|
590
601
|
}
|
|
602
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
603
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
604
|
+
const bleNamesById = new Map();
|
|
605
|
+
function rememberBleName(deviceId, name) {
|
|
606
|
+
const trimmed = name === null || name === void 0 ? void 0 : name.trim();
|
|
607
|
+
if (trimmed) {
|
|
608
|
+
bleNamesById.set(deviceId, trimmed);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function resolveBleName(deviceId) {
|
|
612
|
+
var _a, _b, _c, _d, _e, _f;
|
|
613
|
+
const live = ((_c = (_b = (_a = connectedDevices.get(deviceId)) === null || _a === void 0 ? void 0 : _a.advertisement) === null || _b === void 0 ? void 0 : _b.localName) === null || _c === void 0 ? void 0 : _c.trim()) ||
|
|
614
|
+
((_f = (_e = (_d = discoveredDevices.get(deviceId)) === null || _d === void 0 ? void 0 : _d.advertisement) === null || _e === void 0 ? void 0 : _e.localName) === null || _f === void 0 ? void 0 : _f.trim());
|
|
615
|
+
return live || bleNamesById.get(deviceId);
|
|
616
|
+
}
|
|
617
|
+
function shouldConnectByIdFirst(deviceId) {
|
|
618
|
+
return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
|
|
619
|
+
}
|
|
591
620
|
function handleDeviceDiscovered(peripheral) {
|
|
592
|
-
var _a, _b;
|
|
621
|
+
var _a, _b, _c;
|
|
593
622
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
594
623
|
return;
|
|
595
624
|
}
|
|
596
625
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
597
626
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
627
|
+
rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
|
|
598
628
|
if (isNewDevice) {
|
|
599
629
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
600
630
|
deviceId: peripheral.id,
|
|
601
|
-
name: ((
|
|
602
|
-
serviceUUIDs: ((
|
|
631
|
+
name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
|
|
632
|
+
serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
|
|
603
633
|
});
|
|
604
634
|
}
|
|
605
635
|
}
|
|
@@ -625,7 +655,7 @@ function waitForNobleScanStop(nobleInstance) {
|
|
|
625
655
|
});
|
|
626
656
|
});
|
|
627
657
|
}
|
|
628
|
-
function performTargetedScan(targetDeviceId) {
|
|
658
|
+
function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
|
|
629
659
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
630
660
|
if (!noble) {
|
|
631
661
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
@@ -665,9 +695,9 @@ function performTargetedScan(targetDeviceId) {
|
|
|
665
695
|
const timeoutId = setTimeout(() => {
|
|
666
696
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
667
697
|
finish(null).catch(reject);
|
|
668
|
-
},
|
|
698
|
+
}, timeoutMs);
|
|
669
699
|
nobleInstance.on('discover', onDiscover);
|
|
670
|
-
nobleInstance.startScanning([],
|
|
700
|
+
nobleInstance.startScanning([], true, (error) => {
|
|
671
701
|
if (error) {
|
|
672
702
|
finish(null, error).catch(reject);
|
|
673
703
|
return;
|
|
@@ -720,7 +750,7 @@ function enumerateDevices() {
|
|
|
720
750
|
resolve(devices);
|
|
721
751
|
}), DEVICE_SCAN_TIMEOUT);
|
|
722
752
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
723
|
-
nobleInstance.startScanning([],
|
|
753
|
+
nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
|
|
724
754
|
if (error) {
|
|
725
755
|
yield cleanup();
|
|
726
756
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
|
|
@@ -800,7 +830,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
800
830
|
});
|
|
801
831
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
802
832
|
const services = yield new Promise((resolve, reject) => {
|
|
803
|
-
peripheral.discoverServices(
|
|
833
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
804
834
|
if (error) {
|
|
805
835
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
806
836
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -811,7 +841,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
811
841
|
});
|
|
812
842
|
});
|
|
813
843
|
if (!services || services.length === 0) {
|
|
814
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
844
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
815
845
|
}
|
|
816
846
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
|
|
817
847
|
deviceId: peripheral.id,
|
|
@@ -928,11 +958,11 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
928
958
|
deviceId,
|
|
929
959
|
peripheralState: peripheral.state,
|
|
930
960
|
attempt: attemptNumber,
|
|
931
|
-
maxRetries:
|
|
961
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
932
962
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
933
963
|
});
|
|
934
964
|
if (attemptNumber > 1) {
|
|
935
|
-
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}
|
|
965
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`);
|
|
936
966
|
}
|
|
937
967
|
if (peripheral.state !== 'connected') {
|
|
938
968
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device not connected: ${peripheral.state}`);
|
|
@@ -941,16 +971,16 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
941
971
|
return yield discoverServicesAndCharacteristics(peripheral);
|
|
942
972
|
}
|
|
943
973
|
catch (error) {
|
|
944
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}
|
|
945
|
-
if (attemptNumber <
|
|
946
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}
|
|
974
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
975
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
976
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
947
977
|
}
|
|
948
978
|
throw error;
|
|
949
979
|
}
|
|
950
980
|
}), {
|
|
951
|
-
retries:
|
|
981
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
952
982
|
factor: 1.5,
|
|
953
|
-
minTimeout:
|
|
983
|
+
minTimeout: 500,
|
|
954
984
|
maxTimeout: 3000,
|
|
955
985
|
onFailedAttempt: error => {
|
|
956
986
|
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -964,23 +994,24 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
964
994
|
}
|
|
965
995
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
966
996
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
967
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
968
997
|
try {
|
|
969
|
-
|
|
998
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
970
999
|
}
|
|
971
|
-
catch (
|
|
972
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
973
|
-
deviceId,
|
|
974
|
-
error: initialError,
|
|
975
|
-
});
|
|
1000
|
+
catch (resetError) {
|
|
1001
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
976
1002
|
}
|
|
977
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
978
1003
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
979
1004
|
try {
|
|
980
1005
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
981
1006
|
}
|
|
982
|
-
catch (
|
|
983
|
-
|
|
1007
|
+
catch (discoveryError) {
|
|
1008
|
+
if (peripheral.state === 'connected') {
|
|
1009
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan', discoveryError);
|
|
1010
|
+
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1014
|
+
}
|
|
984
1015
|
return freshScanAndDiscover(deviceId, webContents);
|
|
985
1016
|
}
|
|
986
1017
|
});
|
|
@@ -1040,7 +1071,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1040
1071
|
});
|
|
1041
1072
|
}
|
|
1042
1073
|
function connectDevice(deviceId, webContents) {
|
|
1043
|
-
var _a;
|
|
1074
|
+
var _a, _b, _c;
|
|
1044
1075
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1045
1076
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1046
1077
|
deviceId,
|
|
@@ -1052,34 +1083,47 @@ function connectDevice(deviceId, webContents) {
|
|
|
1052
1083
|
});
|
|
1053
1084
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1054
1085
|
if (!peripheral) {
|
|
1055
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1056
1086
|
if (!noble) {
|
|
1057
1087
|
yield initializeNoble();
|
|
1058
1088
|
}
|
|
1059
1089
|
if (!noble) {
|
|
1060
1090
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1061
1091
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1092
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1093
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
|
|
1094
|
+
deviceId,
|
|
1095
|
+
name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
|
|
1096
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1097
|
+
});
|
|
1098
|
+
const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1099
|
+
var _d;
|
|
1100
|
+
try {
|
|
1101
|
+
return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
|
|
1072
1102
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1103
|
+
catch (scanError) {
|
|
1104
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
|
|
1105
|
+
deviceId,
|
|
1106
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1107
|
+
});
|
|
1108
|
+
return undefined;
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1112
|
+
const found = yield tryDirectConnectById(deviceId);
|
|
1113
|
+
if (found) {
|
|
1114
|
+
discoveredDevices.set(deviceId, found);
|
|
1115
|
+
}
|
|
1116
|
+
return found;
|
|
1117
|
+
});
|
|
1118
|
+
peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
|
|
1119
|
+
if (!peripheral) {
|
|
1120
|
+
peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
|
|
1078
1121
|
}
|
|
1079
1122
|
}
|
|
1080
1123
|
if (!peripheral) {
|
|
1081
1124
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1082
1125
|
}
|
|
1126
|
+
rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
|
|
1083
1127
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1084
1128
|
if (peripheral.state === 'connected') {
|
|
1085
1129
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
@@ -1154,9 +1198,9 @@ function connectDevice(deviceId, webContents) {
|
|
|
1154
1198
|
}
|
|
1155
1199
|
catch (setupError) {
|
|
1156
1200
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1201
|
+
disconnectDevice(deviceId)
|
|
1202
|
+
.catch(() => undefined)
|
|
1203
|
+
.then(() => reject(setupError));
|
|
1160
1204
|
}
|
|
1161
1205
|
}));
|
|
1162
1206
|
});
|
|
@@ -1174,11 +1218,15 @@ function disconnectDevice(deviceId) {
|
|
|
1174
1218
|
deviceDisconnectListeners.delete(deviceId);
|
|
1175
1219
|
}
|
|
1176
1220
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1177
|
-
timeoutMs:
|
|
1221
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1178
1222
|
timeoutBehavior: 'resolve',
|
|
1179
1223
|
});
|
|
1224
|
+
if (peripheral.state === 'connected') {
|
|
1225
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1226
|
+
}
|
|
1180
1227
|
cleanupDevice(deviceId, undefined, {
|
|
1181
1228
|
cleanupConnection: true,
|
|
1229
|
+
cleanupDiscoveredCache: true,
|
|
1182
1230
|
sendDisconnectEvent: false,
|
|
1183
1231
|
cancelOperations: true,
|
|
1184
1232
|
reason: 'manual-disconnect',
|
|
@@ -1269,9 +1317,14 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1269
1317
|
});
|
|
1270
1318
|
});
|
|
1271
1319
|
}
|
|
1320
|
+
const subscribeStartedAt = Date.now();
|
|
1272
1321
|
try {
|
|
1273
1322
|
yield rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1274
1323
|
subscribedDevices.set(deviceId, true);
|
|
1324
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
|
|
1325
|
+
deviceId,
|
|
1326
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1327
|
+
});
|
|
1275
1328
|
}
|
|
1276
1329
|
finally {
|
|
1277
1330
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1389,10 +1442,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1389
1442
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1390
1443
|
}
|
|
1391
1444
|
yield stopScanning().catch(() => undefined);
|
|
1392
|
-
if (noble && persistentStateListener) {
|
|
1393
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1394
|
-
persistentStateListener = null;
|
|
1395
|
-
}
|
|
1396
1445
|
cleanupNobleListeners();
|
|
1397
1446
|
discoveredDevices.clear();
|
|
1398
1447
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
@@ -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":"AAgCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAoFhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAsvDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkMpE"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EBleDisconnectReason } from '@onekeyfe/hd-shared';
|
|
1
2
|
export interface NobleBleWriteOptions {
|
|
2
3
|
pacingDelayMs?: number;
|
|
3
4
|
}
|
|
@@ -25,6 +26,7 @@ export interface NobleBleAPI {
|
|
|
25
26
|
onDeviceDisconnected: (callback: (device: {
|
|
26
27
|
id: string;
|
|
27
28
|
name: string;
|
|
29
|
+
reason?: EBleDisconnectReason;
|
|
28
30
|
}) => void) => () => void;
|
|
29
31
|
checkAvailability: () => Promise<{
|
|
30
32
|
available: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxF,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAOvF,oBAAoB,EAAE,CACpB,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,oBAAoB,CAAA;KAAE,KAAK,IAAI,KACpF,MAAM,IAAI,CAAC;IAChB,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
|
|
@@ -22,6 +22,7 @@ export interface NobleModule {
|
|
|
22
22
|
export interface Logger {
|
|
23
23
|
info(message: string, ...args: any[]): void;
|
|
24
24
|
debug(message: string, ...args: any[]): void;
|
|
25
|
+
warn(message: string, ...args: any[]): void;
|
|
25
26
|
error(message: string, ...args: any[]): void;
|
|
26
27
|
}
|
|
27
28
|
export declare function safeLog(logger: Logger | null, level: 'info' | 'debug' | 'error', message: string, ...args: any[]): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
|
|
1
|
+
{"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-electron",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.0
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.0
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.0",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0",
|
|
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": "cc51df0a415bead21304ad1d8bc92b5312c91344"
|
|
40
40
|
}
|
|
@@ -10,12 +10,12 @@ import type { WebContents } from 'electron';
|
|
|
10
10
|
|
|
11
11
|
type IpcHandler = (...args: unknown[]) => Promise<unknown> | unknown;
|
|
12
12
|
|
|
13
|
-
const createPeripheral = (id: string, localName?: string) => ({
|
|
13
|
+
const createPeripheral = (id: string, localName?: string, serviceUuids = ['0001', 'fffd']) => ({
|
|
14
14
|
id,
|
|
15
15
|
state: 'disconnected',
|
|
16
16
|
advertisement: {
|
|
17
17
|
localName,
|
|
18
|
-
serviceUuids
|
|
18
|
+
serviceUuids,
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
21
|
|
|
@@ -171,6 +171,7 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
171
171
|
const connectPromise = Promise.resolve(connect(undefined, 'target-device'));
|
|
172
172
|
await scanStarted;
|
|
173
173
|
|
|
174
|
+
expect(noble.startScanning).toHaveBeenCalledWith([], true, expect.any(Function));
|
|
174
175
|
expect(noble.stopScanning).toHaveBeenCalledTimes(1);
|
|
175
176
|
expect(peripheral.connect).not.toHaveBeenCalled();
|
|
176
177
|
|
|
@@ -250,7 +251,7 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
250
251
|
expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
251
252
|
});
|
|
252
253
|
|
|
253
|
-
test('
|
|
254
|
+
test('enumerates a Pro2 communication advertisement after Find My changes its name', async () => {
|
|
254
255
|
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
255
256
|
|
|
256
257
|
const handlers = new Map<string, IpcHandler>();
|
|
@@ -274,8 +275,8 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
274
275
|
noble.state = 'poweredOn';
|
|
275
276
|
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
276
277
|
callback?.();
|
|
277
|
-
noble.emit('discover', createPeripheral('find-my-device', 'Pro2 A1B2 - Find My'));
|
|
278
|
-
noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
|
|
278
|
+
noble.emit('discover', createPeripheral('find-my-device', 'Pro2 A1B2 - Find My', ['fffd']));
|
|
279
|
+
noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2 - Find My'));
|
|
279
280
|
resolveScanStarted();
|
|
280
281
|
});
|
|
281
282
|
noble.stopScanning = jest.fn(callback => callback?.());
|
|
@@ -301,17 +302,18 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
301
302
|
|
|
302
303
|
const devicesPromise = Promise.resolve(enumerate());
|
|
303
304
|
await scanStarted;
|
|
305
|
+
expect(noble.startScanning).toHaveBeenCalledWith([], true, expect.any(Function));
|
|
304
306
|
jest.advanceTimersByTime(5000);
|
|
305
307
|
|
|
306
308
|
await expect(devicesPromise).resolves.toEqual([
|
|
307
309
|
expect.objectContaining({
|
|
308
310
|
id: 'onekey-device',
|
|
309
|
-
name: 'Pro2 A1B2',
|
|
311
|
+
name: 'Pro2 A1B2 - Find My',
|
|
310
312
|
}),
|
|
311
313
|
]);
|
|
312
314
|
});
|
|
313
315
|
|
|
314
|
-
test('
|
|
316
|
+
test('accepts a communication peripheral after its scan response gains a name', async () => {
|
|
315
317
|
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
316
318
|
|
|
317
319
|
const handlers = new Map<string, IpcHandler>();
|
|
@@ -368,6 +370,10 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
368
370
|
jest.advanceTimersByTime(5000);
|
|
369
371
|
|
|
370
372
|
await expect(devicesPromise).resolves.toEqual([
|
|
373
|
+
expect.objectContaining({
|
|
374
|
+
id: 'find-my-device',
|
|
375
|
+
name: 'Pro2 A1B2 - Find My',
|
|
376
|
+
}),
|
|
371
377
|
expect.objectContaining({
|
|
372
378
|
id: 'onekey-device',
|
|
373
379
|
name: 'Pro2 A1B2',
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
/* eslint-disable @typescript-eslint/no-var-requires, import/no-extraneous-dependencies */
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
EBleDisconnectReason,
|
|
9
10
|
EOneKeyBleMessageKeys,
|
|
10
11
|
ERRORS,
|
|
11
12
|
HardwareErrorCode,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
createKnownBleUuidAliases,
|
|
16
17
|
hasOnekeyCommunicationService,
|
|
17
18
|
isOnekeyBluetoothDevice,
|
|
18
|
-
|
|
19
|
+
isPro2FamilyBleName,
|
|
19
20
|
matchesKnownBleUuid,
|
|
20
21
|
wait,
|
|
21
22
|
} from '@onekeyfe/hd-shared';
|
|
@@ -90,8 +91,18 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
90
91
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
91
92
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
92
93
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
94
|
+
// A physical teardown must actually reach the OS before the device can return
|
|
95
|
+
// to a clean state; 250ms routinely declared success while CoreBluetooth was
|
|
96
|
+
// still disconnecting, leaving no trace when the teardown never completed.
|
|
97
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
93
98
|
// Renderer release is logical only; this timer physically frees the device.
|
|
94
|
-
|
|
99
|
+
// Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped
|
|
100
|
+
// after sitting idle for minutes (field logs 2026-08-19: every deaf window
|
|
101
|
+
// followed a 3-minute-idle disconnect, while disconnects right after traffic
|
|
102
|
+
// have never produced one across 6.5.0's per-call teardown history). A hot
|
|
103
|
+
// disconnect ~20s after the last operation stays inside the proven-safe
|
|
104
|
+
// pattern and also shrinks the window in which phones cannot see the device.
|
|
105
|
+
const BLE_IDLE_DISCONNECT_MS = 20_000;
|
|
95
106
|
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
96
107
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
97
108
|
|
|
@@ -115,9 +126,7 @@ function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
|
115
126
|
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
116
127
|
const localName = peripheral.advertisement?.localName;
|
|
117
128
|
|
|
118
|
-
|
|
119
|
-
// Find My endpoint before the communication-service fast path accepts it.
|
|
120
|
-
if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
|
|
129
|
+
if (!localName?.trim()) {
|
|
121
130
|
return false;
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -259,6 +268,11 @@ async function initializeNoble(): Promise<void> {
|
|
|
259
268
|
noble = require('@stoprocent/noble') as NobleModule;
|
|
260
269
|
logger?.info('[NobleBLE] Noble library loaded');
|
|
261
270
|
|
|
271
|
+
// Register the process-lifetime state listener before any early return:
|
|
272
|
+
// the poweredOn fast path below would otherwise skip it for the whole
|
|
273
|
+
// session, leaving poweredOff cache/state reconciliation dead.
|
|
274
|
+
setupPersistentStateListener();
|
|
275
|
+
|
|
262
276
|
// Wait for Bluetooth to be ready
|
|
263
277
|
await new Promise<void>((resolve, reject) => {
|
|
264
278
|
if (!noble) {
|
|
@@ -271,9 +285,6 @@ async function initializeNoble(): Promise<void> {
|
|
|
271
285
|
return;
|
|
272
286
|
}
|
|
273
287
|
|
|
274
|
-
// Setup persistent state listener before initialization
|
|
275
|
-
setupPersistentStateListener();
|
|
276
|
-
|
|
277
288
|
const timeout = setTimeout(() => {
|
|
278
289
|
reject(
|
|
279
290
|
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
|
|
@@ -339,6 +350,12 @@ interface DeviceCleanupOptions {
|
|
|
339
350
|
cleanupDiscoveredCache?: boolean;
|
|
340
351
|
/** Whether to send disconnect event */
|
|
341
352
|
sendDisconnectEvent?: boolean;
|
|
353
|
+
/**
|
|
354
|
+
* Why the link went down, forwarded to renderers on BLE_DEVICE_DISCONNECTED.
|
|
355
|
+
* Defaults to a real peripheral drop; the keep-alive path overrides it so
|
|
356
|
+
* consumers can tell an internal idle release from a device that left.
|
|
357
|
+
*/
|
|
358
|
+
disconnectReason?: EBleDisconnectReason;
|
|
342
359
|
/** Whether to cancel ongoing operations */
|
|
343
360
|
cancelOperations?: boolean;
|
|
344
361
|
/** Cleanup reason (for logging) */
|
|
@@ -394,12 +411,31 @@ function armIdleDisconnect(
|
|
|
394
411
|
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
395
412
|
const peripheral = connectedDevices.get(deviceId);
|
|
396
413
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
397
|
-
|
|
414
|
+
// Unsubscribe CCCD before dropping the link, matching every other
|
|
415
|
+
// teardown path: the 1S leaves its notify session half-open when the
|
|
416
|
+
// link drops without an unsubscribe and then ignores application
|
|
417
|
+
// protocol traffic on NEW links for tens of minutes (field log
|
|
418
|
+
// 2026-08-19: the lone unsubscribe-skipping idle teardown caused a
|
|
419
|
+
// 6-attempt reconnect loop; unsubscribe-first teardowns reconnected
|
|
420
|
+
// instantly).
|
|
421
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
422
|
+
.catch(error => {
|
|
423
|
+
logger?.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
424
|
+
deviceId,
|
|
425
|
+
error: error instanceof Error ? error.message : String(error),
|
|
426
|
+
});
|
|
427
|
+
})
|
|
428
|
+
.then(() => disconnectDevice(deviceId))
|
|
398
429
|
.then(() => {
|
|
399
430
|
// A call is still in flight here; it must reject, not hang.
|
|
431
|
+
// Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
|
|
432
|
+
// we reclaimed the link ourselves and have no evidence the device
|
|
433
|
+
// left, so consumers must not mark it disconnected. The in-flight
|
|
434
|
+
// call still fails, via the renderer dropping its device state.
|
|
400
435
|
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
401
436
|
id: deviceId,
|
|
402
437
|
name: deviceName,
|
|
438
|
+
reason: EBleDisconnectReason.IdleKeepAlive,
|
|
403
439
|
});
|
|
404
440
|
})
|
|
405
441
|
.catch(error => {
|
|
@@ -429,6 +465,7 @@ function cleanupDevice(
|
|
|
429
465
|
sendDisconnectEvent = false,
|
|
430
466
|
cancelOperations = true,
|
|
431
467
|
reason = 'unknown',
|
|
468
|
+
disconnectReason = EBleDisconnectReason.DeviceDisconnected,
|
|
432
469
|
} = options;
|
|
433
470
|
|
|
434
471
|
logger?.info('[NobleBLE] Starting device cleanup', {
|
|
@@ -478,6 +515,7 @@ function cleanupDevice(
|
|
|
478
515
|
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
479
516
|
id: deviceId,
|
|
480
517
|
name: deviceName,
|
|
518
|
+
reason: disconnectReason,
|
|
481
519
|
});
|
|
482
520
|
}
|
|
483
521
|
|
|
@@ -497,6 +535,9 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
|
|
|
497
535
|
|
|
498
536
|
cleanupDevice(deviceId, webContents, {
|
|
499
537
|
cleanupConnection: true,
|
|
538
|
+
// Same stale-object hazard as manual disconnect: an externally dropped
|
|
539
|
+
// link invalidates the cached peripheral for the next reconnect.
|
|
540
|
+
cleanupDiscoveredCache: true,
|
|
500
541
|
sendDisconnectEvent: true,
|
|
501
542
|
cancelOperations: true,
|
|
502
543
|
reason: 'auto-disconnect',
|
|
@@ -750,6 +791,44 @@ async function transmitHexDataToDevice(
|
|
|
750
791
|
}
|
|
751
792
|
|
|
752
793
|
// Handle discovered device (for general enumeration only)
|
|
794
|
+
// A scan that finds nothing costs this much before the direct-connect
|
|
795
|
+
// fallback runs. Every advertisement in the 6.5.0 control logs arrived within
|
|
796
|
+
// 631ms, so this leaves ~2x headroom while keeping a miss cheap.
|
|
797
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
798
|
+
|
|
799
|
+
// Attempts before a link is written off as unable to serve the OneKey service.
|
|
800
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
801
|
+
|
|
802
|
+
// Advertised names, kept for the life of the process: device caches are purged
|
|
803
|
+
// on every disconnect, but the family a device belongs to never changes and
|
|
804
|
+
// decides which connection strategy is safe for it.
|
|
805
|
+
const bleNamesById = new Map<string, string>();
|
|
806
|
+
|
|
807
|
+
function rememberBleName(deviceId: string, name?: string | null): void {
|
|
808
|
+
const trimmed = name?.trim();
|
|
809
|
+
if (trimmed) {
|
|
810
|
+
bleNamesById.set(deviceId, trimmed);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function resolveBleName(deviceId: string): string | undefined {
|
|
815
|
+
const live =
|
|
816
|
+
connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
|
|
817
|
+
discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
|
|
818
|
+
return live || bleNamesById.get(deviceId);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Pro2/Neo connect by id first: they are unaffected by the stale-session
|
|
823
|
+
* problem and advertise under Find My names a OneKey-filtered scan cannot
|
|
824
|
+
* match. Everything else — Classic family, and any device whose name is not
|
|
825
|
+
* known yet — scans first, because connecting by id can resolve a session the
|
|
826
|
+
* device no longer serves (see setupConnectionAndDiscoverServices).
|
|
827
|
+
*/
|
|
828
|
+
function shouldConnectByIdFirst(deviceId: string): boolean {
|
|
829
|
+
return isPro2FamilyBleName(resolveBleName(deviceId));
|
|
830
|
+
}
|
|
831
|
+
|
|
753
832
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
754
833
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
755
834
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
@@ -759,6 +838,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
759
838
|
|
|
760
839
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
761
840
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
841
|
+
rememberBleName(peripheral.id, peripheral.advertisement?.localName);
|
|
762
842
|
if (isNewDevice) {
|
|
763
843
|
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
764
844
|
deviceId: peripheral.id,
|
|
@@ -793,7 +873,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
|
793
873
|
|
|
794
874
|
// Perform targeted scan for a specific device ID
|
|
795
875
|
// Uses self-contained local listener pattern - no global state needed
|
|
796
|
-
async function performTargetedScan(
|
|
876
|
+
async function performTargetedScan(
|
|
877
|
+
targetDeviceId: string,
|
|
878
|
+
timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
|
|
879
|
+
): Promise<Peripheral | null> {
|
|
797
880
|
if (!noble) {
|
|
798
881
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
799
882
|
}
|
|
@@ -838,13 +921,14 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
838
921
|
const timeoutId = setTimeout(() => {
|
|
839
922
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
840
923
|
finish(null).catch(reject);
|
|
841
|
-
},
|
|
924
|
+
}, timeoutMs);
|
|
842
925
|
|
|
843
926
|
// Add local listener for this scan
|
|
844
927
|
nobleInstance.on('discover', onDiscover);
|
|
845
928
|
|
|
846
|
-
//
|
|
847
|
-
|
|
929
|
+
// Allow repeated advertisements so a service-only packet can be followed by
|
|
930
|
+
// the named scan response needed to validate the target peripheral.
|
|
931
|
+
nobleInstance.startScanning([], true, (error?: Error) => {
|
|
848
932
|
if (error) {
|
|
849
933
|
finish(null, error).catch(reject);
|
|
850
934
|
return;
|
|
@@ -915,9 +999,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
915
999
|
}, DEVICE_SCAN_TIMEOUT);
|
|
916
1000
|
|
|
917
1001
|
// Start scanning without a service UUID filter so Pro2 advertisements with
|
|
918
|
-
// short vendor UUIDs can be found
|
|
1002
|
+
// short vendor UUIDs can be found. Repeated advertisements are required when
|
|
1003
|
+
// the local name arrives in a later scan response; discoveredDevices handles deduplication.
|
|
919
1004
|
logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
920
|
-
nobleInstance.startScanning([],
|
|
1005
|
+
nobleInstance.startScanning([], true, async (error?: Error) => {
|
|
921
1006
|
if (error) {
|
|
922
1007
|
await cleanup();
|
|
923
1008
|
logger?.error('[NobleBLE] Failed to start scanning:', error);
|
|
@@ -1038,9 +1123,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
1038
1123
|
|
|
1039
1124
|
// Main discovery logic as async function
|
|
1040
1125
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
1041
|
-
// Step 1: Discover
|
|
1126
|
+
// Step 1: Discover the OneKey service by UUID. Filtering is not just a
|
|
1127
|
+
// narrowing: an unfiltered discovery is answered from the OS GATT cache, so
|
|
1128
|
+
// a device whose stack still advertises and accepts links but no longer
|
|
1129
|
+
// serves its application layer still looks healthy — the link comes up, the
|
|
1130
|
+
// cached services resolve, the protocol frame goes out and nothing ever
|
|
1131
|
+
// answers. A targeted query returns nothing in that state, which is what
|
|
1132
|
+
// 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
|
|
1133
|
+
// scan) — the sequence that brings such a device back. Pro2/Neo expose the
|
|
1134
|
+
// same service UUID, and the selection below only ever accepts that one.
|
|
1042
1135
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
1043
|
-
peripheral.discoverServices(
|
|
1136
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
1044
1137
|
if (error) {
|
|
1045
1138
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
1046
1139
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -1051,7 +1144,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
1051
1144
|
});
|
|
1052
1145
|
|
|
1053
1146
|
if (!services || services.length === 0) {
|
|
1054
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
1147
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
1055
1148
|
}
|
|
1056
1149
|
|
|
1057
1150
|
logger?.debug('[NobleBLE] services discovered', {
|
|
@@ -1235,12 +1328,14 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1235
1328
|
deviceId,
|
|
1236
1329
|
peripheralState: peripheral.state,
|
|
1237
1330
|
attempt: attemptNumber,
|
|
1238
|
-
maxRetries:
|
|
1331
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
1239
1332
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
1240
1333
|
});
|
|
1241
1334
|
|
|
1242
1335
|
if (attemptNumber > 1) {
|
|
1243
|
-
logger?.info(
|
|
1336
|
+
logger?.info(
|
|
1337
|
+
`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`
|
|
1338
|
+
);
|
|
1244
1339
|
}
|
|
1245
1340
|
|
|
1246
1341
|
// Verify connection state before attempting service discovery
|
|
@@ -1254,20 +1349,32 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1254
1349
|
try {
|
|
1255
1350
|
return await discoverServicesAndCharacteristics(peripheral);
|
|
1256
1351
|
} catch (error) {
|
|
1257
|
-
logger?.error(
|
|
1352
|
+
logger?.error(
|
|
1353
|
+
`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1354
|
+
);
|
|
1258
1355
|
|
|
1259
|
-
if (attemptNumber <
|
|
1260
|
-
logger?.error(
|
|
1356
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
1357
|
+
logger?.error(
|
|
1358
|
+
`[NobleBLE] Will retry service discovery (attempt ${
|
|
1359
|
+
attemptNumber + 1
|
|
1360
|
+
}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1361
|
+
);
|
|
1261
1362
|
}
|
|
1262
1363
|
|
|
1263
1364
|
throw error; // p-retry will handle the retry logic
|
|
1264
1365
|
}
|
|
1265
1366
|
},
|
|
1266
1367
|
{
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1368
|
+
// One retry, not four. An empty result here is the signature of a device
|
|
1369
|
+
// whose stack no longer serves its application layer, and retrying the
|
|
1370
|
+
// same link has never recovered it — every observed run failed all the
|
|
1371
|
+
// way through. The recovery is the teardown and cold reconnect the caller
|
|
1372
|
+
// performs afterwards, so reaching it sooner is what shortens the wait.
|
|
1373
|
+
// The single retry stays for a genuinely transient miss.
|
|
1374
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
1375
|
+
factor: 1.5,
|
|
1376
|
+
minTimeout: 500,
|
|
1377
|
+
maxTimeout: 3000,
|
|
1271
1378
|
onFailedAttempt: error => {
|
|
1272
1379
|
// This runs after each failed attempt
|
|
1273
1380
|
logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -1292,26 +1399,45 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1292
1399
|
deviceId: string,
|
|
1293
1400
|
webContents: WebContents
|
|
1294
1401
|
): Promise<CharacteristicPair> {
|
|
1295
|
-
|
|
1296
|
-
|
|
1402
|
+
// Reset the link before any protocol traffic reaches the device. 6.5.0 did
|
|
1403
|
+
// this on every cold setup and never produced a protocol-deaf Classic 1S
|
|
1404
|
+
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1405
|
+
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1406
|
+
// reset entirely for this device, because discovery always succeeds: the link
|
|
1407
|
+
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1408
|
+
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1409
|
+
// link with an intact subscription returns before reaching here, so reuse
|
|
1410
|
+
// inside a workflow is unaffected.
|
|
1297
1411
|
try {
|
|
1298
|
-
|
|
1299
|
-
} catch (
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
});
|
|
1412
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1413
|
+
} catch (resetError) {
|
|
1414
|
+
// A failed reset must not abort the attempt: discovery on the existing
|
|
1415
|
+
// link, then the fresh-scan fallback, still have a chance to recover.
|
|
1416
|
+
logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
1304
1417
|
}
|
|
1305
|
-
|
|
1306
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1307
1418
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1419
|
+
|
|
1308
1420
|
try {
|
|
1309
1421
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1310
|
-
} catch (
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1422
|
+
} catch (discoveryError) {
|
|
1423
|
+
// A connected peripheral does not advertise, so the fresh scan below would
|
|
1424
|
+
// only run out its timeout. Drop the link first, then rescan: the cold
|
|
1425
|
+
// reconnect is what recovers a device whose stack stopped serving its
|
|
1426
|
+
// application layer. Recover here rather than throwing to the caller —
|
|
1427
|
+
// one acquire pays ~2s for the rescan, while a thrown error costs the
|
|
1428
|
+
// renderer a full retry round trip.
|
|
1429
|
+
if (peripheral.state === 'connected') {
|
|
1430
|
+
logger?.error(
|
|
1431
|
+
'[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan',
|
|
1432
|
+
discoveryError
|
|
1433
|
+
);
|
|
1434
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1435
|
+
} else {
|
|
1436
|
+
logger?.error(
|
|
1437
|
+
'[NobleBLE] Service discovery failed, attempting fresh scan...',
|
|
1438
|
+
discoveryError
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1315
1441
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1316
1442
|
}
|
|
1317
1443
|
}
|
|
@@ -1400,10 +1526,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1400
1526
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1401
1527
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1402
1528
|
|
|
1403
|
-
// If device not discovered, try a targeted scan for this specific device
|
|
1404
1529
|
if (!peripheral) {
|
|
1405
|
-
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1406
|
-
|
|
1407
1530
|
// Initialize Noble if not already done
|
|
1408
1531
|
if (!noble) {
|
|
1409
1532
|
await initializeNoble();
|
|
@@ -1413,27 +1536,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1413
1536
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1414
1537
|
}
|
|
1415
1538
|
|
|
1416
|
-
//
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1539
|
+
// A live advertisement proves the device holds no link and will open a
|
|
1540
|
+
// fresh session; connecting by id resolves through the OS cache instead
|
|
1541
|
+
// and can hand back a session the device no longer serves (see
|
|
1542
|
+
// setupConnectionAndDiscoverServices). Scanning is also the faster of the
|
|
1543
|
+
// two when the device does advertise: ~96ms median against ~784ms for
|
|
1544
|
+
// connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
|
|
1545
|
+
// because they are unaffected and may advertise under an unmatchable Find
|
|
1546
|
+
// My name.
|
|
1547
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1548
|
+
logger?.info('[NobleBLE] Resolving device for cold connect', {
|
|
1549
|
+
deviceId,
|
|
1550
|
+
name: resolveBleName(deviceId) ?? 'unknown',
|
|
1551
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1552
|
+
});
|
|
1422
1553
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1554
|
+
const scanForPeripheral = async () => {
|
|
1555
|
+
try {
|
|
1556
|
+
return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
|
|
1557
|
+
} catch (scanError) {
|
|
1558
|
+
logger?.info('[NobleBLE] Targeted scan failed', {
|
|
1559
|
+
deviceId,
|
|
1560
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1561
|
+
});
|
|
1562
|
+
return undefined;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
const connectById = async () => {
|
|
1567
|
+
const found = await tryDirectConnectById(deviceId);
|
|
1568
|
+
if (found) {
|
|
1569
|
+
discoveredDevices.set(deviceId, found);
|
|
1432
1570
|
}
|
|
1433
|
-
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1571
|
+
return found;
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
|
|
1575
|
+
if (!peripheral) {
|
|
1576
|
+
// The preferred route came up empty: not advertising (held elsewhere, or
|
|
1577
|
+
// silent), or not reachable by id. Try the other one before giving up.
|
|
1578
|
+
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1437
1579
|
}
|
|
1438
1580
|
}
|
|
1439
1581
|
|
|
@@ -1442,6 +1584,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1442
1584
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1443
1585
|
}
|
|
1444
1586
|
|
|
1587
|
+
// Also covers the connect-by-id route, whose simulated discovery carries no
|
|
1588
|
+
// service UUIDs and therefore never reaches handleDeviceDiscovered.
|
|
1589
|
+
rememberBleName(deviceId, peripheral.advertisement?.localName);
|
|
1590
|
+
|
|
1445
1591
|
logger?.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1446
1592
|
|
|
1447
1593
|
// Check if device is already connected
|
|
@@ -1562,9 +1708,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1562
1708
|
resolve();
|
|
1563
1709
|
} catch (setupError) {
|
|
1564
1710
|
logger?.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1711
|
+
// Never reject from inside a raw disconnect callback: noble only fires
|
|
1712
|
+
// it on a real 'disconnect' event, so a peripheral that is already
|
|
1713
|
+
// down leaves this promise — and the renderer acquire awaiting it —
|
|
1714
|
+
// pending forever. disconnectDevice always settles (it no-ops on an
|
|
1715
|
+
// unknown peripheral and caps the confirm wait).
|
|
1716
|
+
disconnectDevice(deviceId)
|
|
1717
|
+
.catch(() => undefined)
|
|
1718
|
+
.then(() => reject(setupError));
|
|
1568
1719
|
}
|
|
1569
1720
|
});
|
|
1570
1721
|
});
|
|
@@ -1584,11 +1735,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1584
1735
|
}
|
|
1585
1736
|
|
|
1586
1737
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1587
|
-
timeoutMs:
|
|
1738
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1588
1739
|
timeoutBehavior: 'resolve',
|
|
1589
1740
|
});
|
|
1741
|
+
if (peripheral.state === 'connected') {
|
|
1742
|
+
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1743
|
+
}
|
|
1590
1744
|
cleanupDevice(deviceId, undefined, {
|
|
1591
1745
|
cleanupConnection: true,
|
|
1746
|
+
// macOS returns zero GATT services when a previously-disconnected
|
|
1747
|
+
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1748
|
+
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1749
|
+
cleanupDiscoveredCache: true,
|
|
1592
1750
|
sendDisconnectEvent: false,
|
|
1593
1751
|
cancelOperations: true,
|
|
1594
1752
|
reason: 'manual-disconnect',
|
|
@@ -1729,9 +1887,14 @@ async function subscribeNotifications(
|
|
|
1729
1887
|
});
|
|
1730
1888
|
}
|
|
1731
1889
|
|
|
1890
|
+
const subscribeStartedAt = Date.now();
|
|
1732
1891
|
try {
|
|
1733
1892
|
await rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1734
1893
|
subscribedDevices.set(deviceId, true);
|
|
1894
|
+
logger?.info('[NobleBLE] Notification subscription active', {
|
|
1895
|
+
deviceId,
|
|
1896
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1897
|
+
});
|
|
1735
1898
|
} finally {
|
|
1736
1899
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
1737
1900
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1914,10 +2077,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1914
2077
|
}
|
|
1915
2078
|
|
|
1916
2079
|
await stopScanning().catch(() => undefined);
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2080
|
+
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
2081
|
+
// destroy handler also fires on a renderer soft restart, and removing
|
|
2082
|
+
// the listener here permanently killed poweredOff/poweredOn
|
|
2083
|
+
// reconciliation for the rest of the process (nothing re-registers it:
|
|
2084
|
+
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
2085
|
+
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1921
2086
|
cleanupNobleListeners();
|
|
1922
2087
|
discoveredDevices.clear();
|
|
1923
2088
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
package/src/types/desktop-api.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* These types define the core interface for Noble BLE communication
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { EBleDisconnectReason } from '@onekeyfe/hd-shared';
|
|
7
|
+
|
|
6
8
|
// Noble BLE API interface - core BLE functionality
|
|
7
9
|
export interface NobleBleWriteOptions {
|
|
8
10
|
pacingDelayMs?: number;
|
|
@@ -21,7 +23,15 @@ export interface NobleBleAPI {
|
|
|
21
23
|
write: (uuid: string, data: string, options?: NobleBleWriteOptions) => Promise<void>;
|
|
22
24
|
onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
|
|
23
25
|
onMtuChanged?: (callback: (device: { id: string; mtu: number }) => void) => () => void;
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Fires whenever a BLE link drops. `reason` distinguishes a real peripheral
|
|
28
|
+
* drop from the main process freeing an idle link on its keep-alive timer;
|
|
29
|
+
* it is optional so an older host bridge that omits it still type-checks
|
|
30
|
+
* (absent is treated as a real drop by consumers).
|
|
31
|
+
*/
|
|
32
|
+
onDeviceDisconnected: (
|
|
33
|
+
callback: (device: { id: string; name: string; reason?: EBleDisconnectReason }) => void
|
|
34
|
+
) => () => void;
|
|
25
35
|
checkAvailability: () => Promise<{
|
|
26
36
|
available: boolean;
|
|
27
37
|
state: string;
|