@onekeyfe/hd-transport-electron 1.2.0-alpha.172 → 1.2.0-alpha.174
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-b37d926c.js → index-2b9e4f42.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-0fb76826.js} +97 -43
- 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/noble-ble-handler.ts +181 -52
- package/src/types/noble-extended.ts +1 -0
|
@@ -32,7 +32,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
32
32
|
|
|
33
33
|
function initNobleBleSupport(webContents) {
|
|
34
34
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
-
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
35
|
+
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-0fb76826.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-2b9e4f42.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -116,7 +116,8 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
116
116
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
117
117
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
118
118
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
119
|
-
const
|
|
119
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
120
|
+
const BLE_IDLE_DISCONNECT_MS = 20000;
|
|
120
121
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
121
122
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
122
123
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
@@ -242,6 +243,7 @@ function initializeNoble() {
|
|
|
242
243
|
try {
|
|
243
244
|
noble = require('@stoprocent/noble');
|
|
244
245
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
|
|
246
|
+
setupPersistentStateListener();
|
|
245
247
|
yield new Promise((resolve, reject) => {
|
|
246
248
|
if (!noble) {
|
|
247
249
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
|
|
@@ -251,7 +253,6 @@ function initializeNoble() {
|
|
|
251
253
|
resolve();
|
|
252
254
|
return;
|
|
253
255
|
}
|
|
254
|
-
setupPersistentStateListener();
|
|
255
256
|
const timeout = setTimeout(() => {
|
|
256
257
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
|
|
257
258
|
}, BLUETOOTH_INIT_TIMEOUT);
|
|
@@ -336,7 +337,14 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
336
337
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
337
338
|
const peripheral = connectedDevices.get(deviceId);
|
|
338
339
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
339
|
-
const pending =
|
|
340
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
341
|
+
.catch(error => {
|
|
342
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
343
|
+
deviceId,
|
|
344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
345
|
+
});
|
|
346
|
+
})
|
|
347
|
+
.then(() => disconnectDevice(deviceId))
|
|
340
348
|
.then(() => {
|
|
341
349
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
342
350
|
id: deviceId,
|
|
@@ -409,6 +417,7 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
409
417
|
});
|
|
410
418
|
cleanupDevice(deviceId, webContents, {
|
|
411
419
|
cleanupConnection: true,
|
|
420
|
+
cleanupDiscoveredCache: true,
|
|
412
421
|
sendDisconnectEvent: true,
|
|
413
422
|
cancelOperations: true,
|
|
414
423
|
reason: 'auto-disconnect',
|
|
@@ -536,6 +545,7 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
|
|
|
536
545
|
});
|
|
537
546
|
}
|
|
538
547
|
function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
548
|
+
var _a;
|
|
539
549
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
540
550
|
const characteristics = deviceCharacteristics.get(deviceId);
|
|
541
551
|
const peripheral = connectedDevices.get(deviceId);
|
|
@@ -547,6 +557,14 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
547
557
|
const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
|
|
548
558
|
const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
|
|
549
559
|
const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
|
|
560
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Write frame', {
|
|
561
|
+
deviceId,
|
|
562
|
+
bytes: toBuffer.length,
|
|
563
|
+
head: toBuffer.subarray(0, 8).toString('hex'),
|
|
564
|
+
packetCapacity,
|
|
565
|
+
mtu: (_a = peripheral.mtu) !== null && _a !== void 0 ? _a : null,
|
|
566
|
+
pacingDelayMs,
|
|
567
|
+
});
|
|
550
568
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
551
569
|
const writeCharacteristic = doGetWriteCharacteristic();
|
|
552
570
|
if (!writeCharacteristic) {
|
|
@@ -590,18 +608,36 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
590
608
|
}
|
|
591
609
|
});
|
|
592
610
|
}
|
|
611
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
612
|
+
const bleNamesById = new Map();
|
|
613
|
+
function rememberBleName(deviceId, name) {
|
|
614
|
+
const trimmed = name === null || name === void 0 ? void 0 : name.trim();
|
|
615
|
+
if (trimmed) {
|
|
616
|
+
bleNamesById.set(deviceId, trimmed);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function resolveBleName(deviceId) {
|
|
620
|
+
var _a, _b, _c, _d, _e, _f;
|
|
621
|
+
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()) ||
|
|
622
|
+
((_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());
|
|
623
|
+
return live || bleNamesById.get(deviceId);
|
|
624
|
+
}
|
|
625
|
+
function shouldConnectByIdFirst(deviceId) {
|
|
626
|
+
return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
|
|
627
|
+
}
|
|
593
628
|
function handleDeviceDiscovered(peripheral) {
|
|
594
|
-
var _a, _b;
|
|
629
|
+
var _a, _b, _c;
|
|
595
630
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
596
631
|
return;
|
|
597
632
|
}
|
|
598
633
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
599
634
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
635
|
+
rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
|
|
600
636
|
if (isNewDevice) {
|
|
601
637
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
602
638
|
deviceId: peripheral.id,
|
|
603
|
-
name: ((
|
|
604
|
-
serviceUUIDs: ((
|
|
639
|
+
name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
|
|
640
|
+
serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
|
|
605
641
|
});
|
|
606
642
|
}
|
|
607
643
|
}
|
|
@@ -627,7 +663,7 @@ function waitForNobleScanStop(nobleInstance) {
|
|
|
627
663
|
});
|
|
628
664
|
});
|
|
629
665
|
}
|
|
630
|
-
function performTargetedScan(targetDeviceId) {
|
|
666
|
+
function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
|
|
631
667
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
632
668
|
if (!noble) {
|
|
633
669
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
@@ -667,7 +703,7 @@ function performTargetedScan(targetDeviceId) {
|
|
|
667
703
|
const timeoutId = setTimeout(() => {
|
|
668
704
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
669
705
|
finish(null).catch(reject);
|
|
670
|
-
},
|
|
706
|
+
}, timeoutMs);
|
|
671
707
|
nobleInstance.on('discover', onDiscover);
|
|
672
708
|
nobleInstance.startScanning([], true, (error) => {
|
|
673
709
|
if (error) {
|
|
@@ -802,7 +838,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
802
838
|
});
|
|
803
839
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
804
840
|
const services = yield new Promise((resolve, reject) => {
|
|
805
|
-
peripheral.discoverServices(
|
|
841
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
806
842
|
if (error) {
|
|
807
843
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
808
844
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -813,7 +849,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
813
849
|
});
|
|
814
850
|
});
|
|
815
851
|
if (!services || services.length === 0) {
|
|
816
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
852
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
817
853
|
}
|
|
818
854
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
|
|
819
855
|
deviceId: peripheral.id,
|
|
@@ -966,23 +1002,18 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
966
1002
|
}
|
|
967
1003
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
968
1004
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
969
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
970
1005
|
try {
|
|
971
|
-
|
|
1006
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
972
1007
|
}
|
|
973
|
-
catch (
|
|
974
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
975
|
-
deviceId,
|
|
976
|
-
error: initialError,
|
|
977
|
-
});
|
|
1008
|
+
catch (resetError) {
|
|
1009
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
978
1010
|
}
|
|
979
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
980
1011
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
981
1012
|
try {
|
|
982
1013
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
983
1014
|
}
|
|
984
|
-
catch (
|
|
985
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed
|
|
1015
|
+
catch (discoveryError) {
|
|
1016
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
986
1017
|
return freshScanAndDiscover(deviceId, webContents);
|
|
987
1018
|
}
|
|
988
1019
|
});
|
|
@@ -1042,7 +1073,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1042
1073
|
});
|
|
1043
1074
|
}
|
|
1044
1075
|
function connectDevice(deviceId, webContents) {
|
|
1045
|
-
var _a;
|
|
1076
|
+
var _a, _b, _c;
|
|
1046
1077
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1047
1078
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1048
1079
|
deviceId,
|
|
@@ -1054,34 +1085,47 @@ function connectDevice(deviceId, webContents) {
|
|
|
1054
1085
|
});
|
|
1055
1086
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1056
1087
|
if (!peripheral) {
|
|
1057
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1058
1088
|
if (!noble) {
|
|
1059
1089
|
yield initializeNoble();
|
|
1060
1090
|
}
|
|
1061
1091
|
if (!noble) {
|
|
1062
1092
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1063
1093
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1094
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1095
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
|
|
1096
|
+
deviceId,
|
|
1097
|
+
name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
|
|
1098
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1099
|
+
});
|
|
1100
|
+
const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1101
|
+
var _d;
|
|
1102
|
+
try {
|
|
1103
|
+
return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
|
|
1074
1104
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1105
|
+
catch (scanError) {
|
|
1106
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
|
|
1107
|
+
deviceId,
|
|
1108
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1109
|
+
});
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1114
|
+
const found = yield tryDirectConnectById(deviceId);
|
|
1115
|
+
if (found) {
|
|
1116
|
+
discoveredDevices.set(deviceId, found);
|
|
1117
|
+
}
|
|
1118
|
+
return found;
|
|
1119
|
+
});
|
|
1120
|
+
peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
|
|
1121
|
+
if (!peripheral) {
|
|
1122
|
+
peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
|
|
1080
1123
|
}
|
|
1081
1124
|
}
|
|
1082
1125
|
if (!peripheral) {
|
|
1083
1126
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1084
1127
|
}
|
|
1128
|
+
rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
|
|
1085
1129
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1086
1130
|
if (peripheral.state === 'connected') {
|
|
1087
1131
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
@@ -1176,11 +1220,15 @@ function disconnectDevice(deviceId) {
|
|
|
1176
1220
|
deviceDisconnectListeners.delete(deviceId);
|
|
1177
1221
|
}
|
|
1178
1222
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1179
|
-
timeoutMs:
|
|
1223
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1180
1224
|
timeoutBehavior: 'resolve',
|
|
1181
1225
|
});
|
|
1226
|
+
if (peripheral.state === 'connected') {
|
|
1227
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1228
|
+
}
|
|
1182
1229
|
cleanupDevice(deviceId, undefined, {
|
|
1183
1230
|
cleanupConnection: true,
|
|
1231
|
+
cleanupDiscoveredCache: true,
|
|
1184
1232
|
sendDisconnectEvent: false,
|
|
1185
1233
|
cancelOperations: true,
|
|
1186
1234
|
reason: 'manual-disconnect',
|
|
@@ -1263,6 +1311,11 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1263
1311
|
timeoutBehavior: 'reject',
|
|
1264
1312
|
});
|
|
1265
1313
|
notifyCharacteristic.on('data', (data) => {
|
|
1314
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Notify frame', {
|
|
1315
|
+
deviceId,
|
|
1316
|
+
bytes: data.length,
|
|
1317
|
+
head: data.subarray(0, 8).toString('hex'),
|
|
1318
|
+
});
|
|
1266
1319
|
if (!pairedDevices.has(deviceId)) {
|
|
1267
1320
|
pairedDevices.add(deviceId);
|
|
1268
1321
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device paired successfully', { deviceId });
|
|
@@ -1271,9 +1324,14 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1271
1324
|
});
|
|
1272
1325
|
});
|
|
1273
1326
|
}
|
|
1327
|
+
const subscribeStartedAt = Date.now();
|
|
1274
1328
|
try {
|
|
1275
1329
|
yield rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1276
1330
|
subscribedDevices.set(deviceId, true);
|
|
1331
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
|
|
1332
|
+
deviceId,
|
|
1333
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1334
|
+
});
|
|
1277
1335
|
}
|
|
1278
1336
|
finally {
|
|
1279
1337
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1391,10 +1449,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1391
1449
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1392
1450
|
}
|
|
1393
1451
|
yield stopScanning().catch(() => undefined);
|
|
1394
|
-
if (noble && persistentStateListener) {
|
|
1395
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1396
|
-
persistentStateListener = null;
|
|
1397
|
-
}
|
|
1398
1452
|
cleanupNobleListeners();
|
|
1399
1453
|
discoveredDevices.clear();
|
|
1400
1454
|
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;AA+tDD,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.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.174",
|
|
4
4
|
"author": "OneKey",
|
|
5
5
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"electron-log": ">=4.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.174",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.174",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.174",
|
|
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": "ea906ff2ef191338cbc98d083e8217ddfb23c92c"
|
|
40
40
|
}
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
createKnownBleUuidAliases,
|
|
17
17
|
hasOnekeyCommunicationService,
|
|
18
18
|
isOnekeyBluetoothDevice,
|
|
19
|
+
isPro2FamilyBleName,
|
|
19
20
|
matchesKnownBleUuid,
|
|
20
21
|
wait,
|
|
21
22
|
} from '@onekeyfe/hd-shared';
|
|
@@ -90,8 +91,18 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
90
91
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
91
92
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
92
93
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
94
|
+
// A physical teardown must actually reach the OS before the device can return
|
|
95
|
+
// to a clean state; 250ms routinely declared success while CoreBluetooth was
|
|
96
|
+
// still disconnecting, leaving no trace when the teardown never completed.
|
|
97
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
93
98
|
// Renderer release is logical only; this timer physically frees the device.
|
|
94
|
-
|
|
99
|
+
// Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped
|
|
100
|
+
// after sitting idle for minutes (field logs 2026-08-19: every deaf window
|
|
101
|
+
// followed a 3-minute-idle disconnect, while disconnects right after traffic
|
|
102
|
+
// have never produced one across 6.5.0's per-call teardown history). A hot
|
|
103
|
+
// disconnect ~20s after the last operation stays inside the proven-safe
|
|
104
|
+
// pattern and also shrinks the window in which phones cannot see the device.
|
|
105
|
+
const BLE_IDLE_DISCONNECT_MS = 20_000;
|
|
95
106
|
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
96
107
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
97
108
|
|
|
@@ -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',
|
|
@@ -703,6 +733,17 @@ async function transmitHexDataToDevice(
|
|
|
703
733
|
);
|
|
704
734
|
const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
|
|
705
735
|
|
|
736
|
+
// Diagnostics: the frame header alone (report id + magic + message type +
|
|
737
|
+
// length) identifies which protocol frame went out, without logging payload.
|
|
738
|
+
logger?.info('[NobleBLE] Write frame', {
|
|
739
|
+
deviceId,
|
|
740
|
+
bytes: toBuffer.length,
|
|
741
|
+
head: toBuffer.subarray(0, 8).toString('hex'),
|
|
742
|
+
packetCapacity,
|
|
743
|
+
mtu: peripheral.mtu ?? null,
|
|
744
|
+
pacingDelayMs,
|
|
745
|
+
});
|
|
746
|
+
|
|
706
747
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
707
748
|
// macOS / Linux or already paired on Windows: direct write
|
|
708
749
|
const writeCharacteristic = doGetWriteCharacteristic();
|
|
@@ -761,6 +802,41 @@ async function transmitHexDataToDevice(
|
|
|
761
802
|
}
|
|
762
803
|
|
|
763
804
|
// Handle discovered device (for general enumeration only)
|
|
805
|
+
// A scan that finds nothing costs this much before the direct-connect
|
|
806
|
+
// fallback runs. Every advertisement in the 6.5.0 control logs arrived within
|
|
807
|
+
// 631ms, so this leaves ~2x headroom while keeping a miss cheap.
|
|
808
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
809
|
+
|
|
810
|
+
// Advertised names, kept for the life of the process: device caches are purged
|
|
811
|
+
// on every disconnect, but the family a device belongs to never changes and
|
|
812
|
+
// decides which connection strategy is safe for it.
|
|
813
|
+
const bleNamesById = new Map<string, string>();
|
|
814
|
+
|
|
815
|
+
function rememberBleName(deviceId: string, name?: string | null): void {
|
|
816
|
+
const trimmed = name?.trim();
|
|
817
|
+
if (trimmed) {
|
|
818
|
+
bleNamesById.set(deviceId, trimmed);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function resolveBleName(deviceId: string): string | undefined {
|
|
823
|
+
const live =
|
|
824
|
+
connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
|
|
825
|
+
discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
|
|
826
|
+
return live || bleNamesById.get(deviceId);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Pro2/Neo connect by id first: they are unaffected by the stale-session
|
|
831
|
+
* problem and advertise under Find My names a OneKey-filtered scan cannot
|
|
832
|
+
* match. Everything else — Classic family, and any device whose name is not
|
|
833
|
+
* known yet — scans first, because connecting by id can resolve a session the
|
|
834
|
+
* device no longer serves (see setupConnectionAndDiscoverServices).
|
|
835
|
+
*/
|
|
836
|
+
function shouldConnectByIdFirst(deviceId: string): boolean {
|
|
837
|
+
return isPro2FamilyBleName(resolveBleName(deviceId));
|
|
838
|
+
}
|
|
839
|
+
|
|
764
840
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
765
841
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
766
842
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
@@ -770,6 +846,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
770
846
|
|
|
771
847
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
772
848
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
849
|
+
rememberBleName(peripheral.id, peripheral.advertisement?.localName);
|
|
773
850
|
if (isNewDevice) {
|
|
774
851
|
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
775
852
|
deviceId: peripheral.id,
|
|
@@ -804,7 +881,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
|
804
881
|
|
|
805
882
|
// Perform targeted scan for a specific device ID
|
|
806
883
|
// Uses self-contained local listener pattern - no global state needed
|
|
807
|
-
async function performTargetedScan(
|
|
884
|
+
async function performTargetedScan(
|
|
885
|
+
targetDeviceId: string,
|
|
886
|
+
timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
|
|
887
|
+
): Promise<Peripheral | null> {
|
|
808
888
|
if (!noble) {
|
|
809
889
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
810
890
|
}
|
|
@@ -849,7 +929,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
849
929
|
const timeoutId = setTimeout(() => {
|
|
850
930
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
851
931
|
finish(null).catch(reject);
|
|
852
|
-
},
|
|
932
|
+
}, timeoutMs);
|
|
853
933
|
|
|
854
934
|
// Add local listener for this scan
|
|
855
935
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -1051,9 +1131,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
1051
1131
|
|
|
1052
1132
|
// Main discovery logic as async function
|
|
1053
1133
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
1054
|
-
// Step 1: Discover
|
|
1134
|
+
// Step 1: Discover the OneKey service by UUID. Filtering is not just a
|
|
1135
|
+
// narrowing: an unfiltered discovery is answered from the OS GATT cache, so
|
|
1136
|
+
// a device whose stack still advertises and accepts links but no longer
|
|
1137
|
+
// serves its application layer still looks healthy — the link comes up, the
|
|
1138
|
+
// cached services resolve, the protocol frame goes out and nothing ever
|
|
1139
|
+
// answers. A targeted query returns nothing in that state, which is what
|
|
1140
|
+
// 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
|
|
1141
|
+
// scan) — the sequence that brings such a device back. Pro2/Neo expose the
|
|
1142
|
+
// same service UUID, and the selection below only ever accepts that one.
|
|
1055
1143
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
1056
|
-
peripheral.discoverServices(
|
|
1144
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
1057
1145
|
if (error) {
|
|
1058
1146
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
1059
1147
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -1064,7 +1152,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
1064
1152
|
});
|
|
1065
1153
|
|
|
1066
1154
|
if (!services || services.length === 0) {
|
|
1067
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
1155
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
1068
1156
|
}
|
|
1069
1157
|
|
|
1070
1158
|
logger?.debug('[NobleBLE] services discovered', {
|
|
@@ -1305,26 +1393,28 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1305
1393
|
deviceId: string,
|
|
1306
1394
|
webContents: WebContents
|
|
1307
1395
|
): Promise<CharacteristicPair> {
|
|
1308
|
-
|
|
1309
|
-
|
|
1396
|
+
// Reset the link before any protocol traffic reaches the device. 6.5.0 did
|
|
1397
|
+
// this on every cold setup and never produced a protocol-deaf Classic 1S
|
|
1398
|
+
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1399
|
+
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1400
|
+
// reset entirely for this device, because discovery always succeeds: the link
|
|
1401
|
+
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1402
|
+
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1403
|
+
// link with an intact subscription returns before reaching here, so reuse
|
|
1404
|
+
// inside a workflow is unaffected.
|
|
1310
1405
|
try {
|
|
1311
|
-
|
|
1312
|
-
} catch (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1406
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1407
|
+
} catch (resetError) {
|
|
1408
|
+
// A failed reset must not abort the attempt: discovery on the existing
|
|
1409
|
+
// link, then the fresh-scan fallback, still have a chance to recover.
|
|
1410
|
+
logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
1317
1411
|
}
|
|
1318
|
-
|
|
1319
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1320
1412
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1413
|
+
|
|
1321
1414
|
try {
|
|
1322
1415
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1323
|
-
} catch (
|
|
1324
|
-
logger?.error(
|
|
1325
|
-
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1326
|
-
reconnectError
|
|
1327
|
-
);
|
|
1416
|
+
} catch (discoveryError) {
|
|
1417
|
+
logger?.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1328
1418
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1329
1419
|
}
|
|
1330
1420
|
}
|
|
@@ -1413,10 +1503,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1413
1503
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1414
1504
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1415
1505
|
|
|
1416
|
-
// If device not discovered, try a targeted scan for this specific device
|
|
1417
1506
|
if (!peripheral) {
|
|
1418
|
-
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1419
|
-
|
|
1420
1507
|
// Initialize Noble if not already done
|
|
1421
1508
|
if (!noble) {
|
|
1422
1509
|
await initializeNoble();
|
|
@@ -1426,27 +1513,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1426
1513
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1427
1514
|
}
|
|
1428
1515
|
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1516
|
+
// A live advertisement proves the device holds no link and will open a
|
|
1517
|
+
// fresh session; connecting by id resolves through the OS cache instead
|
|
1518
|
+
// and can hand back a session the device no longer serves (see
|
|
1519
|
+
// setupConnectionAndDiscoverServices). Scanning is also the faster of the
|
|
1520
|
+
// two when the device does advertise: ~96ms median against ~784ms for
|
|
1521
|
+
// connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
|
|
1522
|
+
// because they are unaffected and may advertise under an unmatchable Find
|
|
1523
|
+
// My name.
|
|
1524
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1525
|
+
logger?.info('[NobleBLE] Resolving device for cold connect', {
|
|
1526
|
+
deviceId,
|
|
1527
|
+
name: resolveBleName(deviceId) ?? 'unknown',
|
|
1528
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1529
|
+
});
|
|
1435
1530
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1531
|
+
const scanForPeripheral = async () => {
|
|
1532
|
+
try {
|
|
1533
|
+
return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
|
|
1534
|
+
} catch (scanError) {
|
|
1535
|
+
logger?.info('[NobleBLE] Targeted scan failed', {
|
|
1536
|
+
deviceId,
|
|
1537
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1538
|
+
});
|
|
1539
|
+
return undefined;
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
const connectById = async () => {
|
|
1544
|
+
const found = await tryDirectConnectById(deviceId);
|
|
1545
|
+
if (found) {
|
|
1546
|
+
discoveredDevices.set(deviceId, found);
|
|
1445
1547
|
}
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1548
|
+
return found;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
|
|
1552
|
+
if (!peripheral) {
|
|
1553
|
+
// The preferred route came up empty: not advertising (held elsewhere, or
|
|
1554
|
+
// silent), or not reachable by id. Try the other one before giving up.
|
|
1555
|
+
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1450
1556
|
}
|
|
1451
1557
|
}
|
|
1452
1558
|
|
|
@@ -1455,6 +1561,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1455
1561
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1456
1562
|
}
|
|
1457
1563
|
|
|
1564
|
+
// Also covers the connect-by-id route, whose simulated discovery carries no
|
|
1565
|
+
// service UUIDs and therefore never reaches handleDeviceDiscovered.
|
|
1566
|
+
rememberBleName(deviceId, peripheral.advertisement?.localName);
|
|
1567
|
+
|
|
1458
1568
|
logger?.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1459
1569
|
|
|
1460
1570
|
// Check if device is already connected
|
|
@@ -1597,11 +1707,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1597
1707
|
}
|
|
1598
1708
|
|
|
1599
1709
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1600
|
-
timeoutMs:
|
|
1710
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1601
1711
|
timeoutBehavior: 'resolve',
|
|
1602
1712
|
});
|
|
1713
|
+
if (peripheral.state === 'connected') {
|
|
1714
|
+
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1715
|
+
}
|
|
1603
1716
|
cleanupDevice(deviceId, undefined, {
|
|
1604
1717
|
cleanupConnection: true,
|
|
1718
|
+
// macOS returns zero GATT services when a previously-disconnected
|
|
1719
|
+
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1720
|
+
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1721
|
+
cleanupDiscoveredCache: true,
|
|
1605
1722
|
sendDisconnectEvent: false,
|
|
1606
1723
|
cancelOperations: true,
|
|
1607
1724
|
reason: 'manual-disconnect',
|
|
@@ -1732,6 +1849,11 @@ async function subscribeNotifications(
|
|
|
1732
1849
|
});
|
|
1733
1850
|
|
|
1734
1851
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
1852
|
+
logger?.debug('[NobleBLE] Notify frame', {
|
|
1853
|
+
deviceId,
|
|
1854
|
+
bytes: data.length,
|
|
1855
|
+
head: data.subarray(0, 8).toString('hex'),
|
|
1856
|
+
});
|
|
1735
1857
|
// Windows BLE pairing detection: receiving any data means device is paired
|
|
1736
1858
|
if (!pairedDevices.has(deviceId)) {
|
|
1737
1859
|
pairedDevices.add(deviceId);
|
|
@@ -1742,9 +1864,14 @@ async function subscribeNotifications(
|
|
|
1742
1864
|
});
|
|
1743
1865
|
}
|
|
1744
1866
|
|
|
1867
|
+
const subscribeStartedAt = Date.now();
|
|
1745
1868
|
try {
|
|
1746
1869
|
await rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1747
1870
|
subscribedDevices.set(deviceId, true);
|
|
1871
|
+
logger?.info('[NobleBLE] Notification subscription active', {
|
|
1872
|
+
deviceId,
|
|
1873
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1874
|
+
});
|
|
1748
1875
|
} finally {
|
|
1749
1876
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
1750
1877
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1927,10 +2054,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1927
2054
|
}
|
|
1928
2055
|
|
|
1929
2056
|
await stopScanning().catch(() => undefined);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2057
|
+
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
2058
|
+
// destroy handler also fires on a renderer soft restart, and removing
|
|
2059
|
+
// the listener here permanently killed poweredOff/poweredOn
|
|
2060
|
+
// reconciliation for the rest of the process (nothing re-registers it:
|
|
2061
|
+
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
2062
|
+
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1934
2063
|
cleanupNobleListeners();
|
|
1935
2064
|
discoveredDevices.clear();
|
|
1936
2065
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|