@onekeyfe/hd-transport-electron 1.2.0-alpha.4 → 1.2.0-alpha.41
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/ble-ops.d.ts +6 -0
- package/dist/ble-ops.d.ts.map +1 -1
- package/dist/{index-02fc07fe.js → index-5f728113.js} +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-94c42077.js → noble-ble-handler-d58af248.js} +173 -177
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/ble-ops.test.ts +21 -0
- package/src/ble-ops.ts +40 -10
- package/src/noble-ble-handler.ts +157 -210
package/src/noble-ble-handler.ts
CHANGED
|
@@ -9,14 +9,19 @@ import {
|
|
|
9
9
|
EOneKeyBleMessageKeys,
|
|
10
10
|
ERRORS,
|
|
11
11
|
HardwareErrorCode,
|
|
12
|
+
ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
|
|
12
13
|
ONEKEY_SERVICE_UUID,
|
|
13
|
-
|
|
14
|
+
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
15
|
+
createKnownBleUuidAliases,
|
|
16
|
+
hasOnekeyCommunicationService,
|
|
17
|
+
isOnekeyBluetoothDevice,
|
|
18
|
+
matchesKnownBleUuid,
|
|
14
19
|
wait,
|
|
15
20
|
} from '@onekeyfe/hd-shared';
|
|
16
21
|
import pRetry from 'p-retry';
|
|
17
22
|
|
|
18
23
|
import { safeLog } from './types/noble-extended';
|
|
19
|
-
import { softRefreshSubscription } from './ble-ops';
|
|
24
|
+
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
20
25
|
|
|
21
26
|
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
22
27
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
@@ -39,6 +44,7 @@ const bluetoothState: {
|
|
|
39
44
|
|
|
40
45
|
// Global persistent state listener for app layer
|
|
41
46
|
let persistentStateListener: ((state: string) => void) | null = null;
|
|
47
|
+
let persistentDiscoverListener: ((peripheral: Peripheral) => void) | null = null;
|
|
42
48
|
|
|
43
49
|
// Device cache and connection state
|
|
44
50
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
@@ -50,6 +56,10 @@ const subscribedDevices = new Map<string, boolean>(); // Track subscription stat
|
|
|
50
56
|
|
|
51
57
|
// 🔒 Subscription operation state tracking to prevent race conditions
|
|
52
58
|
const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' | 'idle'>();
|
|
59
|
+
const deviceDisconnectListeners = new Map<
|
|
60
|
+
string,
|
|
61
|
+
{ peripheral: Peripheral; listener: () => void }
|
|
62
|
+
>();
|
|
53
63
|
|
|
54
64
|
// Windows-only response watchdog state moved to utils/windows-ble-recovery
|
|
55
65
|
|
|
@@ -59,11 +69,9 @@ const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' |
|
|
|
59
69
|
|
|
60
70
|
// Service UUIDs to scan for - using constants from hd-shared
|
|
61
71
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const NORMALIZED_WRITE_UUID = '0002';
|
|
66
|
-
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
72
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
73
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
74
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
67
75
|
|
|
68
76
|
// Timeout and interval constants
|
|
69
77
|
const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
|
|
@@ -72,6 +80,7 @@ const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has lon
|
|
|
72
80
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
73
81
|
const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow)
|
|
74
82
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
83
|
+
const BLE_CLEANUP_TIMEOUT = 250;
|
|
75
84
|
|
|
76
85
|
// Write-related constants
|
|
77
86
|
const BLE_PACKET_SIZE = 192;
|
|
@@ -82,34 +91,16 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
|
82
91
|
/status:\s*3/i, // Windows pairing cancelled / GATT write failed
|
|
83
92
|
];
|
|
84
93
|
|
|
85
|
-
function getBleUuidKey(uuid?: string | null) {
|
|
86
|
-
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
87
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
91
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
92
|
-
'0001',
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
function isGenericBleService(uuid?: string | null) {
|
|
96
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function hasOneKeyAdvertisementService(peripheral: Peripheral) {
|
|
100
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
|
|
101
|
-
return serviceUuids.some(uuid => {
|
|
102
|
-
const uuidKey = getBleUuidKey(uuid);
|
|
103
|
-
return (
|
|
104
|
-
NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
105
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
94
|
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
111
|
-
const
|
|
112
|
-
return
|
|
95
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
96
|
+
return (
|
|
97
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
98
|
+
isOnekeyBluetoothDevice({
|
|
99
|
+
id: peripheral.id,
|
|
100
|
+
localName: peripheral.advertisement?.localName,
|
|
101
|
+
serviceUuids,
|
|
102
|
+
})
|
|
103
|
+
);
|
|
113
104
|
}
|
|
114
105
|
|
|
115
106
|
/**
|
|
@@ -117,12 +108,6 @@ function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
|
117
108
|
* renderer-side transport. Packet reassembly is handled by ElectronBleTransport.
|
|
118
109
|
*/
|
|
119
110
|
function emitRawNotification(deviceId: string, data: Buffer): void {
|
|
120
|
-
logger?.info('[NobleBLE] Raw notification', {
|
|
121
|
-
deviceId,
|
|
122
|
-
dataLength: data.length,
|
|
123
|
-
firstBytes: data.subarray(0, 8).toString('hex'),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
111
|
const appCb = notificationCallbacks.get(deviceId);
|
|
127
112
|
if (appCb) appCb(data.toString('hex'));
|
|
128
113
|
}
|
|
@@ -296,9 +281,12 @@ async function initializeNoble(): Promise<void> {
|
|
|
296
281
|
});
|
|
297
282
|
|
|
298
283
|
// Set up device discovery
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
284
|
+
if (!persistentDiscoverListener) {
|
|
285
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
286
|
+
handleDeviceDiscovered(peripheral);
|
|
287
|
+
};
|
|
288
|
+
noble.on('discover', persistentDiscoverListener);
|
|
289
|
+
}
|
|
302
290
|
|
|
303
291
|
logger?.info('[NobleBLE] Noble initialized successfully');
|
|
304
292
|
} catch (error) {
|
|
@@ -360,6 +348,11 @@ function cleanupDevice(
|
|
|
360
348
|
|
|
361
349
|
// 1. Clean up connection state
|
|
362
350
|
if (cleanupConnection) {
|
|
351
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
352
|
+
if (disconnectEntry) {
|
|
353
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
354
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
355
|
+
}
|
|
363
356
|
connectedDevices.delete(deviceId);
|
|
364
357
|
deviceCharacteristics.delete(deviceId);
|
|
365
358
|
notificationCallbacks.delete(deviceId);
|
|
@@ -409,13 +402,16 @@ function setupDisconnectListener(
|
|
|
409
402
|
deviceId: string,
|
|
410
403
|
webContents: WebContents
|
|
411
404
|
): void {
|
|
412
|
-
|
|
413
|
-
|
|
405
|
+
const existing = deviceDisconnectListeners.get(deviceId);
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.peripheral.removeListener('disconnect', existing.listener);
|
|
408
|
+
}
|
|
414
409
|
|
|
415
|
-
|
|
416
|
-
peripheral.on('disconnect', () => {
|
|
410
|
+
const listener = () => {
|
|
417
411
|
handleDeviceDisconnect(deviceId, webContents);
|
|
418
|
-
}
|
|
412
|
+
};
|
|
413
|
+
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
414
|
+
peripheral.on('disconnect', listener);
|
|
419
415
|
}
|
|
420
416
|
|
|
421
417
|
// ===== Write helpers (inline) =====
|
|
@@ -551,12 +547,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
551
547
|
}
|
|
552
548
|
|
|
553
549
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
554
|
-
logger?.info('[NobleBLE] Writing data:', {
|
|
555
|
-
deviceId,
|
|
556
|
-
dataLength: toBuffer.length,
|
|
557
|
-
firstBytes: toBuffer.subarray(0, 8).toString('hex'),
|
|
558
|
-
});
|
|
559
|
-
|
|
560
550
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
561
551
|
|
|
562
552
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
@@ -618,9 +608,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
618
608
|
|
|
619
609
|
// Handle discovered device (for general enumeration only)
|
|
620
610
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
621
|
-
const deviceName = peripheral.advertisement?.localName || 'Unknown Device';
|
|
622
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids || [];
|
|
623
|
-
|
|
624
611
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
625
612
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
626
613
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
@@ -630,10 +617,10 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
630
617
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
631
618
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
632
619
|
if (isNewDevice) {
|
|
633
|
-
logger?.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
serviceUUIDs: serviceUuids,
|
|
620
|
+
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
621
|
+
deviceId: peripheral.id,
|
|
622
|
+
name: peripheral.advertisement?.localName || 'Unknown Device',
|
|
623
|
+
serviceUUIDs: peripheral.advertisement?.serviceUuids || [],
|
|
637
624
|
});
|
|
638
625
|
}
|
|
639
626
|
}
|
|
@@ -643,16 +630,14 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
643
630
|
function ensureDiscoverListener(): void {
|
|
644
631
|
if (!noble) return;
|
|
645
632
|
|
|
646
|
-
|
|
647
|
-
const listenerCount = (noble as any).listenerCount('discover');
|
|
648
|
-
|
|
649
|
-
if (listenerCount === 0) {
|
|
633
|
+
if (!persistentDiscoverListener) {
|
|
650
634
|
logger?.info('[NobleBLE] Discover listener missing, re-adding it');
|
|
651
|
-
|
|
635
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
652
636
|
handleDeviceDiscovered(peripheral);
|
|
653
|
-
}
|
|
637
|
+
};
|
|
638
|
+
noble.on('discover', persistentDiscoverListener);
|
|
654
639
|
} else {
|
|
655
|
-
logger?.debug('[NobleBLE] Discover listener already
|
|
640
|
+
logger?.debug('[NobleBLE] Discover listener already registered');
|
|
656
641
|
}
|
|
657
642
|
}
|
|
658
643
|
|
|
@@ -671,7 +656,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
671
656
|
return new Promise((resolve, reject) => {
|
|
672
657
|
// Local discover listener - only matches target device
|
|
673
658
|
const onDiscover = (peripheral: Peripheral) => {
|
|
674
|
-
if (peripheral.id === targetDeviceId) {
|
|
659
|
+
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
675
660
|
logger?.info('[NobleBLE] Target device found during targeted scan:', {
|
|
676
661
|
id: peripheral.id,
|
|
677
662
|
name: peripheral.advertisement?.localName,
|
|
@@ -789,30 +774,24 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
789
774
|
// Stop scanning
|
|
790
775
|
async function stopScanning(): Promise<void> {
|
|
791
776
|
if (!noble) return;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
noble.stopScanning(() => {
|
|
800
|
-
logger?.info('[NobleBLE] Scanning stopped');
|
|
801
|
-
resolve();
|
|
802
|
-
});
|
|
777
|
+
const nobleInstance = noble;
|
|
778
|
+
await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
779
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
780
|
+
timeoutBehavior: 'resolve',
|
|
803
781
|
});
|
|
782
|
+
logger?.info('[NobleBLE] Scanning stopped');
|
|
804
783
|
}
|
|
805
784
|
|
|
806
785
|
// Clean up all Noble listeners (for app exit)
|
|
807
786
|
function cleanupNobleListeners(): void {
|
|
808
787
|
if (!noble) return;
|
|
809
788
|
|
|
810
|
-
// Remove all listeners to prevent memory leaks
|
|
811
|
-
// Noble uses EventEmitter, need to use removeAllListeners
|
|
812
789
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
790
|
+
if (persistentDiscoverListener) {
|
|
791
|
+
noble.removeListener('discover', persistentDiscoverListener);
|
|
792
|
+
persistentDiscoverListener = null;
|
|
793
|
+
}
|
|
794
|
+
logger?.info('[NobleBLE] Owned Noble listeners cleaned up');
|
|
816
795
|
} catch (error) {
|
|
817
796
|
logger?.error('[NobleBLE] Failed to clean up some listeners:', error);
|
|
818
797
|
}
|
|
@@ -910,31 +889,25 @@ async function discoverServicesAndCharacteristics(
|
|
|
910
889
|
});
|
|
911
890
|
});
|
|
912
891
|
|
|
913
|
-
// Log all discovered services
|
|
914
|
-
logger?.info(
|
|
915
|
-
'[NobleBLE] All services:',
|
|
916
|
-
services?.map(s => s.uuid)
|
|
917
|
-
);
|
|
918
|
-
|
|
919
892
|
if (!services || services.length === 0) {
|
|
920
893
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
921
894
|
}
|
|
922
895
|
|
|
896
|
+
logger?.debug('[NobleBLE] services discovered', {
|
|
897
|
+
deviceId: peripheral.id,
|
|
898
|
+
serviceUUIDs: services.map(service => service.uuid),
|
|
899
|
+
});
|
|
900
|
+
|
|
923
901
|
// Find OneKey service — Noble may expose 128-bit UUIDs as short UUID keys.
|
|
924
|
-
|
|
925
|
-
if (!service) {
|
|
926
|
-
logger?.info('[NobleBLE] Known OneKey service UUID not found, trying first vendor service');
|
|
927
|
-
service =
|
|
928
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
929
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
930
|
-
services[0];
|
|
931
|
-
}
|
|
902
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
932
903
|
if (!service) {
|
|
933
904
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
934
905
|
}
|
|
935
906
|
const selectedService = service;
|
|
936
|
-
logger?.
|
|
937
|
-
|
|
907
|
+
logger?.debug('[NobleBLE] service selected', {
|
|
908
|
+
deviceId: peripheral.id,
|
|
909
|
+
serviceUuid: selectedService.uuid,
|
|
910
|
+
});
|
|
938
911
|
// Step 2: Discover ALL characteristics (no filter)
|
|
939
912
|
const characteristics = await new Promise<Characteristic[]>((resolve, reject) => {
|
|
940
913
|
selectedService.discoverCharacteristics([], (error, chars) => {
|
|
@@ -948,30 +921,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
948
921
|
});
|
|
949
922
|
|
|
950
923
|
// Step 3: Find required characteristics
|
|
951
|
-
logger?.info('[NobleBLE] Discovered characteristics:', {
|
|
952
|
-
count: characteristics?.length || 0,
|
|
953
|
-
uuids: characteristics?.map(c => c.uuid) || [],
|
|
954
|
-
});
|
|
955
|
-
|
|
956
924
|
let writeCharacteristic: Characteristic | null = null;
|
|
957
925
|
let notifyCharacteristic: Characteristic | null = null;
|
|
958
926
|
|
|
959
927
|
for (const characteristic of characteristics) {
|
|
960
|
-
|
|
961
|
-
const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
|
|
962
|
-
|
|
963
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
928
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
964
929
|
writeCharacteristic = characteristic;
|
|
965
|
-
} else if (
|
|
930
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
966
931
|
notifyCharacteristic = characteristic;
|
|
967
932
|
}
|
|
968
933
|
}
|
|
969
934
|
|
|
970
|
-
logger?.info('[NobleBLE] Characteristic discovery result:', {
|
|
971
|
-
writeFound: !!writeCharacteristic,
|
|
972
|
-
notifyFound: !!notifyCharacteristic,
|
|
973
|
-
});
|
|
974
|
-
|
|
975
935
|
if (!writeCharacteristic || !notifyCharacteristic) {
|
|
976
936
|
logger?.error(
|
|
977
937
|
'[NobleBLE] Missing characteristics - write:',
|
|
@@ -985,6 +945,13 @@ async function discoverServicesAndCharacteristics(
|
|
|
985
945
|
);
|
|
986
946
|
}
|
|
987
947
|
|
|
948
|
+
logger?.debug('[NobleBLE] characteristics selected', {
|
|
949
|
+
deviceId: peripheral.id,
|
|
950
|
+
serviceUuid: selectedService.uuid,
|
|
951
|
+
writeUuid: writeCharacteristic.uuid,
|
|
952
|
+
notifyUuid: notifyCharacteristic.uuid,
|
|
953
|
+
});
|
|
954
|
+
|
|
988
955
|
return { write: writeCharacteristic, notify: notifyCharacteristic };
|
|
989
956
|
})();
|
|
990
957
|
|
|
@@ -1017,29 +984,23 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
1017
984
|
|
|
1018
985
|
// Step 2: Force disconnect if connected
|
|
1019
986
|
if (peripheral.state === 'connected') {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
987
|
+
await runBleCallbackOperation(
|
|
988
|
+
callback =>
|
|
989
|
+
peripheral.disconnect(() => {
|
|
990
|
+
logger?.info('[NobleBLE] Force disconnect completed');
|
|
991
|
+
callback();
|
|
992
|
+
}),
|
|
993
|
+
{ timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' }
|
|
994
|
+
);
|
|
1028
995
|
}
|
|
1029
996
|
|
|
1030
997
|
// Step 3: Re-establish connection
|
|
1031
|
-
await
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
logger?.error('[NobleBLE] Force reconnect failed:', error);
|
|
1035
|
-
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
1036
|
-
} else {
|
|
1037
|
-
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1038
|
-
connectedDevices.set(deviceId, peripheral);
|
|
1039
|
-
resolve();
|
|
1040
|
-
}
|
|
1041
|
-
});
|
|
998
|
+
await runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
999
|
+
timeoutMs: CONNECTION_TIMEOUT,
|
|
1000
|
+
timeoutBehavior: 'reject',
|
|
1042
1001
|
});
|
|
1002
|
+
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1003
|
+
connectedDevices.set(deviceId, peripheral);
|
|
1043
1004
|
|
|
1044
1005
|
// Wait for connection to stabilize
|
|
1045
1006
|
await wait(500);
|
|
@@ -1170,16 +1131,26 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1170
1131
|
deviceId: string,
|
|
1171
1132
|
webContents: WebContents
|
|
1172
1133
|
): Promise<CharacteristicPair> {
|
|
1173
|
-
// Force reconnect to clear GATT cache
|
|
1174
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1175
1134
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1176
1135
|
|
|
1177
|
-
// Discover services and characteristics with retry
|
|
1178
1136
|
try {
|
|
1179
1137
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1180
|
-
} catch (
|
|
1181
|
-
|
|
1182
|
-
|
|
1138
|
+
} catch (initialError) {
|
|
1139
|
+
logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
1140
|
+
deviceId,
|
|
1141
|
+
error: initialError,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1146
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1147
|
+
try {
|
|
1148
|
+
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1149
|
+
} catch (reconnectError) {
|
|
1150
|
+
logger?.error(
|
|
1151
|
+
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1152
|
+
reconnectError
|
|
1153
|
+
);
|
|
1183
1154
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1184
1155
|
}
|
|
1185
1156
|
}
|
|
@@ -1342,20 +1313,21 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1342
1313
|
return;
|
|
1343
1314
|
}
|
|
1344
1315
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
peripheral.
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1316
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
1317
|
+
if (disconnectEntry) {
|
|
1318
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
1319
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1323
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1324
|
+
timeoutBehavior: 'resolve',
|
|
1325
|
+
});
|
|
1326
|
+
cleanupDevice(deviceId, undefined, {
|
|
1327
|
+
cleanupConnection: true,
|
|
1328
|
+
sendDisconnectEvent: false,
|
|
1329
|
+
cancelOperations: true,
|
|
1330
|
+
reason: 'manual-disconnect',
|
|
1359
1331
|
});
|
|
1360
1332
|
}
|
|
1361
1333
|
|
|
@@ -1376,17 +1348,11 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
|
|
|
1376
1348
|
subscriptionOperations.set(deviceId, 'unsubscribing');
|
|
1377
1349
|
|
|
1378
1350
|
try {
|
|
1379
|
-
await
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
logger?.error('[NobleBLE] Notification unsubscription failed:', error);
|
|
1383
|
-
reject(error);
|
|
1384
|
-
} else {
|
|
1385
|
-
logger?.info('[NobleBLE] Notification unsubscription successful');
|
|
1386
|
-
resolve();
|
|
1387
|
-
}
|
|
1388
|
-
});
|
|
1351
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1352
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1353
|
+
timeoutBehavior: 'resolve',
|
|
1389
1354
|
});
|
|
1355
|
+
logger?.info('[NobleBLE] Notification unsubscription completed');
|
|
1390
1356
|
|
|
1391
1357
|
// Remove all listeners and clear subscription status
|
|
1392
1358
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -1479,19 +1445,13 @@ async function subscribeNotifications(
|
|
|
1479
1445
|
notifyCharacteristic: Characteristic
|
|
1480
1446
|
): Promise<void> {
|
|
1481
1447
|
// Listeners already cleaned up above, no need to repeat
|
|
1482
|
-
await
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
});
|
|
1448
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1449
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1450
|
+
timeoutBehavior: 'resolve',
|
|
1486
1451
|
});
|
|
1487
|
-
await
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
reject(error);
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
resolve();
|
|
1494
|
-
});
|
|
1452
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1453
|
+
timeoutMs: CONNECTION_TIMEOUT,
|
|
1454
|
+
timeoutBehavior: 'reject',
|
|
1495
1455
|
});
|
|
1496
1456
|
|
|
1497
1457
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
@@ -1516,29 +1476,25 @@ async function subscribeNotifications(
|
|
|
1516
1476
|
|
|
1517
1477
|
// Setup IPC handlers
|
|
1518
1478
|
export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
1519
|
-
// Use console.log for initial logging as electron-log might not be available yet.
|
|
1520
|
-
console.log('[NobleBLE] Attempting to set up Noble BLE handlers.');
|
|
1521
1479
|
try {
|
|
1522
|
-
console.log('[NobleBLE] NOBLE_VERSION_771');
|
|
1523
|
-
|
|
1524
1480
|
// @ts-ignore – electron-log is only available at runtime
|
|
1525
1481
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1526
1482
|
logger = require('electron-log') as Logger;
|
|
1527
|
-
console.log('[NobleBLE] electron-log loaded successfully.');
|
|
1528
1483
|
|
|
1529
1484
|
// @ts-ignore – electron is only available at runtime
|
|
1530
1485
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1531
1486
|
const { ipcMain } = require('electron');
|
|
1532
|
-
console.log('[NobleBLE] electron.ipcMain loaded successfully.');
|
|
1533
1487
|
|
|
1534
1488
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1535
1489
|
|
|
1536
1490
|
// Handle enumerate request
|
|
1537
|
-
console.log(`[NobleBLE] Registering handler for: ${EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE}`);
|
|
1538
1491
|
ipcMain.handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
1539
1492
|
try {
|
|
1540
1493
|
const devices = await enumerateDevices();
|
|
1541
|
-
safeLog(logger, '
|
|
1494
|
+
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
1495
|
+
count: devices.length,
|
|
1496
|
+
devices: devices.map(device => ({ id: device.id, name: device.name })),
|
|
1497
|
+
});
|
|
1542
1498
|
return devices;
|
|
1543
1499
|
} catch (error) {
|
|
1544
1500
|
safeLog(logger, 'error', 'Enumeration failed:', error);
|
|
@@ -1583,7 +1539,6 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1583
1539
|
ipcMain.handle(
|
|
1584
1540
|
EOneKeyBleMessageKeys.NOBLE_BLE_WRITE,
|
|
1585
1541
|
async (_event: IpcMainInvokeEvent, deviceId: string, hexData: string) => {
|
|
1586
|
-
logger?.info('[NobleBLE] IPC WRITE', { deviceId, len: hexData.length });
|
|
1587
1542
|
await transmitHexDataToDevice(deviceId, hexData);
|
|
1588
1543
|
}
|
|
1589
1544
|
);
|
|
@@ -1648,32 +1603,24 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1648
1603
|
// Cleanup on app quit
|
|
1649
1604
|
webContents.on('destroyed', () => {
|
|
1650
1605
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1606
|
+
(async () => {
|
|
1607
|
+
const deviceIds = Array.from(connectedDevices.keys());
|
|
1608
|
+
for (const deviceId of deviceIds) {
|
|
1609
|
+
await unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1610
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1611
|
+
}
|
|
1651
1612
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1613
|
+
await stopScanning().catch(() => undefined);
|
|
1614
|
+
if (noble && persistentStateListener) {
|
|
1615
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1616
|
+
persistentStateListener = null;
|
|
1617
|
+
}
|
|
1618
|
+
cleanupNobleListeners();
|
|
1619
|
+
discoveredDevices.clear();
|
|
1620
|
+
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1621
|
+
})().catch(error => {
|
|
1622
|
+
safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
|
|
1661
1623
|
});
|
|
1662
|
-
|
|
1663
|
-
// 2. Stop scanning
|
|
1664
|
-
stopScanning();
|
|
1665
|
-
|
|
1666
|
-
// 3. Clean up Noble listeners
|
|
1667
|
-
if (noble && persistentStateListener) {
|
|
1668
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1669
|
-
persistentStateListener = null;
|
|
1670
|
-
}
|
|
1671
|
-
cleanupNobleListeners();
|
|
1672
|
-
|
|
1673
|
-
// 4. Clear discovered devices cache
|
|
1674
|
-
discoveredDevices.clear();
|
|
1675
|
-
|
|
1676
|
-
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1677
1624
|
});
|
|
1678
1625
|
|
|
1679
1626
|
safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
|