@onekeyfe/hd-transport-electron 1.2.2-alpha.0 → 1.2.2-alpha.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-a518d449.js → index-b37d926c.js} +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-56d2eab3.js → noble-ble-handler-e8b87173.js} +53 -100
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/types/noble-extended.d.ts +0 -1
- package/dist/types/noble-extended.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/noble-ble-handler.ts +64 -216
- package/src/types/noble-extended.ts +0 -1
|
@@ -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-e8b87173.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-b37d926c.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -116,8 +116,7 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
116
116
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
117
117
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
118
118
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
119
|
-
const
|
|
120
|
-
const BLE_IDLE_DISCONNECT_MS = 20000;
|
|
119
|
+
const BLE_IDLE_DISCONNECT_MS = 3 * 60000;
|
|
121
120
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
122
121
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
123
122
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
@@ -243,7 +242,6 @@ function initializeNoble() {
|
|
|
243
242
|
try {
|
|
244
243
|
noble = require('@stoprocent/noble');
|
|
245
244
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
|
|
246
|
-
setupPersistentStateListener();
|
|
247
245
|
yield new Promise((resolve, reject) => {
|
|
248
246
|
if (!noble) {
|
|
249
247
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
|
|
@@ -253,6 +251,7 @@ function initializeNoble() {
|
|
|
253
251
|
resolve();
|
|
254
252
|
return;
|
|
255
253
|
}
|
|
254
|
+
setupPersistentStateListener();
|
|
256
255
|
const timeout = setTimeout(() => {
|
|
257
256
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
|
|
258
257
|
}, BLUETOOTH_INIT_TIMEOUT);
|
|
@@ -337,14 +336,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
337
336
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
338
337
|
const peripheral = connectedDevices.get(deviceId);
|
|
339
338
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
340
|
-
const pending =
|
|
341
|
-
.catch(error => {
|
|
342
|
-
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
343
|
-
deviceId,
|
|
344
|
-
error: error instanceof Error ? error.message : String(error),
|
|
345
|
-
});
|
|
346
|
-
})
|
|
347
|
-
.then(() => disconnectDevice(deviceId))
|
|
339
|
+
const pending = disconnectDevice(deviceId)
|
|
348
340
|
.then(() => {
|
|
349
341
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
350
342
|
id: deviceId,
|
|
@@ -417,7 +409,6 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
417
409
|
});
|
|
418
410
|
cleanupDevice(deviceId, webContents, {
|
|
419
411
|
cleanupConnection: true,
|
|
420
|
-
cleanupDiscoveredCache: true,
|
|
421
412
|
sendDisconnectEvent: true,
|
|
422
413
|
cancelOperations: true,
|
|
423
414
|
reason: 'auto-disconnect',
|
|
@@ -599,37 +590,18 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
599
590
|
}
|
|
600
591
|
});
|
|
601
592
|
}
|
|
602
|
-
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
603
|
-
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
604
|
-
const bleNamesById = new Map();
|
|
605
|
-
function rememberBleName(deviceId, name) {
|
|
606
|
-
const trimmed = name === null || name === void 0 ? void 0 : name.trim();
|
|
607
|
-
if (trimmed) {
|
|
608
|
-
bleNamesById.set(deviceId, trimmed);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function resolveBleName(deviceId) {
|
|
612
|
-
var _a, _b, _c, _d, _e, _f;
|
|
613
|
-
const live = ((_c = (_b = (_a = connectedDevices.get(deviceId)) === null || _a === void 0 ? void 0 : _a.advertisement) === null || _b === void 0 ? void 0 : _b.localName) === null || _c === void 0 ? void 0 : _c.trim()) ||
|
|
614
|
-
((_f = (_e = (_d = discoveredDevices.get(deviceId)) === null || _d === void 0 ? void 0 : _d.advertisement) === null || _e === void 0 ? void 0 : _e.localName) === null || _f === void 0 ? void 0 : _f.trim());
|
|
615
|
-
return live || bleNamesById.get(deviceId);
|
|
616
|
-
}
|
|
617
|
-
function shouldConnectByIdFirst(deviceId) {
|
|
618
|
-
return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
|
|
619
|
-
}
|
|
620
593
|
function handleDeviceDiscovered(peripheral) {
|
|
621
|
-
var _a, _b
|
|
594
|
+
var _a, _b;
|
|
622
595
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
623
596
|
return;
|
|
624
597
|
}
|
|
625
598
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
626
599
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
627
|
-
rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
|
|
628
600
|
if (isNewDevice) {
|
|
629
601
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
630
602
|
deviceId: peripheral.id,
|
|
631
|
-
name: ((
|
|
632
|
-
serviceUUIDs: ((
|
|
603
|
+
name: ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device',
|
|
604
|
+
serviceUUIDs: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.serviceUuids) || [],
|
|
633
605
|
});
|
|
634
606
|
}
|
|
635
607
|
}
|
|
@@ -655,7 +627,7 @@ function waitForNobleScanStop(nobleInstance) {
|
|
|
655
627
|
});
|
|
656
628
|
});
|
|
657
629
|
}
|
|
658
|
-
function performTargetedScan(targetDeviceId
|
|
630
|
+
function performTargetedScan(targetDeviceId) {
|
|
659
631
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
660
632
|
if (!noble) {
|
|
661
633
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
@@ -695,7 +667,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
|
|
|
695
667
|
const timeoutId = setTimeout(() => {
|
|
696
668
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
697
669
|
finish(null).catch(reject);
|
|
698
|
-
},
|
|
670
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
699
671
|
nobleInstance.on('discover', onDiscover);
|
|
700
672
|
nobleInstance.startScanning([], true, (error) => {
|
|
701
673
|
if (error) {
|
|
@@ -830,7 +802,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
830
802
|
});
|
|
831
803
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
832
804
|
const services = yield new Promise((resolve, reject) => {
|
|
833
|
-
peripheral.discoverServices(
|
|
805
|
+
peripheral.discoverServices([], (error, svc) => {
|
|
834
806
|
if (error) {
|
|
835
807
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
836
808
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -841,7 +813,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
841
813
|
});
|
|
842
814
|
});
|
|
843
815
|
if (!services || services.length === 0) {
|
|
844
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No
|
|
816
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
845
817
|
}
|
|
846
818
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
|
|
847
819
|
deviceId: peripheral.id,
|
|
@@ -958,11 +930,11 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
958
930
|
deviceId,
|
|
959
931
|
peripheralState: peripheral.state,
|
|
960
932
|
attempt: attemptNumber,
|
|
961
|
-
maxRetries:
|
|
933
|
+
maxRetries: 5,
|
|
962
934
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
963
935
|
});
|
|
964
936
|
if (attemptNumber > 1) {
|
|
965
|
-
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}
|
|
937
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/5`);
|
|
966
938
|
}
|
|
967
939
|
if (peripheral.state !== 'connected') {
|
|
968
940
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device not connected: ${peripheral.state}`);
|
|
@@ -971,16 +943,16 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
971
943
|
return yield discoverServicesAndCharacteristics(peripheral);
|
|
972
944
|
}
|
|
973
945
|
catch (error) {
|
|
974
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}
|
|
975
|
-
if (attemptNumber <
|
|
976
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}
|
|
946
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/5)`);
|
|
947
|
+
if (attemptNumber < 5) {
|
|
948
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/5)`);
|
|
977
949
|
}
|
|
978
950
|
throw error;
|
|
979
951
|
}
|
|
980
952
|
}), {
|
|
981
|
-
retries:
|
|
953
|
+
retries: 4,
|
|
982
954
|
factor: 1.5,
|
|
983
|
-
minTimeout:
|
|
955
|
+
minTimeout: 1000,
|
|
984
956
|
maxTimeout: 3000,
|
|
985
957
|
onFailedAttempt: error => {
|
|
986
958
|
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -994,24 +966,23 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
994
966
|
}
|
|
995
967
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
996
968
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
969
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
997
970
|
try {
|
|
998
|
-
yield
|
|
971
|
+
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
999
972
|
}
|
|
1000
|
-
catch (
|
|
1001
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
973
|
+
catch (initialError) {
|
|
974
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
975
|
+
deviceId,
|
|
976
|
+
error: initialError,
|
|
977
|
+
});
|
|
1002
978
|
}
|
|
979
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
1003
980
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1004
981
|
try {
|
|
1005
982
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1006
983
|
}
|
|
1007
|
-
catch (
|
|
1008
|
-
|
|
1009
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan', discoveryError);
|
|
1010
|
-
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1014
|
-
}
|
|
984
|
+
catch (reconnectError) {
|
|
985
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
|
|
1015
986
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1016
987
|
}
|
|
1017
988
|
});
|
|
@@ -1071,7 +1042,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1071
1042
|
});
|
|
1072
1043
|
}
|
|
1073
1044
|
function connectDevice(deviceId, webContents) {
|
|
1074
|
-
var _a
|
|
1045
|
+
var _a;
|
|
1075
1046
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1076
1047
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1077
1048
|
deviceId,
|
|
@@ -1083,47 +1054,34 @@ function connectDevice(deviceId, webContents) {
|
|
|
1083
1054
|
});
|
|
1084
1055
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1085
1056
|
if (!peripheral) {
|
|
1057
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1086
1058
|
if (!noble) {
|
|
1087
1059
|
yield initializeNoble();
|
|
1088
1060
|
}
|
|
1089
1061
|
if (!noble) {
|
|
1090
1062
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1091
1063
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
deviceId,
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
catch (scanError) {
|
|
1104
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
|
|
1105
|
-
deviceId,
|
|
1106
|
-
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1107
|
-
});
|
|
1108
|
-
return undefined;
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1112
|
-
const found = yield tryDirectConnectById(deviceId);
|
|
1113
|
-
if (found) {
|
|
1114
|
-
discoveredDevices.set(deviceId, found);
|
|
1064
|
+
peripheral = yield tryDirectConnectById(deviceId);
|
|
1065
|
+
if (peripheral) {
|
|
1066
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (!peripheral) {
|
|
1070
|
+
try {
|
|
1071
|
+
const foundPeripheral = yield performTargetedScan(deviceId);
|
|
1072
|
+
if (!foundPeripheral) {
|
|
1073
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found even after targeted scan`);
|
|
1115
1074
|
}
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1075
|
+
peripheral = foundPeripheral;
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Targeted scan failed:', error);
|
|
1079
|
+
throw error;
|
|
1121
1080
|
}
|
|
1122
1081
|
}
|
|
1123
1082
|
if (!peripheral) {
|
|
1124
1083
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1125
1084
|
}
|
|
1126
|
-
rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
|
|
1127
1085
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1128
1086
|
if (peripheral.state === 'connected') {
|
|
1129
1087
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
@@ -1198,9 +1156,9 @@ function connectDevice(deviceId, webContents) {
|
|
|
1198
1156
|
}
|
|
1199
1157
|
catch (setupError) {
|
|
1200
1158
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1159
|
+
connectedPeripheral.disconnect(() => {
|
|
1160
|
+
reject(setupError);
|
|
1161
|
+
});
|
|
1204
1162
|
}
|
|
1205
1163
|
}));
|
|
1206
1164
|
});
|
|
@@ -1218,15 +1176,11 @@ function disconnectDevice(deviceId) {
|
|
|
1218
1176
|
deviceDisconnectListeners.delete(deviceId);
|
|
1219
1177
|
}
|
|
1220
1178
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1221
|
-
timeoutMs:
|
|
1179
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1222
1180
|
timeoutBehavior: 'resolve',
|
|
1223
1181
|
});
|
|
1224
|
-
if (peripheral.state === 'connected') {
|
|
1225
|
-
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1226
|
-
}
|
|
1227
1182
|
cleanupDevice(deviceId, undefined, {
|
|
1228
1183
|
cleanupConnection: true,
|
|
1229
|
-
cleanupDiscoveredCache: true,
|
|
1230
1184
|
sendDisconnectEvent: false,
|
|
1231
1185
|
cancelOperations: true,
|
|
1232
1186
|
reason: 'manual-disconnect',
|
|
@@ -1317,14 +1271,9 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1317
1271
|
});
|
|
1318
1272
|
});
|
|
1319
1273
|
}
|
|
1320
|
-
const subscribeStartedAt = Date.now();
|
|
1321
1274
|
try {
|
|
1322
1275
|
yield rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1323
1276
|
subscribedDevices.set(deviceId, true);
|
|
1324
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
|
|
1325
|
-
deviceId,
|
|
1326
|
-
ms: Date.now() - subscribeStartedAt,
|
|
1327
|
-
});
|
|
1328
1277
|
}
|
|
1329
1278
|
finally {
|
|
1330
1279
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1442,6 +1391,10 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1442
1391
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1443
1392
|
}
|
|
1444
1393
|
yield stopScanning().catch(() => undefined);
|
|
1394
|
+
if (noble && persistentStateListener) {
|
|
1395
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1396
|
+
persistentStateListener = null;
|
|
1397
|
+
}
|
|
1445
1398
|
cleanupNobleListeners();
|
|
1446
1399
|
discoveredDevices.clear();
|
|
1447
1400
|
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":"AA+BA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AA0EhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA2mDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAgMpE"}
|
|
@@ -22,7 +22,6 @@ 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;
|
|
26
25
|
error(message: string, ...args: any[]): void;
|
|
27
26
|
}
|
|
28
27
|
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,
|
|
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"}
|
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.100",
|
|
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.100",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.100",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.2-alpha.100",
|
|
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": "c40dad085297b0e3cc2020bd218c146dff6ed3e1"
|
|
40
40
|
}
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
createKnownBleUuidAliases,
|
|
17
17
|
hasOnekeyCommunicationService,
|
|
18
18
|
isOnekeyBluetoothDevice,
|
|
19
|
-
isPro2FamilyBleName,
|
|
20
19
|
matchesKnownBleUuid,
|
|
21
20
|
wait,
|
|
22
21
|
} from '@onekeyfe/hd-shared';
|
|
@@ -91,18 +90,8 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
91
90
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
92
91
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
93
92
|
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;
|
|
98
93
|
// Renderer release is logical only; this timer physically frees the device.
|
|
99
|
-
|
|
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;
|
|
94
|
+
const BLE_IDLE_DISCONNECT_MS = 3 * 60_000;
|
|
106
95
|
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
107
96
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
108
97
|
|
|
@@ -268,11 +257,6 @@ async function initializeNoble(): Promise<void> {
|
|
|
268
257
|
noble = require('@stoprocent/noble') as NobleModule;
|
|
269
258
|
logger?.info('[NobleBLE] Noble library loaded');
|
|
270
259
|
|
|
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
|
-
|
|
276
260
|
// Wait for Bluetooth to be ready
|
|
277
261
|
await new Promise<void>((resolve, reject) => {
|
|
278
262
|
if (!noble) {
|
|
@@ -285,6 +269,9 @@ async function initializeNoble(): Promise<void> {
|
|
|
285
269
|
return;
|
|
286
270
|
}
|
|
287
271
|
|
|
272
|
+
// Setup persistent state listener before initialization
|
|
273
|
+
setupPersistentStateListener();
|
|
274
|
+
|
|
288
275
|
const timeout = setTimeout(() => {
|
|
289
276
|
reject(
|
|
290
277
|
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
|
|
@@ -411,21 +398,7 @@ function armIdleDisconnect(
|
|
|
411
398
|
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
412
399
|
const peripheral = connectedDevices.get(deviceId);
|
|
413
400
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
414
|
-
|
|
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))
|
|
401
|
+
const pending = disconnectDevice(deviceId)
|
|
429
402
|
.then(() => {
|
|
430
403
|
// A call is still in flight here; it must reject, not hang.
|
|
431
404
|
// Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
|
|
@@ -535,9 +508,6 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
|
|
|
535
508
|
|
|
536
509
|
cleanupDevice(deviceId, webContents, {
|
|
537
510
|
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,
|
|
541
511
|
sendDisconnectEvent: true,
|
|
542
512
|
cancelOperations: true,
|
|
543
513
|
reason: 'auto-disconnect',
|
|
@@ -791,44 +761,6 @@ async function transmitHexDataToDevice(
|
|
|
791
761
|
}
|
|
792
762
|
|
|
793
763
|
// Handle discovered device (for general enumeration only)
|
|
794
|
-
// A scan that finds nothing costs this much before the direct-connect
|
|
795
|
-
// fallback runs. Every advertisement in the 6.5.0 control logs arrived within
|
|
796
|
-
// 631ms, so this leaves ~2x headroom while keeping a miss cheap.
|
|
797
|
-
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
798
|
-
|
|
799
|
-
// Attempts before a link is written off as unable to serve the OneKey service.
|
|
800
|
-
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
801
|
-
|
|
802
|
-
// Advertised names, kept for the life of the process: device caches are purged
|
|
803
|
-
// on every disconnect, but the family a device belongs to never changes and
|
|
804
|
-
// decides which connection strategy is safe for it.
|
|
805
|
-
const bleNamesById = new Map<string, string>();
|
|
806
|
-
|
|
807
|
-
function rememberBleName(deviceId: string, name?: string | null): void {
|
|
808
|
-
const trimmed = name?.trim();
|
|
809
|
-
if (trimmed) {
|
|
810
|
-
bleNamesById.set(deviceId, trimmed);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
function resolveBleName(deviceId: string): string | undefined {
|
|
815
|
-
const live =
|
|
816
|
-
connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
|
|
817
|
-
discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
|
|
818
|
-
return live || bleNamesById.get(deviceId);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Pro2/Neo connect by id first: they are unaffected by the stale-session
|
|
823
|
-
* problem and advertise under Find My names a OneKey-filtered scan cannot
|
|
824
|
-
* match. Everything else — Classic family, and any device whose name is not
|
|
825
|
-
* known yet — scans first, because connecting by id can resolve a session the
|
|
826
|
-
* device no longer serves (see setupConnectionAndDiscoverServices).
|
|
827
|
-
*/
|
|
828
|
-
function shouldConnectByIdFirst(deviceId: string): boolean {
|
|
829
|
-
return isPro2FamilyBleName(resolveBleName(deviceId));
|
|
830
|
-
}
|
|
831
|
-
|
|
832
764
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
833
765
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
834
766
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
@@ -838,7 +770,6 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
838
770
|
|
|
839
771
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
840
772
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
841
|
-
rememberBleName(peripheral.id, peripheral.advertisement?.localName);
|
|
842
773
|
if (isNewDevice) {
|
|
843
774
|
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
844
775
|
deviceId: peripheral.id,
|
|
@@ -873,10 +804,7 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
|
873
804
|
|
|
874
805
|
// Perform targeted scan for a specific device ID
|
|
875
806
|
// Uses self-contained local listener pattern - no global state needed
|
|
876
|
-
async function performTargetedScan(
|
|
877
|
-
targetDeviceId: string,
|
|
878
|
-
timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
|
|
879
|
-
): Promise<Peripheral | null> {
|
|
807
|
+
async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
|
|
880
808
|
if (!noble) {
|
|
881
809
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
882
810
|
}
|
|
@@ -921,7 +849,7 @@ async function performTargetedScan(
|
|
|
921
849
|
const timeoutId = setTimeout(() => {
|
|
922
850
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
923
851
|
finish(null).catch(reject);
|
|
924
|
-
},
|
|
852
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
925
853
|
|
|
926
854
|
// Add local listener for this scan
|
|
927
855
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -1123,17 +1051,9 @@ async function discoverServicesAndCharacteristics(
|
|
|
1123
1051
|
|
|
1124
1052
|
// Main discovery logic as async function
|
|
1125
1053
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
1126
|
-
// Step 1: Discover
|
|
1127
|
-
// narrowing: an unfiltered discovery is answered from the OS GATT cache, so
|
|
1128
|
-
// a device whose stack still advertises and accepts links but no longer
|
|
1129
|
-
// serves its application layer still looks healthy — the link comes up, the
|
|
1130
|
-
// cached services resolve, the protocol frame goes out and nothing ever
|
|
1131
|
-
// answers. A targeted query returns nothing in that state, which is what
|
|
1132
|
-
// 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
|
|
1133
|
-
// scan) — the sequence that brings such a device back. Pro2/Neo expose the
|
|
1134
|
-
// same service UUID, and the selection below only ever accepts that one.
|
|
1054
|
+
// Step 1: Discover ALL services (no filter — Pro2 may use different service UUID)
|
|
1135
1055
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
1136
|
-
peripheral.discoverServices(
|
|
1056
|
+
peripheral.discoverServices([], (error, svc) => {
|
|
1137
1057
|
if (error) {
|
|
1138
1058
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
1139
1059
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -1144,7 +1064,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
1144
1064
|
});
|
|
1145
1065
|
|
|
1146
1066
|
if (!services || services.length === 0) {
|
|
1147
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No
|
|
1067
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
1148
1068
|
}
|
|
1149
1069
|
|
|
1150
1070
|
logger?.debug('[NobleBLE] services discovered', {
|
|
@@ -1328,14 +1248,12 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1328
1248
|
deviceId,
|
|
1329
1249
|
peripheralState: peripheral.state,
|
|
1330
1250
|
attempt: attemptNumber,
|
|
1331
|
-
maxRetries:
|
|
1251
|
+
maxRetries: 5,
|
|
1332
1252
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
1333
1253
|
});
|
|
1334
1254
|
|
|
1335
1255
|
if (attemptNumber > 1) {
|
|
1336
|
-
logger?.info(
|
|
1337
|
-
`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`
|
|
1338
|
-
);
|
|
1256
|
+
logger?.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/5`);
|
|
1339
1257
|
}
|
|
1340
1258
|
|
|
1341
1259
|
// Verify connection state before attempting service discovery
|
|
@@ -1349,32 +1267,20 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1349
1267
|
try {
|
|
1350
1268
|
return await discoverServicesAndCharacteristics(peripheral);
|
|
1351
1269
|
} catch (error) {
|
|
1352
|
-
logger?.error(
|
|
1353
|
-
`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1354
|
-
);
|
|
1270
|
+
logger?.error(`[NobleBLE] No services found (attempt ${attemptNumber}/5)`);
|
|
1355
1271
|
|
|
1356
|
-
if (attemptNumber <
|
|
1357
|
-
logger?.error(
|
|
1358
|
-
`[NobleBLE] Will retry service discovery (attempt ${
|
|
1359
|
-
attemptNumber + 1
|
|
1360
|
-
}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1361
|
-
);
|
|
1272
|
+
if (attemptNumber < 5) {
|
|
1273
|
+
logger?.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/5)`);
|
|
1362
1274
|
}
|
|
1363
1275
|
|
|
1364
1276
|
throw error; // p-retry will handle the retry logic
|
|
1365
1277
|
}
|
|
1366
1278
|
},
|
|
1367
1279
|
{
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
// performs afterwards, so reaching it sooner is what shortens the wait.
|
|
1373
|
-
// The single retry stays for a genuinely transient miss.
|
|
1374
|
-
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
1375
|
-
factor: 1.5,
|
|
1376
|
-
minTimeout: 500,
|
|
1377
|
-
maxTimeout: 3000,
|
|
1280
|
+
retries: 4, // Total 5 attempts (initial + 4 retries)
|
|
1281
|
+
factor: 1.5, // Exponential backoff: 1000ms → 1500ms → 2250ms → 3000ms
|
|
1282
|
+
minTimeout: 1000, // Start with 1 second delay
|
|
1283
|
+
maxTimeout: 3000, // Maximum 3 seconds delay
|
|
1378
1284
|
onFailedAttempt: error => {
|
|
1379
1285
|
// This runs after each failed attempt
|
|
1380
1286
|
logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -1399,45 +1305,26 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1399
1305
|
deviceId: string,
|
|
1400
1306
|
webContents: WebContents
|
|
1401
1307
|
): Promise<CharacteristicPair> {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1405
|
-
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1406
|
-
// reset entirely for this device, because discovery always succeeds: the link
|
|
1407
|
-
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1408
|
-
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1409
|
-
// link with an intact subscription returns before reaching here, so reuse
|
|
1410
|
-
// inside a workflow is unaffected.
|
|
1308
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1309
|
+
|
|
1411
1310
|
try {
|
|
1412
|
-
await
|
|
1413
|
-
} catch (
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1311
|
+
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1312
|
+
} catch (initialError) {
|
|
1313
|
+
logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
1314
|
+
deviceId,
|
|
1315
|
+
error: initialError,
|
|
1316
|
+
});
|
|
1417
1317
|
}
|
|
1418
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1419
1318
|
|
|
1319
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1320
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1420
1321
|
try {
|
|
1421
1322
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1422
|
-
} catch (
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
// one acquire pays ~2s for the rescan, while a thrown error costs the
|
|
1428
|
-
// renderer a full retry round trip.
|
|
1429
|
-
if (peripheral.state === 'connected') {
|
|
1430
|
-
logger?.error(
|
|
1431
|
-
'[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan',
|
|
1432
|
-
discoveryError
|
|
1433
|
-
);
|
|
1434
|
-
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1435
|
-
} else {
|
|
1436
|
-
logger?.error(
|
|
1437
|
-
'[NobleBLE] Service discovery failed, attempting fresh scan...',
|
|
1438
|
-
discoveryError
|
|
1439
|
-
);
|
|
1440
|
-
}
|
|
1323
|
+
} catch (reconnectError) {
|
|
1324
|
+
logger?.error(
|
|
1325
|
+
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1326
|
+
reconnectError
|
|
1327
|
+
);
|
|
1441
1328
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1442
1329
|
}
|
|
1443
1330
|
}
|
|
@@ -1526,7 +1413,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1526
1413
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1527
1414
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1528
1415
|
|
|
1416
|
+
// If device not discovered, try a targeted scan for this specific device
|
|
1529
1417
|
if (!peripheral) {
|
|
1418
|
+
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1419
|
+
|
|
1530
1420
|
// Initialize Noble if not already done
|
|
1531
1421
|
if (!noble) {
|
|
1532
1422
|
await initializeNoble();
|
|
@@ -1536,46 +1426,27 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1536
1426
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1537
1427
|
}
|
|
1538
1428
|
|
|
1539
|
-
//
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
// because they are unaffected and may advertise under an unmatchable Find
|
|
1546
|
-
// My name.
|
|
1547
|
-
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1548
|
-
logger?.info('[NobleBLE] Resolving device for cold connect', {
|
|
1549
|
-
deviceId,
|
|
1550
|
-
name: resolveBleName(deviceId) ?? 'unknown',
|
|
1551
|
-
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1552
|
-
});
|
|
1553
|
-
|
|
1554
|
-
const scanForPeripheral = async () => {
|
|
1555
|
-
try {
|
|
1556
|
-
return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
|
|
1557
|
-
} catch (scanError) {
|
|
1558
|
-
logger?.info('[NobleBLE] Targeted scan failed', {
|
|
1559
|
-
deviceId,
|
|
1560
|
-
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1561
|
-
});
|
|
1562
|
-
return undefined;
|
|
1563
|
-
}
|
|
1564
|
-
};
|
|
1429
|
+
// Reaches a bonded device that is not advertising; else falls to the scan.
|
|
1430
|
+
peripheral = await tryDirectConnectById(deviceId);
|
|
1431
|
+
if (peripheral) {
|
|
1432
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1565
1435
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1436
|
+
if (!peripheral) {
|
|
1437
|
+
// Perform a targeted scan to find the specific device
|
|
1438
|
+
try {
|
|
1439
|
+
const foundPeripheral = await performTargetedScan(deviceId);
|
|
1440
|
+
if (!foundPeripheral) {
|
|
1441
|
+
throw ERRORS.TypedError(
|
|
1442
|
+
HardwareErrorCode.DeviceNotFound,
|
|
1443
|
+
`Device ${deviceId} not found even after targeted scan`
|
|
1444
|
+
);
|
|
1570
1445
|
}
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
if (!peripheral) {
|
|
1576
|
-
// The preferred route came up empty: not advertising (held elsewhere, or
|
|
1577
|
-
// silent), or not reachable by id. Try the other one before giving up.
|
|
1578
|
-
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1446
|
+
peripheral = foundPeripheral;
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
logger?.error('[NobleBLE] Targeted scan failed:', error);
|
|
1449
|
+
throw error;
|
|
1579
1450
|
}
|
|
1580
1451
|
}
|
|
1581
1452
|
|
|
@@ -1584,10 +1455,6 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1584
1455
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1585
1456
|
}
|
|
1586
1457
|
|
|
1587
|
-
// Also covers the connect-by-id route, whose simulated discovery carries no
|
|
1588
|
-
// service UUIDs and therefore never reaches handleDeviceDiscovered.
|
|
1589
|
-
rememberBleName(deviceId, peripheral.advertisement?.localName);
|
|
1590
|
-
|
|
1591
1458
|
logger?.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1592
1459
|
|
|
1593
1460
|
// Check if device is already connected
|
|
@@ -1708,14 +1575,9 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1708
1575
|
resolve();
|
|
1709
1576
|
} catch (setupError) {
|
|
1710
1577
|
logger?.error('[NobleBLE] Connection setup failed:', setupError);
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
// pending forever. disconnectDevice always settles (it no-ops on an
|
|
1715
|
-
// unknown peripheral and caps the confirm wait).
|
|
1716
|
-
disconnectDevice(deviceId)
|
|
1717
|
-
.catch(() => undefined)
|
|
1718
|
-
.then(() => reject(setupError));
|
|
1578
|
+
connectedPeripheral.disconnect(() => {
|
|
1579
|
+
reject(setupError);
|
|
1580
|
+
});
|
|
1719
1581
|
}
|
|
1720
1582
|
});
|
|
1721
1583
|
});
|
|
@@ -1735,18 +1597,11 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1735
1597
|
}
|
|
1736
1598
|
|
|
1737
1599
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1738
|
-
timeoutMs:
|
|
1600
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1739
1601
|
timeoutBehavior: 'resolve',
|
|
1740
1602
|
});
|
|
1741
|
-
if (peripheral.state === 'connected') {
|
|
1742
|
-
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1743
|
-
}
|
|
1744
1603
|
cleanupDevice(deviceId, undefined, {
|
|
1745
1604
|
cleanupConnection: true,
|
|
1746
|
-
// macOS returns zero GATT services when a previously-disconnected
|
|
1747
|
-
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1748
|
-
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1749
|
-
cleanupDiscoveredCache: true,
|
|
1750
1605
|
sendDisconnectEvent: false,
|
|
1751
1606
|
cancelOperations: true,
|
|
1752
1607
|
reason: 'manual-disconnect',
|
|
@@ -1887,14 +1742,9 @@ async function subscribeNotifications(
|
|
|
1887
1742
|
});
|
|
1888
1743
|
}
|
|
1889
1744
|
|
|
1890
|
-
const subscribeStartedAt = Date.now();
|
|
1891
1745
|
try {
|
|
1892
1746
|
await rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1893
1747
|
subscribedDevices.set(deviceId, true);
|
|
1894
|
-
logger?.info('[NobleBLE] Notification subscription active', {
|
|
1895
|
-
deviceId,
|
|
1896
|
-
ms: Date.now() - subscribeStartedAt,
|
|
1897
|
-
});
|
|
1898
1748
|
} finally {
|
|
1899
1749
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
1900
1750
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -2077,12 +1927,10 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
2077
1927
|
}
|
|
2078
1928
|
|
|
2079
1929
|
await stopScanning().catch(() => undefined);
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
2085
|
-
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1930
|
+
if (noble && persistentStateListener) {
|
|
1931
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1932
|
+
persistentStateListener = null;
|
|
1933
|
+
}
|
|
2086
1934
|
cleanupNobleListeners();
|
|
2087
1935
|
discoveredDevices.clear();
|
|
2088
1936
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|