@onekeyfe/hd-transport-electron 1.2.2-alpha.100 → 1.2.2-alpha.12
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-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -1
- package/dist/{index-b37d926c.js → index-eea100c1.js} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-e8b87173.js → noble-ble-handler-e4eaea92.js} +133 -60
- package/dist/noble-ble-handler.d.ts +1 -0
- package/dist/noble-ble-handler.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__/ble-packet-capacity.test.ts +8 -1
- package/src/__tests__/noble-ble-handler.test.ts +19 -1
- package/src/ble-packet-capacity.ts +14 -0
- package/src/noble-ble-handler.ts +243 -77
- package/src/types/noble-extended.ts +1 -0
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export declare function resolveNobleAttMtu(reportedMtu: number | null | undefined, platform?: NodeJS.Platform): number | undefined;
|
|
1
3
|
export declare function resolveBlePacketCapacity(mtu: number | null | undefined, maximumPacketCapacity: number, fallbackPacketCapacity: number): number;
|
|
2
4
|
//# sourceMappingURL=ble-packet-capacity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ble-packet-capacity.d.ts","sourceRoot":"","sources":["../src/ble-packet-capacity.ts"],"names":[],"mappings":"AAEA,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9B,qBAAqB,EAAE,MAAM,EAC7B,sBAAsB,EAAE,MAAM,GAC7B,MAAM,CAMR"}
|
|
1
|
+
{"version":3,"file":"ble-packet-capacity.d.ts","sourceRoot":"","sources":["../src/ble-packet-capacity.ts"],"names":[],"mappings":";AAEA,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,MAAM,GAAG,SAAS,CASpB;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9B,qBAAqB,EAAE,MAAM,EAC7B,sBAAsB,EAAE,MAAM,GAC7B,MAAM,CAMR"}
|
|
@@ -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-e4eaea92.js'); });
|
|
36
36
|
setupNobleBleHandlers(webContents);
|
|
37
37
|
});
|
|
38
38
|
}
|
package/dist/index.d.ts
CHANGED
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-eea100c1.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -9,6 +9,15 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
|
|
|
9
9
|
var pRetry__default = /*#__PURE__*/_interopDefaultLegacy(pRetry);
|
|
10
10
|
|
|
11
11
|
const BLE_ATT_HEADER_BYTES = 3;
|
|
12
|
+
function resolveNobleAttMtu(reportedMtu, platform = process.platform) {
|
|
13
|
+
if (typeof reportedMtu !== 'number' || !Number.isFinite(reportedMtu) || reportedMtu <= 0) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const normalizedMtu = Math.floor(reportedMtu);
|
|
17
|
+
return platform === 'darwin' || platform === 'win32'
|
|
18
|
+
? normalizedMtu + BLE_ATT_HEADER_BYTES
|
|
19
|
+
: normalizedMtu;
|
|
20
|
+
}
|
|
12
21
|
function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapacity) {
|
|
13
22
|
if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
|
|
14
23
|
return fallbackPacketCapacity;
|
|
@@ -91,6 +100,19 @@ const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
|
|
|
91
100
|
|
|
92
101
|
let noble = null;
|
|
93
102
|
let logger = null;
|
|
103
|
+
function createNobleBleConnectionError(error, messagePrefix = '') {
|
|
104
|
+
const errorMessage = error.message;
|
|
105
|
+
const normalizedErrorMessage = errorMessage.toLowerCase();
|
|
106
|
+
let errorCode = hdShared.HardwareErrorCode.BleConnectedError;
|
|
107
|
+
if (hdShared.isBleStaleBondErrorText(errorMessage)) {
|
|
108
|
+
errorCode =
|
|
109
|
+
normalizedErrorMessage.includes('peer removed pairing information') ||
|
|
110
|
+
normalizedErrorMessage.includes('cberrordomain:14')
|
|
111
|
+
? hdShared.HardwareErrorCode.BlePeerRemovedPairingInformation
|
|
112
|
+
: hdShared.HardwareErrorCode.BleDeviceBondError;
|
|
113
|
+
}
|
|
114
|
+
return hdShared.ERRORS.TypedError(errorCode, `${messagePrefix}${errorMessage}`);
|
|
115
|
+
}
|
|
94
116
|
const bluetoothState = {
|
|
95
117
|
available: false,
|
|
96
118
|
unsupported: false,
|
|
@@ -116,7 +138,8 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
116
138
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
117
139
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
118
140
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
119
|
-
const
|
|
141
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
142
|
+
const BLE_IDLE_DISCONNECT_MS = 20000;
|
|
120
143
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
121
144
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
122
145
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
@@ -242,6 +265,7 @@ function initializeNoble() {
|
|
|
242
265
|
try {
|
|
243
266
|
noble = require('@stoprocent/noble');
|
|
244
267
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
|
|
268
|
+
setupPersistentStateListener();
|
|
245
269
|
yield new Promise((resolve, reject) => {
|
|
246
270
|
if (!noble) {
|
|
247
271
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
|
|
@@ -251,7 +275,6 @@ function initializeNoble() {
|
|
|
251
275
|
resolve();
|
|
252
276
|
return;
|
|
253
277
|
}
|
|
254
|
-
setupPersistentStateListener();
|
|
255
278
|
const timeout = setTimeout(() => {
|
|
256
279
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
|
|
257
280
|
}, BLUETOOTH_INIT_TIMEOUT);
|
|
@@ -336,7 +359,14 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
336
359
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
337
360
|
const peripheral = connectedDevices.get(deviceId);
|
|
338
361
|
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 =
|
|
362
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
363
|
+
.catch(error => {
|
|
364
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
365
|
+
deviceId,
|
|
366
|
+
error: error instanceof Error ? error.message : String(error),
|
|
367
|
+
});
|
|
368
|
+
})
|
|
369
|
+
.then(() => disconnectDevice(deviceId))
|
|
340
370
|
.then(() => {
|
|
341
371
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
342
372
|
id: deviceId,
|
|
@@ -409,6 +439,7 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
409
439
|
});
|
|
410
440
|
cleanupDevice(deviceId, webContents, {
|
|
411
441
|
cleanupConnection: true,
|
|
442
|
+
cleanupDiscoveredCache: true,
|
|
412
443
|
sendDisconnectEvent: true,
|
|
413
444
|
cancelOperations: true,
|
|
414
445
|
reason: 'auto-disconnect',
|
|
@@ -432,13 +463,14 @@ function setupMtuListener(peripheral, deviceId, webContents) {
|
|
|
432
463
|
existing.peripheral.removeListener('mtu', existing.listener);
|
|
433
464
|
}
|
|
434
465
|
const listener = (mtu) => {
|
|
435
|
-
|
|
466
|
+
const normalizedMtu = resolveNobleAttMtu(mtu);
|
|
467
|
+
if (normalizedMtu === undefined)
|
|
436
468
|
return;
|
|
437
469
|
if (webContents.isDestroyed())
|
|
438
470
|
return;
|
|
439
471
|
webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
440
472
|
id: deviceId,
|
|
441
|
-
mtu,
|
|
473
|
+
mtu: normalizedMtu,
|
|
442
474
|
});
|
|
443
475
|
};
|
|
444
476
|
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
@@ -545,7 +577,7 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
545
577
|
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
546
578
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
547
579
|
const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
|
|
548
|
-
const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
|
|
580
|
+
const packetCapacity = resolveBlePacketCapacity(resolveNobleAttMtu(peripheral.mtu), BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
|
|
549
581
|
const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
|
|
550
582
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
551
583
|
const writeCharacteristic = doGetWriteCharacteristic();
|
|
@@ -590,18 +622,37 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
590
622
|
}
|
|
591
623
|
});
|
|
592
624
|
}
|
|
625
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
626
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
627
|
+
const bleNamesById = new Map();
|
|
628
|
+
function rememberBleName(deviceId, name) {
|
|
629
|
+
const trimmed = name === null || name === void 0 ? void 0 : name.trim();
|
|
630
|
+
if (trimmed) {
|
|
631
|
+
bleNamesById.set(deviceId, trimmed);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function resolveBleName(deviceId) {
|
|
635
|
+
var _a, _b, _c, _d, _e, _f;
|
|
636
|
+
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()) ||
|
|
637
|
+
((_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());
|
|
638
|
+
return live || bleNamesById.get(deviceId);
|
|
639
|
+
}
|
|
640
|
+
function shouldConnectByIdFirst(deviceId) {
|
|
641
|
+
return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
|
|
642
|
+
}
|
|
593
643
|
function handleDeviceDiscovered(peripheral) {
|
|
594
|
-
var _a, _b;
|
|
644
|
+
var _a, _b, _c;
|
|
595
645
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
596
646
|
return;
|
|
597
647
|
}
|
|
598
648
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
599
649
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
650
|
+
rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
|
|
600
651
|
if (isNewDevice) {
|
|
601
652
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
602
653
|
deviceId: peripheral.id,
|
|
603
|
-
name: ((
|
|
604
|
-
serviceUUIDs: ((
|
|
654
|
+
name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
|
|
655
|
+
serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
|
|
605
656
|
});
|
|
606
657
|
}
|
|
607
658
|
}
|
|
@@ -627,7 +678,7 @@ function waitForNobleScanStop(nobleInstance) {
|
|
|
627
678
|
});
|
|
628
679
|
});
|
|
629
680
|
}
|
|
630
|
-
function performTargetedScan(targetDeviceId) {
|
|
681
|
+
function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
|
|
631
682
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
632
683
|
if (!noble) {
|
|
633
684
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
@@ -667,7 +718,7 @@ function performTargetedScan(targetDeviceId) {
|
|
|
667
718
|
const timeoutId = setTimeout(() => {
|
|
668
719
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
669
720
|
finish(null).catch(reject);
|
|
670
|
-
},
|
|
721
|
+
}, timeoutMs);
|
|
671
722
|
nobleInstance.on('discover', onDiscover);
|
|
672
723
|
nobleInstance.startScanning([], true, (error) => {
|
|
673
724
|
if (error) {
|
|
@@ -763,12 +814,14 @@ function getDevice(deviceId) {
|
|
|
763
814
|
const peripheral = discoveredDevices.get(deviceId);
|
|
764
815
|
if (peripheral) {
|
|
765
816
|
const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
766
|
-
|
|
817
|
+
const mtu = resolveNobleAttMtu(peripheral.mtu);
|
|
818
|
+
return Object.assign({ commType: 'electron-ble', id: peripheral.id, name: deviceName, state: peripheral.state || 'disconnected' }, (mtu === undefined ? {} : { mtu }));
|
|
767
819
|
}
|
|
768
820
|
const connectedPeripheral = connectedDevices.get(deviceId);
|
|
769
821
|
if (connectedPeripheral) {
|
|
770
822
|
const deviceName = ((_b = connectedPeripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device';
|
|
771
|
-
|
|
823
|
+
const mtu = resolveNobleAttMtu(connectedPeripheral.mtu);
|
|
824
|
+
return Object.assign({ commType: 'electron-ble', id: connectedPeripheral.id, name: deviceName, state: connectedPeripheral.state || 'connected' }, (mtu === undefined ? {} : { mtu }));
|
|
772
825
|
}
|
|
773
826
|
return {
|
|
774
827
|
commType: 'electron-ble',
|
|
@@ -802,7 +855,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
802
855
|
});
|
|
803
856
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
804
857
|
const services = yield new Promise((resolve, reject) => {
|
|
805
|
-
peripheral.discoverServices(
|
|
858
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
806
859
|
if (error) {
|
|
807
860
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
808
861
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -813,7 +866,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
813
866
|
});
|
|
814
867
|
});
|
|
815
868
|
if (!services || services.length === 0) {
|
|
816
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
869
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
817
870
|
}
|
|
818
871
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
|
|
819
872
|
deviceId: peripheral.id,
|
|
@@ -909,7 +962,7 @@ function freshScanAndDiscover(deviceId, webContents) {
|
|
|
909
962
|
yield new Promise((resolve, reject) => {
|
|
910
963
|
freshPeripheral.connect((error) => {
|
|
911
964
|
if (error) {
|
|
912
|
-
reject(
|
|
965
|
+
reject(createNobleBleConnectionError(error, 'Fresh peripheral connection failed: '));
|
|
913
966
|
}
|
|
914
967
|
else {
|
|
915
968
|
connectedDevices.set(deviceId, freshPeripheral);
|
|
@@ -930,11 +983,11 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
930
983
|
deviceId,
|
|
931
984
|
peripheralState: peripheral.state,
|
|
932
985
|
attempt: attemptNumber,
|
|
933
|
-
maxRetries:
|
|
986
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
934
987
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
935
988
|
});
|
|
936
989
|
if (attemptNumber > 1) {
|
|
937
|
-
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}
|
|
990
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`);
|
|
938
991
|
}
|
|
939
992
|
if (peripheral.state !== 'connected') {
|
|
940
993
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device not connected: ${peripheral.state}`);
|
|
@@ -943,16 +996,16 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
943
996
|
return yield discoverServicesAndCharacteristics(peripheral);
|
|
944
997
|
}
|
|
945
998
|
catch (error) {
|
|
946
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}
|
|
947
|
-
if (attemptNumber <
|
|
948
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}
|
|
999
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
1000
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
1001
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
949
1002
|
}
|
|
950
1003
|
throw error;
|
|
951
1004
|
}
|
|
952
1005
|
}), {
|
|
953
|
-
retries:
|
|
1006
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
954
1007
|
factor: 1.5,
|
|
955
|
-
minTimeout:
|
|
1008
|
+
minTimeout: 500,
|
|
956
1009
|
maxTimeout: 3000,
|
|
957
1010
|
onFailedAttempt: error => {
|
|
958
1011
|
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -966,23 +1019,24 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
966
1019
|
}
|
|
967
1020
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
968
1021
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
969
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
970
1022
|
try {
|
|
971
|
-
|
|
1023
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
972
1024
|
}
|
|
973
|
-
catch (
|
|
974
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
975
|
-
deviceId,
|
|
976
|
-
error: initialError,
|
|
977
|
-
});
|
|
1025
|
+
catch (resetError) {
|
|
1026
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
978
1027
|
}
|
|
979
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
980
1028
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
981
1029
|
try {
|
|
982
1030
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
983
1031
|
}
|
|
984
|
-
catch (
|
|
985
|
-
|
|
1032
|
+
catch (discoveryError) {
|
|
1033
|
+
if (peripheral.state === 'connected') {
|
|
1034
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan', discoveryError);
|
|
1035
|
+
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1039
|
+
}
|
|
986
1040
|
return freshScanAndDiscover(deviceId, webContents);
|
|
987
1041
|
}
|
|
988
1042
|
});
|
|
@@ -1042,7 +1096,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1042
1096
|
});
|
|
1043
1097
|
}
|
|
1044
1098
|
function connectDevice(deviceId, webContents) {
|
|
1045
|
-
var _a;
|
|
1099
|
+
var _a, _b, _c;
|
|
1046
1100
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1047
1101
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1048
1102
|
deviceId,
|
|
@@ -1054,34 +1108,47 @@ function connectDevice(deviceId, webContents) {
|
|
|
1054
1108
|
});
|
|
1055
1109
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1056
1110
|
if (!peripheral) {
|
|
1057
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1058
1111
|
if (!noble) {
|
|
1059
1112
|
yield initializeNoble();
|
|
1060
1113
|
}
|
|
1061
1114
|
if (!noble) {
|
|
1062
1115
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1063
1116
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1117
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1118
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
|
|
1119
|
+
deviceId,
|
|
1120
|
+
name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
|
|
1121
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1122
|
+
});
|
|
1123
|
+
const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1124
|
+
var _d;
|
|
1125
|
+
try {
|
|
1126
|
+
return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
|
|
1074
1127
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1128
|
+
catch (scanError) {
|
|
1129
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
|
|
1130
|
+
deviceId,
|
|
1131
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1132
|
+
});
|
|
1133
|
+
return undefined;
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1137
|
+
const found = yield tryDirectConnectById(deviceId);
|
|
1138
|
+
if (found) {
|
|
1139
|
+
discoveredDevices.set(deviceId, found);
|
|
1140
|
+
}
|
|
1141
|
+
return found;
|
|
1142
|
+
});
|
|
1143
|
+
peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
|
|
1144
|
+
if (!peripheral) {
|
|
1145
|
+
peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
|
|
1080
1146
|
}
|
|
1081
1147
|
}
|
|
1082
1148
|
if (!peripheral) {
|
|
1083
1149
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1084
1150
|
}
|
|
1151
|
+
rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
|
|
1085
1152
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1086
1153
|
if (peripheral.state === 'connected') {
|
|
1087
1154
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
@@ -1143,7 +1210,7 @@ function connectDevice(deviceId, webContents) {
|
|
|
1143
1210
|
}
|
|
1144
1211
|
if (error) {
|
|
1145
1212
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
|
|
1146
|
-
reject(
|
|
1213
|
+
reject(createNobleBleConnectionError(error));
|
|
1147
1214
|
return;
|
|
1148
1215
|
}
|
|
1149
1216
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connected to device:', deviceId);
|
|
@@ -1156,9 +1223,9 @@ function connectDevice(deviceId, webContents) {
|
|
|
1156
1223
|
}
|
|
1157
1224
|
catch (setupError) {
|
|
1158
1225
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1226
|
+
disconnectDevice(deviceId)
|
|
1227
|
+
.catch(() => undefined)
|
|
1228
|
+
.then(() => reject(setupError));
|
|
1162
1229
|
}
|
|
1163
1230
|
}));
|
|
1164
1231
|
});
|
|
@@ -1176,11 +1243,15 @@ function disconnectDevice(deviceId) {
|
|
|
1176
1243
|
deviceDisconnectListeners.delete(deviceId);
|
|
1177
1244
|
}
|
|
1178
1245
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1179
|
-
timeoutMs:
|
|
1246
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1180
1247
|
timeoutBehavior: 'resolve',
|
|
1181
1248
|
});
|
|
1249
|
+
if (peripheral.state === 'connected') {
|
|
1250
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1251
|
+
}
|
|
1182
1252
|
cleanupDevice(deviceId, undefined, {
|
|
1183
1253
|
cleanupConnection: true,
|
|
1254
|
+
cleanupDiscoveredCache: true,
|
|
1184
1255
|
sendDisconnectEvent: false,
|
|
1185
1256
|
cancelOperations: true,
|
|
1186
1257
|
reason: 'manual-disconnect',
|
|
@@ -1271,9 +1342,14 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1271
1342
|
});
|
|
1272
1343
|
});
|
|
1273
1344
|
}
|
|
1345
|
+
const subscribeStartedAt = Date.now();
|
|
1274
1346
|
try {
|
|
1275
1347
|
yield rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1276
1348
|
subscribedDevices.set(deviceId, true);
|
|
1349
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
|
|
1350
|
+
deviceId,
|
|
1351
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1352
|
+
});
|
|
1277
1353
|
}
|
|
1278
1354
|
finally {
|
|
1279
1355
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1391,10 +1467,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1391
1467
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1392
1468
|
}
|
|
1393
1469
|
yield stopScanning().catch(() => undefined);
|
|
1394
|
-
if (noble && persistentStateListener) {
|
|
1395
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1396
|
-
persistentStateListener = null;
|
|
1397
|
-
}
|
|
1398
1470
|
cleanupNobleListeners();
|
|
1399
1471
|
discoveredDevices.clear();
|
|
1400
1472
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
@@ -1410,5 +1482,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1410
1482
|
}
|
|
1411
1483
|
}
|
|
1412
1484
|
|
|
1485
|
+
exports.createNobleBleConnectionError = createNobleBleConnectionError;
|
|
1413
1486
|
exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
|
|
1414
1487
|
exports.setupNobleBleHandlers = setupNobleBleHandlers;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WebContents } from 'electron';
|
|
2
2
|
import type { NobleBleWriteOptions } from './types/desktop-api';
|
|
3
|
+
export declare function createNobleBleConnectionError(error: Error, messagePrefix?: string): import("@onekeyfe/hd-shared").HardwareError;
|
|
3
4
|
export declare function resolveNobleBleWritePacingDelay(options?: NobleBleWriteOptions): number;
|
|
4
5
|
export declare function setupNobleBleHandlers(webContents: WebContents): void;
|
|
5
6
|
//# sourceMappingURL=noble-ble-handler.d.ts.map
|
|
@@ -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":"AAiCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAOhE,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,SAAK,+CAa7E;AA+ED,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAovDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkMpE"}
|
|
@@ -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.2-alpha.
|
|
3
|
+
"version": "1.2.2-alpha.12",
|
|
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.2-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.2-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.2-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.2-alpha.12",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.12",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.12",
|
|
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": "9b5a45bc55819d22ed752899b4c40aad70de4527"
|
|
40
40
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import { resolveBlePacketCapacity } from '../ble-packet-capacity';
|
|
1
|
+
import { resolveBlePacketCapacity, resolveNobleAttMtu } from '../ble-packet-capacity';
|
|
2
2
|
|
|
3
3
|
describe('resolveBlePacketCapacity', () => {
|
|
4
|
+
test('normalizes Noble platform reports to ATT MTU', () => {
|
|
5
|
+
expect(resolveNobleAttMtu(244, 'darwin')).toBe(247);
|
|
6
|
+
expect(resolveNobleAttMtu(244, 'win32')).toBe(247);
|
|
7
|
+
expect(resolveNobleAttMtu(247, 'linux')).toBe(247);
|
|
8
|
+
expect(resolveNobleAttMtu(null, 'darwin')).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
4
11
|
test('uses ATT MTU payload capacity with an upper bound', () => {
|
|
5
12
|
expect(resolveBlePacketCapacity(247, 244, 192)).toBe(244);
|
|
6
13
|
expect(resolveBlePacketCapacity(185, 244, 192)).toBe(182);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { EOneKeyBleMessageKeys, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
@@ -31,6 +31,24 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
31
31
|
expect(NOBLE_BLE_CONNECTION_TIMEOUT_MS).toBe(10_000);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
test('maps macOS stale pairing failures without reclassifying generic connection errors', async () => {
|
|
35
|
+
const { createNobleBleConnectionError } = await import('../noble-ble-handler');
|
|
36
|
+
|
|
37
|
+
expect(
|
|
38
|
+
createNobleBleConnectionError(
|
|
39
|
+
new Error('CBErrorDomain:14 Peer removed pairing information on the device side')
|
|
40
|
+
)
|
|
41
|
+
).toMatchObject({
|
|
42
|
+
errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
43
|
+
});
|
|
44
|
+
expect(createNobleBleConnectionError(new Error('Encryption is insufficient'))).toMatchObject({
|
|
45
|
+
errorCode: HardwareErrorCode.BleDeviceBondError,
|
|
46
|
+
});
|
|
47
|
+
expect(createNobleBleConnectionError(new Error('connection failed'))).toMatchObject({
|
|
48
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
34
52
|
test('keeps safe pacing by default and allows an explicit high-throughput bypass', async () => {
|
|
35
53
|
const { resolveNobleBleWritePacingDelay } = await import('../noble-ble-handler');
|
|
36
54
|
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
const BLE_ATT_HEADER_BYTES = 3;
|
|
2
2
|
|
|
3
|
+
export function resolveNobleAttMtu(
|
|
4
|
+
reportedMtu: number | null | undefined,
|
|
5
|
+
platform: NodeJS.Platform = process.platform
|
|
6
|
+
): number | undefined {
|
|
7
|
+
if (typeof reportedMtu !== 'number' || !Number.isFinite(reportedMtu) || reportedMtu <= 0) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const normalizedMtu = Math.floor(reportedMtu);
|
|
12
|
+
return platform === 'darwin' || platform === 'win32'
|
|
13
|
+
? normalizedMtu + BLE_ATT_HEADER_BYTES
|
|
14
|
+
: normalizedMtu;
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
export function resolveBlePacketCapacity(
|
|
4
18
|
mtu: number | null | undefined,
|
|
5
19
|
maximumPacketCapacity: number,
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -15,13 +15,15 @@ import {
|
|
|
15
15
|
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
16
16
|
createKnownBleUuidAliases,
|
|
17
17
|
hasOnekeyCommunicationService,
|
|
18
|
+
isBleStaleBondErrorText,
|
|
18
19
|
isOnekeyBluetoothDevice,
|
|
20
|
+
isPro2FamilyBleName,
|
|
19
21
|
matchesKnownBleUuid,
|
|
20
22
|
wait,
|
|
21
23
|
} from '@onekeyfe/hd-shared';
|
|
22
24
|
import pRetry from 'p-retry';
|
|
23
25
|
|
|
24
|
-
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
26
|
+
import { resolveBlePacketCapacity, resolveNobleAttMtu } from './ble-packet-capacity';
|
|
25
27
|
import { safeLog } from './types/noble-extended';
|
|
26
28
|
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
27
29
|
import {
|
|
@@ -38,6 +40,21 @@ import type { CharacteristicPair, DeviceInfo, Logger, NobleModule } from './type
|
|
|
38
40
|
let noble: NobleModule | null = null;
|
|
39
41
|
let logger: Logger | null = null;
|
|
40
42
|
|
|
43
|
+
export function createNobleBleConnectionError(error: Error, messagePrefix = '') {
|
|
44
|
+
const errorMessage = error.message;
|
|
45
|
+
const normalizedErrorMessage = errorMessage.toLowerCase();
|
|
46
|
+
let errorCode = HardwareErrorCode.BleConnectedError;
|
|
47
|
+
if (isBleStaleBondErrorText(errorMessage)) {
|
|
48
|
+
errorCode =
|
|
49
|
+
normalizedErrorMessage.includes('peer removed pairing information') ||
|
|
50
|
+
normalizedErrorMessage.includes('cberrordomain:14')
|
|
51
|
+
? HardwareErrorCode.BlePeerRemovedPairingInformation
|
|
52
|
+
: HardwareErrorCode.BleDeviceBondError;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ERRORS.TypedError(errorCode, `${messagePrefix}${errorMessage}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
// Bluetooth state management
|
|
42
59
|
const bluetoothState: {
|
|
43
60
|
available: boolean;
|
|
@@ -90,8 +107,18 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
90
107
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
91
108
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
92
109
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
110
|
+
// A physical teardown must actually reach the OS before the device can return
|
|
111
|
+
// to a clean state; 250ms routinely declared success while CoreBluetooth was
|
|
112
|
+
// still disconnecting, leaving no trace when the teardown never completed.
|
|
113
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
93
114
|
// Renderer release is logical only; this timer physically frees the device.
|
|
94
|
-
|
|
115
|
+
// Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped
|
|
116
|
+
// after sitting idle for minutes (field logs 2026-08-19: every deaf window
|
|
117
|
+
// followed a 3-minute-idle disconnect, while disconnects right after traffic
|
|
118
|
+
// have never produced one across 6.5.0's per-call teardown history). A hot
|
|
119
|
+
// disconnect ~20s after the last operation stays inside the proven-safe
|
|
120
|
+
// pattern and also shrinks the window in which phones cannot see the device.
|
|
121
|
+
const BLE_IDLE_DISCONNECT_MS = 20_000;
|
|
95
122
|
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
96
123
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
97
124
|
|
|
@@ -257,6 +284,11 @@ async function initializeNoble(): Promise<void> {
|
|
|
257
284
|
noble = require('@stoprocent/noble') as NobleModule;
|
|
258
285
|
logger?.info('[NobleBLE] Noble library loaded');
|
|
259
286
|
|
|
287
|
+
// Register the process-lifetime state listener before any early return:
|
|
288
|
+
// the poweredOn fast path below would otherwise skip it for the whole
|
|
289
|
+
// session, leaving poweredOff cache/state reconciliation dead.
|
|
290
|
+
setupPersistentStateListener();
|
|
291
|
+
|
|
260
292
|
// Wait for Bluetooth to be ready
|
|
261
293
|
await new Promise<void>((resolve, reject) => {
|
|
262
294
|
if (!noble) {
|
|
@@ -269,9 +301,6 @@ async function initializeNoble(): Promise<void> {
|
|
|
269
301
|
return;
|
|
270
302
|
}
|
|
271
303
|
|
|
272
|
-
// Setup persistent state listener before initialization
|
|
273
|
-
setupPersistentStateListener();
|
|
274
|
-
|
|
275
304
|
const timeout = setTimeout(() => {
|
|
276
305
|
reject(
|
|
277
306
|
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
|
|
@@ -398,7 +427,21 @@ function armIdleDisconnect(
|
|
|
398
427
|
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
399
428
|
const peripheral = connectedDevices.get(deviceId);
|
|
400
429
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
401
|
-
|
|
430
|
+
// Unsubscribe CCCD before dropping the link, matching every other
|
|
431
|
+
// teardown path: the 1S leaves its notify session half-open when the
|
|
432
|
+
// link drops without an unsubscribe and then ignores application
|
|
433
|
+
// protocol traffic on NEW links for tens of minutes (field log
|
|
434
|
+
// 2026-08-19: the lone unsubscribe-skipping idle teardown caused a
|
|
435
|
+
// 6-attempt reconnect loop; unsubscribe-first teardowns reconnected
|
|
436
|
+
// instantly).
|
|
437
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
438
|
+
.catch(error => {
|
|
439
|
+
logger?.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
440
|
+
deviceId,
|
|
441
|
+
error: error instanceof Error ? error.message : String(error),
|
|
442
|
+
});
|
|
443
|
+
})
|
|
444
|
+
.then(() => disconnectDevice(deviceId))
|
|
402
445
|
.then(() => {
|
|
403
446
|
// A call is still in flight here; it must reject, not hang.
|
|
404
447
|
// Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
|
|
@@ -508,6 +551,9 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
|
|
|
508
551
|
|
|
509
552
|
cleanupDevice(deviceId, webContents, {
|
|
510
553
|
cleanupConnection: true,
|
|
554
|
+
// Same stale-object hazard as manual disconnect: an externally dropped
|
|
555
|
+
// link invalidates the cached peripheral for the next reconnect.
|
|
556
|
+
cleanupDiscoveredCache: true,
|
|
511
557
|
sendDisconnectEvent: true,
|
|
512
558
|
cancelOperations: true,
|
|
513
559
|
reason: 'auto-disconnect',
|
|
@@ -544,12 +590,13 @@ function setupMtuListener(
|
|
|
544
590
|
}
|
|
545
591
|
|
|
546
592
|
const listener = (mtu: number) => {
|
|
547
|
-
|
|
593
|
+
const normalizedMtu = resolveNobleAttMtu(mtu);
|
|
594
|
+
if (normalizedMtu === undefined) return;
|
|
548
595
|
// A kept-alive link can outlive the renderer this closure captured.
|
|
549
596
|
if (webContents.isDestroyed()) return;
|
|
550
597
|
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
551
598
|
id: deviceId,
|
|
552
|
-
mtu,
|
|
599
|
+
mtu: normalizedMtu,
|
|
553
600
|
});
|
|
554
601
|
};
|
|
555
602
|
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
@@ -697,7 +744,7 @@ async function transmitHexDataToDevice(
|
|
|
697
744
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
698
745
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
699
746
|
const packetCapacity = resolveBlePacketCapacity(
|
|
700
|
-
peripheral.mtu,
|
|
747
|
+
resolveNobleAttMtu(peripheral.mtu),
|
|
701
748
|
BLE_PACKET_SIZE_MAXIMUM,
|
|
702
749
|
BLE_PACKET_SIZE_FALLBACK
|
|
703
750
|
);
|
|
@@ -761,6 +808,44 @@ async function transmitHexDataToDevice(
|
|
|
761
808
|
}
|
|
762
809
|
|
|
763
810
|
// Handle discovered device (for general enumeration only)
|
|
811
|
+
// A scan that finds nothing costs this much before the direct-connect
|
|
812
|
+
// fallback runs. Every advertisement in the 6.5.0 control logs arrived within
|
|
813
|
+
// 631ms, so this leaves ~2x headroom while keeping a miss cheap.
|
|
814
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
815
|
+
|
|
816
|
+
// Attempts before a link is written off as unable to serve the OneKey service.
|
|
817
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
818
|
+
|
|
819
|
+
// Advertised names, kept for the life of the process: device caches are purged
|
|
820
|
+
// on every disconnect, but the family a device belongs to never changes and
|
|
821
|
+
// decides which connection strategy is safe for it.
|
|
822
|
+
const bleNamesById = new Map<string, string>();
|
|
823
|
+
|
|
824
|
+
function rememberBleName(deviceId: string, name?: string | null): void {
|
|
825
|
+
const trimmed = name?.trim();
|
|
826
|
+
if (trimmed) {
|
|
827
|
+
bleNamesById.set(deviceId, trimmed);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function resolveBleName(deviceId: string): string | undefined {
|
|
832
|
+
const live =
|
|
833
|
+
connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
|
|
834
|
+
discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
|
|
835
|
+
return live || bleNamesById.get(deviceId);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Pro2/Neo connect by id first: they are unaffected by the stale-session
|
|
840
|
+
* problem and advertise under Find My names a OneKey-filtered scan cannot
|
|
841
|
+
* match. Everything else — Classic family, and any device whose name is not
|
|
842
|
+
* known yet — scans first, because connecting by id can resolve a session the
|
|
843
|
+
* device no longer serves (see setupConnectionAndDiscoverServices).
|
|
844
|
+
*/
|
|
845
|
+
function shouldConnectByIdFirst(deviceId: string): boolean {
|
|
846
|
+
return isPro2FamilyBleName(resolveBleName(deviceId));
|
|
847
|
+
}
|
|
848
|
+
|
|
764
849
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
765
850
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
766
851
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
@@ -770,6 +855,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
770
855
|
|
|
771
856
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
772
857
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
858
|
+
rememberBleName(peripheral.id, peripheral.advertisement?.localName);
|
|
773
859
|
if (isNewDevice) {
|
|
774
860
|
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
775
861
|
deviceId: peripheral.id,
|
|
@@ -804,7 +890,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
|
804
890
|
|
|
805
891
|
// Perform targeted scan for a specific device ID
|
|
806
892
|
// Uses self-contained local listener pattern - no global state needed
|
|
807
|
-
async function performTargetedScan(
|
|
893
|
+
async function performTargetedScan(
|
|
894
|
+
targetDeviceId: string,
|
|
895
|
+
timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
|
|
896
|
+
): Promise<Peripheral | null> {
|
|
808
897
|
if (!noble) {
|
|
809
898
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
810
899
|
}
|
|
@@ -849,7 +938,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
849
938
|
const timeoutId = setTimeout(() => {
|
|
850
939
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
851
940
|
finish(null).catch(reject);
|
|
852
|
-
},
|
|
941
|
+
}, timeoutMs);
|
|
853
942
|
|
|
854
943
|
// Add local listener for this scan
|
|
855
944
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -975,12 +1064,13 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
975
1064
|
const peripheral = discoveredDevices.get(deviceId);
|
|
976
1065
|
if (peripheral) {
|
|
977
1066
|
const deviceName = peripheral.advertisement?.localName || 'Unknown Device';
|
|
1067
|
+
const mtu = resolveNobleAttMtu(peripheral.mtu);
|
|
978
1068
|
return {
|
|
979
1069
|
commType: 'electron-ble',
|
|
980
1070
|
id: peripheral.id,
|
|
981
1071
|
name: deviceName,
|
|
982
1072
|
state: peripheral.state || 'disconnected',
|
|
983
|
-
...(
|
|
1073
|
+
...(mtu === undefined ? {} : { mtu }),
|
|
984
1074
|
};
|
|
985
1075
|
}
|
|
986
1076
|
|
|
@@ -988,12 +1078,13 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
988
1078
|
const connectedPeripheral = connectedDevices.get(deviceId);
|
|
989
1079
|
if (connectedPeripheral) {
|
|
990
1080
|
const deviceName = connectedPeripheral.advertisement?.localName || 'Unknown Device';
|
|
1081
|
+
const mtu = resolveNobleAttMtu(connectedPeripheral.mtu);
|
|
991
1082
|
return {
|
|
992
1083
|
commType: 'electron-ble',
|
|
993
1084
|
id: connectedPeripheral.id,
|
|
994
1085
|
name: deviceName,
|
|
995
1086
|
state: connectedPeripheral.state || 'connected',
|
|
996
|
-
...(
|
|
1087
|
+
...(mtu === undefined ? {} : { mtu }),
|
|
997
1088
|
};
|
|
998
1089
|
}
|
|
999
1090
|
|
|
@@ -1051,9 +1142,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
1051
1142
|
|
|
1052
1143
|
// Main discovery logic as async function
|
|
1053
1144
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
1054
|
-
// Step 1: Discover
|
|
1145
|
+
// Step 1: Discover the OneKey service by UUID. Filtering is not just a
|
|
1146
|
+
// narrowing: an unfiltered discovery is answered from the OS GATT cache, so
|
|
1147
|
+
// a device whose stack still advertises and accepts links but no longer
|
|
1148
|
+
// serves its application layer still looks healthy — the link comes up, the
|
|
1149
|
+
// cached services resolve, the protocol frame goes out and nothing ever
|
|
1150
|
+
// answers. A targeted query returns nothing in that state, which is what
|
|
1151
|
+
// 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
|
|
1152
|
+
// scan) — the sequence that brings such a device back. Pro2/Neo expose the
|
|
1153
|
+
// same service UUID, and the selection below only ever accepts that one.
|
|
1055
1154
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
1056
|
-
peripheral.discoverServices(
|
|
1155
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
1057
1156
|
if (error) {
|
|
1058
1157
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
1059
1158
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -1064,7 +1163,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
1064
1163
|
});
|
|
1065
1164
|
|
|
1066
1165
|
if (!services || services.length === 0) {
|
|
1067
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
1166
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
1068
1167
|
}
|
|
1069
1168
|
|
|
1070
1169
|
logger?.debug('[NobleBLE] services discovered', {
|
|
@@ -1213,12 +1312,7 @@ async function freshScanAndDiscover(
|
|
|
1213
1312
|
await new Promise<void>((resolve, reject) => {
|
|
1214
1313
|
freshPeripheral.connect((error: Error | undefined) => {
|
|
1215
1314
|
if (error) {
|
|
1216
|
-
reject(
|
|
1217
|
-
ERRORS.TypedError(
|
|
1218
|
-
HardwareErrorCode.BleConnectedError,
|
|
1219
|
-
`Fresh peripheral connection failed: ${error.message}`
|
|
1220
|
-
)
|
|
1221
|
-
);
|
|
1315
|
+
reject(createNobleBleConnectionError(error, 'Fresh peripheral connection failed: '));
|
|
1222
1316
|
} else {
|
|
1223
1317
|
connectedDevices.set(deviceId, freshPeripheral);
|
|
1224
1318
|
resolve();
|
|
@@ -1248,12 +1342,14 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1248
1342
|
deviceId,
|
|
1249
1343
|
peripheralState: peripheral.state,
|
|
1250
1344
|
attempt: attemptNumber,
|
|
1251
|
-
maxRetries:
|
|
1345
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
1252
1346
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
1253
1347
|
});
|
|
1254
1348
|
|
|
1255
1349
|
if (attemptNumber > 1) {
|
|
1256
|
-
logger?.info(
|
|
1350
|
+
logger?.info(
|
|
1351
|
+
`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`
|
|
1352
|
+
);
|
|
1257
1353
|
}
|
|
1258
1354
|
|
|
1259
1355
|
// Verify connection state before attempting service discovery
|
|
@@ -1267,20 +1363,32 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1267
1363
|
try {
|
|
1268
1364
|
return await discoverServicesAndCharacteristics(peripheral);
|
|
1269
1365
|
} catch (error) {
|
|
1270
|
-
logger?.error(
|
|
1366
|
+
logger?.error(
|
|
1367
|
+
`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1368
|
+
);
|
|
1271
1369
|
|
|
1272
|
-
if (attemptNumber <
|
|
1273
|
-
logger?.error(
|
|
1370
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
1371
|
+
logger?.error(
|
|
1372
|
+
`[NobleBLE] Will retry service discovery (attempt ${
|
|
1373
|
+
attemptNumber + 1
|
|
1374
|
+
}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1375
|
+
);
|
|
1274
1376
|
}
|
|
1275
1377
|
|
|
1276
1378
|
throw error; // p-retry will handle the retry logic
|
|
1277
1379
|
}
|
|
1278
1380
|
},
|
|
1279
1381
|
{
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1382
|
+
// One retry, not four. An empty result here is the signature of a device
|
|
1383
|
+
// whose stack no longer serves its application layer, and retrying the
|
|
1384
|
+
// same link has never recovered it — every observed run failed all the
|
|
1385
|
+
// way through. The recovery is the teardown and cold reconnect the caller
|
|
1386
|
+
// performs afterwards, so reaching it sooner is what shortens the wait.
|
|
1387
|
+
// The single retry stays for a genuinely transient miss.
|
|
1388
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
1389
|
+
factor: 1.5,
|
|
1390
|
+
minTimeout: 500,
|
|
1391
|
+
maxTimeout: 3000,
|
|
1284
1392
|
onFailedAttempt: error => {
|
|
1285
1393
|
// This runs after each failed attempt
|
|
1286
1394
|
logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -1305,26 +1413,45 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1305
1413
|
deviceId: string,
|
|
1306
1414
|
webContents: WebContents
|
|
1307
1415
|
): Promise<CharacteristicPair> {
|
|
1308
|
-
|
|
1309
|
-
|
|
1416
|
+
// Reset the link before any protocol traffic reaches the device. 6.5.0 did
|
|
1417
|
+
// this on every cold setup and never produced a protocol-deaf Classic 1S
|
|
1418
|
+
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1419
|
+
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1420
|
+
// reset entirely for this device, because discovery always succeeds: the link
|
|
1421
|
+
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1422
|
+
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1423
|
+
// link with an intact subscription returns before reaching here, so reuse
|
|
1424
|
+
// inside a workflow is unaffected.
|
|
1310
1425
|
try {
|
|
1311
|
-
|
|
1312
|
-
} catch (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1426
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1427
|
+
} catch (resetError) {
|
|
1428
|
+
// A failed reset must not abort the attempt: discovery on the existing
|
|
1429
|
+
// link, then the fresh-scan fallback, still have a chance to recover.
|
|
1430
|
+
logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
1317
1431
|
}
|
|
1318
|
-
|
|
1319
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1320
1432
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1433
|
+
|
|
1321
1434
|
try {
|
|
1322
1435
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1323
|
-
} catch (
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1436
|
+
} catch (discoveryError) {
|
|
1437
|
+
// A connected peripheral does not advertise, so the fresh scan below would
|
|
1438
|
+
// only run out its timeout. Drop the link first, then rescan: the cold
|
|
1439
|
+
// reconnect is what recovers a device whose stack stopped serving its
|
|
1440
|
+
// application layer. Recover here rather than throwing to the caller —
|
|
1441
|
+
// one acquire pays ~2s for the rescan, while a thrown error costs the
|
|
1442
|
+
// renderer a full retry round trip.
|
|
1443
|
+
if (peripheral.state === 'connected') {
|
|
1444
|
+
logger?.error(
|
|
1445
|
+
'[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan',
|
|
1446
|
+
discoveryError
|
|
1447
|
+
);
|
|
1448
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1449
|
+
} else {
|
|
1450
|
+
logger?.error(
|
|
1451
|
+
'[NobleBLE] Service discovery failed, attempting fresh scan...',
|
|
1452
|
+
discoveryError
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1328
1455
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1329
1456
|
}
|
|
1330
1457
|
}
|
|
@@ -1413,10 +1540,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1413
1540
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1414
1541
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1415
1542
|
|
|
1416
|
-
// If device not discovered, try a targeted scan for this specific device
|
|
1417
1543
|
if (!peripheral) {
|
|
1418
|
-
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1419
|
-
|
|
1420
1544
|
// Initialize Noble if not already done
|
|
1421
1545
|
if (!noble) {
|
|
1422
1546
|
await initializeNoble();
|
|
@@ -1426,27 +1550,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1426
1550
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1427
1551
|
}
|
|
1428
1552
|
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1553
|
+
// A live advertisement proves the device holds no link and will open a
|
|
1554
|
+
// fresh session; connecting by id resolves through the OS cache instead
|
|
1555
|
+
// and can hand back a session the device no longer serves (see
|
|
1556
|
+
// setupConnectionAndDiscoverServices). Scanning is also the faster of the
|
|
1557
|
+
// two when the device does advertise: ~96ms median against ~784ms for
|
|
1558
|
+
// connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
|
|
1559
|
+
// because they are unaffected and may advertise under an unmatchable Find
|
|
1560
|
+
// My name.
|
|
1561
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1562
|
+
logger?.info('[NobleBLE] Resolving device for cold connect', {
|
|
1563
|
+
deviceId,
|
|
1564
|
+
name: resolveBleName(deviceId) ?? 'unknown',
|
|
1565
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1566
|
+
});
|
|
1435
1567
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1568
|
+
const scanForPeripheral = async () => {
|
|
1569
|
+
try {
|
|
1570
|
+
return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
|
|
1571
|
+
} catch (scanError) {
|
|
1572
|
+
logger?.info('[NobleBLE] Targeted scan failed', {
|
|
1573
|
+
deviceId,
|
|
1574
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1575
|
+
});
|
|
1576
|
+
return undefined;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
const connectById = async () => {
|
|
1581
|
+
const found = await tryDirectConnectById(deviceId);
|
|
1582
|
+
if (found) {
|
|
1583
|
+
discoveredDevices.set(deviceId, found);
|
|
1445
1584
|
}
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1585
|
+
return found;
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
|
|
1589
|
+
if (!peripheral) {
|
|
1590
|
+
// The preferred route came up empty: not advertising (held elsewhere, or
|
|
1591
|
+
// silent), or not reachable by id. Try the other one before giving up.
|
|
1592
|
+
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1450
1593
|
}
|
|
1451
1594
|
}
|
|
1452
1595
|
|
|
@@ -1455,6 +1598,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1455
1598
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1456
1599
|
}
|
|
1457
1600
|
|
|
1601
|
+
// Also covers the connect-by-id route, whose simulated discovery carries no
|
|
1602
|
+
// service UUIDs and therefore never reaches handleDeviceDiscovered.
|
|
1603
|
+
rememberBleName(deviceId, peripheral.advertisement?.localName);
|
|
1604
|
+
|
|
1458
1605
|
logger?.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1459
1606
|
|
|
1460
1607
|
// Check if device is already connected
|
|
@@ -1556,7 +1703,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1556
1703
|
|
|
1557
1704
|
if (error) {
|
|
1558
1705
|
logger?.error('[NobleBLE] Connection failed:', error);
|
|
1559
|
-
reject(
|
|
1706
|
+
reject(createNobleBleConnectionError(error));
|
|
1560
1707
|
return;
|
|
1561
1708
|
}
|
|
1562
1709
|
|
|
@@ -1575,9 +1722,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1575
1722
|
resolve();
|
|
1576
1723
|
} catch (setupError) {
|
|
1577
1724
|
logger?.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1725
|
+
// Never reject from inside a raw disconnect callback: noble only fires
|
|
1726
|
+
// it on a real 'disconnect' event, so a peripheral that is already
|
|
1727
|
+
// down leaves this promise — and the renderer acquire awaiting it —
|
|
1728
|
+
// pending forever. disconnectDevice always settles (it no-ops on an
|
|
1729
|
+
// unknown peripheral and caps the confirm wait).
|
|
1730
|
+
disconnectDevice(deviceId)
|
|
1731
|
+
.catch(() => undefined)
|
|
1732
|
+
.then(() => reject(setupError));
|
|
1581
1733
|
}
|
|
1582
1734
|
});
|
|
1583
1735
|
});
|
|
@@ -1597,11 +1749,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1597
1749
|
}
|
|
1598
1750
|
|
|
1599
1751
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1600
|
-
timeoutMs:
|
|
1752
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1601
1753
|
timeoutBehavior: 'resolve',
|
|
1602
1754
|
});
|
|
1755
|
+
if (peripheral.state === 'connected') {
|
|
1756
|
+
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1757
|
+
}
|
|
1603
1758
|
cleanupDevice(deviceId, undefined, {
|
|
1604
1759
|
cleanupConnection: true,
|
|
1760
|
+
// macOS returns zero GATT services when a previously-disconnected
|
|
1761
|
+
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1762
|
+
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1763
|
+
cleanupDiscoveredCache: true,
|
|
1605
1764
|
sendDisconnectEvent: false,
|
|
1606
1765
|
cancelOperations: true,
|
|
1607
1766
|
reason: 'manual-disconnect',
|
|
@@ -1742,9 +1901,14 @@ async function subscribeNotifications(
|
|
|
1742
1901
|
});
|
|
1743
1902
|
}
|
|
1744
1903
|
|
|
1904
|
+
const subscribeStartedAt = Date.now();
|
|
1745
1905
|
try {
|
|
1746
1906
|
await rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1747
1907
|
subscribedDevices.set(deviceId, true);
|
|
1908
|
+
logger?.info('[NobleBLE] Notification subscription active', {
|
|
1909
|
+
deviceId,
|
|
1910
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1911
|
+
});
|
|
1748
1912
|
} finally {
|
|
1749
1913
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
1750
1914
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1927,10 +2091,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1927
2091
|
}
|
|
1928
2092
|
|
|
1929
2093
|
await stopScanning().catch(() => undefined);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2094
|
+
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
2095
|
+
// destroy handler also fires on a renderer soft restart, and removing
|
|
2096
|
+
// the listener here permanently killed poweredOff/poweredOn
|
|
2097
|
+
// reconciliation for the rest of the process (nothing re-registers it:
|
|
2098
|
+
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
2099
|
+
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1934
2100
|
cleanupNobleListeners();
|
|
1935
2101
|
discoveredDevices.clear();
|
|
1936
2102
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|