@onekeyfe/hd-transport-electron 1.1.34-alpha.0 → 1.1.34-alpha.2
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-de36348d.js → index-b69f3b16.js} +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-2e41f27c.js → noble-ble-handler-4423b8bd.js} +174 -9
- 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/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 +269 -26
- package/src/types/desktop-api.ts +2 -0
- package/src/types/noble-extended.ts +9 -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-4423b8bd.js'); });
|
|
36
36
|
setupNobleBleHandlers(webContents);
|
|
37
37
|
});
|
|
38
38
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ interface NobleModule {
|
|
|
17
17
|
stopScanning(callback?: () => void): void;
|
|
18
18
|
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
19
19
|
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
20
|
+
connectAsync?(idOrAddress: string): Promise<Peripheral | undefined>;
|
|
20
21
|
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|
|
21
22
|
removeListener(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
22
23
|
}
|
|
@@ -36,6 +37,7 @@ interface NobleBleAPI {
|
|
|
36
37
|
name: string;
|
|
37
38
|
} | null>;
|
|
38
39
|
connect: (uuid: string) => Promise<void>;
|
|
40
|
+
release?: (uuid: string) => Promise<void>;
|
|
39
41
|
disconnect: (uuid: string) => Promise<void>;
|
|
40
42
|
subscribe: (uuid: string) => Promise<void>;
|
|
41
43
|
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-b69f3b16.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var hdTransport = require('@onekeyfe/hd-transport');
|
|
6
6
|
var pRetry = require('p-retry');
|
|
@@ -78,6 +78,10 @@ const subscribedDevices = new Map();
|
|
|
78
78
|
const subscriptionOperations = new Map();
|
|
79
79
|
const devicePacketStates = new Map();
|
|
80
80
|
const ONEKEY_SERVICE_UUIDS = [hdShared.ONEKEY_SERVICE_UUID];
|
|
81
|
+
const uuid16Key = (uuid) => {
|
|
82
|
+
const stripped = (uuid !== null && uuid !== void 0 ? uuid : '').replace(/-/g, '').toLowerCase();
|
|
83
|
+
return stripped.length >= 8 ? stripped.substring(4, 8) : stripped;
|
|
84
|
+
};
|
|
81
85
|
const NORMALIZED_WRITE_UUID = '0002';
|
|
82
86
|
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
83
87
|
const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
@@ -85,6 +89,9 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
85
89
|
const FAST_SCAN_TIMEOUT = 1500;
|
|
86
90
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
87
91
|
const CONNECTION_TIMEOUT = 3000;
|
|
92
|
+
const BLE_IDLE_DISCONNECT_MS = 60000;
|
|
93
|
+
const BLE_DISCONNECT_TIMEOUT_MS = 2000;
|
|
94
|
+
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
88
95
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
89
96
|
const BLE_PACKET_SIZE = 192;
|
|
90
97
|
const UNIFIED_WRITE_DELAY = 5;
|
|
@@ -244,6 +251,48 @@ function updateBluetoothState(state) {
|
|
|
244
251
|
bluetoothState.initialized = true;
|
|
245
252
|
}
|
|
246
253
|
}
|
|
254
|
+
const idleDisconnectTimers = new Map();
|
|
255
|
+
function clearIdleDisconnect(deviceId) {
|
|
256
|
+
const timer = idleDisconnectTimers.get(deviceId);
|
|
257
|
+
if (timer) {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
idleDisconnectTimers.delete(deviceId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
|
|
263
|
+
clearIdleDisconnect(deviceId);
|
|
264
|
+
idleDisconnectTimers.set(deviceId, setTimeout(() => {
|
|
265
|
+
var _a;
|
|
266
|
+
idleDisconnectTimers.delete(deviceId);
|
|
267
|
+
if (!connectedDevices.has(deviceId))
|
|
268
|
+
return;
|
|
269
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device:', deviceId, reason);
|
|
270
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
271
|
+
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
272
|
+
disconnectDevice(deviceId)
|
|
273
|
+
.then(() => {
|
|
274
|
+
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
275
|
+
id: deviceId,
|
|
276
|
+
name: deviceName,
|
|
277
|
+
});
|
|
278
|
+
})
|
|
279
|
+
.catch(error => {
|
|
280
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Keep-alive disconnect failed:', error);
|
|
281
|
+
});
|
|
282
|
+
}, ms));
|
|
283
|
+
}
|
|
284
|
+
function broadcastToAllWebContents(channel, payload) {
|
|
285
|
+
try {
|
|
286
|
+
const { webContents: electronWebContents } = require('electron');
|
|
287
|
+
for (const wc of electronWebContents.getAllWebContents()) {
|
|
288
|
+
if (!wc.isDestroyed())
|
|
289
|
+
wc.send(channel, payload);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] broadcast failed:', { channel, error: String(error) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
247
296
|
function initializeNoble() {
|
|
248
297
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
249
298
|
if (noble)
|
|
@@ -315,6 +364,7 @@ function cleanupDevice(deviceId, webContents, options = {}) {
|
|
|
315
364
|
sendDisconnectEvent,
|
|
316
365
|
cancelOperations,
|
|
317
366
|
});
|
|
367
|
+
clearIdleDisconnect(deviceId);
|
|
318
368
|
const peripheral = connectedDevices.get(deviceId);
|
|
319
369
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
320
370
|
if (cleanupConnection) {
|
|
@@ -457,6 +507,7 @@ function transmitHexDataToDevice(deviceId, hexData) {
|
|
|
457
507
|
if (!peripheral || !characteristics) {
|
|
458
508
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Device ${deviceId} not connected or characteristics not available`);
|
|
459
509
|
}
|
|
510
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
460
511
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
461
512
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Writing data:', {
|
|
462
513
|
deviceId,
|
|
@@ -706,7 +757,7 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
706
757
|
});
|
|
707
758
|
const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
|
|
708
759
|
const services = yield new Promise((resolve, reject) => {
|
|
709
|
-
peripheral.discoverServices(
|
|
760
|
+
peripheral.discoverServices([], (error, svc) => {
|
|
710
761
|
if (error) {
|
|
711
762
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
|
|
712
763
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -719,10 +770,14 @@ function discoverServicesAndCharacteristics(peripheral) {
|
|
|
719
770
|
if (!services || services.length === 0) {
|
|
720
771
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
721
772
|
}
|
|
722
|
-
const
|
|
773
|
+
const wanted = ONEKEY_SERVICE_UUIDS.map(uuid16Key);
|
|
774
|
+
const service = services.find(svc => wanted.includes(uuid16Key(svc.uuid)));
|
|
775
|
+
if (!service) {
|
|
776
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, `No OneKey service in result set: ${services.map(svc => svc.uuid).join('|')}`);
|
|
777
|
+
}
|
|
723
778
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Found service:', service.uuid);
|
|
724
779
|
const characteristics = yield new Promise((resolve, reject) => {
|
|
725
|
-
service.discoverCharacteristics([
|
|
780
|
+
service.discoverCharacteristics([], (error, chars) => {
|
|
726
781
|
if (error) {
|
|
727
782
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Characteristic discovery failed:', error);
|
|
728
783
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, error.message));
|
|
@@ -801,8 +856,21 @@ function forceReconnectPeripheral(peripheral, deviceId) {
|
|
|
801
856
|
});
|
|
802
857
|
}
|
|
803
858
|
function freshScanAndDiscover(deviceId, webContents) {
|
|
859
|
+
var _a;
|
|
804
860
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
805
861
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Performing fresh scan to get new peripheral object for device:', deviceId);
|
|
862
|
+
const stalePeripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : discoveredDevices.get(deviceId);
|
|
863
|
+
if (stalePeripheral && stalePeripheral.state === 'connected') {
|
|
864
|
+
stalePeripheral.removeAllListeners('disconnect');
|
|
865
|
+
yield new Promise(resolve => {
|
|
866
|
+
const timer = setTimeout(resolve, BLE_DISCONNECT_TIMEOUT_MS);
|
|
867
|
+
stalePeripheral.disconnect(() => {
|
|
868
|
+
clearTimeout(timer);
|
|
869
|
+
resolve();
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
connectedDevices.delete(deviceId);
|
|
873
|
+
}
|
|
806
874
|
const freshPeripheral = yield performTargetedScan(deviceId);
|
|
807
875
|
if (!freshPeripheral) {
|
|
808
876
|
discoveredDevices.delete(deviceId);
|
|
@@ -827,7 +895,13 @@ function freshScanAndDiscover(deviceId, webContents) {
|
|
|
827
895
|
setupDisconnectListener(freshPeripheral, deviceId, webContents);
|
|
828
896
|
yield hdShared.wait(500);
|
|
829
897
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Attempting service discovery with fresh peripheral');
|
|
830
|
-
|
|
898
|
+
try {
|
|
899
|
+
return yield discoverServicesAndCharacteristics(freshPeripheral);
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
831
905
|
});
|
|
832
906
|
}
|
|
833
907
|
function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
@@ -873,10 +947,22 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
873
947
|
}
|
|
874
948
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
875
949
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
876
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
877
950
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
951
|
+
if (peripheral.state === 'connected') {
|
|
952
|
+
try {
|
|
953
|
+
const result = yield discoverServicesAndCharacteristics(peripheral);
|
|
954
|
+
connectedDevices.set(deviceId, peripheral);
|
|
955
|
+
return result;
|
|
956
|
+
}
|
|
957
|
+
catch (directError) {
|
|
958
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct discovery miss, escalating:', String(directError));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
878
961
|
try {
|
|
879
|
-
|
|
962
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
963
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
964
|
+
const result = yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
965
|
+
return result;
|
|
880
966
|
}
|
|
881
967
|
catch (error) {
|
|
882
968
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', error);
|
|
@@ -884,6 +970,51 @@ function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
|
884
970
|
}
|
|
885
971
|
});
|
|
886
972
|
}
|
|
973
|
+
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
974
|
+
const DIRECT_CONNECT_COOLDOWN_MS = 15000;
|
|
975
|
+
const directConnectCooldownUntil = new Map();
|
|
976
|
+
function tryDirectConnectById(deviceId) {
|
|
977
|
+
var _a;
|
|
978
|
+
return index.__awaiter(this, void 0, void 0, function* () {
|
|
979
|
+
if (!noble || typeof noble.connectAsync !== 'function')
|
|
980
|
+
return undefined;
|
|
981
|
+
if (((_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0) > Date.now()) {
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
const directPromise = noble.connectAsync(deviceId);
|
|
986
|
+
const raced = yield Promise.race([
|
|
987
|
+
directPromise,
|
|
988
|
+
new Promise(resolve => {
|
|
989
|
+
setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
|
|
990
|
+
}),
|
|
991
|
+
]);
|
|
992
|
+
if (raced === 'timeout') {
|
|
993
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
994
|
+
directPromise
|
|
995
|
+
.then(late => {
|
|
996
|
+
const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
|
|
997
|
+
if (latePeripheral &&
|
|
998
|
+
latePeripheral.state === 'connected' &&
|
|
999
|
+
!connectedDevices.has(deviceId)) {
|
|
1000
|
+
latePeripheral.removeAllListeners('disconnect');
|
|
1001
|
+
latePeripheral.disconnect(() => { });
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
.catch(() => { });
|
|
1005
|
+
return undefined;
|
|
1006
|
+
}
|
|
1007
|
+
const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
|
|
1008
|
+
if (!peripheral || peripheral.state !== 'connected')
|
|
1009
|
+
return undefined;
|
|
1010
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1011
|
+
return peripheral;
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
887
1018
|
function connectDevice(deviceId, webContents) {
|
|
888
1019
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
889
1020
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
@@ -903,8 +1034,9 @@ function connectDevice(deviceId, webContents) {
|
|
|
903
1034
|
if (!noble) {
|
|
904
1035
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
905
1036
|
}
|
|
1037
|
+
peripheral = yield tryDirectConnectById(deviceId);
|
|
906
1038
|
try {
|
|
907
|
-
const foundPeripheral = yield performTargetedScan(deviceId);
|
|
1039
|
+
const foundPeripheral = peripheral !== null && peripheral !== void 0 ? peripheral : (yield performTargetedScan(deviceId));
|
|
908
1040
|
if (!foundPeripheral) {
|
|
909
1041
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found even after targeted scan`);
|
|
910
1042
|
}
|
|
@@ -953,12 +1085,21 @@ function connectDevice(deviceId, webContents) {
|
|
|
953
1085
|
return;
|
|
954
1086
|
}
|
|
955
1087
|
return new Promise((resolve, reject) => {
|
|
1088
|
+
let timedOut = false;
|
|
956
1089
|
const timeout = setTimeout(() => {
|
|
1090
|
+
timedOut = true;
|
|
957
1091
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
958
1092
|
}, CONNECTION_TIMEOUT);
|
|
959
1093
|
const connectedPeripheral = peripheral;
|
|
960
1094
|
connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
|
|
961
1095
|
clearTimeout(timeout);
|
|
1096
|
+
if (timedOut) {
|
|
1097
|
+
if (!error && connectedPeripheral.state === 'connected') {
|
|
1098
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Late connection after timeout, disconnecting:', deviceId);
|
|
1099
|
+
connectedPeripheral.disconnect(() => { });
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
962
1103
|
if (error) {
|
|
963
1104
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
|
|
964
1105
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -990,7 +1131,11 @@ function disconnectDevice(deviceId) {
|
|
|
990
1131
|
}
|
|
991
1132
|
return new Promise(resolve => {
|
|
992
1133
|
peripheral.removeAllListeners('disconnect');
|
|
993
|
-
|
|
1134
|
+
let settled = false;
|
|
1135
|
+
const finish = () => {
|
|
1136
|
+
if (settled)
|
|
1137
|
+
return;
|
|
1138
|
+
settled = true;
|
|
994
1139
|
cleanupDevice(deviceId, undefined, {
|
|
995
1140
|
cleanupConnection: true,
|
|
996
1141
|
sendDisconnectEvent: false,
|
|
@@ -998,6 +1143,11 @@ function disconnectDevice(deviceId) {
|
|
|
998
1143
|
reason: 'manual-disconnect',
|
|
999
1144
|
});
|
|
1000
1145
|
resolve();
|
|
1146
|
+
};
|
|
1147
|
+
const timer = setTimeout(finish, BLE_DISCONNECT_TIMEOUT_MS);
|
|
1148
|
+
peripheral.disconnect(() => {
|
|
1149
|
+
clearTimeout(timer);
|
|
1150
|
+
finish();
|
|
1001
1151
|
});
|
|
1002
1152
|
});
|
|
1003
1153
|
});
|
|
@@ -1162,11 +1312,17 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1162
1312
|
hasCharacteristics: deviceCharacteristics.has(deviceId),
|
|
1163
1313
|
totalConnectedDevices: connectedDevices.size,
|
|
1164
1314
|
});
|
|
1315
|
+
clearIdleDisconnect(deviceId);
|
|
1165
1316
|
yield connectDevice(deviceId, webContents);
|
|
1317
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1166
1318
|
}));
|
|
1167
1319
|
ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1168
1320
|
yield disconnectDevice(deviceId);
|
|
1169
1321
|
}));
|
|
1322
|
+
ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE, (_event, deviceId) => {
|
|
1323
|
+
if (connectedDevices.has(deviceId))
|
|
1324
|
+
armIdleDisconnect(deviceId);
|
|
1325
|
+
});
|
|
1170
1326
|
ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, (_event, deviceId, hexData) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1171
1327
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] IPC WRITE', { deviceId, len: hexData.length });
|
|
1172
1328
|
yield transmitHexDataToDevice(deviceId, hexData);
|
|
@@ -1175,9 +1331,11 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1175
1331
|
yield subscribeNotifications(deviceId, (data) => {
|
|
1176
1332
|
webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
|
|
1177
1333
|
});
|
|
1334
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1178
1335
|
}));
|
|
1179
1336
|
ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
|
|
1180
1337
|
yield unsubscribeNotifications(deviceId);
|
|
1338
|
+
armIdleDisconnect(deviceId);
|
|
1181
1339
|
}));
|
|
1182
1340
|
ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1183
1341
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
@@ -1215,12 +1373,19 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1215
1373
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1216
1374
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
1217
1375
|
deviceIds.forEach(deviceId => {
|
|
1376
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
1218
1377
|
cleanupDevice(deviceId, undefined, {
|
|
1219
1378
|
cleanupConnection: true,
|
|
1220
1379
|
sendDisconnectEvent: false,
|
|
1221
1380
|
cancelOperations: true,
|
|
1222
1381
|
reason: 'app-quit',
|
|
1223
1382
|
});
|
|
1383
|
+
if (peripheral && peripheral.state === 'connected') {
|
|
1384
|
+
peripheral.removeAllListeners('disconnect');
|
|
1385
|
+
peripheral.disconnect(() => {
|
|
1386
|
+
safeLog(logger, 'info', `Disconnected ${deviceId} on window teardown`);
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1224
1389
|
});
|
|
1225
1390
|
stopScanning();
|
|
1226
1391
|
if (noble && persistentStateListener) {
|
|
@@ -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":"AAsBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AA8uDhE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAmMpE"}
|
|
@@ -8,6 +8,7 @@ export interface NobleBleAPI {
|
|
|
8
8
|
name: string;
|
|
9
9
|
} | null>;
|
|
10
10
|
connect: (uuid: string) => Promise<void>;
|
|
11
|
+
release?: (uuid: string) => Promise<void>;
|
|
11
12
|
disconnect: (uuid: string) => Promise<void>;
|
|
12
13
|
subscribe: (uuid: string) => Promise<void>;
|
|
13
14
|
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,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,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC1E,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,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,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,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,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,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"}
|
|
@@ -15,6 +15,7 @@ export interface NobleModule {
|
|
|
15
15
|
stopScanning(callback?: () => void): void;
|
|
16
16
|
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
17
17
|
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
18
|
+
connectAsync?(idOrAddress: string): Promise<Peripheral | undefined>;
|
|
18
19
|
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|
|
19
20
|
removeListener(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
20
21
|
}
|
|
@@ -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;CACf;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;
|
|
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;CACf;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;IASxE,YAAY,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IACpE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-electron",
|
|
3
|
-
"version": "1.1.34-alpha.
|
|
3
|
+
"version": "1.1.34-alpha.2",
|
|
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.1.34-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.1.34-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.1.34-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.1.34-alpha.2",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.1.34-alpha.2",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.1.34-alpha.2",
|
|
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": "dee167c35d191f3f4e02c4cb6228e1f926f929ec"
|
|
40
40
|
}
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -9,9 +9,7 @@ import {
|
|
|
9
9
|
EOneKeyBleMessageKeys,
|
|
10
10
|
ERRORS,
|
|
11
11
|
HardwareErrorCode,
|
|
12
|
-
ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
|
|
13
12
|
ONEKEY_SERVICE_UUID,
|
|
14
|
-
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
15
13
|
isHeaderChunk,
|
|
16
14
|
isOnekeyDevice,
|
|
17
15
|
wait,
|
|
@@ -74,6 +72,13 @@ const devicePacketStates = new Map<string, PacketAssemblyState>();
|
|
|
74
72
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
75
73
|
|
|
76
74
|
// Pre-normalized characteristic identifiers for fast comparison
|
|
75
|
+
// Reduce any uuid form (long base-form with/without dashes, or short) to its
|
|
76
|
+
// 16-bit key for comparisons — noble/mac reports base-UUIDs short-form.
|
|
77
|
+
const uuid16Key = (uuid: string): string => {
|
|
78
|
+
const stripped = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
79
|
+
return stripped.length >= 8 ? stripped.substring(4, 8) : stripped;
|
|
80
|
+
};
|
|
81
|
+
|
|
77
82
|
const NORMALIZED_WRITE_UUID = '0002';
|
|
78
83
|
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
79
84
|
|
|
@@ -83,6 +88,15 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
83
88
|
const FAST_SCAN_TIMEOUT = 1500; // 1.5 seconds for fast targeted scanning
|
|
84
89
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
85
90
|
const CONNECTION_TIMEOUT = 3000; // 3 seconds for device connection
|
|
91
|
+
// Idle window before a kept-alive link is dropped so other hosts (phone) can
|
|
92
|
+
// connect; matches the device's 60s auto-lock default.
|
|
93
|
+
const BLE_IDLE_DISCONNECT_MS = 60_000;
|
|
94
|
+
// noble's disconnect callback can hang on a wedged peripheral — bound it.
|
|
95
|
+
const BLE_DISCONNECT_TIMEOUT_MS = 2000;
|
|
96
|
+
// Backstop while a request is outstanding: the idle clock must not run while
|
|
97
|
+
// the user is on the device's confirm screen, but a dead call must not hold
|
|
98
|
+
// the link forever either.
|
|
99
|
+
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
86
100
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
87
101
|
|
|
88
102
|
// Write-related constants
|
|
@@ -302,6 +316,64 @@ function updateBluetoothState(state: string): void {
|
|
|
302
316
|
}
|
|
303
317
|
}
|
|
304
318
|
|
|
319
|
+
// ===== Keep-alive idle disconnect =====
|
|
320
|
+
// One timer per device: a write swaps it for the busy backstop, a complete
|
|
321
|
+
// response re-arms the 60s idle clock. Lives in the main process so a renderer
|
|
322
|
+
// reload cannot orphan a held link.
|
|
323
|
+
const idleDisconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
324
|
+
|
|
325
|
+
function clearIdleDisconnect(deviceId: string): void {
|
|
326
|
+
const timer = idleDisconnectTimers.get(deviceId);
|
|
327
|
+
if (timer) {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
idleDisconnectTimers.delete(deviceId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function armIdleDisconnect(
|
|
334
|
+
deviceId: string,
|
|
335
|
+
ms: number = BLE_IDLE_DISCONNECT_MS,
|
|
336
|
+
reason: 'idle' | 'busy-backstop' = 'idle'
|
|
337
|
+
): void {
|
|
338
|
+
clearIdleDisconnect(deviceId);
|
|
339
|
+
idleDisconnectTimers.set(
|
|
340
|
+
deviceId,
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
idleDisconnectTimers.delete(deviceId);
|
|
343
|
+
if (!connectedDevices.has(deviceId)) return;
|
|
344
|
+
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device:', deviceId, reason);
|
|
345
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
346
|
+
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
347
|
+
disconnectDevice(deviceId)
|
|
348
|
+
.then(() => {
|
|
349
|
+
// Notify renderers: on the busy backstop a call is still in flight
|
|
350
|
+
// and must reject instead of hanging on a dead link.
|
|
351
|
+
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
352
|
+
id: deviceId,
|
|
353
|
+
name: deviceName,
|
|
354
|
+
});
|
|
355
|
+
})
|
|
356
|
+
.catch(error => {
|
|
357
|
+
logger?.error('[NobleBLE] Keep-alive disconnect failed:', error);
|
|
358
|
+
});
|
|
359
|
+
}, ms)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function broadcastToAllWebContents(channel: string, payload: unknown): void {
|
|
364
|
+
try {
|
|
365
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
366
|
+
const { webContents: electronWebContents } = require('electron') as {
|
|
367
|
+
webContents: { getAllWebContents(): WebContents[] };
|
|
368
|
+
};
|
|
369
|
+
for (const wc of electronWebContents.getAllWebContents()) {
|
|
370
|
+
if (!wc.isDestroyed()) wc.send(channel, payload);
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
logger?.error('[NobleBLE] broadcast failed:', { channel, error: String(error) });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
305
377
|
// Initialize Noble
|
|
306
378
|
async function initializeNoble(): Promise<void> {
|
|
307
379
|
if (noble) return;
|
|
@@ -418,6 +490,7 @@ function cleanupDevice(
|
|
|
418
490
|
sendDisconnectEvent,
|
|
419
491
|
cancelOperations,
|
|
420
492
|
});
|
|
493
|
+
clearIdleDisconnect(deviceId);
|
|
421
494
|
|
|
422
495
|
// Get device info before cleanup
|
|
423
496
|
const peripheral = connectedDevices.get(deviceId);
|
|
@@ -616,6 +689,9 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
616
689
|
`Device ${deviceId} not connected or characteristics not available`
|
|
617
690
|
);
|
|
618
691
|
}
|
|
692
|
+
// Request outstanding: swap the idle clock for the busy backstop; the
|
|
693
|
+
// complete-response handler re-arms the 60s idle clock.
|
|
694
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
619
695
|
|
|
620
696
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
621
697
|
logger?.info('[NobleBLE] Writing data:', {
|
|
@@ -952,9 +1028,13 @@ async function discoverServicesAndCharacteristics(
|
|
|
952
1028
|
|
|
953
1029
|
// Main discovery logic as async function
|
|
954
1030
|
const discoveryPromise = (async (): Promise<CharacteristicPair> => {
|
|
955
|
-
// Step 1: Discover services (promisified)
|
|
1031
|
+
// Step 1: Discover services (promisified). In unfiltered mode we discover
|
|
1032
|
+
// everything and pick the OneKey service in JS — field data (Classic,
|
|
1033
|
+
// 2026-07-19) showed the UUID-filtered query returning empty on a link
|
|
1034
|
+
// where an unfiltered probe saw 5 services, so the filter itself is under
|
|
1035
|
+
// suspicion (same failure family as the Windows scan-filter issue).
|
|
956
1036
|
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
957
|
-
peripheral.discoverServices(
|
|
1037
|
+
peripheral.discoverServices([], (error, svc) => {
|
|
958
1038
|
if (error) {
|
|
959
1039
|
logger?.error('[NobleBLE] Service discovery failed:', error);
|
|
960
1040
|
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
@@ -968,22 +1048,28 @@ async function discoverServicesAndCharacteristics(
|
|
|
968
1048
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
|
|
969
1049
|
}
|
|
970
1050
|
|
|
971
|
-
|
|
1051
|
+
// Pick by 16-bit key: constants are long base-forms, noble/mac reports
|
|
1052
|
+
// base-UUID services short-form ('0001').
|
|
1053
|
+
const wanted = ONEKEY_SERVICE_UUIDS.map(uuid16Key);
|
|
1054
|
+
const service = services.find(svc => wanted.includes(uuid16Key(svc.uuid)));
|
|
1055
|
+
if (!service) {
|
|
1056
|
+
// Carry the uuid list so the trace shows what the device exposed.
|
|
1057
|
+
throw ERRORS.TypedError(
|
|
1058
|
+
HardwareErrorCode.BleServiceNotFound,
|
|
1059
|
+
`No OneKey service in result set: ${services.map(svc => svc.uuid).join('|')}`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
972
1062
|
logger?.info('[NobleBLE] Found service:', service.uuid);
|
|
973
1063
|
|
|
974
|
-
// Step 2: Discover characteristics (promisified)
|
|
975
1064
|
const characteristics = await new Promise<Characteristic[]>((resolve, reject) => {
|
|
976
|
-
service.discoverCharacteristics(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
} else {
|
|
983
|
-
resolve(chars);
|
|
984
|
-
}
|
|
1065
|
+
service.discoverCharacteristics([], (error, chars) => {
|
|
1066
|
+
if (error) {
|
|
1067
|
+
logger?.error('[NobleBLE] Characteristic discovery failed:', error);
|
|
1068
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound, error.message));
|
|
1069
|
+
} else {
|
|
1070
|
+
resolve(chars);
|
|
985
1071
|
}
|
|
986
|
-
);
|
|
1072
|
+
});
|
|
987
1073
|
});
|
|
988
1074
|
|
|
989
1075
|
// Step 3: Find required characteristics
|
|
@@ -1086,7 +1172,6 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
1086
1172
|
// NOTE: Caller MUST call setupDisconnectListener() after this function returns
|
|
1087
1173
|
}
|
|
1088
1174
|
|
|
1089
|
-
// Last resort: Fresh scan to get completely new peripheral object and discover services
|
|
1090
1175
|
async function freshScanAndDiscover(
|
|
1091
1176
|
deviceId: string,
|
|
1092
1177
|
webContents: WebContents
|
|
@@ -1096,6 +1181,21 @@ async function freshScanAndDiscover(
|
|
|
1096
1181
|
deviceId
|
|
1097
1182
|
);
|
|
1098
1183
|
|
|
1184
|
+
// The device does not advertise while we hold a link — drop the old one
|
|
1185
|
+
// first (bounded) so it goes back on air before we scan.
|
|
1186
|
+
const stalePeripheral = connectedDevices.get(deviceId) ?? discoveredDevices.get(deviceId);
|
|
1187
|
+
if (stalePeripheral && stalePeripheral.state === 'connected') {
|
|
1188
|
+
stalePeripheral.removeAllListeners('disconnect');
|
|
1189
|
+
await new Promise<void>(resolve => {
|
|
1190
|
+
const timer = setTimeout(resolve, BLE_DISCONNECT_TIMEOUT_MS);
|
|
1191
|
+
stalePeripheral.disconnect(() => {
|
|
1192
|
+
clearTimeout(timer);
|
|
1193
|
+
resolve();
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
connectedDevices.delete(deviceId);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1099
1199
|
const freshPeripheral = await performTargetedScan(deviceId);
|
|
1100
1200
|
if (!freshPeripheral) {
|
|
1101
1201
|
// Deep cleanup: fresh scan found no device, reset to initial state
|
|
@@ -1136,9 +1236,17 @@ async function freshScanAndDiscover(
|
|
|
1136
1236
|
// Wait for connection to stabilize (fresh peripheral doesn't need GATT cache clearing)
|
|
1137
1237
|
await wait(500);
|
|
1138
1238
|
|
|
1139
|
-
// Attempt service discovery with fresh peripheral
|
|
1239
|
+
// Attempt service discovery with fresh peripheral. On failure the fresh
|
|
1240
|
+
// peripheral is already physically connected and registered in
|
|
1241
|
+
// connectedDevices with no idle timer armed — keep-alive release() is
|
|
1242
|
+
// logical-only and would never drop it — so tear it down before rethrowing.
|
|
1140
1243
|
logger?.info('[NobleBLE] Attempting service discovery with fresh peripheral');
|
|
1141
|
-
|
|
1244
|
+
try {
|
|
1245
|
+
return await discoverServicesAndCharacteristics(freshPeripheral);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1248
|
+
throw error;
|
|
1249
|
+
}
|
|
1142
1250
|
}
|
|
1143
1251
|
|
|
1144
1252
|
// Enhanced service discovery with p-retry for robust BLE connection
|
|
@@ -1209,20 +1317,94 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1209
1317
|
deviceId: string,
|
|
1210
1318
|
webContents: WebContents
|
|
1211
1319
|
): Promise<CharacteristicPair> {
|
|
1212
|
-
// Force reconnect to clear GATT cache
|
|
1213
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1214
1320
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1215
1321
|
|
|
1216
|
-
//
|
|
1322
|
+
// Optimistic direct discovery first; the force-reconnect fallback below only
|
|
1323
|
+
// runs if it fails. Discovery is UNFILTERED with 16-bit-key matching in JS:
|
|
1324
|
+
// the device answers only the 2-byte-encoded targeted query, so a uuid-
|
|
1325
|
+
// filtered discovery is unreliable (macOS CoreBluetooth sends the 128-bit
|
|
1326
|
+
// base form and misses). See uuid16Key.
|
|
1327
|
+
if (peripheral.state === 'connected') {
|
|
1328
|
+
try {
|
|
1329
|
+
const result = await discoverServicesAndCharacteristics(peripheral);
|
|
1330
|
+
connectedDevices.set(deviceId, peripheral);
|
|
1331
|
+
return result;
|
|
1332
|
+
} catch (directError) {
|
|
1333
|
+
logger?.info('[NobleBLE] Direct discovery miss, escalating:', String(directError));
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Fallback ladder; a force-reconnect failure must also escalate to the
|
|
1338
|
+
// fresh scan instead of propagating out.
|
|
1217
1339
|
try {
|
|
1218
|
-
|
|
1340
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1341
|
+
// forceReconnectPeripheral strips listeners — re-attach.
|
|
1342
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1343
|
+
|
|
1344
|
+
const result = await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1345
|
+
return result;
|
|
1219
1346
|
} catch (error) {
|
|
1220
1347
|
// Last resort: fresh scan to get new peripheral object
|
|
1221
1348
|
logger?.error('[NobleBLE] Service discovery failed, attempting fresh scan...', error);
|
|
1349
|
+
// Broken-OS-bond signature lands here.
|
|
1222
1350
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1223
1351
|
}
|
|
1224
1352
|
}
|
|
1225
1353
|
|
|
1354
|
+
// noble/mac never resolves connect-by-id for an unretrievable peripheral —
|
|
1355
|
+
// time-box it, and after a timeout skip direct for a while so the retry
|
|
1356
|
+
// ladder doesn't re-pay the 2s against an absent device.
|
|
1357
|
+
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
1358
|
+
const DIRECT_CONNECT_COOLDOWN_MS = 15_000;
|
|
1359
|
+
const directConnectCooldownUntil = new Map<string, number>();
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Bounded connect-by-id with no scan; undefined -> caller falls back to the
|
|
1363
|
+
* targeted scan. A late success after the timeout is dropped unless claimed.
|
|
1364
|
+
*/
|
|
1365
|
+
async function tryDirectConnectById(deviceId: string): Promise<Peripheral | undefined> {
|
|
1366
|
+
if (!noble || typeof noble.connectAsync !== 'function') return undefined;
|
|
1367
|
+
if ((directConnectCooldownUntil.get(deviceId) ?? 0) > Date.now()) {
|
|
1368
|
+
return undefined;
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
// The late-orphan guard must attach to THIS pending connect.
|
|
1372
|
+
const directPromise = noble.connectAsync(deviceId);
|
|
1373
|
+
const raced = await Promise.race([
|
|
1374
|
+
directPromise,
|
|
1375
|
+
new Promise<'timeout'>(resolve => {
|
|
1376
|
+
setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
|
|
1377
|
+
}),
|
|
1378
|
+
]);
|
|
1379
|
+
if (raced === 'timeout') {
|
|
1380
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1381
|
+
// If the pending connect completes later, drop it unless someone claimed it.
|
|
1382
|
+
directPromise
|
|
1383
|
+
.then(late => {
|
|
1384
|
+
const latePeripheral = late ?? discoveredDevices.get(deviceId);
|
|
1385
|
+
if (
|
|
1386
|
+
latePeripheral &&
|
|
1387
|
+
latePeripheral.state === 'connected' &&
|
|
1388
|
+
!connectedDevices.has(deviceId)
|
|
1389
|
+
) {
|
|
1390
|
+
latePeripheral.removeAllListeners('disconnect');
|
|
1391
|
+
latePeripheral.disconnect(() => {});
|
|
1392
|
+
}
|
|
1393
|
+
})
|
|
1394
|
+
.catch(() => {});
|
|
1395
|
+
return undefined;
|
|
1396
|
+
}
|
|
1397
|
+
// Backends emit a `discover` for the peripheral as a side effect, so the
|
|
1398
|
+
// cache may hold it even when connectAsync resolves without a value.
|
|
1399
|
+
const peripheral = raced ?? discoveredDevices.get(deviceId);
|
|
1400
|
+
if (!peripheral || peripheral.state !== 'connected') return undefined;
|
|
1401
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1402
|
+
return peripheral;
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
return undefined;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1226
1408
|
// Connect to device - supports both discovered and direct connection modes
|
|
1227
1409
|
async function connectDevice(deviceId: string, webContents: WebContents): Promise<void> {
|
|
1228
1410
|
logger?.info('[NobleBLE] Connect device request:', {
|
|
@@ -1249,9 +1431,13 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1249
1431
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1250
1432
|
}
|
|
1251
1433
|
|
|
1434
|
+
// Bounded connect-by-id first: skips the ~650ms targeted scan and reaches
|
|
1435
|
+
// a device that is not advertising; falls through to the scan on timeout.
|
|
1436
|
+
peripheral = await tryDirectConnectById(deviceId);
|
|
1437
|
+
|
|
1252
1438
|
// Perform a targeted scan to find the specific device
|
|
1253
1439
|
try {
|
|
1254
|
-
const foundPeripheral = await performTargetedScan(deviceId);
|
|
1440
|
+
const foundPeripheral = peripheral ?? (await performTargetedScan(deviceId));
|
|
1255
1441
|
if (!foundPeripheral) {
|
|
1256
1442
|
throw ERRORS.TypedError(
|
|
1257
1443
|
HardwareErrorCode.DeviceNotFound,
|
|
@@ -1337,7 +1523,11 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1337
1523
|
}
|
|
1338
1524
|
|
|
1339
1525
|
return new Promise((resolve, reject) => {
|
|
1526
|
+
// After the timeout rejects, a late successful connect would be an
|
|
1527
|
+
// ownerless link with no idle timer — detect and drop it.
|
|
1528
|
+
let timedOut = false;
|
|
1340
1529
|
const timeout = setTimeout(() => {
|
|
1530
|
+
timedOut = true;
|
|
1341
1531
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
1342
1532
|
}, CONNECTION_TIMEOUT);
|
|
1343
1533
|
|
|
@@ -1346,6 +1536,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1346
1536
|
connectedPeripheral.connect(async (error: Error | undefined) => {
|
|
1347
1537
|
clearTimeout(timeout);
|
|
1348
1538
|
|
|
1539
|
+
if (timedOut) {
|
|
1540
|
+
if (!error && connectedPeripheral.state === 'connected') {
|
|
1541
|
+
logger?.info('[NobleBLE] Late connection after timeout, disconnecting:', deviceId);
|
|
1542
|
+
connectedPeripheral.disconnect(() => {});
|
|
1543
|
+
}
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1349
1547
|
if (error) {
|
|
1350
1548
|
logger?.error('[NobleBLE] Connection failed:', error);
|
|
1351
1549
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -1386,7 +1584,13 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1386
1584
|
// Remove disconnect listener to avoid triggering handleDeviceDisconnect
|
|
1387
1585
|
peripheral.removeAllListeners('disconnect');
|
|
1388
1586
|
|
|
1389
|
-
|
|
1587
|
+
// Time-box the disconnect: on a wedged peripheral CoreBluetooth may never
|
|
1588
|
+
// invoke the callback, and callers (e.g. the idle/backstop timer) rely on
|
|
1589
|
+
// this resolving so cleanup + the renderer-facing disconnect broadcast run.
|
|
1590
|
+
let settled = false;
|
|
1591
|
+
const finish = () => {
|
|
1592
|
+
if (settled) return;
|
|
1593
|
+
settled = true;
|
|
1390
1594
|
// Clean up device state using unified function
|
|
1391
1595
|
cleanupDevice(deviceId, undefined, {
|
|
1392
1596
|
cleanupConnection: true,
|
|
@@ -1395,6 +1599,11 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1395
1599
|
reason: 'manual-disconnect',
|
|
1396
1600
|
});
|
|
1397
1601
|
resolve();
|
|
1602
|
+
};
|
|
1603
|
+
const timer = setTimeout(finish, BLE_DISCONNECT_TIMEOUT_MS);
|
|
1604
|
+
peripheral.disconnect(() => {
|
|
1605
|
+
clearTimeout(timer);
|
|
1606
|
+
finish();
|
|
1398
1607
|
});
|
|
1399
1608
|
});
|
|
1400
1609
|
}
|
|
@@ -1564,6 +1773,11 @@ async function subscribeNotifications(
|
|
|
1564
1773
|
return;
|
|
1565
1774
|
}
|
|
1566
1775
|
if (result.isComplete && result.completePacket) {
|
|
1776
|
+
// A complete response does NOT mean the operation is over — the run
|
|
1777
|
+
// may continue (further request/response, or an on-device prompt whose
|
|
1778
|
+
// Ack the host writes after slow human input). Do NOT re-arm the 60s
|
|
1779
|
+
// idle clock here; that starts only on logical release. The busy
|
|
1780
|
+
// backstop remains the in-flight ceiling.
|
|
1567
1781
|
const appCb = notificationCallbacks.get(deviceId);
|
|
1568
1782
|
if (appCb) appCb(result.completePacket);
|
|
1569
1783
|
}
|
|
@@ -1632,7 +1846,15 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1632
1846
|
hasCharacteristics: deviceCharacteristics.has(deviceId),
|
|
1633
1847
|
totalConnectedDevices: connectedDevices.size,
|
|
1634
1848
|
});
|
|
1849
|
+
// An armed idle timer must not fire mid-connect (pairing can take ~30s).
|
|
1850
|
+
clearIdleDisconnect(deviceId);
|
|
1635
1851
|
await connectDevice(deviceId, webContents);
|
|
1852
|
+
// A logical operation is now in flight (acquire). Arm the long busy
|
|
1853
|
+
// backstop, NOT the 60s idle clock — the 60s countdown starts only when
|
|
1854
|
+
// the renderer signals logical release (NOBLE_BLE_RELEASE). This keeps
|
|
1855
|
+
// the link up across slow on-device prompts (PIN/passphrase) that have
|
|
1856
|
+
// no outstanding write.
|
|
1857
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1636
1858
|
}
|
|
1637
1859
|
);
|
|
1638
1860
|
|
|
@@ -1644,6 +1866,16 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1644
1866
|
}
|
|
1645
1867
|
);
|
|
1646
1868
|
|
|
1869
|
+
// Handle logical release: the operation is done, start the 60s idle
|
|
1870
|
+
// countdown so an unused device is freed for other hosts. The physical
|
|
1871
|
+
// link is kept (reused by the next call) unless the countdown elapses.
|
|
1872
|
+
ipcMain.handle(
|
|
1873
|
+
EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE,
|
|
1874
|
+
(_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1875
|
+
if (connectedDevices.has(deviceId)) armIdleDisconnect(deviceId);
|
|
1876
|
+
}
|
|
1877
|
+
);
|
|
1878
|
+
|
|
1647
1879
|
// Handle write request
|
|
1648
1880
|
ipcMain.handle(
|
|
1649
1881
|
EOneKeyBleMessageKeys.NOBLE_BLE_WRITE,
|
|
@@ -1661,6 +1893,8 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1661
1893
|
// Send data back to renderer process
|
|
1662
1894
|
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
|
|
1663
1895
|
});
|
|
1896
|
+
// Still acquiring (in flight) — busy backstop, not the 60s idle clock.
|
|
1897
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1664
1898
|
}
|
|
1665
1899
|
);
|
|
1666
1900
|
|
|
@@ -1669,6 +1903,7 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1669
1903
|
EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE,
|
|
1670
1904
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1671
1905
|
await unsubscribeNotifications(deviceId);
|
|
1906
|
+
armIdleDisconnect(deviceId);
|
|
1672
1907
|
}
|
|
1673
1908
|
);
|
|
1674
1909
|
|
|
@@ -1714,15 +1949,23 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1714
1949
|
webContents.on('destroyed', () => {
|
|
1715
1950
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1716
1951
|
|
|
1717
|
-
//
|
|
1952
|
+
// Physically disconnect too: the main process can outlive this window
|
|
1953
|
+
// (tray) and a held link would block the device for every other host.
|
|
1718
1954
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
1719
1955
|
deviceIds.forEach(deviceId => {
|
|
1956
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
1720
1957
|
cleanupDevice(deviceId, undefined, {
|
|
1721
1958
|
cleanupConnection: true,
|
|
1722
1959
|
sendDisconnectEvent: false,
|
|
1723
1960
|
cancelOperations: true,
|
|
1724
1961
|
reason: 'app-quit',
|
|
1725
1962
|
});
|
|
1963
|
+
if (peripheral && peripheral.state === 'connected') {
|
|
1964
|
+
peripheral.removeAllListeners('disconnect');
|
|
1965
|
+
peripheral.disconnect(() => {
|
|
1966
|
+
safeLog(logger, 'info', `Disconnected ${deviceId} on window teardown`);
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1726
1969
|
});
|
|
1727
1970
|
|
|
1728
1971
|
// 2. Stop scanning
|
package/src/types/desktop-api.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface NobleBleAPI {
|
|
|
8
8
|
enumerate: () => Promise<{ id: string; name: string }[]>;
|
|
9
9
|
getDevice: (uuid: string) => Promise<{ id: string; name: string } | null>;
|
|
10
10
|
connect: (uuid: string) => Promise<void>;
|
|
11
|
+
/** Logical end-of-operation (keep-alive): starts the idle-disconnect countdown. */
|
|
12
|
+
release?: (uuid: string) => Promise<void>;
|
|
11
13
|
disconnect: (uuid: string) => Promise<void>;
|
|
12
14
|
subscribe: (uuid: string) => Promise<void>;
|
|
13
15
|
unsubscribe: (uuid: string) => Promise<void>;
|
|
@@ -30,6 +30,15 @@ export interface NobleModule {
|
|
|
30
30
|
stopScanning(callback?: () => void): void;
|
|
31
31
|
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
32
32
|
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
33
|
+
/**
|
|
34
|
+
* Connect by id/address with NO scan. Both native backends support this and
|
|
35
|
+
* emit a `discover` for the peripheral as a side effect (Windows synthesizes
|
|
36
|
+
* one for an unknown address; macOS resolves via
|
|
37
|
+
* retrievePeripheralsWithIdentifiers). Optional so an old noble can omit it.
|
|
38
|
+
* CAUTION: on macOS this never resolves for an id CoreBluetooth cannot
|
|
39
|
+
* retrieve — callers must time-box it.
|
|
40
|
+
*/
|
|
41
|
+
connectAsync?(idOrAddress: string): Promise<Peripheral | undefined>;
|
|
33
42
|
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|
|
34
43
|
removeListener(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
35
44
|
}
|