@onekeyfe/hd-transport-electron 1.2.2-alpha.100 → 1.2.2-alpha.11
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-00297050.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-95c5cf0d.js} +117 -58
- 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/ble-packet-capacity.ts +14 -0
- package/src/noble-ble-handler.ts +225 -70
- 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-95c5cf0d.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-00297050.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;
|
|
@@ -116,7 +125,8 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
116
125
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
117
126
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
118
127
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
119
|
-
const
|
|
128
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
129
|
+
const BLE_IDLE_DISCONNECT_MS = 20000;
|
|
120
130
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
121
131
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
122
132
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
@@ -242,6 +252,7 @@ function initializeNoble() {
|
|
|
242
252
|
try {
|
|
243
253
|
noble = require('@stoprocent/noble');
|
|
244
254
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
|
|
255
|
+
setupPersistentStateListener();
|
|
245
256
|
yield new Promise((resolve, reject) => {
|
|
246
257
|
if (!noble) {
|
|
247
258
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
|
|
@@ -251,7 +262,6 @@ function initializeNoble() {
|
|
|
251
262
|
resolve();
|
|
252
263
|
return;
|
|
253
264
|
}
|
|
254
|
-
setupPersistentStateListener();
|
|
255
265
|
const timeout = setTimeout(() => {
|
|
256
266
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
|
|
257
267
|
}, BLUETOOTH_INIT_TIMEOUT);
|
|
@@ -336,7 +346,14 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
336
346
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
337
347
|
const peripheral = connectedDevices.get(deviceId);
|
|
338
348
|
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 =
|
|
349
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
350
|
+
.catch(error => {
|
|
351
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
352
|
+
deviceId,
|
|
353
|
+
error: error instanceof Error ? error.message : String(error),
|
|
354
|
+
});
|
|
355
|
+
})
|
|
356
|
+
.then(() => disconnectDevice(deviceId))
|
|
340
357
|
.then(() => {
|
|
341
358
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
342
359
|
id: deviceId,
|
|
@@ -409,6 +426,7 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
409
426
|
});
|
|
410
427
|
cleanupDevice(deviceId, webContents, {
|
|
411
428
|
cleanupConnection: true,
|
|
429
|
+
cleanupDiscoveredCache: true,
|
|
412
430
|
sendDisconnectEvent: true,
|
|
413
431
|
cancelOperations: true,
|
|
414
432
|
reason: 'auto-disconnect',
|
|
@@ -432,13 +450,14 @@ function setupMtuListener(peripheral, deviceId, webContents) {
|
|
|
432
450
|
existing.peripheral.removeListener('mtu', existing.listener);
|
|
433
451
|
}
|
|
434
452
|
const listener = (mtu) => {
|
|
435
|
-
|
|
453
|
+
const normalizedMtu = resolveNobleAttMtu(mtu);
|
|
454
|
+
if (normalizedMtu === undefined)
|
|
436
455
|
return;
|
|
437
456
|
if (webContents.isDestroyed())
|
|
438
457
|
return;
|
|
439
458
|
webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
440
459
|
id: deviceId,
|
|
441
|
-
mtu,
|
|
460
|
+
mtu: normalizedMtu,
|
|
442
461
|
});
|
|
443
462
|
};
|
|
444
463
|
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
@@ -545,7 +564,7 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
545
564
|
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
546
565
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
547
566
|
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);
|
|
567
|
+
const packetCapacity = resolveBlePacketCapacity(resolveNobleAttMtu(peripheral.mtu), BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
|
|
549
568
|
const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
|
|
550
569
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
551
570
|
const writeCharacteristic = doGetWriteCharacteristic();
|
|
@@ -590,18 +609,37 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
590
609
|
}
|
|
591
610
|
});
|
|
592
611
|
}
|
|
612
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
613
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
614
|
+
const bleNamesById = new Map();
|
|
615
|
+
function rememberBleName(deviceId, name) {
|
|
616
|
+
const trimmed = name === null || name === void 0 ? void 0 : name.trim();
|
|
617
|
+
if (trimmed) {
|
|
618
|
+
bleNamesById.set(deviceId, trimmed);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function resolveBleName(deviceId) {
|
|
622
|
+
var _a, _b, _c, _d, _e, _f;
|
|
623
|
+
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()) ||
|
|
624
|
+
((_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());
|
|
625
|
+
return live || bleNamesById.get(deviceId);
|
|
626
|
+
}
|
|
627
|
+
function shouldConnectByIdFirst(deviceId) {
|
|
628
|
+
return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
|
|
629
|
+
}
|
|
593
630
|
function handleDeviceDiscovered(peripheral) {
|
|
594
|
-
var _a, _b;
|
|
631
|
+
var _a, _b, _c;
|
|
595
632
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
596
633
|
return;
|
|
597
634
|
}
|
|
598
635
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
599
636
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
637
|
+
rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
|
|
600
638
|
if (isNewDevice) {
|
|
601
639
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
602
640
|
deviceId: peripheral.id,
|
|
603
|
-
name: ((
|
|
604
|
-
serviceUUIDs: ((
|
|
641
|
+
name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
|
|
642
|
+
serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
|
|
605
643
|
});
|
|
606
644
|
}
|
|
607
645
|
}
|
|
@@ -627,7 +665,7 @@ function waitForNobleScanStop(nobleInstance) {
|
|
|
627
665
|
});
|
|
628
666
|
});
|
|
629
667
|
}
|
|
630
|
-
function performTargetedScan(targetDeviceId) {
|
|
668
|
+
function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
|
|
631
669
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
632
670
|
if (!noble) {
|
|
633
671
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
@@ -667,7 +705,7 @@ function performTargetedScan(targetDeviceId) {
|
|
|
667
705
|
const timeoutId = setTimeout(() => {
|
|
668
706
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
669
707
|
finish(null).catch(reject);
|
|
670
|
-
},
|
|
708
|
+
}, timeoutMs);
|
|
671
709
|
nobleInstance.on('discover', onDiscover);
|
|
672
710
|
nobleInstance.startScanning([], true, (error) => {
|
|
673
711
|
if (error) {
|
|
@@ -763,12 +801,14 @@ function getDevice(deviceId) {
|
|
|
763
801
|
const peripheral = discoveredDevices.get(deviceId);
|
|
764
802
|
if (peripheral) {
|
|
765
803
|
const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
766
|
-
|
|
804
|
+
const mtu = resolveNobleAttMtu(peripheral.mtu);
|
|
805
|
+
return Object.assign({ commType: 'electron-ble', id: peripheral.id, name: deviceName, state: peripheral.state || 'disconnected' }, (mtu === undefined ? {} : { mtu }));
|
|
767
806
|
}
|
|
768
807
|
const connectedPeripheral = connectedDevices.get(deviceId);
|
|
769
808
|
if (connectedPeripheral) {
|
|
770
809
|
const deviceName = ((_b = connectedPeripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device';
|
|
771
|
-
|
|
810
|
+
const mtu = resolveNobleAttMtu(connectedPeripheral.mtu);
|
|
811
|
+
return Object.assign({ commType: 'electron-ble', id: connectedPeripheral.id, name: deviceName, state: connectedPeripheral.state || 'connected' }, (mtu === undefined ? {} : { mtu }));
|
|
772
812
|
}
|
|
773
813
|
return {
|
|
774
814
|
commType: 'electron-ble',
|
|
@@ -802,7 +842,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
802
842
|
});
|
|
803
843
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
804
844
|
const services = yield new Promise((resolve, reject) => {
|
|
805
|
-
peripheral.discoverServices(
|
|
845
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
806
846
|
if (error) {
|
|
807
847
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
808
848
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -813,7 +853,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
813
853
|
});
|
|
814
854
|
});
|
|
815
855
|
if (!services || services.length === 0) {
|
|
816
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
856
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
817
857
|
}
|
|
818
858
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
|
|
819
859
|
deviceId: peripheral.id,
|
|
@@ -930,11 +970,11 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
930
970
|
deviceId,
|
|
931
971
|
peripheralState: peripheral.state,
|
|
932
972
|
attempt: attemptNumber,
|
|
933
|
-
maxRetries:
|
|
973
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
934
974
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
935
975
|
});
|
|
936
976
|
if (attemptNumber > 1) {
|
|
937
|
-
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}
|
|
977
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`);
|
|
938
978
|
}
|
|
939
979
|
if (peripheral.state !== 'connected') {
|
|
940
980
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device not connected: ${peripheral.state}`);
|
|
@@ -943,16 +983,16 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
943
983
|
return yield discoverServicesAndCharacteristics(peripheral);
|
|
944
984
|
}
|
|
945
985
|
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}
|
|
986
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
987
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
988
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
949
989
|
}
|
|
950
990
|
throw error;
|
|
951
991
|
}
|
|
952
992
|
}), {
|
|
953
|
-
retries:
|
|
993
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
954
994
|
factor: 1.5,
|
|
955
|
-
minTimeout:
|
|
995
|
+
minTimeout: 500,
|
|
956
996
|
maxTimeout: 3000,
|
|
957
997
|
onFailedAttempt: error => {
|
|
958
998
|
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -966,23 +1006,24 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
966
1006
|
}
|
|
967
1007
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
968
1008
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
969
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
970
1009
|
try {
|
|
971
|
-
|
|
1010
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
972
1011
|
}
|
|
973
|
-
catch (
|
|
974
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
975
|
-
deviceId,
|
|
976
|
-
error: initialError,
|
|
977
|
-
});
|
|
1012
|
+
catch (resetError) {
|
|
1013
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
978
1014
|
}
|
|
979
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
980
1015
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
981
1016
|
try {
|
|
982
1017
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
983
1018
|
}
|
|
984
|
-
catch (
|
|
985
|
-
|
|
1019
|
+
catch (discoveryError) {
|
|
1020
|
+
if (peripheral.state === 'connected') {
|
|
1021
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan', discoveryError);
|
|
1022
|
+
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1026
|
+
}
|
|
986
1027
|
return freshScanAndDiscover(deviceId, webContents);
|
|
987
1028
|
}
|
|
988
1029
|
});
|
|
@@ -1042,7 +1083,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1042
1083
|
});
|
|
1043
1084
|
}
|
|
1044
1085
|
function connectDevice(deviceId, webContents) {
|
|
1045
|
-
var _a;
|
|
1086
|
+
var _a, _b, _c;
|
|
1046
1087
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1047
1088
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1048
1089
|
deviceId,
|
|
@@ -1054,34 +1095,47 @@ function connectDevice(deviceId, webContents) {
|
|
|
1054
1095
|
});
|
|
1055
1096
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1056
1097
|
if (!peripheral) {
|
|
1057
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1058
1098
|
if (!noble) {
|
|
1059
1099
|
yield initializeNoble();
|
|
1060
1100
|
}
|
|
1061
1101
|
if (!noble) {
|
|
1062
1102
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1063
1103
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1104
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1105
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
|
|
1106
|
+
deviceId,
|
|
1107
|
+
name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
|
|
1108
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1109
|
+
});
|
|
1110
|
+
const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1111
|
+
var _d;
|
|
1112
|
+
try {
|
|
1113
|
+
return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
|
|
1074
1114
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1115
|
+
catch (scanError) {
|
|
1116
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
|
|
1117
|
+
deviceId,
|
|
1118
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1119
|
+
});
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1124
|
+
const found = yield tryDirectConnectById(deviceId);
|
|
1125
|
+
if (found) {
|
|
1126
|
+
discoveredDevices.set(deviceId, found);
|
|
1127
|
+
}
|
|
1128
|
+
return found;
|
|
1129
|
+
});
|
|
1130
|
+
peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
|
|
1131
|
+
if (!peripheral) {
|
|
1132
|
+
peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
|
|
1080
1133
|
}
|
|
1081
1134
|
}
|
|
1082
1135
|
if (!peripheral) {
|
|
1083
1136
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1084
1137
|
}
|
|
1138
|
+
rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
|
|
1085
1139
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1086
1140
|
if (peripheral.state === 'connected') {
|
|
1087
1141
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
@@ -1156,9 +1210,9 @@ function connectDevice(deviceId, webContents) {
|
|
|
1156
1210
|
}
|
|
1157
1211
|
catch (setupError) {
|
|
1158
1212
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1213
|
+
disconnectDevice(deviceId)
|
|
1214
|
+
.catch(() => undefined)
|
|
1215
|
+
.then(() => reject(setupError));
|
|
1162
1216
|
}
|
|
1163
1217
|
}));
|
|
1164
1218
|
});
|
|
@@ -1176,11 +1230,15 @@ function disconnectDevice(deviceId) {
|
|
|
1176
1230
|
deviceDisconnectListeners.delete(deviceId);
|
|
1177
1231
|
}
|
|
1178
1232
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1179
|
-
timeoutMs:
|
|
1233
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1180
1234
|
timeoutBehavior: 'resolve',
|
|
1181
1235
|
});
|
|
1236
|
+
if (peripheral.state === 'connected') {
|
|
1237
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1238
|
+
}
|
|
1182
1239
|
cleanupDevice(deviceId, undefined, {
|
|
1183
1240
|
cleanupConnection: true,
|
|
1241
|
+
cleanupDiscoveredCache: true,
|
|
1184
1242
|
sendDisconnectEvent: false,
|
|
1185
1243
|
cancelOperations: true,
|
|
1186
1244
|
reason: 'manual-disconnect',
|
|
@@ -1271,9 +1329,14 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1271
1329
|
});
|
|
1272
1330
|
});
|
|
1273
1331
|
}
|
|
1332
|
+
const subscribeStartedAt = Date.now();
|
|
1274
1333
|
try {
|
|
1275
1334
|
yield rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1276
1335
|
subscribedDevices.set(deviceId, true);
|
|
1336
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
|
|
1337
|
+
deviceId,
|
|
1338
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1339
|
+
});
|
|
1277
1340
|
}
|
|
1278
1341
|
finally {
|
|
1279
1342
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1391,10 +1454,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1391
1454
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1392
1455
|
}
|
|
1393
1456
|
yield stopScanning().catch(() => undefined);
|
|
1394
|
-
if (noble && persistentStateListener) {
|
|
1395
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1396
|
-
persistentStateListener = null;
|
|
1397
|
-
}
|
|
1398
1457
|
cleanupNobleListeners();
|
|
1399
1458
|
discoveredDevices.clear();
|
|
1400
1459
|
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;AAyvDD,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.11",
|
|
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.11",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.11",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.11",
|
|
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": "8928100acb893d72361eb74422d5e2a0f7af77d8"
|
|
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,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
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
createKnownBleUuidAliases,
|
|
17
17
|
hasOnekeyCommunicationService,
|
|
18
18
|
isOnekeyBluetoothDevice,
|
|
19
|
+
isPro2FamilyBleName,
|
|
19
20
|
matchesKnownBleUuid,
|
|
20
21
|
wait,
|
|
21
22
|
} from '@onekeyfe/hd-shared';
|
|
22
23
|
import pRetry from 'p-retry';
|
|
23
24
|
|
|
24
|
-
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
25
|
+
import { resolveBlePacketCapacity, resolveNobleAttMtu } from './ble-packet-capacity';
|
|
25
26
|
import { safeLog } from './types/noble-extended';
|
|
26
27
|
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
27
28
|
import {
|
|
@@ -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
|
|
|
@@ -257,6 +268,11 @@ async function initializeNoble(): Promise<void> {
|
|
|
257
268
|
noble = require('@stoprocent/noble') as NobleModule;
|
|
258
269
|
logger?.info('[NobleBLE] Noble library loaded');
|
|
259
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
|
+
|
|
260
276
|
// Wait for Bluetooth to be ready
|
|
261
277
|
await new Promise<void>((resolve, reject) => {
|
|
262
278
|
if (!noble) {
|
|
@@ -269,9 +285,6 @@ async function initializeNoble(): Promise<void> {
|
|
|
269
285
|
return;
|
|
270
286
|
}
|
|
271
287
|
|
|
272
|
-
// Setup persistent state listener before initialization
|
|
273
|
-
setupPersistentStateListener();
|
|
274
|
-
|
|
275
288
|
const timeout = setTimeout(() => {
|
|
276
289
|
reject(
|
|
277
290
|
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
|
|
@@ -398,7 +411,21 @@ function armIdleDisconnect(
|
|
|
398
411
|
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
399
412
|
const peripheral = connectedDevices.get(deviceId);
|
|
400
413
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
401
|
-
|
|
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))
|
|
402
429
|
.then(() => {
|
|
403
430
|
// A call is still in flight here; it must reject, not hang.
|
|
404
431
|
// Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
|
|
@@ -508,6 +535,9 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
|
|
|
508
535
|
|
|
509
536
|
cleanupDevice(deviceId, webContents, {
|
|
510
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,
|
|
511
541
|
sendDisconnectEvent: true,
|
|
512
542
|
cancelOperations: true,
|
|
513
543
|
reason: 'auto-disconnect',
|
|
@@ -544,12 +574,13 @@ function setupMtuListener(
|
|
|
544
574
|
}
|
|
545
575
|
|
|
546
576
|
const listener = (mtu: number) => {
|
|
547
|
-
|
|
577
|
+
const normalizedMtu = resolveNobleAttMtu(mtu);
|
|
578
|
+
if (normalizedMtu === undefined) return;
|
|
548
579
|
// A kept-alive link can outlive the renderer this closure captured.
|
|
549
580
|
if (webContents.isDestroyed()) return;
|
|
550
581
|
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
551
582
|
id: deviceId,
|
|
552
|
-
mtu,
|
|
583
|
+
mtu: normalizedMtu,
|
|
553
584
|
});
|
|
554
585
|
};
|
|
555
586
|
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
@@ -697,7 +728,7 @@ async function transmitHexDataToDevice(
|
|
|
697
728
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
698
729
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
699
730
|
const packetCapacity = resolveBlePacketCapacity(
|
|
700
|
-
peripheral.mtu,
|
|
731
|
+
resolveNobleAttMtu(peripheral.mtu),
|
|
701
732
|
BLE_PACKET_SIZE_MAXIMUM,
|
|
702
733
|
BLE_PACKET_SIZE_FALLBACK
|
|
703
734
|
);
|
|
@@ -761,6 +792,44 @@ async function transmitHexDataToDevice(
|
|
|
761
792
|
}
|
|
762
793
|
|
|
763
794
|
// Handle discovered device (for general enumeration only)
|
|
795
|
+
// A scan that finds nothing costs this much before the direct-connect
|
|
796
|
+
// fallback runs. Every advertisement in the 6.5.0 control logs arrived within
|
|
797
|
+
// 631ms, so this leaves ~2x headroom while keeping a miss cheap.
|
|
798
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
799
|
+
|
|
800
|
+
// Attempts before a link is written off as unable to serve the OneKey service.
|
|
801
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
802
|
+
|
|
803
|
+
// Advertised names, kept for the life of the process: device caches are purged
|
|
804
|
+
// on every disconnect, but the family a device belongs to never changes and
|
|
805
|
+
// decides which connection strategy is safe for it.
|
|
806
|
+
const bleNamesById = new Map<string, string>();
|
|
807
|
+
|
|
808
|
+
function rememberBleName(deviceId: string, name?: string | null): void {
|
|
809
|
+
const trimmed = name?.trim();
|
|
810
|
+
if (trimmed) {
|
|
811
|
+
bleNamesById.set(deviceId, trimmed);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function resolveBleName(deviceId: string): string | undefined {
|
|
816
|
+
const live =
|
|
817
|
+
connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
|
|
818
|
+
discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
|
|
819
|
+
return live || bleNamesById.get(deviceId);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Pro2/Neo connect by id first: they are unaffected by the stale-session
|
|
824
|
+
* problem and advertise under Find My names a OneKey-filtered scan cannot
|
|
825
|
+
* match. Everything else — Classic family, and any device whose name is not
|
|
826
|
+
* known yet — scans first, because connecting by id can resolve a session the
|
|
827
|
+
* device no longer serves (see setupConnectionAndDiscoverServices).
|
|
828
|
+
*/
|
|
829
|
+
function shouldConnectByIdFirst(deviceId: string): boolean {
|
|
830
|
+
return isPro2FamilyBleName(resolveBleName(deviceId));
|
|
831
|
+
}
|
|
832
|
+
|
|
764
833
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
765
834
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
766
835
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
@@ -770,6 +839,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
770
839
|
|
|
771
840
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
772
841
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
842
|
+
rememberBleName(peripheral.id, peripheral.advertisement?.localName);
|
|
773
843
|
if (isNewDevice) {
|
|
774
844
|
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
775
845
|
deviceId: peripheral.id,
|
|
@@ -804,7 +874,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
|
804
874
|
|
|
805
875
|
// Perform targeted scan for a specific device ID
|
|
806
876
|
// Uses self-contained local listener pattern - no global state needed
|
|
807
|
-
async function performTargetedScan(
|
|
877
|
+
async function performTargetedScan(
|
|
878
|
+
targetDeviceId: string,
|
|
879
|
+
timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
|
|
880
|
+
): Promise<Peripheral | null> {
|
|
808
881
|
if (!noble) {
|
|
809
882
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
810
883
|
}
|
|
@@ -849,7 +922,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
849
922
|
const timeoutId = setTimeout(() => {
|
|
850
923
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
851
924
|
finish(null).catch(reject);
|
|
852
|
-
},
|
|
925
|
+
}, timeoutMs);
|
|
853
926
|
|
|
854
927
|
// Add local listener for this scan
|
|
855
928
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -975,12 +1048,13 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
975
1048
|
const peripheral = discoveredDevices.get(deviceId);
|
|
976
1049
|
if (peripheral) {
|
|
977
1050
|
const deviceName = peripheral.advertisement?.localName || 'Unknown Device';
|
|
1051
|
+
const mtu = resolveNobleAttMtu(peripheral.mtu);
|
|
978
1052
|
return {
|
|
979
1053
|
commType: 'electron-ble',
|
|
980
1054
|
id: peripheral.id,
|
|
981
1055
|
name: deviceName,
|
|
982
1056
|
state: peripheral.state || 'disconnected',
|
|
983
|
-
...(
|
|
1057
|
+
...(mtu === undefined ? {} : { mtu }),
|
|
984
1058
|
};
|
|
985
1059
|
}
|
|
986
1060
|
|
|
@@ -988,12 +1062,13 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
988
1062
|
const connectedPeripheral = connectedDevices.get(deviceId);
|
|
989
1063
|
if (connectedPeripheral) {
|
|
990
1064
|
const deviceName = connectedPeripheral.advertisement?.localName || 'Unknown Device';
|
|
1065
|
+
const mtu = resolveNobleAttMtu(connectedPeripheral.mtu);
|
|
991
1066
|
return {
|
|
992
1067
|
commType: 'electron-ble',
|
|
993
1068
|
id: connectedPeripheral.id,
|
|
994
1069
|
name: deviceName,
|
|
995
1070
|
state: connectedPeripheral.state || 'connected',
|
|
996
|
-
...(
|
|
1071
|
+
...(mtu === undefined ? {} : { mtu }),
|
|
997
1072
|
};
|
|
998
1073
|
}
|
|
999
1074
|
|
|
@@ -1051,9 +1126,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
1051
1126
|
|
|
1052
1127
|
// Main discovery logic as async function
|
|
1053
1128
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
1054
|
-
// Step 1: Discover
|
|
1129
|
+
// Step 1: Discover the OneKey service by UUID. Filtering is not just a
|
|
1130
|
+
// narrowing: an unfiltered discovery is answered from the OS GATT cache, so
|
|
1131
|
+
// a device whose stack still advertises and accepts links but no longer
|
|
1132
|
+
// serves its application layer still looks healthy — the link comes up, the
|
|
1133
|
+
// cached services resolve, the protocol frame goes out and nothing ever
|
|
1134
|
+
// answers. A targeted query returns nothing in that state, which is what
|
|
1135
|
+
// 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
|
|
1136
|
+
// scan) — the sequence that brings such a device back. Pro2/Neo expose the
|
|
1137
|
+
// same service UUID, and the selection below only ever accepts that one.
|
|
1055
1138
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
1056
|
-
peripheral.discoverServices(
|
|
1139
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
1057
1140
|
if (error) {
|
|
1058
1141
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
1059
1142
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -1064,7 +1147,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
1064
1147
|
});
|
|
1065
1148
|
|
|
1066
1149
|
if (!services || services.length === 0) {
|
|
1067
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
1150
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
1068
1151
|
}
|
|
1069
1152
|
|
|
1070
1153
|
logger?.debug('[NobleBLE] services discovered', {
|
|
@@ -1248,12 +1331,14 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1248
1331
|
deviceId,
|
|
1249
1332
|
peripheralState: peripheral.state,
|
|
1250
1333
|
attempt: attemptNumber,
|
|
1251
|
-
maxRetries:
|
|
1334
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
1252
1335
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
1253
1336
|
});
|
|
1254
1337
|
|
|
1255
1338
|
if (attemptNumber > 1) {
|
|
1256
|
-
logger?.info(
|
|
1339
|
+
logger?.info(
|
|
1340
|
+
`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`
|
|
1341
|
+
);
|
|
1257
1342
|
}
|
|
1258
1343
|
|
|
1259
1344
|
// Verify connection state before attempting service discovery
|
|
@@ -1267,20 +1352,32 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1267
1352
|
try {
|
|
1268
1353
|
return await discoverServicesAndCharacteristics(peripheral);
|
|
1269
1354
|
} catch (error) {
|
|
1270
|
-
logger?.error(
|
|
1355
|
+
logger?.error(
|
|
1356
|
+
`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1357
|
+
);
|
|
1271
1358
|
|
|
1272
|
-
if (attemptNumber <
|
|
1273
|
-
logger?.error(
|
|
1359
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
1360
|
+
logger?.error(
|
|
1361
|
+
`[NobleBLE] Will retry service discovery (attempt ${
|
|
1362
|
+
attemptNumber + 1
|
|
1363
|
+
}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1364
|
+
);
|
|
1274
1365
|
}
|
|
1275
1366
|
|
|
1276
1367
|
throw error; // p-retry will handle the retry logic
|
|
1277
1368
|
}
|
|
1278
1369
|
},
|
|
1279
1370
|
{
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1371
|
+
// One retry, not four. An empty result here is the signature of a device
|
|
1372
|
+
// whose stack no longer serves its application layer, and retrying the
|
|
1373
|
+
// same link has never recovered it — every observed run failed all the
|
|
1374
|
+
// way through. The recovery is the teardown and cold reconnect the caller
|
|
1375
|
+
// performs afterwards, so reaching it sooner is what shortens the wait.
|
|
1376
|
+
// The single retry stays for a genuinely transient miss.
|
|
1377
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
1378
|
+
factor: 1.5,
|
|
1379
|
+
minTimeout: 500,
|
|
1380
|
+
maxTimeout: 3000,
|
|
1284
1381
|
onFailedAttempt: error => {
|
|
1285
1382
|
// This runs after each failed attempt
|
|
1286
1383
|
logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -1305,26 +1402,45 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1305
1402
|
deviceId: string,
|
|
1306
1403
|
webContents: WebContents
|
|
1307
1404
|
): Promise<CharacteristicPair> {
|
|
1308
|
-
|
|
1309
|
-
|
|
1405
|
+
// Reset the link before any protocol traffic reaches the device. 6.5.0 did
|
|
1406
|
+
// this on every cold setup and never produced a protocol-deaf Classic 1S
|
|
1407
|
+
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1408
|
+
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1409
|
+
// reset entirely for this device, because discovery always succeeds: the link
|
|
1410
|
+
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1411
|
+
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1412
|
+
// link with an intact subscription returns before reaching here, so reuse
|
|
1413
|
+
// inside a workflow is unaffected.
|
|
1310
1414
|
try {
|
|
1311
|
-
|
|
1312
|
-
} catch (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1415
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1416
|
+
} catch (resetError) {
|
|
1417
|
+
// A failed reset must not abort the attempt: discovery on the existing
|
|
1418
|
+
// link, then the fresh-scan fallback, still have a chance to recover.
|
|
1419
|
+
logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
1317
1420
|
}
|
|
1318
|
-
|
|
1319
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1320
1421
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1422
|
+
|
|
1321
1423
|
try {
|
|
1322
1424
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1323
|
-
} catch (
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1425
|
+
} catch (discoveryError) {
|
|
1426
|
+
// A connected peripheral does not advertise, so the fresh scan below would
|
|
1427
|
+
// only run out its timeout. Drop the link first, then rescan: the cold
|
|
1428
|
+
// reconnect is what recovers a device whose stack stopped serving its
|
|
1429
|
+
// application layer. Recover here rather than throwing to the caller —
|
|
1430
|
+
// one acquire pays ~2s for the rescan, while a thrown error costs the
|
|
1431
|
+
// renderer a full retry round trip.
|
|
1432
|
+
if (peripheral.state === 'connected') {
|
|
1433
|
+
logger?.error(
|
|
1434
|
+
'[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan',
|
|
1435
|
+
discoveryError
|
|
1436
|
+
);
|
|
1437
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1438
|
+
} else {
|
|
1439
|
+
logger?.error(
|
|
1440
|
+
'[NobleBLE] Service discovery failed, attempting fresh scan...',
|
|
1441
|
+
discoveryError
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1328
1444
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1329
1445
|
}
|
|
1330
1446
|
}
|
|
@@ -1413,10 +1529,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1413
1529
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1414
1530
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1415
1531
|
|
|
1416
|
-
// If device not discovered, try a targeted scan for this specific device
|
|
1417
1532
|
if (!peripheral) {
|
|
1418
|
-
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1419
|
-
|
|
1420
1533
|
// Initialize Noble if not already done
|
|
1421
1534
|
if (!noble) {
|
|
1422
1535
|
await initializeNoble();
|
|
@@ -1426,27 +1539,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1426
1539
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1427
1540
|
}
|
|
1428
1541
|
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1542
|
+
// A live advertisement proves the device holds no link and will open a
|
|
1543
|
+
// fresh session; connecting by id resolves through the OS cache instead
|
|
1544
|
+
// and can hand back a session the device no longer serves (see
|
|
1545
|
+
// setupConnectionAndDiscoverServices). Scanning is also the faster of the
|
|
1546
|
+
// two when the device does advertise: ~96ms median against ~784ms for
|
|
1547
|
+
// connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
|
|
1548
|
+
// because they are unaffected and may advertise under an unmatchable Find
|
|
1549
|
+
// My name.
|
|
1550
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1551
|
+
logger?.info('[NobleBLE] Resolving device for cold connect', {
|
|
1552
|
+
deviceId,
|
|
1553
|
+
name: resolveBleName(deviceId) ?? 'unknown',
|
|
1554
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1555
|
+
});
|
|
1435
1556
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1557
|
+
const scanForPeripheral = async () => {
|
|
1558
|
+
try {
|
|
1559
|
+
return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
|
|
1560
|
+
} catch (scanError) {
|
|
1561
|
+
logger?.info('[NobleBLE] Targeted scan failed', {
|
|
1562
|
+
deviceId,
|
|
1563
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1564
|
+
});
|
|
1565
|
+
return undefined;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
const connectById = async () => {
|
|
1570
|
+
const found = await tryDirectConnectById(deviceId);
|
|
1571
|
+
if (found) {
|
|
1572
|
+
discoveredDevices.set(deviceId, found);
|
|
1445
1573
|
}
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1574
|
+
return found;
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
|
|
1578
|
+
if (!peripheral) {
|
|
1579
|
+
// The preferred route came up empty: not advertising (held elsewhere, or
|
|
1580
|
+
// silent), or not reachable by id. Try the other one before giving up.
|
|
1581
|
+
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1450
1582
|
}
|
|
1451
1583
|
}
|
|
1452
1584
|
|
|
@@ -1455,6 +1587,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1455
1587
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1456
1588
|
}
|
|
1457
1589
|
|
|
1590
|
+
// Also covers the connect-by-id route, whose simulated discovery carries no
|
|
1591
|
+
// service UUIDs and therefore never reaches handleDeviceDiscovered.
|
|
1592
|
+
rememberBleName(deviceId, peripheral.advertisement?.localName);
|
|
1593
|
+
|
|
1458
1594
|
logger?.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1459
1595
|
|
|
1460
1596
|
// Check if device is already connected
|
|
@@ -1575,9 +1711,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1575
1711
|
resolve();
|
|
1576
1712
|
} catch (setupError) {
|
|
1577
1713
|
logger?.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1714
|
+
// Never reject from inside a raw disconnect callback: noble only fires
|
|
1715
|
+
// it on a real 'disconnect' event, so a peripheral that is already
|
|
1716
|
+
// down leaves this promise — and the renderer acquire awaiting it —
|
|
1717
|
+
// pending forever. disconnectDevice always settles (it no-ops on an
|
|
1718
|
+
// unknown peripheral and caps the confirm wait).
|
|
1719
|
+
disconnectDevice(deviceId)
|
|
1720
|
+
.catch(() => undefined)
|
|
1721
|
+
.then(() => reject(setupError));
|
|
1581
1722
|
}
|
|
1582
1723
|
});
|
|
1583
1724
|
});
|
|
@@ -1597,11 +1738,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1597
1738
|
}
|
|
1598
1739
|
|
|
1599
1740
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1600
|
-
timeoutMs:
|
|
1741
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1601
1742
|
timeoutBehavior: 'resolve',
|
|
1602
1743
|
});
|
|
1744
|
+
if (peripheral.state === 'connected') {
|
|
1745
|
+
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1746
|
+
}
|
|
1603
1747
|
cleanupDevice(deviceId, undefined, {
|
|
1604
1748
|
cleanupConnection: true,
|
|
1749
|
+
// macOS returns zero GATT services when a previously-disconnected
|
|
1750
|
+
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1751
|
+
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1752
|
+
cleanupDiscoveredCache: true,
|
|
1605
1753
|
sendDisconnectEvent: false,
|
|
1606
1754
|
cancelOperations: true,
|
|
1607
1755
|
reason: 'manual-disconnect',
|
|
@@ -1742,9 +1890,14 @@ async function subscribeNotifications(
|
|
|
1742
1890
|
});
|
|
1743
1891
|
}
|
|
1744
1892
|
|
|
1893
|
+
const subscribeStartedAt = Date.now();
|
|
1745
1894
|
try {
|
|
1746
1895
|
await rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1747
1896
|
subscribedDevices.set(deviceId, true);
|
|
1897
|
+
logger?.info('[NobleBLE] Notification subscription active', {
|
|
1898
|
+
deviceId,
|
|
1899
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1900
|
+
});
|
|
1748
1901
|
} finally {
|
|
1749
1902
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
1750
1903
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1927,10 +2080,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1927
2080
|
}
|
|
1928
2081
|
|
|
1929
2082
|
await stopScanning().catch(() => undefined);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2083
|
+
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
2084
|
+
// destroy handler also fires on a renderer soft restart, and removing
|
|
2085
|
+
// the listener here permanently killed poweredOff/poweredOn
|
|
2086
|
+
// reconciliation for the rest of the process (nothing re-registers it:
|
|
2087
|
+
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
2088
|
+
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1934
2089
|
cleanupNobleListeners();
|
|
1935
2090
|
discoveredDevices.clear();
|
|
1936
2091
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|