@onekeyfe/hd-transport-electron 1.2.0-alpha.4 → 1.2.0-alpha.41

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