@onekeyfe/hd-transport-electron 1.2.0-alpha.8 → 1.2.0-alpha.80
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-6da32f94.js} +1 -1
- package/dist/index.d.ts +16 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-94c42077.js → noble-ble-handler-ce551d9a.js} +477 -263
- package/dist/noble-ble-handler.d.ts +2 -0
- 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 +10 -1
- 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 +377 -0
- package/src/ble-ops.ts +40 -10
- package/src/ble-packet-capacity.ts +13 -0
- package/src/index.ts +1 -1
- package/src/noble-ble-handler.ts +540 -290
- package/src/noble-ble-timeouts.ts +2 -0
- package/src/types/desktop-api.ts +10 -2
- package/src/types/noble-extended.ts +1 -0
package/src/noble-ble-handler.ts
CHANGED
|
@@ -9,17 +9,29 @@ 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
|
-
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
32
|
+
import type { IpcMain, IpcMainInvokeEvent, WebContents } from 'electron';
|
|
22
33
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
34
|
+
import type { NobleBleWriteOptions } from './types/desktop-api';
|
|
23
35
|
import type { CharacteristicPair, DeviceInfo, Logger, NobleModule } from './types/noble-extended';
|
|
24
36
|
|
|
25
37
|
// Noble will be dynamically imported to avoid bundling issues
|
|
@@ -39,6 +51,7 @@ const bluetoothState: {
|
|
|
39
51
|
|
|
40
52
|
// Global persistent state listener for app layer
|
|
41
53
|
let persistentStateListener: ((state: string) => void) | null = null;
|
|
54
|
+
let persistentDiscoverListener: ((peripheral: Peripheral) => void) | null = null;
|
|
42
55
|
|
|
43
56
|
// Device cache and connection state
|
|
44
57
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
@@ -50,6 +63,14 @@ const subscribedDevices = new Map<string, boolean>(); // Track subscription stat
|
|
|
50
63
|
|
|
51
64
|
// 🔒 Subscription operation state tracking to prevent race conditions
|
|
52
65
|
const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' | 'idle'>();
|
|
66
|
+
const deviceDisconnectListeners = new Map<
|
|
67
|
+
string,
|
|
68
|
+
{ peripheral: Peripheral; listener: () => void }
|
|
69
|
+
>();
|
|
70
|
+
const deviceMtuListeners = new Map<
|
|
71
|
+
string,
|
|
72
|
+
{ peripheral: Peripheral; listener: (mtu: number) => void }
|
|
73
|
+
>();
|
|
53
74
|
|
|
54
75
|
// Windows-only response watchdog state moved to utils/windows-ble-recovery
|
|
55
76
|
|
|
@@ -59,57 +80,55 @@ const subscriptionOperations = new Map<string, 'subscribing' | 'unsubscribing' |
|
|
|
59
80
|
|
|
60
81
|
// Service UUIDs to scan for - using constants from hd-shared
|
|
61
82
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const NORMALIZED_WRITE_UUID = '0002';
|
|
66
|
-
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
83
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
84
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
85
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
67
86
|
|
|
68
87
|
// Timeout and interval constants
|
|
69
88
|
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)
|
|
89
|
+
const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
72
90
|
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
91
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
92
|
+
const BLE_CLEANUP_TIMEOUT = 250;
|
|
93
|
+
// Renderer release is logical only; this timer physically frees the device.
|
|
94
|
+
const BLE_IDLE_DISCONNECT_MS = 3 * 60_000;
|
|
95
|
+
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
96
|
+
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
75
97
|
|
|
76
98
|
// Write-related constants
|
|
77
|
-
const
|
|
78
|
-
const
|
|
99
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
100
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
101
|
+
const DEFAULT_WRITE_PACING_DELAY_MS = 5;
|
|
79
102
|
const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 } as const;
|
|
80
103
|
const IS_WINDOWS = process.platform === 'win32';
|
|
81
104
|
const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
82
105
|
/status:\s*3/i, // Windows pairing cancelled / GATT write failed
|
|
83
106
|
];
|
|
84
107
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
export function resolveNobleBleWritePacingDelay(options?: NobleBleWriteOptions) {
|
|
109
|
+
return typeof options?.pacingDelayMs === 'number' && Number.isFinite(options.pacingDelayMs)
|
|
110
|
+
? Math.min(Math.max(Math.floor(options.pacingDelayMs), 0), 1000)
|
|
111
|
+
: DEFAULT_WRITE_PACING_DELAY_MS;
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
function isGenericBleService(uuid?: string | null) {
|
|
96
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
97
|
-
}
|
|
114
|
+
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
115
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
116
|
+
const localName = peripheral.advertisement?.localName;
|
|
98
117
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
105
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
|
|
106
|
-
);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
118
|
+
// Noble localName is the current advertisement name, so reject the Pro2
|
|
119
|
+
// Find My endpoint before the communication-service fast path accepts it.
|
|
120
|
+
if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
109
123
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
return (
|
|
125
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
126
|
+
isOnekeyBluetoothDevice({
|
|
127
|
+
id: peripheral.id,
|
|
128
|
+
localName,
|
|
129
|
+
serviceUuids,
|
|
130
|
+
})
|
|
131
|
+
);
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
/**
|
|
@@ -117,12 +136,6 @@ function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
|
117
136
|
* renderer-side transport. Packet reassembly is handled by ElectronBleTransport.
|
|
118
137
|
*/
|
|
119
138
|
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
139
|
const appCb = notificationCallbacks.get(deviceId);
|
|
127
140
|
if (appCb) appCb(data.toString('hex'));
|
|
128
141
|
}
|
|
@@ -296,9 +309,12 @@ async function initializeNoble(): Promise<void> {
|
|
|
296
309
|
});
|
|
297
310
|
|
|
298
311
|
// Set up device discovery
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
312
|
+
if (!persistentDiscoverListener) {
|
|
313
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
314
|
+
handleDeviceDiscovered(peripheral);
|
|
315
|
+
};
|
|
316
|
+
noble.on('discover', persistentDiscoverListener);
|
|
317
|
+
}
|
|
302
318
|
|
|
303
319
|
logger?.info('[NobleBLE] Noble initialized successfully');
|
|
304
320
|
} catch (error) {
|
|
@@ -329,6 +345,76 @@ interface DeviceCleanupOptions {
|
|
|
329
345
|
reason?: string;
|
|
330
346
|
}
|
|
331
347
|
|
|
348
|
+
// One timer per device, in the main process so a reload cannot orphan a link.
|
|
349
|
+
const idleDisconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
350
|
+
|
|
351
|
+
// A fired timer cannot be cancelled; connect awaits its disconnect or the fast
|
|
352
|
+
// path hands back a link torn down a moment later (BleTimeoutError 713).
|
|
353
|
+
const idleDisconnectInFlight = new Map<string, Promise<void>>();
|
|
354
|
+
|
|
355
|
+
function clearIdleDisconnect(deviceId: string): void {
|
|
356
|
+
const timer = idleDisconnectTimers.get(deviceId);
|
|
357
|
+
if (timer) {
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
idleDisconnectTimers.delete(deviceId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Settle any keep-alive disconnect already running for this device. */
|
|
364
|
+
async function awaitIdleDisconnect(deviceId: string): Promise<void> {
|
|
365
|
+
const pending = idleDisconnectInFlight.get(deviceId);
|
|
366
|
+
if (pending) await pending.catch(() => undefined);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function broadcastToAllWebContents(channel: string, payload: unknown): void {
|
|
370
|
+
try {
|
|
371
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, global-require
|
|
372
|
+
const { webContents: electronWebContents } = require('electron') as {
|
|
373
|
+
webContents: { getAllWebContents(): WebContents[] };
|
|
374
|
+
};
|
|
375
|
+
for (const wc of electronWebContents.getAllWebContents()) {
|
|
376
|
+
if (!wc.isDestroyed()) wc.send(channel, payload);
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger?.error('[NobleBLE] broadcast failed:', { channel, error: String(error) });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function armIdleDisconnect(
|
|
384
|
+
deviceId: string,
|
|
385
|
+
ms: number = BLE_IDLE_DISCONNECT_MS,
|
|
386
|
+
reason: 'idle' | 'busy-backstop' = 'idle'
|
|
387
|
+
): void {
|
|
388
|
+
clearIdleDisconnect(deviceId);
|
|
389
|
+
idleDisconnectTimers.set(
|
|
390
|
+
deviceId,
|
|
391
|
+
setTimeout(() => {
|
|
392
|
+
idleDisconnectTimers.delete(deviceId);
|
|
393
|
+
if (!connectedDevices.has(deviceId)) return;
|
|
394
|
+
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
395
|
+
const peripheral = connectedDevices.get(deviceId);
|
|
396
|
+
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
397
|
+
const pending = disconnectDevice(deviceId)
|
|
398
|
+
.then(() => {
|
|
399
|
+
// A call is still in flight here; it must reject, not hang.
|
|
400
|
+
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
401
|
+
id: deviceId,
|
|
402
|
+
name: deviceName,
|
|
403
|
+
});
|
|
404
|
+
})
|
|
405
|
+
.catch(error => {
|
|
406
|
+
logger?.error('[NobleBLE] Keep-alive disconnect failed', { deviceId, error });
|
|
407
|
+
})
|
|
408
|
+
.finally(() => {
|
|
409
|
+
if (idleDisconnectInFlight.get(deviceId) === pending) {
|
|
410
|
+
idleDisconnectInFlight.delete(deviceId);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
idleDisconnectInFlight.set(deviceId, pending);
|
|
414
|
+
}, ms)
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
332
418
|
/**
|
|
333
419
|
* Unified device cleanup function - single entry point for all cleanup operations
|
|
334
420
|
*/
|
|
@@ -354,12 +440,24 @@ function cleanupDevice(
|
|
|
354
440
|
cancelOperations,
|
|
355
441
|
});
|
|
356
442
|
|
|
443
|
+
clearIdleDisconnect(deviceId);
|
|
444
|
+
|
|
357
445
|
// Get device info before cleanup
|
|
358
446
|
const peripheral = connectedDevices.get(deviceId);
|
|
359
447
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
360
448
|
|
|
361
449
|
// 1. Clean up connection state
|
|
362
450
|
if (cleanupConnection) {
|
|
451
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
452
|
+
if (disconnectEntry) {
|
|
453
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
454
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
455
|
+
}
|
|
456
|
+
const mtuEntry = deviceMtuListeners.get(deviceId);
|
|
457
|
+
if (mtuEntry) {
|
|
458
|
+
mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
|
|
459
|
+
deviceMtuListeners.delete(deviceId);
|
|
460
|
+
}
|
|
363
461
|
connectedDevices.delete(deviceId);
|
|
364
462
|
deviceCharacteristics.delete(deviceId);
|
|
365
463
|
notificationCallbacks.delete(deviceId);
|
|
@@ -373,9 +471,11 @@ function cleanupDevice(
|
|
|
373
471
|
discoveredDevices.delete(deviceId);
|
|
374
472
|
}
|
|
375
473
|
|
|
376
|
-
// 3. Send disconnect event (if needed)
|
|
377
|
-
|
|
378
|
-
|
|
474
|
+
// 3. Send disconnect event (if needed). Broadcast, not the captured
|
|
475
|
+
// webContents: a kept-alive link outlives a renderer soft restart, and the
|
|
476
|
+
// new renderer still needs to hear the device dropped (same as the idle path).
|
|
477
|
+
if (sendDisconnectEvent) {
|
|
478
|
+
broadcastToAllWebContents(EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
379
479
|
id: deviceId,
|
|
380
480
|
name: deviceName,
|
|
381
481
|
});
|
|
@@ -409,18 +509,45 @@ function setupDisconnectListener(
|
|
|
409
509
|
deviceId: string,
|
|
410
510
|
webContents: WebContents
|
|
411
511
|
): void {
|
|
412
|
-
|
|
413
|
-
|
|
512
|
+
const existing = deviceDisconnectListeners.get(deviceId);
|
|
513
|
+
if (existing) {
|
|
514
|
+
existing.peripheral.removeListener('disconnect', existing.listener);
|
|
515
|
+
}
|
|
414
516
|
|
|
415
|
-
|
|
416
|
-
peripheral.on('disconnect', () => {
|
|
517
|
+
const listener = () => {
|
|
417
518
|
handleDeviceDisconnect(deviceId, webContents);
|
|
418
|
-
}
|
|
519
|
+
};
|
|
520
|
+
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
521
|
+
peripheral.on('disconnect', listener);
|
|
522
|
+
setupMtuListener(peripheral, deviceId, webContents);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function setupMtuListener(
|
|
526
|
+
peripheral: Peripheral,
|
|
527
|
+
deviceId: string,
|
|
528
|
+
webContents: WebContents
|
|
529
|
+
): void {
|
|
530
|
+
const existing = deviceMtuListeners.get(deviceId);
|
|
531
|
+
if (existing) {
|
|
532
|
+
existing.peripheral.removeListener('mtu', existing.listener);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const listener = (mtu: number) => {
|
|
536
|
+
if (!Number.isFinite(mtu) || mtu <= 0) return;
|
|
537
|
+
// A kept-alive link can outlive the renderer this closure captured.
|
|
538
|
+
if (webContents.isDestroyed()) return;
|
|
539
|
+
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
540
|
+
id: deviceId,
|
|
541
|
+
mtu,
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
545
|
+
peripheral.on('mtu', listener);
|
|
419
546
|
}
|
|
420
547
|
|
|
421
548
|
// ===== Write helpers (inline) =====
|
|
422
549
|
|
|
423
|
-
async function
|
|
550
|
+
async function writeCharacteristicWithoutResponse(
|
|
424
551
|
deviceId: string,
|
|
425
552
|
writeCharacteristic: Characteristic,
|
|
426
553
|
buffer: Buffer
|
|
@@ -468,7 +595,7 @@ async function attemptWindowsWriteUntilPaired(
|
|
|
468
595
|
}
|
|
469
596
|
|
|
470
597
|
try {
|
|
471
|
-
await
|
|
598
|
+
await writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
|
|
472
599
|
} catch (e) {
|
|
473
600
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
474
601
|
logger?.error('[BLE-Write] Windows write error', {
|
|
@@ -540,7 +667,11 @@ async function attemptWindowsWriteUntilPaired(
|
|
|
540
667
|
);
|
|
541
668
|
}
|
|
542
669
|
|
|
543
|
-
async function transmitHexDataToDevice(
|
|
670
|
+
async function transmitHexDataToDevice(
|
|
671
|
+
deviceId: string,
|
|
672
|
+
hexData: string,
|
|
673
|
+
options?: NobleBleWriteOptions
|
|
674
|
+
): Promise<void> {
|
|
544
675
|
const characteristics = deviceCharacteristics.get(deviceId);
|
|
545
676
|
const peripheral = connectedDevices.get(deviceId);
|
|
546
677
|
if (!peripheral || !characteristics) {
|
|
@@ -549,15 +680,17 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
549
680
|
`Device ${deviceId} not connected or characteristics not available`
|
|
550
681
|
);
|
|
551
682
|
}
|
|
683
|
+
// Request outstanding: swap the idle clock for the busy backstop.
|
|
684
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
552
685
|
|
|
553
686
|
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
687
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
688
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
689
|
+
peripheral.mtu,
|
|
690
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
691
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
692
|
+
);
|
|
693
|
+
const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
|
|
561
694
|
|
|
562
695
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
563
696
|
// macOS / Linux or already paired on Windows: direct write
|
|
@@ -568,14 +701,14 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
568
701
|
`Write characteristic not available for ${deviceId}`
|
|
569
702
|
);
|
|
570
703
|
}
|
|
571
|
-
if (toBuffer.length <=
|
|
572
|
-
await wait(
|
|
573
|
-
await
|
|
704
|
+
if (toBuffer.length <= packetCapacity) {
|
|
705
|
+
if (pacingDelayMs > 0) await wait(pacingDelayMs);
|
|
706
|
+
await writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
|
|
574
707
|
return;
|
|
575
708
|
}
|
|
576
709
|
// chunked
|
|
577
710
|
for (let offset = 0; offset < toBuffer.length; ) {
|
|
578
|
-
const chunkSize = Math.min(
|
|
711
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
579
712
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
580
713
|
offset += chunkSize;
|
|
581
714
|
const latest = doGetWriteCharacteristic();
|
|
@@ -585,23 +718,23 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
585
718
|
`Write characteristic not available for ${deviceId}`
|
|
586
719
|
);
|
|
587
720
|
}
|
|
588
|
-
await
|
|
589
|
-
if (offset < toBuffer.length) {
|
|
590
|
-
await wait(
|
|
721
|
+
await writeCharacteristicWithoutResponse(deviceId, latest, chunk);
|
|
722
|
+
if (offset < toBuffer.length && pacingDelayMs > 0) {
|
|
723
|
+
await wait(pacingDelayMs);
|
|
591
724
|
}
|
|
592
725
|
}
|
|
593
726
|
return;
|
|
594
727
|
}
|
|
595
728
|
|
|
596
729
|
// Windows unpaired path: use loop
|
|
597
|
-
if (toBuffer.length <=
|
|
598
|
-
await wait(
|
|
730
|
+
if (toBuffer.length <= packetCapacity) {
|
|
731
|
+
if (pacingDelayMs > 0) await wait(pacingDelayMs);
|
|
599
732
|
await attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
|
|
600
733
|
return;
|
|
601
734
|
}
|
|
602
735
|
// chunked loop
|
|
603
736
|
for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
|
|
604
|
-
const chunkSize = Math.min(
|
|
737
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
605
738
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
606
739
|
offset += chunkSize;
|
|
607
740
|
await attemptWindowsWriteUntilPaired(
|
|
@@ -610,17 +743,14 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
610
743
|
chunk,
|
|
611
744
|
`chunk-${idx + 1}`
|
|
612
745
|
);
|
|
613
|
-
if (offset < toBuffer.length) {
|
|
614
|
-
await wait(
|
|
746
|
+
if (offset < toBuffer.length && pacingDelayMs > 0) {
|
|
747
|
+
await wait(pacingDelayMs);
|
|
615
748
|
}
|
|
616
749
|
}
|
|
617
750
|
}
|
|
618
751
|
|
|
619
752
|
// Handle discovered device (for general enumeration only)
|
|
620
753
|
function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
621
|
-
const deviceName = peripheral.advertisement?.localName || 'Unknown Device';
|
|
622
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids || [];
|
|
623
|
-
|
|
624
754
|
// Only process OneKey candidates for general discovery. Avoid logging every
|
|
625
755
|
// ambient BLE peripheral; it makes Pro2 debugging hard to read.
|
|
626
756
|
if (!isOneKeyPeripheral(peripheral)) {
|
|
@@ -630,10 +760,10 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
630
760
|
const isNewDevice = !discoveredDevices.has(peripheral.id);
|
|
631
761
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
632
762
|
if (isNewDevice) {
|
|
633
|
-
logger?.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
serviceUUIDs: serviceUuids,
|
|
763
|
+
logger?.debug('[NobleBLE] OneKey BLE device discovered', {
|
|
764
|
+
deviceId: peripheral.id,
|
|
765
|
+
name: peripheral.advertisement?.localName || 'Unknown Device',
|
|
766
|
+
serviceUUIDs: peripheral.advertisement?.serviceUuids || [],
|
|
637
767
|
});
|
|
638
768
|
}
|
|
639
769
|
}
|
|
@@ -643,19 +773,24 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
|
|
|
643
773
|
function ensureDiscoverListener(): void {
|
|
644
774
|
if (!noble) return;
|
|
645
775
|
|
|
646
|
-
|
|
647
|
-
const listenerCount = (noble as any).listenerCount('discover');
|
|
648
|
-
|
|
649
|
-
if (listenerCount === 0) {
|
|
776
|
+
if (!persistentDiscoverListener) {
|
|
650
777
|
logger?.info('[NobleBLE] Discover listener missing, re-adding it');
|
|
651
|
-
|
|
778
|
+
persistentDiscoverListener = (peripheral: Peripheral) => {
|
|
652
779
|
handleDeviceDiscovered(peripheral);
|
|
653
|
-
}
|
|
780
|
+
};
|
|
781
|
+
noble.on('discover', persistentDiscoverListener);
|
|
654
782
|
} else {
|
|
655
|
-
logger?.debug('[NobleBLE] Discover listener already
|
|
783
|
+
logger?.debug('[NobleBLE] Discover listener already registered');
|
|
656
784
|
}
|
|
657
785
|
}
|
|
658
786
|
|
|
787
|
+
async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
788
|
+
await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
789
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
790
|
+
timeoutBehavior: 'resolve',
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
659
794
|
// Perform targeted scan for a specific device ID
|
|
660
795
|
// Uses self-contained local listener pattern - no global state needed
|
|
661
796
|
async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
|
|
@@ -669,28 +804,41 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
669
804
|
logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
|
|
670
805
|
|
|
671
806
|
return new Promise((resolve, reject) => {
|
|
807
|
+
let settled = false;
|
|
808
|
+
|
|
809
|
+
const finish = async (peripheral: Peripheral | null, error?: Error) => {
|
|
810
|
+
if (settled) return;
|
|
811
|
+
settled = true;
|
|
812
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
813
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
814
|
+
await waitForNobleScanStop(nobleInstance);
|
|
815
|
+
|
|
816
|
+
if (error) {
|
|
817
|
+
logger?.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
818
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (peripheral) {
|
|
822
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
823
|
+
}
|
|
824
|
+
resolve(peripheral);
|
|
825
|
+
};
|
|
826
|
+
|
|
672
827
|
// Local discover listener - only matches target device
|
|
673
828
|
const onDiscover = (peripheral: Peripheral) => {
|
|
674
|
-
if (peripheral.id === targetDeviceId) {
|
|
829
|
+
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
675
830
|
logger?.info('[NobleBLE] Target device found during targeted scan:', {
|
|
676
831
|
id: peripheral.id,
|
|
677
832
|
name: peripheral.advertisement?.localName,
|
|
678
833
|
});
|
|
679
|
-
|
|
680
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
681
|
-
nobleInstance.stopScanning();
|
|
682
|
-
discoveredDevices.set(peripheral.id, peripheral);
|
|
683
|
-
resolve(peripheral);
|
|
834
|
+
finish(peripheral).catch(reject);
|
|
684
835
|
}
|
|
685
836
|
};
|
|
686
837
|
|
|
687
|
-
// Timeout handler - must be after onDiscover so it can reference it
|
|
688
838
|
const timeoutId = setTimeout(() => {
|
|
689
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
690
|
-
nobleInstance.stopScanning();
|
|
691
839
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
692
|
-
|
|
693
|
-
},
|
|
840
|
+
finish(null).catch(reject);
|
|
841
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
694
842
|
|
|
695
843
|
// Add local listener for this scan
|
|
696
844
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -698,10 +846,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
698
846
|
// Start scanning — no service UUID filter (Pro2 may use different service UUID)
|
|
699
847
|
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
700
848
|
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));
|
|
849
|
+
finish(null, error).catch(reject);
|
|
705
850
|
return;
|
|
706
851
|
}
|
|
707
852
|
logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
|
|
@@ -735,34 +880,36 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
735
880
|
const devices: DeviceInfo[] = [];
|
|
736
881
|
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
737
882
|
|
|
738
|
-
// Cleanup function: clears
|
|
739
|
-
|
|
883
|
+
// Cleanup function: clears timers and waits until Noble confirms scanning
|
|
884
|
+
// has stopped. Resolving enumerate before this callback creates a race with
|
|
885
|
+
// an immediately-following connection attempt.
|
|
886
|
+
const cleanup = async () => {
|
|
740
887
|
clearTimeout(timeoutId);
|
|
741
888
|
if (intervalId) clearInterval(intervalId);
|
|
742
|
-
nobleInstance
|
|
889
|
+
await waitForNobleScanStop(nobleInstance);
|
|
743
890
|
};
|
|
744
891
|
|
|
745
892
|
// Collect discovered devices into the devices array
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
id,
|
|
754
|
-
name: deviceName,
|
|
755
|
-
state: peripheral.state || 'disconnected',
|
|
756
|
-
});
|
|
757
|
-
}
|
|
893
|
+
const pushDevice = (peripheral: Peripheral, id: string) => {
|
|
894
|
+
if (devices.some(d => d.id === id)) return;
|
|
895
|
+
devices.push({
|
|
896
|
+
commType: 'electron-ble',
|
|
897
|
+
id,
|
|
898
|
+
name: peripheral.advertisement?.localName || 'Unknown Device',
|
|
899
|
+
state: peripheral.state || 'disconnected',
|
|
758
900
|
});
|
|
759
901
|
};
|
|
902
|
+
const checkDevices = () => {
|
|
903
|
+
discoveredDevices.forEach(pushDevice);
|
|
904
|
+
// A linked device stops advertising, so the scan never rediscovers it.
|
|
905
|
+
connectedDevices.forEach(pushDevice);
|
|
906
|
+
};
|
|
760
907
|
|
|
761
908
|
// Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
|
|
762
|
-
const timeoutId = setTimeout(() => {
|
|
909
|
+
const timeoutId = setTimeout(async () => {
|
|
763
910
|
// Final collection before resolving — catches devices discovered near the deadline
|
|
764
911
|
checkDevices();
|
|
765
|
-
cleanup();
|
|
912
|
+
await cleanup();
|
|
766
913
|
logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
|
|
767
914
|
resolve(devices);
|
|
768
915
|
}, DEVICE_SCAN_TIMEOUT);
|
|
@@ -770,9 +917,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
770
917
|
// Start scanning without a service UUID filter so Pro2 advertisements with
|
|
771
918
|
// short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
|
|
772
919
|
logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
773
|
-
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
920
|
+
nobleInstance.startScanning([], false, async (error?: Error) => {
|
|
774
921
|
if (error) {
|
|
775
|
-
cleanup();
|
|
922
|
+
await cleanup();
|
|
776
923
|
logger?.error('[NobleBLE] Failed to start scanning:', error);
|
|
777
924
|
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
778
925
|
return;
|
|
@@ -789,30 +936,21 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
789
936
|
// Stop scanning
|
|
790
937
|
async function stopScanning(): Promise<void> {
|
|
791
938
|
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
|
-
});
|
|
939
|
+
const nobleInstance = noble;
|
|
940
|
+
await waitForNobleScanStop(nobleInstance);
|
|
941
|
+
logger?.info('[NobleBLE] Scanning stopped');
|
|
804
942
|
}
|
|
805
943
|
|
|
806
944
|
// Clean up all Noble listeners (for app exit)
|
|
807
945
|
function cleanupNobleListeners(): void {
|
|
808
946
|
if (!noble) return;
|
|
809
947
|
|
|
810
|
-
// Remove all listeners to prevent memory leaks
|
|
811
|
-
// Noble uses EventEmitter, need to use removeAllListeners
|
|
812
948
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
949
|
+
if (persistentDiscoverListener) {
|
|
950
|
+
noble.removeListener('discover', persistentDiscoverListener);
|
|
951
|
+
persistentDiscoverListener = null;
|
|
952
|
+
}
|
|
953
|
+
logger?.info('[NobleBLE] Owned Noble listeners cleaned up');
|
|
816
954
|
} catch (error) {
|
|
817
955
|
logger?.error('[NobleBLE] Failed to clean up some listeners:', error);
|
|
818
956
|
}
|
|
@@ -829,6 +967,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
829
967
|
id: peripheral.id,
|
|
830
968
|
name: deviceName,
|
|
831
969
|
state: peripheral.state || 'disconnected',
|
|
970
|
+
...(typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}),
|
|
832
971
|
};
|
|
833
972
|
}
|
|
834
973
|
|
|
@@ -841,6 +980,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
841
980
|
id: connectedPeripheral.id,
|
|
842
981
|
name: deviceName,
|
|
843
982
|
state: connectedPeripheral.state || 'connected',
|
|
983
|
+
...(typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}),
|
|
844
984
|
};
|
|
845
985
|
}
|
|
846
986
|
|
|
@@ -910,31 +1050,25 @@ async function discoverServicesAndCharacteristics(
|
|
|
910
1050
|
});
|
|
911
1051
|
});
|
|
912
1052
|
|
|
913
|
-
// Log all discovered services
|
|
914
|
-
logger?.info(
|
|
915
|
-
'[NobleBLE] All services:',
|
|
916
|
-
services?.map(s => s.uuid)
|
|
917
|
-
);
|
|
918
|
-
|
|
919
1053
|
if (!services || services.length === 0) {
|
|
920
1054
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
|
|
921
1055
|
}
|
|
922
1056
|
|
|
1057
|
+
logger?.debug('[NobleBLE] services discovered', {
|
|
1058
|
+
deviceId: peripheral.id,
|
|
1059
|
+
serviceUUIDs: services.map(service => service.uuid),
|
|
1060
|
+
});
|
|
1061
|
+
|
|
923
1062
|
// 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
|
-
}
|
|
1063
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
932
1064
|
if (!service) {
|
|
933
1065
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
934
1066
|
}
|
|
935
1067
|
const selectedService = service;
|
|
936
|
-
logger?.
|
|
937
|
-
|
|
1068
|
+
logger?.debug('[NobleBLE] service selected', {
|
|
1069
|
+
deviceId: peripheral.id,
|
|
1070
|
+
serviceUuid: selectedService.uuid,
|
|
1071
|
+
});
|
|
938
1072
|
// Step 2: Discover ALL characteristics (no filter)
|
|
939
1073
|
const characteristics = await new Promise<Characteristic[]>((resolve, reject) => {
|
|
940
1074
|
selectedService.discoverCharacteristics([], (error, chars) => {
|
|
@@ -948,30 +1082,17 @@ async function discoverServicesAndCharacteristics(
|
|
|
948
1082
|
});
|
|
949
1083
|
|
|
950
1084
|
// 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
1085
|
let writeCharacteristic: Characteristic | null = null;
|
|
957
1086
|
let notifyCharacteristic: Characteristic | null = null;
|
|
958
1087
|
|
|
959
1088
|
for (const characteristic of characteristics) {
|
|
960
|
-
|
|
961
|
-
const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
|
|
962
|
-
|
|
963
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
1089
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
964
1090
|
writeCharacteristic = characteristic;
|
|
965
|
-
} else if (
|
|
1091
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
966
1092
|
notifyCharacteristic = characteristic;
|
|
967
1093
|
}
|
|
968
1094
|
}
|
|
969
1095
|
|
|
970
|
-
logger?.info('[NobleBLE] Characteristic discovery result:', {
|
|
971
|
-
writeFound: !!writeCharacteristic,
|
|
972
|
-
notifyFound: !!notifyCharacteristic,
|
|
973
|
-
});
|
|
974
|
-
|
|
975
1096
|
if (!writeCharacteristic || !notifyCharacteristic) {
|
|
976
1097
|
logger?.error(
|
|
977
1098
|
'[NobleBLE] Missing characteristics - write:',
|
|
@@ -985,6 +1106,13 @@ async function discoverServicesAndCharacteristics(
|
|
|
985
1106
|
);
|
|
986
1107
|
}
|
|
987
1108
|
|
|
1109
|
+
logger?.debug('[NobleBLE] characteristics selected', {
|
|
1110
|
+
deviceId: peripheral.id,
|
|
1111
|
+
serviceUuid: selectedService.uuid,
|
|
1112
|
+
writeUuid: writeCharacteristic.uuid,
|
|
1113
|
+
notifyUuid: notifyCharacteristic.uuid,
|
|
1114
|
+
});
|
|
1115
|
+
|
|
988
1116
|
return { write: writeCharacteristic, notify: notifyCharacteristic };
|
|
989
1117
|
})();
|
|
990
1118
|
|
|
@@ -1017,29 +1145,23 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
1017
1145
|
|
|
1018
1146
|
// Step 2: Force disconnect if connected
|
|
1019
1147
|
if (peripheral.state === 'connected') {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1148
|
+
await runBleCallbackOperation(
|
|
1149
|
+
callback =>
|
|
1150
|
+
peripheral.disconnect(() => {
|
|
1151
|
+
logger?.info('[NobleBLE] Force disconnect completed');
|
|
1152
|
+
callback();
|
|
1153
|
+
}),
|
|
1154
|
+
{ timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' }
|
|
1155
|
+
);
|
|
1028
1156
|
}
|
|
1029
1157
|
|
|
1030
1158
|
// 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
|
-
});
|
|
1159
|
+
await runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
1160
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1161
|
+
timeoutBehavior: 'reject',
|
|
1042
1162
|
});
|
|
1163
|
+
logger?.info('[NobleBLE] Force reconnect successful');
|
|
1164
|
+
connectedDevices.set(deviceId, peripheral);
|
|
1043
1165
|
|
|
1044
1166
|
// Wait for connection to stabilize
|
|
1045
1167
|
await wait(500);
|
|
@@ -1170,20 +1292,100 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1170
1292
|
deviceId: string,
|
|
1171
1293
|
webContents: WebContents
|
|
1172
1294
|
): Promise<CharacteristicPair> {
|
|
1173
|
-
// Force reconnect to clear GATT cache
|
|
1174
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1175
1295
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1176
1296
|
|
|
1177
|
-
// Discover services and characteristics with retry
|
|
1178
1297
|
try {
|
|
1179
1298
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1180
|
-
} catch (
|
|
1181
|
-
|
|
1182
|
-
|
|
1299
|
+
} catch (initialError) {
|
|
1300
|
+
logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
|
|
1301
|
+
deviceId,
|
|
1302
|
+
error: initialError,
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1307
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1308
|
+
try {
|
|
1309
|
+
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1310
|
+
} catch (reconnectError) {
|
|
1311
|
+
logger?.error(
|
|
1312
|
+
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1313
|
+
reconnectError
|
|
1314
|
+
);
|
|
1183
1315
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1184
1316
|
}
|
|
1185
1317
|
}
|
|
1186
1318
|
|
|
1319
|
+
// noble/mac never resolves for an id CoreBluetooth cannot retrieve.
|
|
1320
|
+
const DIRECT_CONNECT_TIMEOUT_MS = 2000;
|
|
1321
|
+
// Floor after a timeout: do not pay the probe on every attempt.
|
|
1322
|
+
const DIRECT_CONNECT_COOLDOWN_MS = 15_000;
|
|
1323
|
+
const directConnectCooldownUntil = new Map<string, number>();
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Connect straight by id, skipping the ~650ms targeted scan. Both native
|
|
1327
|
+
* backends support it and emit a `discover` as a side effect: macOS resolves
|
|
1328
|
+
* via `retrievePeripheralsWithIdentifiers`, Windows synthesizes one for an
|
|
1329
|
+
* unknown address. Returns undefined when unavailable so the caller scans.
|
|
1330
|
+
*
|
|
1331
|
+
* Ported from the Trezor connector, where it is field-proven.
|
|
1332
|
+
*/
|
|
1333
|
+
async function tryDirectConnectById(deviceId: string): Promise<Peripheral | undefined> {
|
|
1334
|
+
const nobleInstance = noble as
|
|
1335
|
+
| (typeof noble & {
|
|
1336
|
+
connectAsync?: (id: string) => Promise<Peripheral | undefined>;
|
|
1337
|
+
})
|
|
1338
|
+
| null;
|
|
1339
|
+
if (!nobleInstance?.connectAsync) return undefined;
|
|
1340
|
+
|
|
1341
|
+
const cooldownUntil = directConnectCooldownUntil.get(deviceId) ?? 0;
|
|
1342
|
+
if (Date.now() < cooldownUntil) return undefined;
|
|
1343
|
+
|
|
1344
|
+
try {
|
|
1345
|
+
// The late-orphan guard must attach to THIS pending connect.
|
|
1346
|
+
const directPromise = nobleInstance.connectAsync(deviceId);
|
|
1347
|
+
const raced = await Promise.race([
|
|
1348
|
+
directPromise,
|
|
1349
|
+
new Promise<'timeout'>(resolve => {
|
|
1350
|
+
setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
|
|
1351
|
+
}),
|
|
1352
|
+
]);
|
|
1353
|
+
if (raced === 'timeout') {
|
|
1354
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1355
|
+
logger?.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
|
|
1356
|
+
deviceId,
|
|
1357
|
+
});
|
|
1358
|
+
// Promise.race times out the caller only; a late success orphans the link.
|
|
1359
|
+
directPromise
|
|
1360
|
+
.then(late => {
|
|
1361
|
+
const latePeripheral = late ?? discoveredDevices.get(deviceId);
|
|
1362
|
+
if (
|
|
1363
|
+
latePeripheral &&
|
|
1364
|
+
latePeripheral.state === 'connected' &&
|
|
1365
|
+
!connectedDevices.has(deviceId)
|
|
1366
|
+
) {
|
|
1367
|
+
latePeripheral.removeAllListeners('disconnect');
|
|
1368
|
+
latePeripheral.disconnect(() => undefined);
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
.catch(() => undefined);
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}
|
|
1374
|
+
// Backends emit `discover` as a side effect, so the cache may hold it.
|
|
1375
|
+
const peripheral = raced ?? discoveredDevices.get(deviceId);
|
|
1376
|
+
if (!peripheral || peripheral.state !== 'connected') return undefined;
|
|
1377
|
+
logger?.info('[NobleBLE] Direct connect-by-id succeeded', { deviceId });
|
|
1378
|
+
return peripheral;
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1381
|
+
logger?.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
|
|
1382
|
+
deviceId,
|
|
1383
|
+
error: String(error),
|
|
1384
|
+
});
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1187
1389
|
// Connect to device - supports both discovered and direct connection modes
|
|
1188
1390
|
async function connectDevice(deviceId: string, webContents: WebContents): Promise<void> {
|
|
1189
1391
|
logger?.info('[NobleBLE] Connect device request:', {
|
|
@@ -1195,7 +1397,8 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1195
1397
|
totalConnected: connectedDevices.size,
|
|
1196
1398
|
});
|
|
1197
1399
|
|
|
1198
|
-
|
|
1400
|
+
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1401
|
+
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1199
1402
|
|
|
1200
1403
|
// If device not discovered, try a targeted scan for this specific device
|
|
1201
1404
|
if (!peripheral) {
|
|
@@ -1210,6 +1413,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1210
1413
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1211
1414
|
}
|
|
1212
1415
|
|
|
1416
|
+
// Reaches a bonded device that is not advertising; else falls to the scan.
|
|
1417
|
+
peripheral = await tryDirectConnectById(deviceId);
|
|
1418
|
+
if (peripheral) {
|
|
1419
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (!peripheral) {
|
|
1213
1424
|
// Perform a targeted scan to find the specific device
|
|
1214
1425
|
try {
|
|
1215
1426
|
const foundPeripheral = await performTargetedScan(deviceId);
|
|
@@ -1240,9 +1451,11 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1240
1451
|
// If already connected but not in our connected devices map, add it
|
|
1241
1452
|
if (!connectedDevices.has(deviceId)) {
|
|
1242
1453
|
connectedDevices.set(deviceId, peripheral);
|
|
1243
|
-
// Set up unified disconnect listener
|
|
1244
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1245
1454
|
}
|
|
1455
|
+
// Re-bind unconditionally (idempotent): on a kept-alive link the disconnect
|
|
1456
|
+
// and MTU listeners may still hold the webContents of a soft-restarted
|
|
1457
|
+
// renderer, and the reuse fast path below returns before any other setup.
|
|
1458
|
+
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1246
1459
|
|
|
1247
1460
|
// Check if we already have characteristics for this device
|
|
1248
1461
|
if (deviceCharacteristics.has(deviceId)) {
|
|
@@ -1286,26 +1499,48 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1286
1499
|
}
|
|
1287
1500
|
|
|
1288
1501
|
// Setup connection and discover services
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1502
|
+
try {
|
|
1503
|
+
const characteristics = await setupConnectionAndDiscoverServices(
|
|
1504
|
+
peripheral,
|
|
1505
|
+
deviceId,
|
|
1506
|
+
webContents
|
|
1507
|
+
);
|
|
1508
|
+
deviceCharacteristics.set(deviceId, characteristics);
|
|
1509
|
+
logger?.info('[NobleBLE] Device ready for communication:', deviceId);
|
|
1510
|
+
} catch (setupError) {
|
|
1511
|
+
// The caller re-arms the timer only on success, so a left-up link strands.
|
|
1512
|
+
logger?.error('[NobleBLE] Connection setup failed on kept-alive link:', setupError);
|
|
1513
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1514
|
+
throw setupError;
|
|
1515
|
+
}
|
|
1296
1516
|
return;
|
|
1297
1517
|
}
|
|
1298
1518
|
|
|
1299
1519
|
return new Promise((resolve, reject) => {
|
|
1520
|
+
let connectionTimedOut = false;
|
|
1300
1521
|
const timeout = setTimeout(() => {
|
|
1522
|
+
connectionTimedOut = true;
|
|
1301
1523
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
1302
|
-
},
|
|
1524
|
+
}, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
|
|
1303
1525
|
|
|
1304
1526
|
// TypeScript type assertion - peripheral is guaranteed to be defined at this point
|
|
1305
1527
|
const connectedPeripheral = peripheral as Peripheral;
|
|
1306
1528
|
connectedPeripheral.connect(async (error: Error | undefined) => {
|
|
1307
1529
|
clearTimeout(timeout);
|
|
1308
1530
|
|
|
1531
|
+
// Noble may invoke the callback after the SDK timed out and released the request.
|
|
1532
|
+
// Ignore it to avoid initializing disposed commands or leaving an orphaned connection.
|
|
1533
|
+
if (connectionTimedOut) {
|
|
1534
|
+
if (!error) {
|
|
1535
|
+
try {
|
|
1536
|
+
connectedPeripheral.disconnect(() => undefined);
|
|
1537
|
+
} catch {
|
|
1538
|
+
// Best-effort cleanup only.
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1309
1544
|
if (error) {
|
|
1310
1545
|
logger?.error('[NobleBLE] Connection failed:', error);
|
|
1311
1546
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -1342,20 +1577,21 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1342
1577
|
return;
|
|
1343
1578
|
}
|
|
1344
1579
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
peripheral.
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1580
|
+
const disconnectEntry = deviceDisconnectListeners.get(deviceId);
|
|
1581
|
+
if (disconnectEntry) {
|
|
1582
|
+
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
1583
|
+
deviceDisconnectListeners.delete(deviceId);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1587
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1588
|
+
timeoutBehavior: 'resolve',
|
|
1589
|
+
});
|
|
1590
|
+
cleanupDevice(deviceId, undefined, {
|
|
1591
|
+
cleanupConnection: true,
|
|
1592
|
+
sendDisconnectEvent: false,
|
|
1593
|
+
cancelOperations: true,
|
|
1594
|
+
reason: 'manual-disconnect',
|
|
1359
1595
|
});
|
|
1360
1596
|
}
|
|
1361
1597
|
|
|
@@ -1376,17 +1612,11 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
|
|
|
1376
1612
|
subscriptionOperations.set(deviceId, 'unsubscribing');
|
|
1377
1613
|
|
|
1378
1614
|
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
|
-
});
|
|
1615
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1616
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1617
|
+
timeoutBehavior: 'resolve',
|
|
1389
1618
|
});
|
|
1619
|
+
logger?.info('[NobleBLE] Notification unsubscription completed');
|
|
1390
1620
|
|
|
1391
1621
|
// Remove all listeners and clear subscription status
|
|
1392
1622
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -1479,19 +1709,13 @@ async function subscribeNotifications(
|
|
|
1479
1709
|
notifyCharacteristic: Characteristic
|
|
1480
1710
|
): Promise<void> {
|
|
1481
1711
|
// Listeners already cleaned up above, no need to repeat
|
|
1482
|
-
await
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
});
|
|
1712
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
1713
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
1714
|
+
timeoutBehavior: 'resolve',
|
|
1486
1715
|
});
|
|
1487
|
-
await
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
reject(error);
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
resolve();
|
|
1494
|
-
});
|
|
1716
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1717
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1718
|
+
timeoutBehavior: 'reject',
|
|
1495
1719
|
});
|
|
1496
1720
|
|
|
1497
1721
|
notifyCharacteristic.on('data', (data: Buffer) => {
|
|
@@ -1516,29 +1740,31 @@ async function subscribeNotifications(
|
|
|
1516
1740
|
|
|
1517
1741
|
// Setup IPC handlers
|
|
1518
1742
|
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
1743
|
try {
|
|
1522
|
-
console.log('[NobleBLE] NOBLE_VERSION_771');
|
|
1523
|
-
|
|
1524
1744
|
// @ts-ignore – electron-log is only available at runtime
|
|
1525
1745
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1526
1746
|
logger = require('electron-log') as Logger;
|
|
1527
|
-
console.log('[NobleBLE] electron-log loaded successfully.');
|
|
1528
1747
|
|
|
1529
1748
|
// @ts-ignore – electron is only available at runtime
|
|
1530
1749
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
1531
|
-
const { ipcMain } = require('electron');
|
|
1532
|
-
|
|
1750
|
+
const { ipcMain } = require('electron') as { ipcMain: IpcMain };
|
|
1751
|
+
|
|
1752
|
+
// Electron throws on duplicate channels and setup re-runs on soft restart.
|
|
1753
|
+
const handle: IpcMain['handle'] = (channel, listener) => {
|
|
1754
|
+
ipcMain.removeHandler(channel);
|
|
1755
|
+
ipcMain.handle(channel, listener);
|
|
1756
|
+
};
|
|
1533
1757
|
|
|
1534
1758
|
safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
|
|
1535
1759
|
|
|
1536
1760
|
// Handle enumerate request
|
|
1537
|
-
|
|
1538
|
-
ipcMain.handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
1761
|
+
handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => {
|
|
1539
1762
|
try {
|
|
1540
1763
|
const devices = await enumerateDevices();
|
|
1541
|
-
safeLog(logger, '
|
|
1764
|
+
safeLog(logger, 'debug', 'Enumeration completed', {
|
|
1765
|
+
count: devices.length,
|
|
1766
|
+
devices: devices.map(device => ({ id: device.id, name: device.name })),
|
|
1767
|
+
});
|
|
1542
1768
|
return devices;
|
|
1543
1769
|
} catch (error) {
|
|
1544
1770
|
safeLog(logger, 'error', 'Enumeration failed:', error);
|
|
@@ -1547,18 +1773,18 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1547
1773
|
});
|
|
1548
1774
|
|
|
1549
1775
|
// Handle stop scan request
|
|
1550
|
-
|
|
1776
|
+
handle(EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, async () => {
|
|
1551
1777
|
await stopScanning();
|
|
1552
1778
|
});
|
|
1553
1779
|
|
|
1554
1780
|
// Handle get device request
|
|
1555
|
-
|
|
1781
|
+
handle(
|
|
1556
1782
|
EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE,
|
|
1557
1783
|
(_event: IpcMainInvokeEvent, deviceId: string) => getDevice(deviceId)
|
|
1558
1784
|
);
|
|
1559
1785
|
|
|
1560
1786
|
// Handle connect request
|
|
1561
|
-
|
|
1787
|
+
handle(
|
|
1562
1788
|
EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT,
|
|
1563
1789
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1564
1790
|
logger?.info('[NobleBLE] IPC CONNECT request received:', {
|
|
@@ -1567,12 +1793,37 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1567
1793
|
hasCharacteristics: deviceCharacteristics.has(deviceId),
|
|
1568
1794
|
totalConnectedDevices: connectedDevices.size,
|
|
1569
1795
|
});
|
|
1570
|
-
|
|
1796
|
+
// Must not fire mid-connect (pairing can take ~30s).
|
|
1797
|
+
clearIdleDisconnect(deviceId);
|
|
1798
|
+
// A fired timer must settle first, or the fast path returns a dying link.
|
|
1799
|
+
await awaitIdleDisconnect(deviceId);
|
|
1800
|
+
try {
|
|
1801
|
+
await connectDevice(deviceId, webContents);
|
|
1802
|
+
} finally {
|
|
1803
|
+
// This timer is the only thing that frees a kept-alive link.
|
|
1804
|
+
if (connectedDevices.has(deviceId)) {
|
|
1805
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
);
|
|
1810
|
+
|
|
1811
|
+
// Logical release: start the idle countdown, keep the link for the next call.
|
|
1812
|
+
handle(
|
|
1813
|
+
EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE,
|
|
1814
|
+
(_event: IpcMainInvokeEvent, deviceId: string, keepSession?: boolean) => {
|
|
1815
|
+
if (!connectedDevices.has(deviceId)) return;
|
|
1816
|
+
// Mid-flow caller will be back; the short window would cut an update.
|
|
1817
|
+
if (keepSession) {
|
|
1818
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
armIdleDisconnect(deviceId);
|
|
1571
1822
|
}
|
|
1572
1823
|
);
|
|
1573
1824
|
|
|
1574
1825
|
// Handle disconnect request
|
|
1575
|
-
|
|
1826
|
+
handle(
|
|
1576
1827
|
EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT,
|
|
1577
1828
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1578
1829
|
await disconnectDevice(deviceId);
|
|
@@ -1580,35 +1831,42 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1580
1831
|
);
|
|
1581
1832
|
|
|
1582
1833
|
// Handle write request
|
|
1583
|
-
|
|
1834
|
+
handle(
|
|
1584
1835
|
EOneKeyBleMessageKeys.NOBLE_BLE_WRITE,
|
|
1585
|
-
async (
|
|
1586
|
-
|
|
1587
|
-
|
|
1836
|
+
async (
|
|
1837
|
+
_event: IpcMainInvokeEvent,
|
|
1838
|
+
deviceId: string,
|
|
1839
|
+
hexData: string,
|
|
1840
|
+
options?: NobleBleWriteOptions
|
|
1841
|
+
) => {
|
|
1842
|
+
await transmitHexDataToDevice(deviceId, hexData, options);
|
|
1588
1843
|
}
|
|
1589
1844
|
);
|
|
1590
1845
|
|
|
1591
1846
|
// Handle subscribe request
|
|
1592
|
-
|
|
1847
|
+
handle(
|
|
1593
1848
|
EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE,
|
|
1594
1849
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1595
1850
|
await subscribeNotifications(deviceId, (data: string) => {
|
|
1596
1851
|
// Send data back to renderer process
|
|
1597
1852
|
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
|
|
1598
1853
|
});
|
|
1854
|
+
// Still acquiring (in flight) — busy backstop, not the idle clock.
|
|
1855
|
+
armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
|
|
1599
1856
|
}
|
|
1600
1857
|
);
|
|
1601
1858
|
|
|
1602
1859
|
// Handle unsubscribe request
|
|
1603
|
-
|
|
1860
|
+
handle(
|
|
1604
1861
|
EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE,
|
|
1605
1862
|
async (_event: IpcMainInvokeEvent, deviceId: string) => {
|
|
1606
1863
|
await unsubscribeNotifications(deviceId);
|
|
1864
|
+
armIdleDisconnect(deviceId);
|
|
1607
1865
|
}
|
|
1608
1866
|
);
|
|
1609
1867
|
|
|
1610
1868
|
// Handle cancel pairing: cleanup all connected devices
|
|
1611
|
-
|
|
1869
|
+
handle(EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, async () => {
|
|
1612
1870
|
const deviceIds = Array.from(connectedDevices.keys());
|
|
1613
1871
|
logger?.info('[NobleBLE] Cancel pairing invoked', {
|
|
1614
1872
|
platform: process.platform,
|
|
@@ -1629,7 +1887,7 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1629
1887
|
});
|
|
1630
1888
|
|
|
1631
1889
|
// Handle Bluetooth availability check request
|
|
1632
|
-
|
|
1890
|
+
handle(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, async () => {
|
|
1633
1891
|
try {
|
|
1634
1892
|
const bluetoothStatus = await checkBluetoothAvailability();
|
|
1635
1893
|
safeLog(logger, 'info', 'Bluetooth availability check completed:', bluetoothStatus);
|
|
@@ -1648,32 +1906,24 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1648
1906
|
// Cleanup on app quit
|
|
1649
1907
|
webContents.on('destroyed', () => {
|
|
1650
1908
|
safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
|
|
1909
|
+
(async () => {
|
|
1910
|
+
const deviceIds = Array.from(connectedDevices.keys());
|
|
1911
|
+
for (const deviceId of deviceIds) {
|
|
1912
|
+
await unsubscribeNotifications(deviceId).catch(() => undefined);
|
|
1913
|
+
await disconnectDevice(deviceId).catch(() => undefined);
|
|
1914
|
+
}
|
|
1651
1915
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1916
|
+
await stopScanning().catch(() => undefined);
|
|
1917
|
+
if (noble && persistentStateListener) {
|
|
1918
|
+
noble.removeListener('stateChange', persistentStateListener);
|
|
1919
|
+
persistentStateListener = null;
|
|
1920
|
+
}
|
|
1921
|
+
cleanupNobleListeners();
|
|
1922
|
+
discoveredDevices.clear();
|
|
1923
|
+
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
1924
|
+
})().catch(error => {
|
|
1925
|
+
safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
|
|
1661
1926
|
});
|
|
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
1927
|
});
|
|
1678
1928
|
|
|
1679
1929
|
safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
|