@onekeyfe/hd-transport-electron 1.2.0-alpha.176 → 1.2.0-alpha.177
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-d72435df.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-4ab8a151.js} +105 -50
- 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 +207 -61
- 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-4ab8a151.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-d72435df.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,37 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
590
608
|
}
|
|
591
609
|
});
|
|
592
610
|
}
|
|
611
|
+
const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
|
|
612
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
613
|
+
const bleNamesById = new Map();
|
|
614
|
+
function rememberBleName(deviceId, name) {
|
|
615
|
+
const trimmed = name === null || name === void 0 ? void 0 : name.trim();
|
|
616
|
+
if (trimmed) {
|
|
617
|
+
bleNamesById.set(deviceId, trimmed);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function resolveBleName(deviceId) {
|
|
621
|
+
var _a, _b, _c, _d, _e, _f;
|
|
622
|
+
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()) ||
|
|
623
|
+
((_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());
|
|
624
|
+
return live || bleNamesById.get(deviceId);
|
|
625
|
+
}
|
|
626
|
+
function shouldConnectByIdFirst(deviceId) {
|
|
627
|
+
return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
|
|
628
|
+
}
|
|
593
629
|
function handleDeviceDiscovered(peripheral) {
|
|
594
|
-
var _a, _b;
|
|
630
|
+
var _a, _b, _c;
|
|
595
631
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
596
632
|
return;
|
|
597
633
|
}
|
|
598
634
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
599
635
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
636
|
+
rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
|
|
600
637
|
if (isNewDevice) {
|
|
601
638
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
602
639
|
deviceId: peripheral.id,
|
|
603
|
-
name: ((
|
|
604
|
-
serviceUUIDs: ((
|
|
640
|
+
name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
|
|
641
|
+
serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
|
|
605
642
|
});
|
|
606
643
|
}
|
|
607
644
|
}
|
|
@@ -627,7 +664,7 @@ function waitForNobleScanStop(nobleInstance) {
|
|
|
627
664
|
});
|
|
628
665
|
});
|
|
629
666
|
}
|
|
630
|
-
function performTargetedScan(targetDeviceId) {
|
|
667
|
+
function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
|
|
631
668
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
632
669
|
if (!noble) {
|
|
633
670
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
@@ -667,7 +704,7 @@ function performTargetedScan(targetDeviceId) {
|
|
|
667
704
|
const timeoutId = setTimeout(() => {
|
|
668
705
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
669
706
|
finish(null).catch(reject);
|
|
670
|
-
},
|
|
707
|
+
}, timeoutMs);
|
|
671
708
|
nobleInstance.on('discover', onDiscover);
|
|
672
709
|
nobleInstance.startScanning([], true, (error) => {
|
|
673
710
|
if (error) {
|
|
@@ -802,7 +839,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
802
839
|
});
|
|
803
840
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
804
841
|
const services = yield new Promise((resolve, reject) => {
|
|
805
|
-
peripheral.discoverServices(
|
|
842
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
806
843
|
if (error) {
|
|
807
844
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
808
845
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -813,7 +850,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
813
850
|
});
|
|
814
851
|
});
|
|
815
852
|
if (!services || services.length === 0) {
|
|
816
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
853
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
817
854
|
}
|
|
818
855
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
|
|
819
856
|
deviceId: peripheral.id,
|
|
@@ -930,11 +967,11 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
930
967
|
deviceId,
|
|
931
968
|
peripheralState: peripheral.state,
|
|
932
969
|
attempt: attemptNumber,
|
|
933
|
-
maxRetries:
|
|
970
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
934
971
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
935
972
|
});
|
|
936
973
|
if (attemptNumber > 1) {
|
|
937
|
-
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}
|
|
974
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`);
|
|
938
975
|
}
|
|
939
976
|
if (peripheral.state !== 'connected') {
|
|
940
977
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device not connected: ${peripheral.state}`);
|
|
@@ -943,16 +980,16 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
943
980
|
return yield discoverServicesAndCharacteristics(peripheral);
|
|
944
981
|
}
|
|
945
982
|
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}
|
|
983
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
984
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
985
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
|
|
949
986
|
}
|
|
950
987
|
throw error;
|
|
951
988
|
}
|
|
952
989
|
}), {
|
|
953
|
-
retries:
|
|
990
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
954
991
|
factor: 1.5,
|
|
955
|
-
minTimeout:
|
|
992
|
+
minTimeout: 500,
|
|
956
993
|
maxTimeout: 3000,
|
|
957
994
|
onFailedAttempt: error => {
|
|
958
995
|
logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -966,23 +1003,18 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
966
1003
|
}
|
|
967
1004
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
968
1005
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
969
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
970
1006
|
try {
|
|
971
|
-
|
|
1007
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
972
1008
|
}
|
|
973
|
-
catch (
|
|
974
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
975
|
-
deviceId,
|
|
976
|
-
error: initialError,
|
|
977
|
-
});
|
|
1009
|
+
catch (resetError) {
|
|
1010
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
978
1011
|
}
|
|
979
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
980
1012
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
981
1013
|
try {
|
|
982
1014
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
983
1015
|
}
|
|
984
|
-
catch (
|
|
985
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed
|
|
1016
|
+
catch (discoveryError) {
|
|
1017
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
986
1018
|
return freshScanAndDiscover(deviceId, webContents);
|
|
987
1019
|
}
|
|
988
1020
|
});
|
|
@@ -1042,7 +1074,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1042
1074
|
});
|
|
1043
1075
|
}
|
|
1044
1076
|
function connectDevice(deviceId, webContents) {
|
|
1045
|
-
var _a;
|
|
1077
|
+
var _a, _b, _c;
|
|
1046
1078
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1047
1079
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1048
1080
|
deviceId,
|
|
@@ -1054,34 +1086,47 @@ function connectDevice(deviceId, webContents) {
|
|
|
1054
1086
|
});
|
|
1055
1087
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1056
1088
|
if (!peripheral) {
|
|
1057
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1058
1089
|
if (!noble) {
|
|
1059
1090
|
yield initializeNoble();
|
|
1060
1091
|
}
|
|
1061
1092
|
if (!noble) {
|
|
1062
1093
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1063
1094
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1095
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1096
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
|
|
1097
|
+
deviceId,
|
|
1098
|
+
name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
|
|
1099
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1100
|
+
});
|
|
1101
|
+
const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1102
|
+
var _d;
|
|
1103
|
+
try {
|
|
1104
|
+
return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
|
|
1074
1105
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1106
|
+
catch (scanError) {
|
|
1107
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
|
|
1108
|
+
deviceId,
|
|
1109
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1110
|
+
});
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1115
|
+
const found = yield tryDirectConnectById(deviceId);
|
|
1116
|
+
if (found) {
|
|
1117
|
+
discoveredDevices.set(deviceId, found);
|
|
1118
|
+
}
|
|
1119
|
+
return found;
|
|
1120
|
+
});
|
|
1121
|
+
peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
|
|
1122
|
+
if (!peripheral) {
|
|
1123
|
+
peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
|
|
1080
1124
|
}
|
|
1081
1125
|
}
|
|
1082
1126
|
if (!peripheral) {
|
|
1083
1127
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1084
1128
|
}
|
|
1129
|
+
rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
|
|
1085
1130
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1086
1131
|
if (peripheral.state === 'connected') {
|
|
1087
1132
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
@@ -1176,11 +1221,15 @@ function disconnectDevice(deviceId) {
|
|
|
1176
1221
|
deviceDisconnectListeners.delete(deviceId);
|
|
1177
1222
|
}
|
|
1178
1223
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1179
|
-
timeoutMs:
|
|
1224
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1180
1225
|
timeoutBehavior: 'resolve',
|
|
1181
1226
|
});
|
|
1227
|
+
if (peripheral.state === 'connected') {
|
|
1228
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1229
|
+
}
|
|
1182
1230
|
cleanupDevice(deviceId, undefined, {
|
|
1183
1231
|
cleanupConnection: true,
|
|
1232
|
+
cleanupDiscoveredCache: true,
|
|
1184
1233
|
sendDisconnectEvent: false,
|
|
1185
1234
|
cancelOperations: true,
|
|
1186
1235
|
reason: 'manual-disconnect',
|
|
@@ -1263,6 +1312,11 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1263
1312
|
timeoutBehavior: 'reject',
|
|
1264
1313
|
});
|
|
1265
1314
|
notifyCharacteristic.on('data', (data) => {
|
|
1315
|
+
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Notify frame', {
|
|
1316
|
+
deviceId,
|
|
1317
|
+
bytes: data.length,
|
|
1318
|
+
head: data.subarray(0, 8).toString('hex'),
|
|
1319
|
+
});
|
|
1266
1320
|
if (!pairedDevices.has(deviceId)) {
|
|
1267
1321
|
pairedDevices.add(deviceId);
|
|
1268
1322
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device paired successfully', { deviceId });
|
|
@@ -1271,9 +1325,14 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1271
1325
|
});
|
|
1272
1326
|
});
|
|
1273
1327
|
}
|
|
1328
|
+
const subscribeStartedAt = Date.now();
|
|
1274
1329
|
try {
|
|
1275
1330
|
yield rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1276
1331
|
subscribedDevices.set(deviceId, true);
|
|
1332
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
|
|
1333
|
+
deviceId,
|
|
1334
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1335
|
+
});
|
|
1277
1336
|
}
|
|
1278
1337
|
finally {
|
|
1279
1338
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1391,10 +1450,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1391
1450
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1392
1451
|
}
|
|
1393
1452
|
yield stopScanning().catch(() => undefined);
|
|
1394
|
-
if (noble && persistentStateListener) {
|
|
1395
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1396
|
-
persistentStateListener = null;
|
|
1397
|
-
}
|
|
1398
1453
|
cleanupNobleListeners();
|
|
1399
1454
|
discoveredDevices.clear();
|
|
1400
1455
|
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;AAgvDD,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.177",
|
|
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.177",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.177",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.177",
|
|
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": "294d4c3ce64a2036742447bcfada14eee6c9591e"
|
|
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,44 @@ 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
|
+
// Attempts before a link is written off as unable to serve the OneKey service.
|
|
811
|
+
const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
|
|
812
|
+
|
|
813
|
+
// Advertised names, kept for the life of the process: device caches are purged
|
|
814
|
+
// on every disconnect, but the family a device belongs to never changes and
|
|
815
|
+
// decides which connection strategy is safe for it.
|
|
816
|
+
const bleNamesById = new Map<string, string>();
|
|
817
|
+
|
|
818
|
+
function rememberBleName(deviceId: string, name?: string | null): void {
|
|
819
|
+
const trimmed = name?.trim();
|
|
820
|
+
if (trimmed) {
|
|
821
|
+
bleNamesById.set(deviceId, trimmed);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function resolveBleName(deviceId: string): string | undefined {
|
|
826
|
+
const live =
|
|
827
|
+
connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
|
|
828
|
+
discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
|
|
829
|
+
return live || bleNamesById.get(deviceId);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Pro2/Neo connect by id first: they are unaffected by the stale-session
|
|
834
|
+
* problem and advertise under Find My names a OneKey-filtered scan cannot
|
|
835
|
+
* match. Everything else — Classic family, and any device whose name is not
|
|
836
|
+
* known yet — scans first, because connecting by id can resolve a session the
|
|
837
|
+
* device no longer serves (see setupConnectionAndDiscoverServices).
|
|
838
|
+
*/
|
|
839
|
+
function shouldConnectByIdFirst(deviceId: string): boolean {
|
|
840
|
+
return isPro2FamilyBleName(resolveBleName(deviceId));
|
|
841
|
+
}
|
|
842
|
+
|
|
764
843
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
765
844
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
766
845
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
@@ -770,6 +849,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
770
849
|
|
|
771
850
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
772
851
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
852
|
+
rememberBleName(peripheral.id, peripheral.advertisement?.localName);
|
|
773
853
|
if (isNewDevice) {
|
|
774
854
|
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
775
855
|
deviceId: peripheral.id,
|
|
@@ -804,7 +884,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
|
804
884
|
|
|
805
885
|
// Perform targeted scan for a specific device ID
|
|
806
886
|
// Uses self-contained local listener pattern - no global state needed
|
|
807
|
-
async function performTargetedScan(
|
|
887
|
+
async function performTargetedScan(
|
|
888
|
+
targetDeviceId: string,
|
|
889
|
+
timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
|
|
890
|
+
): Promise<Peripheral | null> {
|
|
808
891
|
if (!noble) {
|
|
809
892
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
810
893
|
}
|
|
@@ -849,7 +932,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
849
932
|
const timeoutId = setTimeout(() => {
|
|
850
933
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
851
934
|
finish(null).catch(reject);
|
|
852
|
-
},
|
|
935
|
+
}, timeoutMs);
|
|
853
936
|
|
|
854
937
|
// Add local listener for this scan
|
|
855
938
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -1051,9 +1134,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
1051
1134
|
|
|
1052
1135
|
// Main discovery logic as async function
|
|
1053
1136
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
1054
|
-
// Step 1: Discover
|
|
1137
|
+
// Step 1: Discover the OneKey service by UUID. Filtering is not just a
|
|
1138
|
+
// narrowing: an unfiltered discovery is answered from the OS GATT cache, so
|
|
1139
|
+
// a device whose stack still advertises and accepts links but no longer
|
|
1140
|
+
// serves its application layer still looks healthy — the link comes up, the
|
|
1141
|
+
// cached services resolve, the protocol frame goes out and nothing ever
|
|
1142
|
+
// answers. A targeted query returns nothing in that state, which is what
|
|
1143
|
+
// 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
|
|
1144
|
+
// scan) — the sequence that brings such a device back. Pro2/Neo expose the
|
|
1145
|
+
// same service UUID, and the selection below only ever accepts that one.
|
|
1055
1146
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
1056
|
-
peripheral.discoverServices(
|
|
1147
|
+
peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
|
|
1057
1148
|
if (error) {
|
|
1058
1149
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
1059
1150
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -1064,7 +1155,7 @@ async function discoverServicesAndCharacteristics(
|
|
|
1064
1155
|
});
|
|
1065
1156
|
|
|
1066
1157
|
if (!services || services.length === 0) {
|
|
1067
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
1158
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
1068
1159
|
}
|
|
1069
1160
|
|
|
1070
1161
|
logger?.debug('[NobleBLE] services discovered', {
|
|
@@ -1248,12 +1339,14 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1248
1339
|
deviceId,
|
|
1249
1340
|
peripheralState: peripheral.state,
|
|
1250
1341
|
attempt: attemptNumber,
|
|
1251
|
-
maxRetries:
|
|
1342
|
+
maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
|
|
1252
1343
|
targetUUIDs: ONEKEY_SERVICE_UUIDS,
|
|
1253
1344
|
});
|
|
1254
1345
|
|
|
1255
1346
|
if (attemptNumber > 1) {
|
|
1256
|
-
logger?.info(
|
|
1347
|
+
logger?.info(
|
|
1348
|
+
`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`
|
|
1349
|
+
);
|
|
1257
1350
|
}
|
|
1258
1351
|
|
|
1259
1352
|
// Verify connection state before attempting service discovery
|
|
@@ -1267,20 +1360,32 @@ async function discoverServicesAndCharacteristicsWithRetry(
|
|
|
1267
1360
|
try {
|
|
1268
1361
|
return await discoverServicesAndCharacteristics(peripheral);
|
|
1269
1362
|
} catch (error) {
|
|
1270
|
-
logger?.error(
|
|
1363
|
+
logger?.error(
|
|
1364
|
+
`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1365
|
+
);
|
|
1271
1366
|
|
|
1272
|
-
if (attemptNumber <
|
|
1273
|
-
logger?.error(
|
|
1367
|
+
if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
|
|
1368
|
+
logger?.error(
|
|
1369
|
+
`[NobleBLE] Will retry service discovery (attempt ${
|
|
1370
|
+
attemptNumber + 1
|
|
1371
|
+
}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
|
|
1372
|
+
);
|
|
1274
1373
|
}
|
|
1275
1374
|
|
|
1276
1375
|
throw error; // p-retry will handle the retry logic
|
|
1277
1376
|
}
|
|
1278
1377
|
},
|
|
1279
1378
|
{
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1379
|
+
// One retry, not four. An empty result here is the signature of a device
|
|
1380
|
+
// whose stack no longer serves its application layer, and retrying the
|
|
1381
|
+
// same link has never recovered it — every observed run failed all the
|
|
1382
|
+
// way through. The recovery is the teardown and cold reconnect the caller
|
|
1383
|
+
// performs afterwards, so reaching it sooner is what shortens the wait.
|
|
1384
|
+
// The single retry stays for a genuinely transient miss.
|
|
1385
|
+
retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
|
|
1386
|
+
factor: 1.5,
|
|
1387
|
+
minTimeout: 500,
|
|
1388
|
+
maxTimeout: 3000,
|
|
1284
1389
|
onFailedAttempt: error => {
|
|
1285
1390
|
// This runs after each failed attempt
|
|
1286
1391
|
logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
|
|
@@ -1305,26 +1410,28 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1305
1410
|
deviceId: string,
|
|
1306
1411
|
webContents: WebContents
|
|
1307
1412
|
): Promise<CharacteristicPair> {
|
|
1308
|
-
|
|
1309
|
-
|
|
1413
|
+
// Reset the link before any protocol traffic reaches the device. 6.5.0 did
|
|
1414
|
+
// this on every cold setup and never produced a protocol-deaf Classic 1S
|
|
1415
|
+
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1416
|
+
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1417
|
+
// reset entirely for this device, because discovery always succeeds: the link
|
|
1418
|
+
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1419
|
+
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1420
|
+
// link with an intact subscription returns before reaching here, so reuse
|
|
1421
|
+
// inside a workflow is unaffected.
|
|
1310
1422
|
try {
|
|
1311
|
-
|
|
1312
|
-
} catch (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1423
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1424
|
+
} catch (resetError) {
|
|
1425
|
+
// A failed reset must not abort the attempt: discovery on the existing
|
|
1426
|
+
// link, then the fresh-scan fallback, still have a chance to recover.
|
|
1427
|
+
logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
1317
1428
|
}
|
|
1318
|
-
|
|
1319
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1320
1429
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1430
|
+
|
|
1321
1431
|
try {
|
|
1322
1432
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1323
|
-
} catch (
|
|
1324
|
-
logger?.error(
|
|
1325
|
-
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1326
|
-
reconnectError
|
|
1327
|
-
);
|
|
1433
|
+
} catch (discoveryError) {
|
|
1434
|
+
logger?.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1328
1435
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1329
1436
|
}
|
|
1330
1437
|
}
|
|
@@ -1413,10 +1520,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1413
1520
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1414
1521
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1415
1522
|
|
|
1416
|
-
// If device not discovered, try a targeted scan for this specific device
|
|
1417
1523
|
if (!peripheral) {
|
|
1418
|
-
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1419
|
-
|
|
1420
1524
|
// Initialize Noble if not already done
|
|
1421
1525
|
if (!noble) {
|
|
1422
1526
|
await initializeNoble();
|
|
@@ -1426,27 +1530,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1426
1530
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1427
1531
|
}
|
|
1428
1532
|
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1533
|
+
// A live advertisement proves the device holds no link and will open a
|
|
1534
|
+
// fresh session; connecting by id resolves through the OS cache instead
|
|
1535
|
+
// and can hand back a session the device no longer serves (see
|
|
1536
|
+
// setupConnectionAndDiscoverServices). Scanning is also the faster of the
|
|
1537
|
+
// two when the device does advertise: ~96ms median against ~784ms for
|
|
1538
|
+
// connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
|
|
1539
|
+
// because they are unaffected and may advertise under an unmatchable Find
|
|
1540
|
+
// My name.
|
|
1541
|
+
const byIdFirst = shouldConnectByIdFirst(deviceId);
|
|
1542
|
+
logger?.info('[NobleBLE] Resolving device for cold connect', {
|
|
1543
|
+
deviceId,
|
|
1544
|
+
name: resolveBleName(deviceId) ?? 'unknown',
|
|
1545
|
+
strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
|
|
1546
|
+
});
|
|
1435
1547
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1548
|
+
const scanForPeripheral = async () => {
|
|
1549
|
+
try {
|
|
1550
|
+
return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
|
|
1551
|
+
} catch (scanError) {
|
|
1552
|
+
logger?.info('[NobleBLE] Targeted scan failed', {
|
|
1553
|
+
deviceId,
|
|
1554
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1555
|
+
});
|
|
1556
|
+
return undefined;
|
|
1445
1557
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
const connectById = async () => {
|
|
1561
|
+
const found = await tryDirectConnectById(deviceId);
|
|
1562
|
+
if (found) {
|
|
1563
|
+
discoveredDevices.set(deviceId, found);
|
|
1564
|
+
}
|
|
1565
|
+
return found;
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
|
|
1569
|
+
if (!peripheral) {
|
|
1570
|
+
// The preferred route came up empty: not advertising (held elsewhere, or
|
|
1571
|
+
// silent), or not reachable by id. Try the other one before giving up.
|
|
1572
|
+
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1450
1573
|
}
|
|
1451
1574
|
}
|
|
1452
1575
|
|
|
@@ -1455,6 +1578,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1455
1578
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
|
|
1456
1579
|
}
|
|
1457
1580
|
|
|
1581
|
+
// Also covers the connect-by-id route, whose simulated discovery carries no
|
|
1582
|
+
// service UUIDs and therefore never reaches handleDeviceDiscovered.
|
|
1583
|
+
rememberBleName(deviceId, peripheral.advertisement?.localName);
|
|
1584
|
+
|
|
1458
1585
|
logger?.info('[NobleBLE] Connecting to device:', deviceId);
|
|
1459
1586
|
|
|
1460
1587
|
// Check if device is already connected
|
|
@@ -1597,11 +1724,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1597
1724
|
}
|
|
1598
1725
|
|
|
1599
1726
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1600
|
-
timeoutMs:
|
|
1727
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1601
1728
|
timeoutBehavior: 'resolve',
|
|
1602
1729
|
});
|
|
1730
|
+
if (peripheral.state === 'connected') {
|
|
1731
|
+
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1732
|
+
}
|
|
1603
1733
|
cleanupDevice(deviceId, undefined, {
|
|
1604
1734
|
cleanupConnection: true,
|
|
1735
|
+
// macOS returns zero GATT services when a previously-disconnected
|
|
1736
|
+
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1737
|
+
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1738
|
+
cleanupDiscoveredCache: true,
|
|
1605
1739
|
sendDisconnectEvent: false,
|
|
1606
1740
|
cancelOperations: true,
|
|
1607
1741
|
reason: 'manual-disconnect',
|
|
@@ -1732,6 +1866,11 @@ async function subscribeNotifications(
|
|
|
1732
1866
|
});
|
|
1733
1867
|
|
|
1734
1868
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
1869
|
+
logger?.debug('[NobleBLE] Notify frame', {
|
|
1870
|
+
deviceId,
|
|
1871
|
+
bytes: data.length,
|
|
1872
|
+
head: data.subarray(0, 8).toString('hex'),
|
|
1873
|
+
});
|
|
1735
1874
|
// Windows BLE pairing detection: receiving any data means device is paired
|
|
1736
1875
|
if (!pairedDevices.has(deviceId)) {
|
|
1737
1876
|
pairedDevices.add(deviceId);
|
|
@@ -1742,9 +1881,14 @@ async function subscribeNotifications(
|
|
|
1742
1881
|
});
|
|
1743
1882
|
}
|
|
1744
1883
|
|
|
1884
|
+
const subscribeStartedAt = Date.now();
|
|
1745
1885
|
try {
|
|
1746
1886
|
await rebuildAppSubscription(deviceId, notifyCharacteristic);
|
|
1747
1887
|
subscribedDevices.set(deviceId, true);
|
|
1888
|
+
logger?.info('[NobleBLE] Notification subscription active', {
|
|
1889
|
+
deviceId,
|
|
1890
|
+
ms: Date.now() - subscribeStartedAt,
|
|
1891
|
+
});
|
|
1748
1892
|
} finally {
|
|
1749
1893
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
1750
1894
|
subscriptionOperations.set(deviceId, 'idle');
|
|
@@ -1927,10 +2071,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1927
2071
|
}
|
|
1928
2072
|
|
|
1929
2073
|
await stopScanning().catch(() => undefined);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2074
|
+
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
2075
|
+
// destroy handler also fires on a renderer soft restart, and removing
|
|
2076
|
+
// the listener here permanently killed poweredOff/poweredOn
|
|
2077
|
+
// reconciliation for the rest of the process (nothing re-registers it:
|
|
2078
|
+
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
2079
|
+
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1934
2080
|
cleanupNobleListeners();
|
|
1935
2081
|
discoveredDevices.clear();
|
|
1936
2082
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|