@onekeyfe/hd-transport-electron 1.2.0-alpha.74 → 1.2.0-alpha.75
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-4f7dc0a5.js → index-6da32f94.js} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-3bd9cd3e.js → noble-ble-handler-ce551d9a.js} +186 -32
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/types/desktop-api.d.ts +1 -0
- package/dist/types/desktop-api.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +15 -0
- package/src/noble-ble-handler.ts +242 -38
- package/src/types/desktop-api.ts +3 -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-ce551d9a.js'); });
|
|
36
36
|
setupNobleBleHandlers(webContents);
|
|
37
37
|
});
|
|
38
38
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ interface NobleBleAPI {
|
|
|
45
45
|
mtu?: number;
|
|
46
46
|
} | null>;
|
|
47
47
|
connect: (uuid: string) => Promise<void>;
|
|
48
|
+
release?: (uuid: string, keepSession?: boolean) => Promise<void>;
|
|
48
49
|
disconnect: (uuid: string) => Promise<void>;
|
|
49
50
|
subscribe: (uuid: string) => Promise<void>;
|
|
50
51
|
unsubscribe: (uuid: string) => Promise<void>;
|
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-6da32f94.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -116,6 +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 BLE_IDLE_DISCONNECT_MS = 3 * 60000;
|
|
120
|
+
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
119
121
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
120
122
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
121
123
|
const DEFAULT_WRITE_PACING_DELAY_MS = 5;
|
|
@@ -296,6 +298,62 @@ function initializeNoble() {
|
|
|
296
298
|
}
|
|
297
299
|
});
|
|
298
300
|
}
|
|
301
|
+
const idleDisconnectTimers = new Map();
|
|
302
|
+
const idleDisconnectInFlight = new Map();
|
|
303
|
+
function clearIdleDisconnect(deviceId) {
|
|
304
|
+
const timer = idleDisconnectTimers.get(deviceId);
|
|
305
|
+
if (timer) {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
idleDisconnectTimers.delete(deviceId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function awaitIdleDisconnect(deviceId) {
|
|
311
|
+
return index.__awaiter(this, void 0, void 0, function* () {
|
|
312
|
+
const pending = idleDisconnectInFlight.get(deviceId);
|
|
313
|
+
if (pending)
|
|
314
|
+
yield pending.catch(() => undefined);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
function broadcastToAllWebContents(channel, payload) {
|
|
318
|
+
try {
|
|
319
|
+
const { webContents: electronWebContents } = require('electron');
|
|
320
|
+
for (const wc of electronWebContents.getAllWebContents()) {
|
|
321
|
+
if (!wc.isDestroyed())
|
|
322
|
+
wc.send(channel, payload);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] broadcast failed:', { channel, error: String(error) });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
|
|
330
|
+
clearIdleDisconnect(deviceId);
|
|
331
|
+
idleDisconnectTimers.set(deviceId, setTimeout(() => {
|
|
332
|
+
var _a;
|
|
333
|
+
idleDisconnectTimers.delete(deviceId);
|
|
334
|
+
if (!connectedDevices.has(deviceId))
|
|
335
|
+
return;
|
|
336
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
337
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
338
|
+
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 = disconnectDevice(deviceId)
|
|
340
|
+
.then(() => {
|
|
341
|
+
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
342
|
+
id: deviceId,
|
|
343
|
+
name: deviceName,
|
|
344
|
+
});
|
|
345
|
+
})
|
|
346
|
+
.catch(error => {
|
|
347
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Keep-alive disconnect failed', { deviceId, error });
|
|
348
|
+
})
|
|
349
|
+
.finally(() => {
|
|
350
|
+
if (idleDisconnectInFlight.get(deviceId) === pending) {
|
|
351
|
+
idleDisconnectInFlight.delete(deviceId);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
idleDisconnectInFlight.set(deviceId, pending);
|
|
355
|
+
}, ms));
|
|
356
|
+
}
|
|
299
357
|
function cleanupDevice(deviceId, webContents, options = {}) {
|
|
300
358
|
var _a;
|
|
301
359
|
const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', } = options;
|
|
@@ -307,6 +365,7 @@ function cleanupDevice(deviceId, webContents, options = {}) {
|
|
|
307
365
|
sendDisconnectEvent,
|
|
308
366
|
cancelOperations,
|
|
309
367
|
});
|
|
368
|
+
clearIdleDisconnect(deviceId);
|
|
310
369
|
const peripheral = connectedDevices.get(deviceId);
|
|
311
370
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
312
371
|
if (cleanupConnection) {
|
|
@@ -330,8 +389,8 @@ function cleanupDevice(deviceId, webContents, options = {}) {
|
|
|
330
389
|
if (cleanupDiscoveredCache) {
|
|
331
390
|
discoveredDevices.delete(deviceId);
|
|
332
391
|
}
|
|
333
|
-
if (sendDisconnectEvent
|
|
334
|
-
|
|
392
|
+
if (sendDisconnectEvent) {
|
|
393
|
+
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
335
394
|
id: deviceId,
|
|
336
395
|
name: deviceName,
|
|
337
396
|
});
|
|
@@ -373,6 +432,8 @@ function setupMtuListener(peripheral, deviceId, webContents) {
|
|
|
373
432
|
const listener = (mtu) => {
|
|
374
433
|
if (!Number.isFinite(mtu) || mtu <= 0)
|
|
375
434
|
return;
|
|
435
|
+
if (webContents.isDestroyed())
|
|
436
|
+
return;
|
|
376
437
|
webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
377
438
|
id: deviceId,
|
|
378
439
|
mtu,
|
|
@@ -479,6 +540,7 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
|
|
|
479
540
|
if (!peripheral || !characteristics) {
|
|
480
541
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Device ${deviceId} not connected or characteristics not available`);
|
|
481
542
|
}
|
|
543
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
482
544
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
483
545
|
const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
|
|
484
546
|
const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
|
|
@@ -636,21 +698,21 @@ function enumerateDevices() {
|
|
|
636
698
|
clearInterval(intervalId);
|
|
637
699
|
yield waitForNobleScanStop(nobleInstance);
|
|
638
700
|
});
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
name: deviceName,
|
|
649
|
-
state: peripheral.state || 'disconnected',
|
|
650
|
-
});
|
|
651
|
-
}
|
|
701
|
+
const pushDevice = (peripheral, id) => {
|
|
702
|
+
var _a;
|
|
703
|
+
if (devices.some(d => d.id === id))
|
|
704
|
+
return;
|
|
705
|
+
devices.push({
|
|
706
|
+
commType: 'electron-ble',
|
|
707
|
+
id,
|
|
708
|
+
name: ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device',
|
|
709
|
+
state: peripheral.state || 'disconnected',
|
|
652
710
|
});
|
|
653
711
|
};
|
|
712
|
+
const checkDevices = () => {
|
|
713
|
+
discoveredDevices.forEach(pushDevice);
|
|
714
|
+
connectedDevices.forEach(pushDevice);
|
|
715
|
+
};
|
|
654
716
|
const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
|
|
655
717
|
checkDevices();
|
|
656
718
|
yield cleanup();
|
|
@@ -923,7 +985,62 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
|
923
985
|
}
|
|
924
986
|
});
|
|
925
987
|
}
|
|
988
|
+
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
989
|
+
const DIRECT_CONNECT_COOLDOWN_MS = 15000;
|
|
990
|
+
const directConnectCooldownUntil = new Map();
|
|
991
|
+
function tryDirectConnectById(deviceId) {
|
|
992
|
+
var _a;
|
|
993
|
+
return index.__awaiter(this, void 0, void 0, function* () {
|
|
994
|
+
const nobleInstance = noble;
|
|
995
|
+
if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
|
|
996
|
+
return undefined;
|
|
997
|
+
const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
|
|
998
|
+
if (Date.now() < cooldownUntil)
|
|
999
|
+
return undefined;
|
|
1000
|
+
try {
|
|
1001
|
+
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1002
|
+
const raced = yield Promise.race([
|
|
1003
|
+
directPromise,
|
|
1004
|
+
new Promise(resolve => {
|
|
1005
|
+
setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
|
|
1006
|
+
}),
|
|
1007
|
+
]);
|
|
1008
|
+
if (raced === 'timeout') {
|
|
1009
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1010
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
|
|
1011
|
+
deviceId,
|
|
1012
|
+
});
|
|
1013
|
+
directPromise
|
|
1014
|
+
.then(late => {
|
|
1015
|
+
const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
|
|
1016
|
+
if (latePeripheral &&
|
|
1017
|
+
latePeripheral.state === 'connected' &&
|
|
1018
|
+
!connectedDevices.has(deviceId)) {
|
|
1019
|
+
latePeripheral.removeAllListeners('disconnect');
|
|
1020
|
+
latePeripheral.disconnect(() => undefined);
|
|
1021
|
+
}
|
|
1022
|
+
})
|
|
1023
|
+
.catch(() => undefined);
|
|
1024
|
+
return undefined;
|
|
1025
|
+
}
|
|
1026
|
+
const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
|
|
1027
|
+
if (!peripheral || peripheral.state !== 'connected')
|
|
1028
|
+
return undefined;
|
|
1029
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id succeeded', { deviceId });
|
|
1030
|
+
return peripheral;
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1034
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
|
|
1035
|
+
deviceId,
|
|
1036
|
+
error: String(error),
|
|
1037
|
+
});
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
926
1042
|
function connectDevice(deviceId, webContents) {
|
|
1043
|
+
var _a;
|
|
927
1044
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
928
1045
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
929
1046
|
deviceId,
|
|
@@ -933,7 +1050,7 @@ function connectDevice(deviceId, webContents) {
|
|
|
933
1050
|
totalDiscovered: discoveredDevices.size,
|
|
934
1051
|
totalConnected: connectedDevices.size,
|
|
935
1052
|
});
|
|
936
|
-
let peripheral = discoveredDevices.get(deviceId);
|
|
1053
|
+
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
937
1054
|
if (!peripheral) {
|
|
938
1055
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
939
1056
|
if (!noble) {
|
|
@@ -942,6 +1059,12 @@ function connectDevice(deviceId, webContents) {
|
|
|
942
1059
|
if (!noble) {
|
|
943
1060
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
944
1061
|
}
|
|
1062
|
+
peripheral = yield tryDirectConnectById(deviceId);
|
|
1063
|
+
if (peripheral) {
|
|
1064
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (!peripheral) {
|
|
945
1068
|
try {
|
|
946
1069
|
const foundPeripheral = yield performTargetedScan(deviceId);
|
|
947
1070
|
if (!foundPeripheral) {
|
|
@@ -962,8 +1085,8 @@ function connectDevice(deviceId, webContents) {
|
|
|
962
1085
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
|
|
963
1086
|
if (!connectedDevices.has(deviceId)) {
|
|
964
1087
|
connectedDevices.set(deviceId, peripheral);
|
|
965
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
966
1088
|
}
|
|
1089
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
967
1090
|
if (deviceCharacteristics.has(deviceId)) {
|
|
968
1091
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
|
|
969
1092
|
const ongoingOperation = subscriptionOperations.get(deviceId);
|
|
@@ -985,9 +1108,16 @@ function connectDevice(deviceId, webContents) {
|
|
|
985
1108
|
notificationCallbacks.delete(deviceId);
|
|
986
1109
|
subscribedDevices.delete(deviceId);
|
|
987
1110
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1111
|
+
try {
|
|
1112
|
+
const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
|
|
1113
|
+
deviceCharacteristics.set(deviceId, characteristics);
|
|
1114
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
|
|
1115
|
+
}
|
|
1116
|
+
catch (setupError) {
|
|
1117
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed on kept-alive link:', setupError);
|
|
1118
|
+
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1119
|
+
throw setupError;
|
|
1120
|
+
}
|
|
991
1121
|
return;
|
|
992
1122
|
}
|
|
993
1123
|
return new Promise((resolve, reject) => {
|
|
@@ -1152,8 +1282,12 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1152
1282
|
try {
|
|
1153
1283
|
logger = require('electron-log');
|
|
1154
1284
|
const { ipcMain } = require('electron');
|
|
1285
|
+
const handle = (channel, listener) => {
|
|
1286
|
+
ipcMain.removeHandler(channel);
|
|
1287
|
+
ipcMain.handle(channel, listener);
|
|
1288
|
+
};
|
|
1155
1289
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1156
|
-
|
|
1290
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1157
1291
|
try {
|
|
1158
1292
|
const devices = yield enumerateDevices();
|
|
1159
1293
|
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
@@ -1167,34 +1301,54 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1167
1301
|
throw error;
|
|
1168
1302
|
}
|
|
1169
1303
|
}));
|
|
1170
|
-
|
|
1304
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1171
1305
|
yield stopScanning();
|
|
1172
1306
|
}));
|
|
1173
|
-
|
|
1174
|
-
|
|
1307
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE, (_event, deviceId) => getDevice(deviceId));
|
|
1308
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1175
1309
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] IPC CONNECT request received:', {
|
|
1176
1310
|
deviceId,
|
|
1177
1311
|
hasPeripheral: connectedDevices.has(deviceId),
|
|
1178
1312
|
hasCharacteristics: deviceCharacteristics.has(deviceId),
|
|
1179
1313
|
totalConnectedDevices: connectedDevices.size,
|
|
1180
1314
|
});
|
|
1181
|
-
|
|
1315
|
+
clearIdleDisconnect(deviceId);
|
|
1316
|
+
yield awaitIdleDisconnect(deviceId);
|
|
1317
|
+
try {
|
|
1318
|
+
yield connectDevice(deviceId, webContents);
|
|
1319
|
+
}
|
|
1320
|
+
finally {
|
|
1321
|
+
if (connectedDevices.has(deviceId)) {
|
|
1322
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1182
1325
|
}));
|
|
1183
|
-
|
|
1326
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE, (_event, deviceId, keepSession) => {
|
|
1327
|
+
if (!connectedDevices.has(deviceId))
|
|
1328
|
+
return;
|
|
1329
|
+
if (keepSession) {
|
|
1330
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
armIdleDisconnect(deviceId);
|
|
1334
|
+
});
|
|
1335
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1184
1336
|
yield disconnectDevice(deviceId);
|
|
1185
1337
|
}));
|
|
1186
|
-
|
|
1338
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, (_event, deviceId, hexData, options) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1187
1339
|
yield transmitHexDataToDevice(deviceId, hexData, options);
|
|
1188
1340
|
}));
|
|
1189
|
-
|
|
1341
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1190
1342
|
yield subscribeNotifications(deviceId, (data) => {
|
|
1191
1343
|
webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
|
|
1192
1344
|
});
|
|
1345
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1193
1346
|
}));
|
|
1194
|
-
|
|
1347
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1195
1348
|
yield unsubscribeNotifications(deviceId);
|
|
1349
|
+
armIdleDisconnect(deviceId);
|
|
1196
1350
|
}));
|
|
1197
|
-
|
|
1351
|
+
handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1198
1352
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
1199
1353
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Cancel pairing invoked', {
|
|
1200
1354
|
platform: process.platform,
|
|
@@ -1210,7 +1364,7 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1210
1364
|
}
|
|
1211
1365
|
}
|
|
1212
1366
|
}));
|
|
1213
|
-
|
|
1367
|
+
handle(hdShared.EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1214
1368
|
try {
|
|
1215
1369
|
const bluetoothStatus = yield checkBluetoothAvailability();
|
|
1216
1370
|
safeLog(logger, 'info', 'Bluetooth availability check completed:', bluetoothStatus);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AA0EhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA8lDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAgMpE"}
|
|
@@ -12,6 +12,7 @@ export interface NobleBleAPI {
|
|
|
12
12
|
mtu?: number;
|
|
13
13
|
} | null>;
|
|
14
14
|
connect: (uuid: string) => Promise<void>;
|
|
15
|
+
release?: (uuid: string, keepSession?: boolean) => Promise<void>;
|
|
15
16
|
disconnect: (uuid: string) => Promise<void>;
|
|
16
17
|
subscribe: (uuid: string) => Promise<void>;
|
|
17
18
|
unsubscribe: (uuid: string) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxF,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxF,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAGzC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACvF,oBAAoB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/F,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
|
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.75",
|
|
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.75",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.75",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.75",
|
|
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": "a9b3ab1cb53f87121ababb2b0ce45789be8fe5ad"
|
|
40
40
|
}
|
|
@@ -47,6 +47,9 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
47
47
|
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
48
48
|
handlers.set(channel, handler);
|
|
49
49
|
}),
|
|
50
|
+
removeHandler: jest.fn((channel: string) => {
|
|
51
|
+
handlers.delete(channel);
|
|
52
|
+
}),
|
|
50
53
|
};
|
|
51
54
|
const noble = new EventEmitter() as EventEmitter & {
|
|
52
55
|
state: string;
|
|
@@ -113,6 +116,9 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
113
116
|
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
114
117
|
handlers.set(channel, handler);
|
|
115
118
|
}),
|
|
119
|
+
removeHandler: jest.fn((channel: string) => {
|
|
120
|
+
handlers.delete(channel);
|
|
121
|
+
}),
|
|
116
122
|
};
|
|
117
123
|
const noble = new EventEmitter() as EventEmitter & {
|
|
118
124
|
state: string;
|
|
@@ -181,6 +187,9 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
181
187
|
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
182
188
|
handlers.set(channel, handler);
|
|
183
189
|
}),
|
|
190
|
+
removeHandler: jest.fn((channel: string) => {
|
|
191
|
+
handlers.delete(channel);
|
|
192
|
+
}),
|
|
184
193
|
};
|
|
185
194
|
const noble = new EventEmitter() as EventEmitter & {
|
|
186
195
|
state: string;
|
|
@@ -249,6 +258,9 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
249
258
|
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
250
259
|
handlers.set(channel, handler);
|
|
251
260
|
}),
|
|
261
|
+
removeHandler: jest.fn((channel: string) => {
|
|
262
|
+
handlers.delete(channel);
|
|
263
|
+
}),
|
|
252
264
|
};
|
|
253
265
|
const noble = new EventEmitter() as EventEmitter & {
|
|
254
266
|
state: string;
|
|
@@ -307,6 +319,9 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
307
319
|
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
308
320
|
handlers.set(channel, handler);
|
|
309
321
|
}),
|
|
322
|
+
removeHandler: jest.fn((channel: string) => {
|
|
323
|
+
handlers.delete(channel);
|
|
324
|
+
}),
|
|
310
325
|
};
|
|
311
326
|
const noble = new EventEmitter() as EventEmitter & {
|
|
312
327
|
state: string;
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
|
|
30
30
|
} from './noble-ble-timeouts';
|
|
31
31
|
|
|
32
|
-
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
32
|
+
import type { IpcMain, IpcMainInvokeEvent, WebContents } from 'electron';
|
|
33
33
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
34
34
|
import type { NobleBleWriteOptions } from './types/desktop-api';
|
|
35
35
|
import type { CharacteristicPair, DeviceInfo, Logger, NobleModule } from './types/noble-extended';
|
|
@@ -90,6 +90,10 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
90
90
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
91
91
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
92
92
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
93
|
+
// Renderer release is logical only; this timer physically frees the device.
|
|
94
|
+
const BLE_IDLE_DISCONNECT_MS = 3 * 60_000;
|
|
95
|
+
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
96
|
+
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
93
97
|
|
|
94
98
|
// Write-related constants
|
|
95
99
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
@@ -341,6 +345,76 @@ interface DeviceCleanupOptions {
|
|
|
341
345
|
reason?: string;
|
|
342
346
|
}
|
|
343
347
|
|
|
348
|
+
// One timer per device, in the main process so a reload cannot orphan a link.
|
|
349
|
+
const idleDisconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
350
|
+
|
|
351
|
+
// A fired timer cannot be cancelled; connect awaits its disconnect or the fast
|
|
352
|
+
// path hands back a link torn down a moment later (BleTimeoutError 713).
|
|
353
|
+
const idleDisconnectInFlight = new Map<string, Promise<void>>();
|
|
354
|
+
|
|
355
|
+
function clearIdleDisconnect(deviceId: string): void {
|
|
356
|
+
const timer = idleDisconnectTimers.get(deviceId);
|
|
357
|
+
if (timer) {
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
idleDisconnectTimers.delete(deviceId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Settle any keep-alive disconnect already running for this device. */
|
|
364
|
+
async function awaitIdleDisconnect(deviceId: string): Promise<void> {
|
|
365
|
+
const pending = idleDisconnectInFlight.get(deviceId);
|
|
366
|
+
if (pending) await pending.catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function broadcastToAllWebContents(channel: string, payload: unknown): void {
|
|
370
|
+
try {
|
|
371
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, global-require
|
|
372
|
+
const { webContents: electronWebContents } = require('electron') as {
|
|
373
|
+
webContents: { getAllWebContents(): WebContents[] };
|
|
374
|
+
};
|
|
375
|
+
for (const wc of electronWebContents.getAllWebContents()) {
|
|
376
|
+
if (!wc.isDestroyed()) wc.send(channel, payload);
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger?.error('[NobleBLE] broadcast failed:', { channel, error: String(error) });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function armIdleDisconnect(
|
|
384
|
+
deviceId: string,
|
|
385
|
+
ms: number = BLE_IDLE_DISCONNECT_MS,
|
|
386
|
+
reason: 'idle' | 'busy-backstop' = 'idle'
|
|
387
|
+
): void {
|
|
388
|
+
clearIdleDisconnect(deviceId);
|
|
389
|
+
idleDisconnectTimers.set(
|
|
390
|
+
deviceId,
|
|
391
|
+
setTimeout(() => {
|
|
392
|
+
idleDisconnectTimers.delete(deviceId);
|
|
393
|
+
if (!connectedDevices.has(deviceId)) return;
|
|
394
|
+
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
395
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
396
|
+
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
397
|
+
const pending = disconnectDevice(deviceId)
|
|
398
|
+
.then(() => {
|
|
399
|
+
// A call is still in flight here; it must reject, not hang.
|
|
400
|
+
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
401
|
+
id: deviceId,
|
|
402
|
+
name: deviceName,
|
|
403
|
+
});
|
|
404
|
+
})
|
|
405
|
+
.catch(error => {
|
|
406
|
+
logger?.error('[NobleBLE] Keep-alive disconnect failed', { deviceId, error });
|
|
407
|
+
})
|
|
408
|
+
.finally(() => {
|
|
409
|
+
if (idleDisconnectInFlight.get(deviceId) === pending) {
|
|
410
|
+
idleDisconnectInFlight.delete(deviceId);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
idleDisconnectInFlight.set(deviceId, pending);
|
|
414
|
+
}, ms)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
344
418
|
/**
|
|
345
419
|
* Unified device cleanup function - single entry point for all cleanup operations
|
|
346
420
|
*/
|
|
@@ -366,6 +440,8 @@ function cleanupDevice(
|
|
|
366
440
|
cancelOperations,
|
|
367
441
|
});
|
|
368
442
|
|
|
443
|
+
clearIdleDisconnect(deviceId);
|
|
444
|
+
|
|
369
445
|
// Get device info before cleanup
|
|
370
446
|
const peripheral = connectedDevices.get(deviceId);
|
|
371
447
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
@@ -395,9 +471,11 @@ function cleanupDevice(
|
|
|
395
471
|
discoveredDevices.delete(deviceId);
|
|
396
472
|
}
|
|
397
473
|
|
|
398
|
-
// 3. Send disconnect event (if needed)
|
|
399
|
-
|
|
400
|
-
|
|
474
|
+
// 3. Send disconnect event (if needed). Broadcast, not the captured
|
|
475
|
+
// webContents: a kept-alive link outlives a renderer soft restart, and the
|
|
476
|
+
// new renderer still needs to hear the device dropped (same as the idle path).
|
|
477
|
+
if (sendDisconnectEvent) {
|
|
478
|
+
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
401
479
|
id: deviceId,
|
|
402
480
|
name: deviceName,
|
|
403
481
|
});
|
|
@@ -456,6 +534,8 @@ function setupMtuListener(
|
|
|
456
534
|
|
|
457
535
|
const listener = (mtu: number) => {
|
|
458
536
|
if (!Number.isFinite(mtu) || mtu <= 0) return;
|
|
537
|
+
// A kept-alive link can outlive the renderer this closure captured.
|
|
538
|
+
if (webContents.isDestroyed()) return;
|
|
459
539
|
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
460
540
|
id: deviceId,
|
|
461
541
|
mtu,
|
|
@@ -600,6 +680,8 @@ async function transmitHexDataToDevice(
|
|
|
600
680
|
`Device ${deviceId} not connected or characteristics not available`
|
|
601
681
|
);
|
|
602
682
|
}
|
|
683
|
+
// Request outstanding: swap the idle clock for the busy backstop.
|
|
684
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
603
685
|
|
|
604
686
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
605
687
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
@@ -808,20 +890,20 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
808
890
|
};
|
|
809
891
|
|
|
810
892
|
// Collect discovered devices into the devices array
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
id,
|
|
819
|
-
name: deviceName,
|
|
820
|
-
state: peripheral.state || 'disconnected',
|
|
821
|
-
});
|
|
822
|
-
}
|
|
893
|
+
const pushDevice = (peripheral: Peripheral, id: string) => {
|
|
894
|
+
if (devices.some(d => d.id === id)) return;
|
|
895
|
+
devices.push({
|
|
896
|
+
commType: 'electron-ble',
|
|
897
|
+
id,
|
|
898
|
+
name: peripheral.advertisement?.localName || 'Unknown Device',
|
|
899
|
+
state: peripheral.state || 'disconnected',
|
|
823
900
|
});
|
|
824
901
|
};
|
|
902
|
+
const checkDevices = () => {
|
|
903
|
+
discoveredDevices.forEach(pushDevice);
|
|
904
|
+
// A linked device stops advertising, so the scan never rediscovers it.
|
|
905
|
+
connectedDevices.forEach(pushDevice);
|
|
906
|
+
};
|
|
825
907
|
|
|
826
908
|
// Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
|
|
827
909
|
const timeoutId = setTimeout(async () => {
|
|
@@ -1234,6 +1316,76 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1234
1316
|
}
|
|
1235
1317
|
}
|
|
1236
1318
|
|
|
1319
|
+
// noble/mac never resolves for an id CoreBluetooth cannot retrieve.
|
|
1320
|
+
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
1321
|
+
// Floor after a timeout: do not pay the probe on every attempt.
|
|
1322
|
+
const DIRECT_CONNECT_COOLDOWN_MS = 15_000;
|
|
1323
|
+
const directConnectCooldownUntil = new Map<string, number>();
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Connect straight by id, skipping the ~650ms targeted scan. Both native
|
|
1327
|
+
* backends support it and emit a `discover` as a side effect: macOS resolves
|
|
1328
|
+
* via `retrievePeripheralsWithIdentifiers`, Windows synthesizes one for an
|
|
1329
|
+
* unknown address. Returns undefined when unavailable so the caller scans.
|
|
1330
|
+
*
|
|
1331
|
+
* Ported from the Trezor connector, where it is field-proven.
|
|
1332
|
+
*/
|
|
1333
|
+
async function tryDirectConnectById(deviceId: string): Promise<Peripheral | undefined> {
|
|
1334
|
+
const nobleInstance = noble as
|
|
1335
|
+
| (typeof noble & {
|
|
1336
|
+
connectAsync?: (id: string) => Promise<Peripheral | undefined>;
|
|
1337
|
+
})
|
|
1338
|
+
| null;
|
|
1339
|
+
if (!nobleInstance?.connectAsync) return undefined;
|
|
1340
|
+
|
|
1341
|
+
const cooldownUntil = directConnectCooldownUntil.get(deviceId) ?? 0;
|
|
1342
|
+
if (Date.now() < cooldownUntil) return undefined;
|
|
1343
|
+
|
|
1344
|
+
try {
|
|
1345
|
+
// The late-orphan guard must attach to THIS pending connect.
|
|
1346
|
+
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1347
|
+
const raced = await Promise.race([
|
|
1348
|
+
directPromise,
|
|
1349
|
+
new Promise<'timeout'>(resolve => {
|
|
1350
|
+
setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
|
|
1351
|
+
}),
|
|
1352
|
+
]);
|
|
1353
|
+
if (raced === 'timeout') {
|
|
1354
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1355
|
+
logger?.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
|
|
1356
|
+
deviceId,
|
|
1357
|
+
});
|
|
1358
|
+
// Promise.race times out the caller only; a late success orphans the link.
|
|
1359
|
+
directPromise
|
|
1360
|
+
.then(late => {
|
|
1361
|
+
const latePeripheral = late ?? discoveredDevices.get(deviceId);
|
|
1362
|
+
if (
|
|
1363
|
+
latePeripheral &&
|
|
1364
|
+
latePeripheral.state === 'connected' &&
|
|
1365
|
+
!connectedDevices.has(deviceId)
|
|
1366
|
+
) {
|
|
1367
|
+
latePeripheral.removeAllListeners('disconnect');
|
|
1368
|
+
latePeripheral.disconnect(() => undefined);
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
.catch(() => undefined);
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}
|
|
1374
|
+
// Backends emit `discover` as a side effect, so the cache may hold it.
|
|
1375
|
+
const peripheral = raced ?? discoveredDevices.get(deviceId);
|
|
1376
|
+
if (!peripheral || peripheral.state !== 'connected') return undefined;
|
|
1377
|
+
logger?.info('[NobleBLE] Direct connect-by-id succeeded', { deviceId });
|
|
1378
|
+
return peripheral;
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1381
|
+
logger?.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
|
|
1382
|
+
deviceId,
|
|
1383
|
+
error: String(error),
|
|
1384
|
+
});
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1237
1389
|
// Connect to device - supports both discovered and direct connection modes
|
|
1238
1390
|
async function connectDevice(deviceId: string, webContents: WebContents): Promise<void> {
|
|
1239
1391
|
logger?.info('[NobleBLE] Connect device request:', {
|
|
@@ -1245,7 +1397,8 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1245
1397
|
totalConnected: connectedDevices.size,
|
|
1246
1398
|
});
|
|
1247
1399
|
|
|
1248
|
-
|
|
1400
|
+
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1401
|
+
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1249
1402
|
|
|
1250
1403
|
// If device not discovered, try a targeted scan for this specific device
|
|
1251
1404
|
if (!peripheral) {
|
|
@@ -1260,6 +1413,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1260
1413
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1261
1414
|
}
|
|
1262
1415
|
|
|
1416
|
+
// Reaches a bonded device that is not advertising; else falls to the scan.
|
|
1417
|
+
peripheral = await tryDirectConnectById(deviceId);
|
|
1418
|
+
if (peripheral) {
|
|
1419
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (!peripheral) {
|
|
1263
1424
|
// Perform a targeted scan to find the specific device
|
|
1264
1425
|
try {
|
|
1265
1426
|
const foundPeripheral = await performTargetedScan(deviceId);
|
|
@@ -1290,9 +1451,11 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1290
1451
|
// If already connected but not in our connected devices map, add it
|
|
1291
1452
|
if (!connectedDevices.has(deviceId)) {
|
|
1292
1453
|
connectedDevices.set(deviceId, peripheral);
|
|
1293
|
-
// Set up unified disconnect listener
|
|
1294
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1295
1454
|
}
|
|
1455
|
+
// Re-bind unconditionally (idempotent): on a kept-alive link the disconnect
|
|
1456
|
+
// and MTU listeners may still hold the webContents of a soft-restarted
|
|
1457
|
+
// renderer, and the reuse fast path below returns before any other setup.
|
|
1458
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1296
1459
|
|
|
1297
1460
|
// Check if we already have characteristics for this device
|
|
1298
1461
|
if (deviceCharacteristics.has(deviceId)) {
|
|
@@ -1336,13 +1499,20 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1336
1499
|
}
|
|
1337
1500
|
|
|
1338
1501
|
// Setup connection and discover services
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1502
|
+
try {
|
|
1503
|
+
const characteristics = await setupConnectionAndDiscoverServices(
|
|
1504
|
+
peripheral,
|
|
1505
|
+
deviceId,
|
|
1506
|
+
webContents
|
|
1507
|
+
);
|
|
1508
|
+
deviceCharacteristics.set(deviceId, characteristics);
|
|
1509
|
+
logger?.info('[NobleBLE] Device ready for communication:', deviceId);
|
|
1510
|
+
} catch (setupError) {
|
|
1511
|
+
// The caller re-arms the timer only on success, so a left-up link strands.
|
|
1512
|
+
logger?.error('[NobleBLE] Connection setup failed on kept-alive link:', setupError);
|
|
1513
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1514
|
+
throw setupError;
|
|
1515
|
+
}
|
|
1346
1516
|
return;
|
|
1347
1517
|
}
|
|
1348
1518
|
|
|
@@ -1577,12 +1747,18 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1577
1747
|
|
|
1578
1748
|
// @ts-ignore – electron is only available at runtime
|
|
1579
1749
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1580
|
-
const { ipcMain } = require('electron');
|
|
1750
|
+
const { ipcMain } = require('electron') as { ipcMain: IpcMain };
|
|
1751
|
+
|
|
1752
|
+
// Electron throws on duplicate channels and setup re-runs on soft restart.
|
|
1753
|
+
const handle: IpcMain['handle'] = (channel, listener) => {
|
|
1754
|
+
ipcMain.removeHandler(channel);
|
|
1755
|
+
ipcMain.handle(channel, listener);
|
|
1756
|
+
};
|
|
1581
1757
|
|
|
1582
1758
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1583
1759
|
|
|
1584
1760
|
// Handle enumerate request
|
|
1585
|
-
|
|
1761
|
+
handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
1586
1762
|
try {
|
|
1587
1763
|
const devices = await enumerateDevices();
|
|
1588
1764
|
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
@@ -1597,18 +1773,18 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1597
1773
|
});
|
|
1598
1774
|
|
|
1599
1775
|
// Handle stop scan request
|
|
1600
|
-
|
|
1776
|
+
handle(EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, async () => {
|
|
1601
1777
|
await stopScanning();
|
|
1602
1778
|
});
|
|
1603
1779
|
|
|
1604
1780
|
// Handle get device request
|
|
1605
|
-
|
|
1781
|
+
handle(
|
|
1606
1782
|
EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE,
|
|
1607
1783
|
(_event: IpcMainInvokeEvent, deviceId: string) => getDevice(deviceId)
|
|
1608
1784
|
);
|
|
1609
1785
|
|
|
1610
1786
|
// Handle connect request
|
|
1611
|
-
|
|
1787
|
+
handle(
|
|
1612
1788
|
EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT,
|
|
1613
1789
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1614
1790
|
logger?.info('[NobleBLE] IPC CONNECT request received:', {
|
|
@@ -1617,12 +1793,37 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1617
1793
|
hasCharacteristics: deviceCharacteristics.has(deviceId),
|
|
1618
1794
|
totalConnectedDevices: connectedDevices.size,
|
|
1619
1795
|
});
|
|
1620
|
-
|
|
1796
|
+
// Must not fire mid-connect (pairing can take ~30s).
|
|
1797
|
+
clearIdleDisconnect(deviceId);
|
|
1798
|
+
// A fired timer must settle first, or the fast path returns a dying link.
|
|
1799
|
+
await awaitIdleDisconnect(deviceId);
|
|
1800
|
+
try {
|
|
1801
|
+
await connectDevice(deviceId, webContents);
|
|
1802
|
+
} finally {
|
|
1803
|
+
// This timer is the only thing that frees a kept-alive link.
|
|
1804
|
+
if (connectedDevices.has(deviceId)) {
|
|
1805
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
);
|
|
1810
|
+
|
|
1811
|
+
// Logical release: start the idle countdown, keep the link for the next call.
|
|
1812
|
+
handle(
|
|
1813
|
+
EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE,
|
|
1814
|
+
(_event: IpcMainInvokeEvent, deviceId: string, keepSession?: boolean) => {
|
|
1815
|
+
if (!connectedDevices.has(deviceId)) return;
|
|
1816
|
+
// Mid-flow caller will be back; the short window would cut an update.
|
|
1817
|
+
if (keepSession) {
|
|
1818
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
armIdleDisconnect(deviceId);
|
|
1621
1822
|
}
|
|
1622
1823
|
);
|
|
1623
1824
|
|
|
1624
1825
|
// Handle disconnect request
|
|
1625
|
-
|
|
1826
|
+
handle(
|
|
1626
1827
|
EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT,
|
|
1627
1828
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1628
1829
|
await disconnectDevice(deviceId);
|
|
@@ -1630,7 +1831,7 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1630
1831
|
);
|
|
1631
1832
|
|
|
1632
1833
|
// Handle write request
|
|
1633
|
-
|
|
1834
|
+
handle(
|
|
1634
1835
|
EOneKeyBleMessageKeys.NOBLE_BLE_WRITE,
|
|
1635
1836
|
async (
|
|
1636
1837
|
_event: IpcMainInvokeEvent,
|
|
@@ -1643,26 +1844,29 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1643
1844
|
);
|
|
1644
1845
|
|
|
1645
1846
|
// Handle subscribe request
|
|
1646
|
-
|
|
1847
|
+
handle(
|
|
1647
1848
|
EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE,
|
|
1648
1849
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1649
1850
|
await subscribeNotifications(deviceId, (data: string) => {
|
|
1650
1851
|
// Send data back to renderer process
|
|
1651
1852
|
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
|
|
1652
1853
|
});
|
|
1854
|
+
// Still acquiring (in flight) — busy backstop, not the idle clock.
|
|
1855
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1653
1856
|
}
|
|
1654
1857
|
);
|
|
1655
1858
|
|
|
1656
1859
|
// Handle unsubscribe request
|
|
1657
|
-
|
|
1860
|
+
handle(
|
|
1658
1861
|
EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE,
|
|
1659
1862
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1660
1863
|
await unsubscribeNotifications(deviceId);
|
|
1864
|
+
armIdleDisconnect(deviceId);
|
|
1661
1865
|
}
|
|
1662
1866
|
);
|
|
1663
1867
|
|
|
1664
1868
|
// Handle cancel pairing: cleanup all connected devices
|
|
1665
|
-
|
|
1869
|
+
handle(EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, async () => {
|
|
1666
1870
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
1667
1871
|
logger?.info('[NobleBLE] Cancel pairing invoked', {
|
|
1668
1872
|
platform: process.platform,
|
|
@@ -1683,7 +1887,7 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1683
1887
|
});
|
|
1684
1888
|
|
|
1685
1889
|
// Handle Bluetooth availability check request
|
|
1686
|
-
|
|
1890
|
+
handle(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, async () => {
|
|
1687
1891
|
try {
|
|
1688
1892
|
const bluetoothStatus = await checkBluetoothAvailability();
|
|
1689
1893
|
safeLog(logger, 'info', 'Bluetooth availability check completed:', bluetoothStatus);
|
package/src/types/desktop-api.ts
CHANGED
|
@@ -12,6 +12,9 @@ export interface NobleBleAPI {
|
|
|
12
12
|
enumerate: () => Promise<{ id: string; name: string }[]>;
|
|
13
13
|
getDevice: (uuid: string) => Promise<{ id: string; name: string; mtu?: number } | null>;
|
|
14
14
|
connect: (uuid: string) => Promise<void>;
|
|
15
|
+
// Logical end-of-operation: link stays up, idle countdown starts. Optional —
|
|
16
|
+
// older hosts do not bridge it, so the transport feature-detects.
|
|
17
|
+
release?: (uuid: string, keepSession?: boolean) => Promise<void>;
|
|
15
18
|
disconnect: (uuid: string) => Promise<void>;
|
|
16
19
|
subscribe: (uuid: string) => Promise<void>;
|
|
17
20
|
unsubscribe: (uuid: string) => Promise<void>;
|