@onekeyfe/hd-transport-electron 1.2.0-alpha.7 → 1.2.0-alpha.71
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/ble-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/{index-02fc07fe.js → index-bd1cfdb6.js} +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-94c42077.js → noble-ble-handler-7ac9c43d.js} +276 -234
- 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/dist/types/desktop-api.d.ts +5 -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/__tests__/ble-ops.test.ts +21 -0
- package/src/__tests__/ble-packet-capacity.test.ts +13 -0
- package/src/__tests__/noble-ble-handler.test.ts +354 -0
- package/src/ble-ops.ts +40 -10
- package/src/ble-packet-capacity.ts +13 -0
- package/src/noble-ble-handler.ts +273 -253
- package/src/noble-ble-timeouts.ts +2 -0
- package/src/types/desktop-api.ts +2 -1
- package/src/types/noble-extended.ts +1 -0
package/src/noble-ble-handler.ts
CHANGED
|
@@ -9,14 +9,25 @@ 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
|
|
|
24
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
18
25
|
import { safeLog } from './types/noble-extended';
|
|
19
|
-
import { softRefreshSubscription } from './ble-ops';
|
|
26
|
+
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
27
|
+
import {
|
|
28
|
+
NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
29
|
+
NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
|
|
30
|
+
} from './noble-ble-timeouts';
|
|
20
31
|
|
|
21
32
|
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
22
33
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
@@ -39,6 +50,7 @@ const bluetoothState: {
|
|
|
39
50
|
|
|
40
51
|
// Global persistent state listener for app layer
|
|
41
52
|
let persistentStateListener: ((state: string) => void) | null = null;
|
|
53
|
+
let persistentDiscoverListener: ((peripheral: Peripheral) => void) | null = null;
|
|
42
54
|
|
|
43
55
|
// Device cache and connection state
|
|
44
56
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
@@ -50,6 +62,14 @@ const subscribedDevices = new Map<string, boolean>(); // Track subscription stat
|
|
|
50
62
|
|
|
51
63
|
// 🔒 Subscription operation state tracking to prevent race conditions
|
|
52
64
|
const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' | 'idle'>();
|
|
65
|
+
const deviceDisconnectListeners = new Map<
|
|
66
|
+
string,
|
|
67
|
+
{ peripheral: Peripheral; listener: () => void }
|
|
68
|
+
>();
|
|
69
|
+
const deviceMtuListeners = new Map<
|
|
70
|
+
string,
|
|
71
|
+
{ peripheral: Peripheral; listener: (mtu: number) => void }
|
|
72
|
+
>();
|
|
53
73
|
|
|
54
74
|
// Windows-only response watchdog state moved to utils/windows-ble-recovery
|
|
55
75
|
|
|
@@ -59,57 +79,44 @@ const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' |
|
|
|
59
79
|
|
|
60
80
|
// Service UUIDs to scan for - using constants from hd-shared
|
|
61
81
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const NORMALIZED_WRITE_UUID = '0002';
|
|
66
|
-
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
82
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
83
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
84
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
67
85
|
|
|
68
86
|
// Timeout and interval constants
|
|
69
87
|
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)
|
|
88
|
+
const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
72
89
|
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
90
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
91
|
+
const BLE_CLEANUP_TIMEOUT = 250;
|
|
75
92
|
|
|
76
93
|
// Write-related constants
|
|
77
|
-
const
|
|
78
|
-
const
|
|
94
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
95
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
79
96
|
const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 } as const;
|
|
80
97
|
const IS_WINDOWS = process.platform === 'win32';
|
|
81
98
|
const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
82
99
|
/status:\s*3/i, // Windows pairing cancelled / GATT write failed
|
|
83
100
|
];
|
|
84
101
|
|
|
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
|
-
}
|
|
102
|
+
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
103
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
104
|
+
const localName = peripheral.advertisement?.localName;
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
105
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
106
|
+
// Noble localName is the current advertisement name, so reject the Pro2
|
|
107
|
+
// Find My endpoint before the communication-service fast path accepts it.
|
|
108
|
+
if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
return (
|
|
113
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
114
|
+
isOnekeyBluetoothDevice({
|
|
115
|
+
id: peripheral.id,
|
|
116
|
+
localName,
|
|
117
|
+
serviceUuids,
|
|
118
|
+
})
|
|
119
|
+
);
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
/**
|
|
@@ -117,12 +124,6 @@ function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
|
117
124
|
* renderer-side transport. Packet reassembly is handled by ElectronBleTransport.
|
|
118
125
|
*/
|
|
119
126
|
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
127
|
const appCb = notificationCallbacks.get(deviceId);
|
|
127
128
|
if (appCb) appCb(data.toString('hex'));
|
|
128
129
|
}
|
|
@@ -296,9 +297,12 @@ async function initializeNoble(): Promise<void> {
|
|
|
296
297
|
});
|
|
297
298
|
|
|
298
299
|
// Set up device discovery
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
if (!persistentDiscoverListener) {
|
|
301
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
302
|
+
handleDeviceDiscovered(peripheral);
|
|
303
|
+
};
|
|
304
|
+
noble.on('discover', persistentDiscoverListener);
|
|
305
|
+
}
|
|
302
306
|
|
|
303
307
|
logger?.info('[NobleBLE] Noble initialized successfully');
|
|
304
308
|
} catch (error) {
|
|
@@ -360,6 +364,16 @@ function cleanupDevice(
|
|
|
360
364
|
|
|
361
365
|
// 1. Clean up connection state
|
|
362
366
|
if (cleanupConnection) {
|
|
367
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
368
|
+
if (disconnectEntry) {
|
|
369
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
370
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
371
|
+
}
|
|
372
|
+
const mtuEntry = deviceMtuListeners.get(deviceId);
|
|
373
|
+
if (mtuEntry) {
|
|
374
|
+
mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
|
|
375
|
+
deviceMtuListeners.delete(deviceId);
|
|
376
|
+
}
|
|
363
377
|
connectedDevices.delete(deviceId);
|
|
364
378
|
deviceCharacteristics.delete(deviceId);
|
|
365
379
|
notificationCallbacks.delete(deviceId);
|
|
@@ -409,18 +423,43 @@ function setupDisconnectListener(
|
|
|
409
423
|
deviceId: string,
|
|
410
424
|
webContents: WebContents
|
|
411
425
|
): void {
|
|
412
|
-
|
|
413
|
-
|
|
426
|
+
const existing = deviceDisconnectListeners.get(deviceId);
|
|
427
|
+
if (existing) {
|
|
428
|
+
existing.peripheral.removeListener('disconnect', existing.listener);
|
|
429
|
+
}
|
|
414
430
|
|
|
415
|
-
|
|
416
|
-
peripheral.on('disconnect', () => {
|
|
431
|
+
const listener = () => {
|
|
417
432
|
handleDeviceDisconnect(deviceId, webContents);
|
|
418
|
-
}
|
|
433
|
+
};
|
|
434
|
+
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
435
|
+
peripheral.on('disconnect', listener);
|
|
436
|
+
setupMtuListener(peripheral, deviceId, webContents);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function setupMtuListener(
|
|
440
|
+
peripheral: Peripheral,
|
|
441
|
+
deviceId: string,
|
|
442
|
+
webContents: WebContents
|
|
443
|
+
): void {
|
|
444
|
+
const existing = deviceMtuListeners.get(deviceId);
|
|
445
|
+
if (existing) {
|
|
446
|
+
existing.peripheral.removeListener('mtu', existing.listener);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const listener = (mtu: number) => {
|
|
450
|
+
if (!Number.isFinite(mtu) || mtu <= 0) return;
|
|
451
|
+
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
452
|
+
id: deviceId,
|
|
453
|
+
mtu,
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
457
|
+
peripheral.on('mtu', listener);
|
|
419
458
|
}
|
|
420
459
|
|
|
421
460
|
// ===== Write helpers (inline) =====
|
|
422
461
|
|
|
423
|
-
async function
|
|
462
|
+
async function writeCharacteristicWithoutResponse(
|
|
424
463
|
deviceId: string,
|
|
425
464
|
writeCharacteristic: Characteristic,
|
|
426
465
|
buffer: Buffer
|
|
@@ -468,7 +507,7 @@ async function attemptWindowsWriteUntilPaired(
|
|
|
468
507
|
}
|
|
469
508
|
|
|
470
509
|
try {
|
|
471
|
-
await
|
|
510
|
+
await writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
|
|
472
511
|
} catch (e) {
|
|
473
512
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
474
513
|
logger?.error('[BLE-Write] Windows write error', {
|
|
@@ -551,13 +590,12 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
551
590
|
}
|
|
552
591
|
|
|
553
592
|
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
593
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
594
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
595
|
+
peripheral.mtu,
|
|
596
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
597
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
598
|
+
);
|
|
561
599
|
|
|
562
600
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
563
601
|
// macOS / Linux or already paired on Windows: direct write
|
|
@@ -568,14 +606,13 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
568
606
|
`Write characteristic not available for ${deviceId}`
|
|
569
607
|
);
|
|
570
608
|
}
|
|
571
|
-
if (toBuffer.length <=
|
|
572
|
-
await
|
|
573
|
-
await writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
|
|
609
|
+
if (toBuffer.length <= packetCapacity) {
|
|
610
|
+
await writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
|
|
574
611
|
return;
|
|
575
612
|
}
|
|
576
613
|
// chunked
|
|
577
614
|
for (let offset = 0; offset < toBuffer.length; ) {
|
|
578
|
-
const chunkSize = Math.min(
|
|
615
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
579
616
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
580
617
|
offset += chunkSize;
|
|
581
618
|
const latest = doGetWriteCharacteristic();
|
|
@@ -585,23 +622,19 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
585
622
|
`Write characteristic not available for ${deviceId}`
|
|
586
623
|
);
|
|
587
624
|
}
|
|
588
|
-
await
|
|
589
|
-
if (offset < toBuffer.length) {
|
|
590
|
-
await wait(UNIFIED_WRITE_DELAY);
|
|
591
|
-
}
|
|
625
|
+
await writeCharacteristicWithoutResponse(deviceId, latest, chunk);
|
|
592
626
|
}
|
|
593
627
|
return;
|
|
594
628
|
}
|
|
595
629
|
|
|
596
630
|
// Windows unpaired path: use loop
|
|
597
|
-
if (toBuffer.length <=
|
|
598
|
-
await wait(UNIFIED_WRITE_DELAY);
|
|
631
|
+
if (toBuffer.length <= packetCapacity) {
|
|
599
632
|
await attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
|
|
600
633
|
return;
|
|
601
634
|
}
|
|
602
635
|
// chunked loop
|
|
603
636
|
for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
|
|
604
|
-
const chunkSize = Math.min(
|
|
637
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
605
638
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
606
639
|
offset += chunkSize;
|
|
607
640
|
await attemptWindowsWriteUntilPaired(
|
|
@@ -610,17 +643,11 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
610
643
|
chunk,
|
|
611
644
|
`chunk-${idx + 1}`
|
|
612
645
|
);
|
|
613
|
-
if (offset < toBuffer.length) {
|
|
614
|
-
await wait(UNIFIED_WRITE_DELAY);
|
|
615
|
-
}
|
|
616
646
|
}
|
|
617
647
|
}
|
|
618
648
|
|
|
619
649
|
// Handle discovered device (for general enumeration only)
|
|
620
650
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
621
|
-
const deviceName = peripheral.advertisement?.localName || 'Unknown Device';
|
|
622
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids || [];
|
|
623
|
-
|
|
624
651
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
625
652
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
626
653
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
@@ -630,10 +657,10 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
630
657
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
631
658
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
632
659
|
if (isNewDevice) {
|
|
633
|
-
logger?.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
serviceUUIDs: serviceUuids,
|
|
660
|
+
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
661
|
+
deviceId: peripheral.id,
|
|
662
|
+
name: peripheral.advertisement?.localName || 'Unknown Device',
|
|
663
|
+
serviceUUIDs: peripheral.advertisement?.serviceUuids || [],
|
|
637
664
|
});
|
|
638
665
|
}
|
|
639
666
|
}
|
|
@@ -643,19 +670,24 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
643
670
|
function ensureDiscoverListener(): void {
|
|
644
671
|
if (!noble) return;
|
|
645
672
|
|
|
646
|
-
|
|
647
|
-
const listenerCount = (noble as any).listenerCount('discover');
|
|
648
|
-
|
|
649
|
-
if (listenerCount === 0) {
|
|
673
|
+
if (!persistentDiscoverListener) {
|
|
650
674
|
logger?.info('[NobleBLE] Discover listener missing, re-adding it');
|
|
651
|
-
|
|
675
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
652
676
|
handleDeviceDiscovered(peripheral);
|
|
653
|
-
}
|
|
677
|
+
};
|
|
678
|
+
noble.on('discover', persistentDiscoverListener);
|
|
654
679
|
} else {
|
|
655
|
-
logger?.debug('[NobleBLE] Discover listener already
|
|
680
|
+
logger?.debug('[NobleBLE] Discover listener already registered');
|
|
656
681
|
}
|
|
657
682
|
}
|
|
658
683
|
|
|
684
|
+
async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
685
|
+
await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
686
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
687
|
+
timeoutBehavior: 'resolve',
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
659
691
|
// Perform targeted scan for a specific device ID
|
|
660
692
|
// Uses self-contained local listener pattern - no global state needed
|
|
661
693
|
async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
|
|
@@ -669,28 +701,41 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
669
701
|
logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
|
|
670
702
|
|
|
671
703
|
return new Promise((resolve, reject) => {
|
|
704
|
+
let settled = false;
|
|
705
|
+
|
|
706
|
+
const finish = async (peripheral: Peripheral | null, error?: Error) => {
|
|
707
|
+
if (settled) return;
|
|
708
|
+
settled = true;
|
|
709
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
710
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
711
|
+
await waitForNobleScanStop(nobleInstance);
|
|
712
|
+
|
|
713
|
+
if (error) {
|
|
714
|
+
logger?.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
715
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (peripheral) {
|
|
719
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
720
|
+
}
|
|
721
|
+
resolve(peripheral);
|
|
722
|
+
};
|
|
723
|
+
|
|
672
724
|
// Local discover listener - only matches target device
|
|
673
725
|
const onDiscover = (peripheral: Peripheral) => {
|
|
674
|
-
if (peripheral.id === targetDeviceId) {
|
|
726
|
+
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
675
727
|
logger?.info('[NobleBLE] Target device found during targeted scan:', {
|
|
676
728
|
id: peripheral.id,
|
|
677
729
|
name: peripheral.advertisement?.localName,
|
|
678
730
|
});
|
|
679
|
-
|
|
680
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
681
|
-
nobleInstance.stopScanning();
|
|
682
|
-
discoveredDevices.set(peripheral.id, peripheral);
|
|
683
|
-
resolve(peripheral);
|
|
731
|
+
finish(peripheral).catch(reject);
|
|
684
732
|
}
|
|
685
733
|
};
|
|
686
734
|
|
|
687
|
-
// Timeout handler - must be after onDiscover so it can reference it
|
|
688
735
|
const timeoutId = setTimeout(() => {
|
|
689
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
690
|
-
nobleInstance.stopScanning();
|
|
691
736
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
692
|
-
|
|
693
|
-
},
|
|
737
|
+
finish(null).catch(reject);
|
|
738
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
694
739
|
|
|
695
740
|
// Add local listener for this scan
|
|
696
741
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -698,10 +743,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
698
743
|
// Start scanning — no service UUID filter (Pro2 may use different service UUID)
|
|
699
744
|
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
700
745
|
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));
|
|
746
|
+
finish(null, error).catch(reject);
|
|
705
747
|
return;
|
|
706
748
|
}
|
|
707
749
|
logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
|
|
@@ -735,11 +777,13 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
735
777
|
const devices: DeviceInfo[] = [];
|
|
736
778
|
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
737
779
|
|
|
738
|
-
// Cleanup function: clears
|
|
739
|
-
|
|
780
|
+
// Cleanup function: clears timers and waits until Noble confirms scanning
|
|
781
|
+
// has stopped. Resolving enumerate before this callback creates a race with
|
|
782
|
+
// an immediately-following connection attempt.
|
|
783
|
+
const cleanup = async () => {
|
|
740
784
|
clearTimeout(timeoutId);
|
|
741
785
|
if (intervalId) clearInterval(intervalId);
|
|
742
|
-
nobleInstance
|
|
786
|
+
await waitForNobleScanStop(nobleInstance);
|
|
743
787
|
};
|
|
744
788
|
|
|
745
789
|
// Collect discovered devices into the devices array
|
|
@@ -759,10 +803,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
759
803
|
};
|
|
760
804
|
|
|
761
805
|
// Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
|
|
762
|
-
const timeoutId = setTimeout(() => {
|
|
806
|
+
const timeoutId = setTimeout(async () => {
|
|
763
807
|
// Final collection before resolving — catches devices discovered near the deadline
|
|
764
808
|
checkDevices();
|
|
765
|
-
cleanup();
|
|
809
|
+
await cleanup();
|
|
766
810
|
logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
|
|
767
811
|
resolve(devices);
|
|
768
812
|
}, DEVICE_SCAN_TIMEOUT);
|
|
@@ -770,9 +814,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
770
814
|
// Start scanning without a service UUID filter so Pro2 advertisements with
|
|
771
815
|
// short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
|
|
772
816
|
logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
773
|
-
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
817
|
+
nobleInstance.startScanning([], false, async (error?: Error) => {
|
|
774
818
|
if (error) {
|
|
775
|
-
cleanup();
|
|
819
|
+
await cleanup();
|
|
776
820
|
logger?.error('[NobleBLE] Failed to start scanning:', error);
|
|
777
821
|
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
778
822
|
return;
|
|
@@ -789,30 +833,21 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
789
833
|
// Stop scanning
|
|
790
834
|
async function stopScanning(): Promise<void> {
|
|
791
835
|
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
|
-
});
|
|
836
|
+
const nobleInstance = noble;
|
|
837
|
+
await waitForNobleScanStop(nobleInstance);
|
|
838
|
+
logger?.info('[NobleBLE] Scanning stopped');
|
|
804
839
|
}
|
|
805
840
|
|
|
806
841
|
// Clean up all Noble listeners (for app exit)
|
|
807
842
|
function cleanupNobleListeners(): void {
|
|
808
843
|
if (!noble) return;
|
|
809
844
|
|
|
810
|
-
// Remove all listeners to prevent memory leaks
|
|
811
|
-
// Noble uses EventEmitter, need to use removeAllListeners
|
|
812
845
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
846
|
+
if (persistentDiscoverListener) {
|
|
847
|
+
noble.removeListener('discover', persistentDiscoverListener);
|
|
848
|
+
persistentDiscoverListener = null;
|
|
849
|
+
}
|
|
850
|
+
logger?.info('[NobleBLE] Owned Noble listeners cleaned up');
|
|
816
851
|
} catch (error) {
|
|
817
852
|
logger?.error('[NobleBLE] Failed to clean up some listeners:', error);
|
|
818
853
|
}
|
|
@@ -829,6 +864,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
829
864
|
id: peripheral.id,
|
|
830
865
|
name: deviceName,
|
|
831
866
|
state: peripheral.state || 'disconnected',
|
|
867
|
+
...(typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}),
|
|
832
868
|
};
|
|
833
869
|
}
|
|
834
870
|
|
|
@@ -841,6 +877,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
841
877
|
id: connectedPeripheral.id,
|
|
842
878
|
name: deviceName,
|
|
843
879
|
state: connectedPeripheral.state || 'connected',
|
|
880
|
+
...(typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}),
|
|
844
881
|
};
|
|
845
882
|
}
|
|
846
883
|
|
|
@@ -910,31 +947,25 @@ async function discoverServicesAndCharacteristics(
|
|
|
910
947
|
});
|
|
911
948
|
});
|
|
912
949
|
|
|
913
|
-
// Log all discovered services
|
|
914
|
-
logger?.info(
|
|
915
|
-
'[NobleBLE] All services:',
|
|
916
|
-
services?.map(s => s.uuid)
|
|
917
|
-
);
|
|
918
|
-
|
|
919
950
|
if (!services || services.length === 0) {
|
|
920
951
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
921
952
|
}
|
|
922
953
|
|
|
954
|
+
logger?.debug('[NobleBLE] services discovered', {
|
|
955
|
+
deviceId: peripheral.id,
|
|
956
|
+
serviceUUIDs: services.map(service => service.uuid),
|
|
957
|
+
});
|
|
958
|
+
|
|
923
959
|
// 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
|
-
}
|
|
960
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
932
961
|
if (!service) {
|
|
933
962
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
934
963
|
}
|
|
935
964
|
const selectedService = service;
|
|
936
|
-
logger?.
|
|
937
|
-
|
|
965
|
+
logger?.debug('[NobleBLE] service selected', {
|
|
966
|
+
deviceId: peripheral.id,
|
|
967
|
+
serviceUuid: selectedService.uuid,
|
|
968
|
+
});
|
|
938
969
|
// Step 2: Discover ALL characteristics (no filter)
|
|
939
970
|
const characteristics = await new Promise<Characteristic[]>((resolve, reject) => {
|
|
940
971
|
selectedService.discoverCharacteristics([], (error, chars) => {
|
|
@@ -948,30 +979,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
948
979
|
});
|
|
949
980
|
|
|
950
981
|
// 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
982
|
let writeCharacteristic: Characteristic | null = null;
|
|
957
983
|
let notifyCharacteristic: Characteristic | null = null;
|
|
958
984
|
|
|
959
985
|
for (const characteristic of characteristics) {
|
|
960
|
-
|
|
961
|
-
const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
|
|
962
|
-
|
|
963
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
986
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
964
987
|
writeCharacteristic = characteristic;
|
|
965
|
-
} else if (
|
|
988
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
966
989
|
notifyCharacteristic = characteristic;
|
|
967
990
|
}
|
|
968
991
|
}
|
|
969
992
|
|
|
970
|
-
logger?.info('[NobleBLE] Characteristic discovery result:', {
|
|
971
|
-
writeFound: !!writeCharacteristic,
|
|
972
|
-
notifyFound: !!notifyCharacteristic,
|
|
973
|
-
});
|
|
974
|
-
|
|
975
993
|
if (!writeCharacteristic || !notifyCharacteristic) {
|
|
976
994
|
logger?.error(
|
|
977
995
|
'[NobleBLE] Missing characteristics - write:',
|
|
@@ -985,6 +1003,13 @@ async function discoverServicesAndCharacteristics(
|
|
|
985
1003
|
);
|
|
986
1004
|
}
|
|
987
1005
|
|
|
1006
|
+
logger?.debug('[NobleBLE] characteristics selected', {
|
|
1007
|
+
deviceId: peripheral.id,
|
|
1008
|
+
serviceUuid: selectedService.uuid,
|
|
1009
|
+
writeUuid: writeCharacteristic.uuid,
|
|
1010
|
+
notifyUuid: notifyCharacteristic.uuid,
|
|
1011
|
+
});
|
|
1012
|
+
|
|
988
1013
|
return { write: writeCharacteristic, notify: notifyCharacteristic };
|
|
989
1014
|
})();
|
|
990
1015
|
|
|
@@ -1017,29 +1042,23 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
1017
1042
|
|
|
1018
1043
|
// Step 2: Force disconnect if connected
|
|
1019
1044
|
if (peripheral.state === 'connected') {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1045
|
+
await runBleCallbackOperation(
|
|
1046
|
+
callback =>
|
|
1047
|
+
peripheral.disconnect(() => {
|
|
1048
|
+
logger?.info('[NobleBLE] Force disconnect completed');
|
|
1049
|
+
callback();
|
|
1050
|
+
}),
|
|
1051
|
+
{ timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' }
|
|
1052
|
+
);
|
|
1028
1053
|
}
|
|
1029
1054
|
|
|
1030
1055
|
// 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
|
-
});
|
|
1056
|
+
await runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
1057
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1058
|
+
timeoutBehavior: 'reject',
|
|
1042
1059
|
});
|
|
1060
|
+
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1061
|
+
connectedDevices.set(deviceId, peripheral);
|
|
1043
1062
|
|
|
1044
1063
|
// Wait for connection to stabilize
|
|
1045
1064
|
await wait(500);
|
|
@@ -1170,16 +1189,26 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1170
1189
|
deviceId: string,
|
|
1171
1190
|
webContents: WebContents
|
|
1172
1191
|
): Promise<CharacteristicPair> {
|
|
1173
|
-
// Force reconnect to clear GATT cache
|
|
1174
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1175
1192
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1176
1193
|
|
|
1177
|
-
// Discover services and characteristics with retry
|
|
1178
1194
|
try {
|
|
1179
1195
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1180
|
-
} catch (
|
|
1181
|
-
|
|
1182
|
-
|
|
1196
|
+
} catch (initialError) {
|
|
1197
|
+
logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
1198
|
+
deviceId,
|
|
1199
|
+
error: initialError,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1204
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1205
|
+
try {
|
|
1206
|
+
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1207
|
+
} catch (reconnectError) {
|
|
1208
|
+
logger?.error(
|
|
1209
|
+
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1210
|
+
reconnectError
|
|
1211
|
+
);
|
|
1183
1212
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1184
1213
|
}
|
|
1185
1214
|
}
|
|
@@ -1297,15 +1326,30 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1297
1326
|
}
|
|
1298
1327
|
|
|
1299
1328
|
return new Promise((resolve, reject) => {
|
|
1329
|
+
let connectionTimedOut = false;
|
|
1300
1330
|
const timeout = setTimeout(() => {
|
|
1331
|
+
connectionTimedOut = true;
|
|
1301
1332
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
1302
|
-
},
|
|
1333
|
+
}, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
|
|
1303
1334
|
|
|
1304
1335
|
// TypeScript type assertion - peripheral is guaranteed to be defined at this point
|
|
1305
1336
|
const connectedPeripheral = peripheral as Peripheral;
|
|
1306
1337
|
connectedPeripheral.connect(async (error: Error | undefined) => {
|
|
1307
1338
|
clearTimeout(timeout);
|
|
1308
1339
|
|
|
1340
|
+
// Noble may invoke the callback after the SDK timed out and released the request.
|
|
1341
|
+
// Ignore it to avoid initializing disposed commands or leaving an orphaned connection.
|
|
1342
|
+
if (connectionTimedOut) {
|
|
1343
|
+
if (!error) {
|
|
1344
|
+
try {
|
|
1345
|
+
connectedPeripheral.disconnect(() => undefined);
|
|
1346
|
+
} catch {
|
|
1347
|
+
// Best-effort cleanup only.
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1309
1353
|
if (error) {
|
|
1310
1354
|
logger?.error('[NobleBLE] Connection failed:', error);
|
|
1311
1355
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -1342,20 +1386,21 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1342
1386
|
return;
|
|
1343
1387
|
}
|
|
1344
1388
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
peripheral.
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1389
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
1390
|
+
if (disconnectEntry) {
|
|
1391
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
1392
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1396
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1397
|
+
timeoutBehavior: 'resolve',
|
|
1398
|
+
});
|
|
1399
|
+
cleanupDevice(deviceId, undefined, {
|
|
1400
|
+
cleanupConnection: true,
|
|
1401
|
+
sendDisconnectEvent: false,
|
|
1402
|
+
cancelOperations: true,
|
|
1403
|
+
reason: 'manual-disconnect',
|
|
1359
1404
|
});
|
|
1360
1405
|
}
|
|
1361
1406
|
|
|
@@ -1376,17 +1421,11 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
|
|
|
1376
1421
|
subscriptionOperations.set(deviceId, 'unsubscribing');
|
|
1377
1422
|
|
|
1378
1423
|
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
|
-
});
|
|
1424
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1425
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1426
|
+
timeoutBehavior: 'resolve',
|
|
1389
1427
|
});
|
|
1428
|
+
logger?.info('[NobleBLE] Notification unsubscription completed');
|
|
1390
1429
|
|
|
1391
1430
|
// Remove all listeners and clear subscription status
|
|
1392
1431
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -1479,19 +1518,13 @@ async function subscribeNotifications(
|
|
|
1479
1518
|
notifyCharacteristic: Characteristic
|
|
1480
1519
|
): Promise<void> {
|
|
1481
1520
|
// Listeners already cleaned up above, no need to repeat
|
|
1482
|
-
await
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
});
|
|
1521
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1522
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1523
|
+
timeoutBehavior: 'resolve',
|
|
1486
1524
|
});
|
|
1487
|
-
await
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
reject(error);
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
resolve();
|
|
1494
|
-
});
|
|
1525
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1526
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1527
|
+
timeoutBehavior: 'reject',
|
|
1495
1528
|
});
|
|
1496
1529
|
|
|
1497
1530
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
@@ -1516,29 +1549,25 @@ async function subscribeNotifications(
|
|
|
1516
1549
|
|
|
1517
1550
|
// Setup IPC handlers
|
|
1518
1551
|
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
1552
|
try {
|
|
1522
|
-
console.log('[NobleBLE] NOBLE_VERSION_771');
|
|
1523
|
-
|
|
1524
1553
|
// @ts-ignore – electron-log is only available at runtime
|
|
1525
1554
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1526
1555
|
logger = require('electron-log') as Logger;
|
|
1527
|
-
console.log('[NobleBLE] electron-log loaded successfully.');
|
|
1528
1556
|
|
|
1529
1557
|
// @ts-ignore – electron is only available at runtime
|
|
1530
1558
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1531
1559
|
const { ipcMain } = require('electron');
|
|
1532
|
-
console.log('[NobleBLE] electron.ipcMain loaded successfully.');
|
|
1533
1560
|
|
|
1534
1561
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1535
1562
|
|
|
1536
1563
|
// Handle enumerate request
|
|
1537
|
-
console.log(`[NobleBLE] Registering handler for: ${EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE}`);
|
|
1538
1564
|
ipcMain.handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
1539
1565
|
try {
|
|
1540
1566
|
const devices = await enumerateDevices();
|
|
1541
|
-
safeLog(logger, '
|
|
1567
|
+
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
1568
|
+
count: devices.length,
|
|
1569
|
+
devices: devices.map(device => ({ id: device.id, name: device.name })),
|
|
1570
|
+
});
|
|
1542
1571
|
return devices;
|
|
1543
1572
|
} catch (error) {
|
|
1544
1573
|
safeLog(logger, 'error', 'Enumeration failed:', error);
|
|
@@ -1583,7 +1612,6 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1583
1612
|
ipcMain.handle(
|
|
1584
1613
|
EOneKeyBleMessageKeys.NOBLE_BLE_WRITE,
|
|
1585
1614
|
async (_event: IpcMainInvokeEvent, deviceId: string, hexData: string) => {
|
|
1586
|
-
logger?.info('[NobleBLE] IPC WRITE', { deviceId, len: hexData.length });
|
|
1587
1615
|
await transmitHexDataToDevice(deviceId, hexData);
|
|
1588
1616
|
}
|
|
1589
1617
|
);
|
|
@@ -1648,32 +1676,24 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1648
1676
|
// Cleanup on app quit
|
|
1649
1677
|
webContents.on('destroyed', () => {
|
|
1650
1678
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1679
|
+
(async () => {
|
|
1680
|
+
const deviceIds = Array.from(connectedDevices.keys());
|
|
1681
|
+
for (const deviceId of deviceIds) {
|
|
1682
|
+
await unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1683
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1684
|
+
}
|
|
1651
1685
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1686
|
+
await stopScanning().catch(() => undefined);
|
|
1687
|
+
if (noble && persistentStateListener) {
|
|
1688
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1689
|
+
persistentStateListener = null;
|
|
1690
|
+
}
|
|
1691
|
+
cleanupNobleListeners();
|
|
1692
|
+
discoveredDevices.clear();
|
|
1693
|
+
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1694
|
+
})().catch(error => {
|
|
1695
|
+
safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
|
|
1661
1696
|
});
|
|
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
1697
|
});
|
|
1678
1698
|
|
|
1679
1699
|
safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
|