@onekeyfe/hd-transport-electron 1.2.0-alpha.7 → 1.2.0-alpha.70
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-84fea839.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-a0b2f06d.js} +232 -204
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/noble-ble-timeouts.d.ts +3 -0
- package/dist/noble-ble-timeouts.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/__tests__/ble-ops.test.ts +21 -0
- package/src/__tests__/noble-ble-handler.test.ts +354 -0
- package/src/ble-ops.ts +40 -10
- package/src/noble-ble-handler.ts +224 -235
- package/src/noble-ble-timeouts.ts +2 -0
package/src/noble-ble-handler.ts
CHANGED
|
@@ -9,14 +9,24 @@ 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
|
+
isPro2FindMyAdvertisementName,
|
|
19
|
+
matchesKnownBleUuid,
|
|
14
20
|
wait,
|
|
15
21
|
} from '@onekeyfe/hd-shared';
|
|
16
22
|
import pRetry from 'p-retry';
|
|
17
23
|
|
|
18
24
|
import { safeLog } from './types/noble-extended';
|
|
19
|
-
import { softRefreshSubscription } from './ble-ops';
|
|
25
|
+
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
26
|
+
import {
|
|
27
|
+
NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
28
|
+
NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
|
|
29
|
+
} from './noble-ble-timeouts';
|
|
20
30
|
|
|
21
31
|
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
22
32
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
@@ -39,6 +49,7 @@ const bluetoothState: {
|
|
|
39
49
|
|
|
40
50
|
// Global persistent state listener for app layer
|
|
41
51
|
let persistentStateListener: ((state: string) => void) | null = null;
|
|
52
|
+
let persistentDiscoverListener: ((peripheral: Peripheral) => void) | null = null;
|
|
42
53
|
|
|
43
54
|
// Device cache and connection state
|
|
44
55
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
@@ -50,6 +61,10 @@ const subscribedDevices = new Map<string, boolean>(); // Track subscription stat
|
|
|
50
61
|
|
|
51
62
|
// 🔒 Subscription operation state tracking to prevent race conditions
|
|
52
63
|
const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' | 'idle'>();
|
|
64
|
+
const deviceDisconnectListeners = new Map<
|
|
65
|
+
string,
|
|
66
|
+
{ peripheral: Peripheral; listener: () => void }
|
|
67
|
+
>();
|
|
53
68
|
|
|
54
69
|
// Windows-only response watchdog state moved to utils/windows-ble-recovery
|
|
55
70
|
|
|
@@ -59,19 +74,16 @@ const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' |
|
|
|
59
74
|
|
|
60
75
|
// Service UUIDs to scan for - using constants from hd-shared
|
|
61
76
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const NORMALIZED_WRITE_UUID = '0002';
|
|
66
|
-
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
77
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
78
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
79
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
67
80
|
|
|
68
81
|
// Timeout and interval constants
|
|
69
82
|
const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
|
|
70
|
-
const DEVICE_SCAN_TIMEOUT =
|
|
71
|
-
const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has longer advertising interval)
|
|
83
|
+
const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
72
84
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
73
|
-
const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow)
|
|
74
85
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
86
|
+
const BLE_CLEANUP_TIMEOUT = 250;
|
|
75
87
|
|
|
76
88
|
// Write-related constants
|
|
77
89
|
const BLE_PACKET_SIZE = 192;
|
|
@@ -82,34 +94,24 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
|
82
94
|
/status:\s*3/i, // Windows pairing cancelled / GATT write failed
|
|
83
95
|
];
|
|
84
96
|
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
|
|
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
|
-
}
|
|
97
|
+
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
98
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
99
|
+
const localName = peripheral.advertisement?.localName;
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
105
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
101
|
+
// Noble localName is the current advertisement name, so reject the Pro2
|
|
102
|
+
// Find My endpoint before the communication-service fast path accepts it.
|
|
103
|
+
if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
return (
|
|
108
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
109
|
+
isOnekeyBluetoothDevice({
|
|
110
|
+
id: peripheral.id,
|
|
111
|
+
localName,
|
|
112
|
+
serviceUuids,
|
|
113
|
+
})
|
|
114
|
+
);
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
|
@@ -117,12 +119,6 @@ function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
|
117
119
|
* renderer-side transport. Packet reassembly is handled by ElectronBleTransport.
|
|
118
120
|
*/
|
|
119
121
|
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
122
|
const appCb = notificationCallbacks.get(deviceId);
|
|
127
123
|
if (appCb) appCb(data.toString('hex'));
|
|
128
124
|
}
|
|
@@ -296,9 +292,12 @@ async function initializeNoble(): Promise<void> {
|
|
|
296
292
|
});
|
|
297
293
|
|
|
298
294
|
// Set up device discovery
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
295
|
+
if (!persistentDiscoverListener) {
|
|
296
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
297
|
+
handleDeviceDiscovered(peripheral);
|
|
298
|
+
};
|
|
299
|
+
noble.on('discover', persistentDiscoverListener);
|
|
300
|
+
}
|
|
302
301
|
|
|
303
302
|
logger?.info('[NobleBLE] Noble initialized successfully');
|
|
304
303
|
} catch (error) {
|
|
@@ -360,6 +359,11 @@ function cleanupDevice(
|
|
|
360
359
|
|
|
361
360
|
// 1. Clean up connection state
|
|
362
361
|
if (cleanupConnection) {
|
|
362
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
363
|
+
if (disconnectEntry) {
|
|
364
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
365
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
366
|
+
}
|
|
363
367
|
connectedDevices.delete(deviceId);
|
|
364
368
|
deviceCharacteristics.delete(deviceId);
|
|
365
369
|
notificationCallbacks.delete(deviceId);
|
|
@@ -409,13 +413,16 @@ function setupDisconnectListener(
|
|
|
409
413
|
deviceId: string,
|
|
410
414
|
webContents: WebContents
|
|
411
415
|
): void {
|
|
412
|
-
|
|
413
|
-
|
|
416
|
+
const existing = deviceDisconnectListeners.get(deviceId);
|
|
417
|
+
if (existing) {
|
|
418
|
+
existing.peripheral.removeListener('disconnect', existing.listener);
|
|
419
|
+
}
|
|
414
420
|
|
|
415
|
-
|
|
416
|
-
peripheral.on('disconnect', () => {
|
|
421
|
+
const listener = () => {
|
|
417
422
|
handleDeviceDisconnect(deviceId, webContents);
|
|
418
|
-
}
|
|
423
|
+
};
|
|
424
|
+
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
425
|
+
peripheral.on('disconnect', listener);
|
|
419
426
|
}
|
|
420
427
|
|
|
421
428
|
// ===== Write helpers (inline) =====
|
|
@@ -551,12 +558,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
551
558
|
}
|
|
552
559
|
|
|
553
560
|
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
561
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
561
562
|
|
|
562
563
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
@@ -618,9 +619,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
618
619
|
|
|
619
620
|
// Handle discovered device (for general enumeration only)
|
|
620
621
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
621
|
-
const deviceName = peripheral.advertisement?.localName || 'Unknown Device';
|
|
622
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids || [];
|
|
623
|
-
|
|
624
622
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
625
623
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
626
624
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
@@ -630,10 +628,10 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
630
628
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
631
629
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
632
630
|
if (isNewDevice) {
|
|
633
|
-
logger?.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
serviceUUIDs: serviceUuids,
|
|
631
|
+
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
632
|
+
deviceId: peripheral.id,
|
|
633
|
+
name: peripheral.advertisement?.localName || 'Unknown Device',
|
|
634
|
+
serviceUUIDs: peripheral.advertisement?.serviceUuids || [],
|
|
637
635
|
});
|
|
638
636
|
}
|
|
639
637
|
}
|
|
@@ -643,19 +641,24 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
643
641
|
function ensureDiscoverListener(): void {
|
|
644
642
|
if (!noble) return;
|
|
645
643
|
|
|
646
|
-
|
|
647
|
-
const listenerCount = (noble as any).listenerCount('discover');
|
|
648
|
-
|
|
649
|
-
if (listenerCount === 0) {
|
|
644
|
+
if (!persistentDiscoverListener) {
|
|
650
645
|
logger?.info('[NobleBLE] Discover listener missing, re-adding it');
|
|
651
|
-
|
|
646
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
652
647
|
handleDeviceDiscovered(peripheral);
|
|
653
|
-
}
|
|
648
|
+
};
|
|
649
|
+
noble.on('discover', persistentDiscoverListener);
|
|
654
650
|
} else {
|
|
655
|
-
logger?.debug('[NobleBLE] Discover listener already
|
|
651
|
+
logger?.debug('[NobleBLE] Discover listener already registered');
|
|
656
652
|
}
|
|
657
653
|
}
|
|
658
654
|
|
|
655
|
+
async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
656
|
+
await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
657
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
658
|
+
timeoutBehavior: 'resolve',
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
659
662
|
// Perform targeted scan for a specific device ID
|
|
660
663
|
// Uses self-contained local listener pattern - no global state needed
|
|
661
664
|
async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
|
|
@@ -669,28 +672,41 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
669
672
|
logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
|
|
670
673
|
|
|
671
674
|
return new Promise((resolve, reject) => {
|
|
675
|
+
let settled = false;
|
|
676
|
+
|
|
677
|
+
const finish = async (peripheral: Peripheral | null, error?: Error) => {
|
|
678
|
+
if (settled) return;
|
|
679
|
+
settled = true;
|
|
680
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
681
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
682
|
+
await waitForNobleScanStop(nobleInstance);
|
|
683
|
+
|
|
684
|
+
if (error) {
|
|
685
|
+
logger?.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
686
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (peripheral) {
|
|
690
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
691
|
+
}
|
|
692
|
+
resolve(peripheral);
|
|
693
|
+
};
|
|
694
|
+
|
|
672
695
|
// Local discover listener - only matches target device
|
|
673
696
|
const onDiscover = (peripheral: Peripheral) => {
|
|
674
|
-
if (peripheral.id === targetDeviceId) {
|
|
697
|
+
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
675
698
|
logger?.info('[NobleBLE] Target device found during targeted scan:', {
|
|
676
699
|
id: peripheral.id,
|
|
677
700
|
name: peripheral.advertisement?.localName,
|
|
678
701
|
});
|
|
679
|
-
|
|
680
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
681
|
-
nobleInstance.stopScanning();
|
|
682
|
-
discoveredDevices.set(peripheral.id, peripheral);
|
|
683
|
-
resolve(peripheral);
|
|
702
|
+
finish(peripheral).catch(reject);
|
|
684
703
|
}
|
|
685
704
|
};
|
|
686
705
|
|
|
687
|
-
// Timeout handler - must be after onDiscover so it can reference it
|
|
688
706
|
const timeoutId = setTimeout(() => {
|
|
689
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
690
|
-
nobleInstance.stopScanning();
|
|
691
707
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
692
|
-
|
|
693
|
-
},
|
|
708
|
+
finish(null).catch(reject);
|
|
709
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
694
710
|
|
|
695
711
|
// Add local listener for this scan
|
|
696
712
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -698,10 +714,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
698
714
|
// Start scanning — no service UUID filter (Pro2 may use different service UUID)
|
|
699
715
|
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
700
716
|
if (error) {
|
|
701
|
-
|
|
702
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
703
|
-
logger?.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
704
|
-
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
717
|
+
finish(null, error).catch(reject);
|
|
705
718
|
return;
|
|
706
719
|
}
|
|
707
720
|
logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
|
|
@@ -735,11 +748,13 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
735
748
|
const devices: DeviceInfo[] = [];
|
|
736
749
|
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
737
750
|
|
|
738
|
-
// Cleanup function: clears
|
|
739
|
-
|
|
751
|
+
// Cleanup function: clears timers and waits until Noble confirms scanning
|
|
752
|
+
// has stopped. Resolving enumerate before this callback creates a race with
|
|
753
|
+
// an immediately-following connection attempt.
|
|
754
|
+
const cleanup = async () => {
|
|
740
755
|
clearTimeout(timeoutId);
|
|
741
756
|
if (intervalId) clearInterval(intervalId);
|
|
742
|
-
nobleInstance
|
|
757
|
+
await waitForNobleScanStop(nobleInstance);
|
|
743
758
|
};
|
|
744
759
|
|
|
745
760
|
// Collect discovered devices into the devices array
|
|
@@ -759,10 +774,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
759
774
|
};
|
|
760
775
|
|
|
761
776
|
// Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
|
|
762
|
-
const timeoutId = setTimeout(() => {
|
|
777
|
+
const timeoutId = setTimeout(async () => {
|
|
763
778
|
// Final collection before resolving — catches devices discovered near the deadline
|
|
764
779
|
checkDevices();
|
|
765
|
-
cleanup();
|
|
780
|
+
await cleanup();
|
|
766
781
|
logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
|
|
767
782
|
resolve(devices);
|
|
768
783
|
}, DEVICE_SCAN_TIMEOUT);
|
|
@@ -770,9 +785,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
770
785
|
// Start scanning without a service UUID filter so Pro2 advertisements with
|
|
771
786
|
// short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
|
|
772
787
|
logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
773
|
-
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
788
|
+
nobleInstance.startScanning([], false, async (error?: Error) => {
|
|
774
789
|
if (error) {
|
|
775
|
-
cleanup();
|
|
790
|
+
await cleanup();
|
|
776
791
|
logger?.error('[NobleBLE] Failed to start scanning:', error);
|
|
777
792
|
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
778
793
|
return;
|
|
@@ -789,30 +804,21 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
789
804
|
// Stop scanning
|
|
790
805
|
async function stopScanning(): Promise<void> {
|
|
791
806
|
if (!noble) return;
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
resolve();
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
noble.stopScanning(() => {
|
|
800
|
-
logger?.info('[NobleBLE] Scanning stopped');
|
|
801
|
-
resolve();
|
|
802
|
-
});
|
|
803
|
-
});
|
|
807
|
+
const nobleInstance = noble;
|
|
808
|
+
await waitForNobleScanStop(nobleInstance);
|
|
809
|
+
logger?.info('[NobleBLE] Scanning stopped');
|
|
804
810
|
}
|
|
805
811
|
|
|
806
812
|
// Clean up all Noble listeners (for app exit)
|
|
807
813
|
function cleanupNobleListeners(): void {
|
|
808
814
|
if (!noble) return;
|
|
809
815
|
|
|
810
|
-
// Remove all listeners to prevent memory leaks
|
|
811
|
-
// Noble uses EventEmitter, need to use removeAllListeners
|
|
812
816
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
817
|
+
if (persistentDiscoverListener) {
|
|
818
|
+
noble.removeListener('discover', persistentDiscoverListener);
|
|
819
|
+
persistentDiscoverListener = null;
|
|
820
|
+
}
|
|
821
|
+
logger?.info('[NobleBLE] Owned Noble listeners cleaned up');
|
|
816
822
|
} catch (error) {
|
|
817
823
|
logger?.error('[NobleBLE] Failed to clean up some listeners:', error);
|
|
818
824
|
}
|
|
@@ -910,31 +916,25 @@ async function discoverServicesAndCharacteristics(
|
|
|
910
916
|
});
|
|
911
917
|
});
|
|
912
918
|
|
|
913
|
-
// Log all discovered services
|
|
914
|
-
logger?.info(
|
|
915
|
-
'[NobleBLE] All services:',
|
|
916
|
-
services?.map(s => s.uuid)
|
|
917
|
-
);
|
|
918
|
-
|
|
919
919
|
if (!services || services.length === 0) {
|
|
920
920
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
921
921
|
}
|
|
922
922
|
|
|
923
|
+
logger?.debug('[NobleBLE] services discovered', {
|
|
924
|
+
deviceId: peripheral.id,
|
|
925
|
+
serviceUUIDs: services.map(service => service.uuid),
|
|
926
|
+
});
|
|
927
|
+
|
|
923
928
|
// 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
|
-
}
|
|
929
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
932
930
|
if (!service) {
|
|
933
931
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
934
932
|
}
|
|
935
933
|
const selectedService = service;
|
|
936
|
-
logger?.
|
|
937
|
-
|
|
934
|
+
logger?.debug('[NobleBLE] service selected', {
|
|
935
|
+
deviceId: peripheral.id,
|
|
936
|
+
serviceUuid: selectedService.uuid,
|
|
937
|
+
});
|
|
938
938
|
// Step 2: Discover ALL characteristics (no filter)
|
|
939
939
|
const characteristics = await new Promise<Characteristic[]>((resolve, reject) => {
|
|
940
940
|
selectedService.discoverCharacteristics([], (error, chars) => {
|
|
@@ -948,30 +948,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
948
948
|
});
|
|
949
949
|
|
|
950
950
|
// 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
951
|
let writeCharacteristic: Characteristic | null = null;
|
|
957
952
|
let notifyCharacteristic: Characteristic | null = null;
|
|
958
953
|
|
|
959
954
|
for (const characteristic of characteristics) {
|
|
960
|
-
|
|
961
|
-
const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
|
|
962
|
-
|
|
963
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
955
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
964
956
|
writeCharacteristic = characteristic;
|
|
965
|
-
} else if (
|
|
957
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
966
958
|
notifyCharacteristic = characteristic;
|
|
967
959
|
}
|
|
968
960
|
}
|
|
969
961
|
|
|
970
|
-
logger?.info('[NobleBLE] Characteristic discovery result:', {
|
|
971
|
-
writeFound: !!writeCharacteristic,
|
|
972
|
-
notifyFound: !!notifyCharacteristic,
|
|
973
|
-
});
|
|
974
|
-
|
|
975
962
|
if (!writeCharacteristic || !notifyCharacteristic) {
|
|
976
963
|
logger?.error(
|
|
977
964
|
'[NobleBLE] Missing characteristics - write:',
|
|
@@ -985,6 +972,13 @@ async function discoverServicesAndCharacteristics(
|
|
|
985
972
|
);
|
|
986
973
|
}
|
|
987
974
|
|
|
975
|
+
logger?.debug('[NobleBLE] characteristics selected', {
|
|
976
|
+
deviceId: peripheral.id,
|
|
977
|
+
serviceUuid: selectedService.uuid,
|
|
978
|
+
writeUuid: writeCharacteristic.uuid,
|
|
979
|
+
notifyUuid: notifyCharacteristic.uuid,
|
|
980
|
+
});
|
|
981
|
+
|
|
988
982
|
return { write: writeCharacteristic, notify: notifyCharacteristic };
|
|
989
983
|
})();
|
|
990
984
|
|
|
@@ -1017,29 +1011,23 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
1017
1011
|
|
|
1018
1012
|
// Step 2: Force disconnect if connected
|
|
1019
1013
|
if (peripheral.state === 'connected') {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1014
|
+
await runBleCallbackOperation(
|
|
1015
|
+
callback =>
|
|
1016
|
+
peripheral.disconnect(() => {
|
|
1017
|
+
logger?.info('[NobleBLE] Force disconnect completed');
|
|
1018
|
+
callback();
|
|
1019
|
+
}),
|
|
1020
|
+
{ timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' }
|
|
1021
|
+
);
|
|
1028
1022
|
}
|
|
1029
1023
|
|
|
1030
1024
|
// 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
|
-
});
|
|
1025
|
+
await runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
1026
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1027
|
+
timeoutBehavior: 'reject',
|
|
1042
1028
|
});
|
|
1029
|
+
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1030
|
+
connectedDevices.set(deviceId, peripheral);
|
|
1043
1031
|
|
|
1044
1032
|
// Wait for connection to stabilize
|
|
1045
1033
|
await wait(500);
|
|
@@ -1170,16 +1158,26 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1170
1158
|
deviceId: string,
|
|
1171
1159
|
webContents: WebContents
|
|
1172
1160
|
): Promise<CharacteristicPair> {
|
|
1173
|
-
// Force reconnect to clear GATT cache
|
|
1174
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1175
1161
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1176
1162
|
|
|
1177
|
-
// Discover services and characteristics with retry
|
|
1178
1163
|
try {
|
|
1179
1164
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1180
|
-
} catch (
|
|
1181
|
-
|
|
1182
|
-
|
|
1165
|
+
} catch (initialError) {
|
|
1166
|
+
logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
1167
|
+
deviceId,
|
|
1168
|
+
error: initialError,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1173
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1174
|
+
try {
|
|
1175
|
+
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1176
|
+
} catch (reconnectError) {
|
|
1177
|
+
logger?.error(
|
|
1178
|
+
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1179
|
+
reconnectError
|
|
1180
|
+
);
|
|
1183
1181
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1184
1182
|
}
|
|
1185
1183
|
}
|
|
@@ -1297,15 +1295,30 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1297
1295
|
}
|
|
1298
1296
|
|
|
1299
1297
|
return new Promise((resolve, reject) => {
|
|
1298
|
+
let connectionTimedOut = false;
|
|
1300
1299
|
const timeout = setTimeout(() => {
|
|
1300
|
+
connectionTimedOut = true;
|
|
1301
1301
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
1302
|
-
},
|
|
1302
|
+
}, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
|
|
1303
1303
|
|
|
1304
1304
|
// TypeScript type assertion - peripheral is guaranteed to be defined at this point
|
|
1305
1305
|
const connectedPeripheral = peripheral as Peripheral;
|
|
1306
1306
|
connectedPeripheral.connect(async (error: Error | undefined) => {
|
|
1307
1307
|
clearTimeout(timeout);
|
|
1308
1308
|
|
|
1309
|
+
// Noble may invoke the callback after the SDK timed out and released the request.
|
|
1310
|
+
// Ignore it to avoid initializing disposed commands or leaving an orphaned connection.
|
|
1311
|
+
if (connectionTimedOut) {
|
|
1312
|
+
if (!error) {
|
|
1313
|
+
try {
|
|
1314
|
+
connectedPeripheral.disconnect(() => undefined);
|
|
1315
|
+
} catch {
|
|
1316
|
+
// Best-effort cleanup only.
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1309
1322
|
if (error) {
|
|
1310
1323
|
logger?.error('[NobleBLE] Connection failed:', error);
|
|
1311
1324
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -1342,20 +1355,21 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1342
1355
|
return;
|
|
1343
1356
|
}
|
|
1344
1357
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
peripheral.
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1358
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
1359
|
+
if (disconnectEntry) {
|
|
1360
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
1361
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1365
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1366
|
+
timeoutBehavior: 'resolve',
|
|
1367
|
+
});
|
|
1368
|
+
cleanupDevice(deviceId, undefined, {
|
|
1369
|
+
cleanupConnection: true,
|
|
1370
|
+
sendDisconnectEvent: false,
|
|
1371
|
+
cancelOperations: true,
|
|
1372
|
+
reason: 'manual-disconnect',
|
|
1359
1373
|
});
|
|
1360
1374
|
}
|
|
1361
1375
|
|
|
@@ -1376,17 +1390,11 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
|
|
|
1376
1390
|
subscriptionOperations.set(deviceId, 'unsubscribing');
|
|
1377
1391
|
|
|
1378
1392
|
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
|
-
});
|
|
1393
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1394
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1395
|
+
timeoutBehavior: 'resolve',
|
|
1389
1396
|
});
|
|
1397
|
+
logger?.info('[NobleBLE] Notification unsubscription completed');
|
|
1390
1398
|
|
|
1391
1399
|
// Remove all listeners and clear subscription status
|
|
1392
1400
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -1479,19 +1487,13 @@ async function subscribeNotifications(
|
|
|
1479
1487
|
notifyCharacteristic: Characteristic
|
|
1480
1488
|
): Promise<void> {
|
|
1481
1489
|
// Listeners already cleaned up above, no need to repeat
|
|
1482
|
-
await
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
});
|
|
1490
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1491
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1492
|
+
timeoutBehavior: 'resolve',
|
|
1486
1493
|
});
|
|
1487
|
-
await
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
reject(error);
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
resolve();
|
|
1494
|
-
});
|
|
1494
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1495
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1496
|
+
timeoutBehavior: 'reject',
|
|
1495
1497
|
});
|
|
1496
1498
|
|
|
1497
1499
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
@@ -1516,29 +1518,25 @@ async function subscribeNotifications(
|
|
|
1516
1518
|
|
|
1517
1519
|
// Setup IPC handlers
|
|
1518
1520
|
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
1521
|
try {
|
|
1522
|
-
console.log('[NobleBLE] NOBLE_VERSION_771');
|
|
1523
|
-
|
|
1524
1522
|
// @ts-ignore – electron-log is only available at runtime
|
|
1525
1523
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1526
1524
|
logger = require('electron-log') as Logger;
|
|
1527
|
-
console.log('[NobleBLE] electron-log loaded successfully.');
|
|
1528
1525
|
|
|
1529
1526
|
// @ts-ignore – electron is only available at runtime
|
|
1530
1527
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1531
1528
|
const { ipcMain } = require('electron');
|
|
1532
|
-
console.log('[NobleBLE] electron.ipcMain loaded successfully.');
|
|
1533
1529
|
|
|
1534
1530
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1535
1531
|
|
|
1536
1532
|
// Handle enumerate request
|
|
1537
|
-
console.log(`[NobleBLE] Registering handler for: ${EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE}`);
|
|
1538
1533
|
ipcMain.handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
1539
1534
|
try {
|
|
1540
1535
|
const devices = await enumerateDevices();
|
|
1541
|
-
safeLog(logger, '
|
|
1536
|
+
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
1537
|
+
count: devices.length,
|
|
1538
|
+
devices: devices.map(device => ({ id: device.id, name: device.name })),
|
|
1539
|
+
});
|
|
1542
1540
|
return devices;
|
|
1543
1541
|
} catch (error) {
|
|
1544
1542
|
safeLog(logger, 'error', 'Enumeration failed:', error);
|
|
@@ -1583,7 +1581,6 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1583
1581
|
ipcMain.handle(
|
|
1584
1582
|
EOneKeyBleMessageKeys.NOBLE_BLE_WRITE,
|
|
1585
1583
|
async (_event: IpcMainInvokeEvent, deviceId: string, hexData: string) => {
|
|
1586
|
-
logger?.info('[NobleBLE] IPC WRITE', { deviceId, len: hexData.length });
|
|
1587
1584
|
await transmitHexDataToDevice(deviceId, hexData);
|
|
1588
1585
|
}
|
|
1589
1586
|
);
|
|
@@ -1648,32 +1645,24 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1648
1645
|
// Cleanup on app quit
|
|
1649
1646
|
webContents.on('destroyed', () => {
|
|
1650
1647
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1648
|
+
(async () => {
|
|
1649
|
+
const deviceIds = Array.from(connectedDevices.keys());
|
|
1650
|
+
for (const deviceId of deviceIds) {
|
|
1651
|
+
await unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1652
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1653
|
+
}
|
|
1651
1654
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1655
|
+
await stopScanning().catch(() => undefined);
|
|
1656
|
+
if (noble && persistentStateListener) {
|
|
1657
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1658
|
+
persistentStateListener = null;
|
|
1659
|
+
}
|
|
1660
|
+
cleanupNobleListeners();
|
|
1661
|
+
discoveredDevices.clear();
|
|
1662
|
+
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1663
|
+
})().catch(error => {
|
|
1664
|
+
safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
|
|
1661
1665
|
});
|
|
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
1666
|
});
|
|
1678
1667
|
|
|
1679
1668
|
safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
|