@onekeyfe/hd-transport-electron 1.2.0-alpha.13 → 1.2.0-alpha.131

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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-55699e30.js');
3
+ var index = require('./index-fe6d3919.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -8,6 +8,14 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
8
8
 
9
9
  var pRetry__default = /*#__PURE__*/_interopDefaultLegacy(pRetry);
10
10
 
11
+ const BLE_ATT_HEADER_BYTES = 3;
12
+ function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapacity) {
13
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
14
+ return fallbackPacketCapacity;
15
+ }
16
+ return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
17
+ }
18
+
11
19
  function safeLog(logger, level, message, ...args) {
12
20
  if (logger) {
13
21
  logger[level](message, ...args);
@@ -17,6 +25,37 @@ function safeLog(logger, level, message, ...args) {
17
25
  }
18
26
  }
19
27
 
28
+ function runBleCallbackOperation(operation, options) {
29
+ return new Promise((resolve, reject) => {
30
+ let settled = false;
31
+ const settle = (error) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ clearTimeout(timeout);
36
+ if (error) {
37
+ reject(error);
38
+ }
39
+ else {
40
+ resolve();
41
+ }
42
+ };
43
+ const timeout = setTimeout(() => {
44
+ if (options.timeoutBehavior === 'reject') {
45
+ settle(new Error(`BLE operation timed out after ${options.timeoutMs}ms`));
46
+ }
47
+ else {
48
+ settle();
49
+ }
50
+ }, options.timeoutMs);
51
+ try {
52
+ operation(settle);
53
+ }
54
+ catch (error) {
55
+ settle(error instanceof Error ? error : new Error(String(error)));
56
+ }
57
+ });
58
+ }
20
59
  function softRefreshSubscription(params) {
21
60
  return index.__awaiter(this, void 0, void 0, function* () {
22
61
  const { deviceId, notifyCharacteristic, subscriptionOperations, subscribedDevices, pairedDevices, onNotificationData, logger, } = params;
@@ -25,17 +64,13 @@ function softRefreshSubscription(params) {
25
64
  }
26
65
  logger === null || logger === void 0 ? void 0 : logger.info('[BLE-OPS] Starting subscription refresh', { deviceId });
27
66
  subscriptionOperations.set(deviceId, 'subscribing');
28
- yield new Promise(resolve => {
29
- notifyCharacteristic.unsubscribe(() => resolve());
67
+ yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
68
+ timeoutMs: 250,
69
+ timeoutBehavior: 'resolve',
30
70
  });
31
- yield new Promise((resolve, reject) => {
32
- notifyCharacteristic.subscribe((error) => {
33
- if (error) {
34
- reject(error);
35
- return;
36
- }
37
- resolve();
38
- });
71
+ yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
72
+ timeoutMs: 8000,
73
+ timeoutBehavior: 'reject',
39
74
  });
40
75
  notifyCharacteristic.removeAllListeners('data');
41
76
  notifyCharacteristic.on('data', (data) => {
@@ -51,6 +86,9 @@ function softRefreshSubscription(params) {
51
86
  });
52
87
  }
53
88
 
89
+ const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
90
+ const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
91
+
54
92
  let noble = null;
55
93
  let logger = null;
56
94
  const bluetoothState = {
@@ -59,6 +97,7 @@ const bluetoothState = {
59
97
  initialized: false,
60
98
  };
61
99
  let persistentStateListener = null;
100
+ let persistentDiscoverListener = null;
62
101
  const discoveredDevices = new Map();
63
102
  const connectedDevices = new Map();
64
103
  const pairedDevices = new Set();
@@ -66,47 +105,45 @@ const deviceCharacteristics = new Map();
66
105
  const notificationCallbacks = new Map();
67
106
  const subscribedDevices = new Map();
68
107
  const subscriptionOperations = new Map();
108
+ const deviceDisconnectListeners = new Map();
109
+ const deviceMtuListeners = new Map();
69
110
  const ONEKEY_SERVICE_UUIDS = [hdShared.ONEKEY_SERVICE_UUID];
70
- const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']);
71
- const NORMALIZED_WRITE_UUID = '0002';
72
- const NORMALIZED_NOTIFY_UUID = '0003';
111
+ const ONEKEY_SERVICE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_SERVICE_UUID);
112
+ const ONEKEY_WRITE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_WRITE_CHARACTERISTIC_UUID);
113
+ const ONEKEY_NOTIFY_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
73
114
  const BLUETOOTH_INIT_TIMEOUT = 10000;
74
- const DEVICE_SCAN_TIMEOUT = 8000;
75
- const FAST_SCAN_TIMEOUT = 8000;
115
+ const DEVICE_SCAN_TIMEOUT = 5000;
76
116
  const DEVICE_CHECK_INTERVAL = 500;
77
- const CONNECTION_TIMEOUT = 8000;
78
117
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
79
- const BLE_PACKET_SIZE = 192;
80
- const UNIFIED_WRITE_DELAY = 5;
118
+ const BLE_CLEANUP_TIMEOUT = 250;
119
+ const BLE_IDLE_DISCONNECT_MS = 3 * 60000;
120
+ const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
121
+ const BLE_PACKET_SIZE_FALLBACK = 192;
122
+ const BLE_PACKET_SIZE_MAXIMUM = 244;
123
+ const DEFAULT_WRITE_PACING_DELAY_MS = 5;
81
124
  const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 };
82
125
  const IS_WINDOWS = process.platform === 'win32';
83
126
  const ABORTABLE_WRITE_ERROR_PATTERNS = [
84
127
  /status:\s*3/i,
85
128
  ];
86
- function getBleUuidKey(uuid) {
87
- const normalized = (uuid !== null && uuid !== void 0 ? uuid : '').replace(/-/g, '').toLowerCase();
88
- return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
89
- }
90
- const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
91
- ...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
92
- '0001',
93
- ]);
94
- function isGenericBleService(uuid) {
95
- return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
96
- }
97
- function hasOneKeyAdvertisementService(peripheral) {
98
- var _a, _b;
99
- const serviceUuids = (_b = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids) !== null && _b !== void 0 ? _b : [];
100
- return serviceUuids.some(uuid => {
101
- const uuidKey = getBleUuidKey(uuid);
102
- return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
103
- PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
104
- });
129
+ function resolveNobleBleWritePacingDelay(options) {
130
+ return typeof (options === null || options === void 0 ? void 0 : options.pacingDelayMs) === 'number' && Number.isFinite(options.pacingDelayMs)
131
+ ? Math.min(Math.max(Math.floor(options.pacingDelayMs), 0), 1000)
132
+ : DEFAULT_WRITE_PACING_DELAY_MS;
105
133
  }
106
134
  function isOneKeyPeripheral(peripheral) {
107
- var _a;
108
- const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || null;
109
- return hdShared.isOnekeyDevice(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
135
+ var _a, _b;
136
+ const serviceUuids = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids;
137
+ const localName = (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName;
138
+ if (!(localName === null || localName === void 0 ? void 0 : localName.trim())) {
139
+ return false;
140
+ }
141
+ return (hdShared.hasOnekeyCommunicationService(serviceUuids) &&
142
+ hdShared.isOnekeyBluetoothDevice({
143
+ id: peripheral.id,
144
+ localName,
145
+ serviceUuids,
146
+ }));
110
147
  }
111
148
  function emitRawNotification(deviceId, data) {
112
149
  const appCb = notificationCallbacks.get(deviceId);
@@ -245,9 +282,12 @@ function initializeNoble() {
245
282
  };
246
283
  noble.on('stateChange', onStateChange);
247
284
  });
248
- noble.on('discover', (peripheral) => {
249
- handleDeviceDiscovered(peripheral);
250
- });
285
+ if (!persistentDiscoverListener) {
286
+ persistentDiscoverListener = (peripheral) => {
287
+ handleDeviceDiscovered(peripheral);
288
+ };
289
+ noble.on('discover', persistentDiscoverListener);
290
+ }
251
291
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble initialized successfully');
252
292
  }
253
293
  catch (error) {
@@ -258,6 +298,62 @@ function initializeNoble() {
258
298
  }
259
299
  });
260
300
  }
301
+ const idleDisconnectTimers = new Map();
302
+ const idleDisconnectInFlight = new Map();
303
+ function clearIdleDisconnect(deviceId) {
304
+ const timer = idleDisconnectTimers.get(deviceId);
305
+ if (timer) {
306
+ clearTimeout(timer);
307
+ idleDisconnectTimers.delete(deviceId);
308
+ }
309
+ }
310
+ function awaitIdleDisconnect(deviceId) {
311
+ return index.__awaiter(this, void 0, void 0, function* () {
312
+ const pending = idleDisconnectInFlight.get(deviceId);
313
+ if (pending)
314
+ yield pending.catch(() => undefined);
315
+ });
316
+ }
317
+ function broadcastToAllWebContents(channel, payload) {
318
+ try {
319
+ const { webContents: electronWebContents } = require('electron');
320
+ for (const wc of electronWebContents.getAllWebContents()) {
321
+ if (!wc.isDestroyed())
322
+ wc.send(channel, payload);
323
+ }
324
+ }
325
+ catch (error) {
326
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] broadcast failed:', { channel, error: String(error) });
327
+ }
328
+ }
329
+ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle') {
330
+ clearIdleDisconnect(deviceId);
331
+ idleDisconnectTimers.set(deviceId, setTimeout(() => {
332
+ var _a;
333
+ idleDisconnectTimers.delete(deviceId);
334
+ if (!connectedDevices.has(deviceId))
335
+ return;
336
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
337
+ const peripheral = connectedDevices.get(deviceId);
338
+ const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
339
+ const pending = disconnectDevice(deviceId)
340
+ .then(() => {
341
+ broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
342
+ id: deviceId,
343
+ name: deviceName,
344
+ });
345
+ })
346
+ .catch(error => {
347
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Keep-alive disconnect failed', { deviceId, error });
348
+ })
349
+ .finally(() => {
350
+ if (idleDisconnectInFlight.get(deviceId) === pending) {
351
+ idleDisconnectInFlight.delete(deviceId);
352
+ }
353
+ });
354
+ idleDisconnectInFlight.set(deviceId, pending);
355
+ }, ms));
356
+ }
261
357
  function cleanupDevice(deviceId, webContents, options = {}) {
262
358
  var _a;
263
359
  const { cleanupConnection = true, cleanupDiscoveredCache = false, sendDisconnectEvent = false, cancelOperations = true, reason = 'unknown', } = options;
@@ -269,9 +365,20 @@ function cleanupDevice(deviceId, webContents, options = {}) {
269
365
  sendDisconnectEvent,
270
366
  cancelOperations,
271
367
  });
368
+ clearIdleDisconnect(deviceId);
272
369
  const peripheral = connectedDevices.get(deviceId);
273
370
  const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
274
371
  if (cleanupConnection) {
372
+ const disconnectEntry = deviceDisconnectListeners.get(deviceId);
373
+ if (disconnectEntry) {
374
+ disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
375
+ deviceDisconnectListeners.delete(deviceId);
376
+ }
377
+ const mtuEntry = deviceMtuListeners.get(deviceId);
378
+ if (mtuEntry) {
379
+ mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
380
+ deviceMtuListeners.delete(deviceId);
381
+ }
275
382
  connectedDevices.delete(deviceId);
276
383
  deviceCharacteristics.delete(deviceId);
277
384
  notificationCallbacks.delete(deviceId);
@@ -282,8 +389,8 @@ function cleanupDevice(deviceId, webContents, options = {}) {
282
389
  if (cleanupDiscoveredCache) {
283
390
  discoveredDevices.delete(deviceId);
284
391
  }
285
- if (sendDisconnectEvent && webContents) {
286
- webContents.send(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
392
+ if (sendDisconnectEvent) {
393
+ broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
287
394
  id: deviceId,
288
395
  name: deviceName,
289
396
  });
@@ -306,12 +413,36 @@ function handleDeviceDisconnect(deviceId, webContents) {
306
413
  });
307
414
  }
308
415
  function setupDisconnectListener(peripheral, deviceId, webContents) {
309
- peripheral.removeAllListeners('disconnect');
310
- peripheral.on('disconnect', () => {
416
+ const existing = deviceDisconnectListeners.get(deviceId);
417
+ if (existing) {
418
+ existing.peripheral.removeListener('disconnect', existing.listener);
419
+ }
420
+ const listener = () => {
311
421
  handleDeviceDisconnect(deviceId, webContents);
312
- });
422
+ };
423
+ deviceDisconnectListeners.set(deviceId, { peripheral, listener });
424
+ peripheral.on('disconnect', listener);
425
+ setupMtuListener(peripheral, deviceId, webContents);
313
426
  }
314
- function writeCharacteristicWithAck(deviceId, writeCharacteristic, buffer) {
427
+ function setupMtuListener(peripheral, deviceId, webContents) {
428
+ const existing = deviceMtuListeners.get(deviceId);
429
+ if (existing) {
430
+ existing.peripheral.removeListener('mtu', existing.listener);
431
+ }
432
+ const listener = (mtu) => {
433
+ if (!Number.isFinite(mtu) || mtu <= 0)
434
+ return;
435
+ if (webContents.isDestroyed())
436
+ return;
437
+ webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
438
+ id: deviceId,
439
+ mtu,
440
+ });
441
+ };
442
+ deviceMtuListeners.set(deviceId, { peripheral, listener });
443
+ peripheral.on('mtu', listener);
444
+ }
445
+ function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
315
446
  return index.__awaiter(this, void 0, void 0, function* () {
316
447
  return new Promise((resolve, reject) => {
317
448
  writeCharacteristic.write(buffer, true, (error) => {
@@ -343,7 +474,7 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
343
474
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Write characteristic not available for ${deviceId}`);
344
475
  }
345
476
  try {
346
- yield writeCharacteristicWithAck(deviceId, latestWrite, payload);
477
+ yield writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
347
478
  }
348
479
  catch (e) {
349
480
  const errorMessage = e instanceof Error ? e.message : String(e);
@@ -402,52 +533,57 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
402
533
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `No response observed after ${RETRY_CONFIG.MAX_ATTEMPTS} writes: ${deviceId}`);
403
534
  });
404
535
  }
405
- function transmitHexDataToDevice(deviceId, hexData) {
536
+ function transmitHexDataToDevice(deviceId, hexData, options) {
406
537
  return index.__awaiter(this, void 0, void 0, function* () {
407
538
  const characteristics = deviceCharacteristics.get(deviceId);
408
539
  const peripheral = connectedDevices.get(deviceId);
409
540
  if (!peripheral || !characteristics) {
410
541
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Device ${deviceId} not connected or characteristics not available`);
411
542
  }
543
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
412
544
  const toBuffer = Buffer.from(hexData, 'hex');
413
545
  const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
546
+ const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
547
+ const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
414
548
  if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
415
549
  const writeCharacteristic = doGetWriteCharacteristic();
416
550
  if (!writeCharacteristic) {
417
551
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
418
552
  }
419
- if (toBuffer.length <= BLE_PACKET_SIZE) {
420
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
421
- yield writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
553
+ if (toBuffer.length <= packetCapacity) {
554
+ if (pacingDelayMs > 0)
555
+ yield hdShared.wait(pacingDelayMs);
556
+ yield writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
422
557
  return;
423
558
  }
424
559
  for (let offset = 0; offset < toBuffer.length;) {
425
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
560
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
426
561
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
427
562
  offset += chunkSize;
428
563
  const latest = doGetWriteCharacteristic();
429
564
  if (!latest) {
430
565
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
431
566
  }
432
- yield writeCharacteristicWithAck(deviceId, latest, chunk);
433
- if (offset < toBuffer.length) {
434
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
567
+ yield writeCharacteristicWithoutResponse(deviceId, latest, chunk);
568
+ if (offset < toBuffer.length && pacingDelayMs > 0) {
569
+ yield hdShared.wait(pacingDelayMs);
435
570
  }
436
571
  }
437
572
  return;
438
573
  }
439
- if (toBuffer.length <= BLE_PACKET_SIZE) {
440
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
574
+ if (toBuffer.length <= packetCapacity) {
575
+ if (pacingDelayMs > 0)
576
+ yield hdShared.wait(pacingDelayMs);
441
577
  yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
442
578
  return;
443
579
  }
444
580
  for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
445
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
581
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
446
582
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
447
583
  offset += chunkSize;
448
584
  yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, chunk, `chunk-${idx + 1}`);
449
- if (offset < toBuffer.length) {
450
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
585
+ if (offset < toBuffer.length && pacingDelayMs > 0) {
586
+ yield hdShared.wait(pacingDelayMs);
451
587
  }
452
588
  }
453
589
  });
@@ -470,17 +606,25 @@ function handleDeviceDiscovered(peripheral) {
470
606
  function ensureDiscoverListener() {
471
607
  if (!noble)
472
608
  return;
473
- const listenerCount = noble.listenerCount('discover');
474
- if (listenerCount === 0) {
609
+ if (!persistentDiscoverListener) {
475
610
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Discover listener missing, re-adding it');
476
- noble.on('discover', (peripheral) => {
611
+ persistentDiscoverListener = (peripheral) => {
477
612
  handleDeviceDiscovered(peripheral);
478
- });
613
+ };
614
+ noble.on('discover', persistentDiscoverListener);
479
615
  }
480
616
  else {
481
- logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already exists, count:', listenerCount);
617
+ logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
482
618
  }
483
619
  }
620
+ function waitForNobleScanStop(nobleInstance) {
621
+ return index.__awaiter(this, void 0, void 0, function* () {
622
+ yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
623
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
624
+ timeoutBehavior: 'resolve',
625
+ });
626
+ });
627
+ }
484
628
  function performTargetedScan(targetDeviceId) {
485
629
  return index.__awaiter(this, void 0, void 0, function* () {
486
630
  if (!noble) {
@@ -489,33 +633,43 @@ function performTargetedScan(targetDeviceId) {
489
633
  const nobleInstance = noble;
490
634
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
491
635
  return new Promise((resolve, reject) => {
636
+ let settled = false;
637
+ const finish = (peripheral, error) => index.__awaiter(this, void 0, void 0, function* () {
638
+ if (settled)
639
+ return;
640
+ settled = true;
641
+ if (timeoutId)
642
+ clearTimeout(timeoutId);
643
+ nobleInstance.removeListener('discover', onDiscover);
644
+ yield waitForNobleScanStop(nobleInstance);
645
+ if (error) {
646
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
647
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
648
+ return;
649
+ }
650
+ if (peripheral) {
651
+ discoveredDevices.set(peripheral.id, peripheral);
652
+ }
653
+ resolve(peripheral);
654
+ });
492
655
  const onDiscover = (peripheral) => {
493
656
  var _a;
494
- if (peripheral.id === targetDeviceId) {
657
+ if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
495
658
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Target device found during targeted scan:', {
496
659
  id: peripheral.id,
497
660
  name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
498
661
  });
499
- clearTimeout(timeoutId);
500
- nobleInstance.removeListener('discover', onDiscover);
501
- nobleInstance.stopScanning();
502
- discoveredDevices.set(peripheral.id, peripheral);
503
- resolve(peripheral);
662
+ finish(peripheral).catch(reject);
504
663
  }
505
664
  };
506
665
  const timeoutId = setTimeout(() => {
507
- nobleInstance.removeListener('discover', onDiscover);
508
- nobleInstance.stopScanning();
509
666
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
510
- resolve(null);
511
- }, FAST_SCAN_TIMEOUT);
667
+ finish(null).catch(reject);
668
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
512
669
  nobleInstance.on('discover', onDiscover);
513
- nobleInstance.startScanning([], false, (error) => {
670
+ nobleInstance.startScanning([], true, (error) => {
514
671
  if (error) {
515
- clearTimeout(timeoutId);
516
- nobleInstance.removeListener('discover', onDiscover);
517
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
518
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
672
+ finish(null, error).catch(reject);
519
673
  return;
520
674
  }
521
675
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -538,44 +692,44 @@ function enumerateDevices() {
538
692
  return new Promise((resolve, reject) => {
539
693
  const devices = [];
540
694
  let intervalId;
541
- const cleanup = () => {
695
+ const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
542
696
  clearTimeout(timeoutId);
543
697
  if (intervalId)
544
698
  clearInterval(intervalId);
545
- nobleInstance.stopScanning();
699
+ yield waitForNobleScanStop(nobleInstance);
700
+ });
701
+ const pushDevice = (peripheral, id) => {
702
+ var _a;
703
+ if (devices.some(d => d.id === id))
704
+ return;
705
+ devices.push({
706
+ commType: 'electron-ble',
707
+ id,
708
+ name: ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device',
709
+ state: peripheral.state || 'disconnected',
710
+ });
546
711
  };
547
712
  const checkDevices = () => {
548
- discoveredDevices.forEach((peripheral, id) => {
549
- var _a;
550
- const existingDevice = devices.find(d => d.id === id);
551
- if (!existingDevice) {
552
- const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
553
- devices.push({
554
- commType: 'electron-ble',
555
- id,
556
- name: deviceName,
557
- state: peripheral.state || 'disconnected',
558
- });
559
- }
560
- });
713
+ discoveredDevices.forEach(pushDevice);
714
+ connectedDevices.forEach(pushDevice);
561
715
  };
562
- const timeoutId = setTimeout(() => {
716
+ const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
563
717
  checkDevices();
564
- cleanup();
718
+ yield cleanup();
565
719
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
566
720
  resolve(devices);
567
- }, DEVICE_SCAN_TIMEOUT);
721
+ }), DEVICE_SCAN_TIMEOUT);
568
722
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
569
- nobleInstance.startScanning([], false, (error) => {
723
+ nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
570
724
  if (error) {
571
- cleanup();
725
+ yield cleanup();
572
726
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
573
727
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
574
728
  return;
575
729
  }
576
730
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning started for OneKey devices');
577
731
  intervalId = setInterval(checkDevices, DEVICE_CHECK_INTERVAL);
578
- });
732
+ }));
579
733
  });
580
734
  });
581
735
  }
@@ -583,25 +737,20 @@ function stopScanning() {
583
737
  return index.__awaiter(this, void 0, void 0, function* () {
584
738
  if (!noble)
585
739
  return;
586
- return new Promise(resolve => {
587
- if (!noble) {
588
- resolve();
589
- return;
590
- }
591
- noble.stopScanning(() => {
592
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
593
- resolve();
594
- });
595
- });
740
+ const nobleInstance = noble;
741
+ yield waitForNobleScanStop(nobleInstance);
742
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
596
743
  });
597
744
  }
598
745
  function cleanupNobleListeners() {
599
746
  if (!noble)
600
747
  return;
601
748
  try {
602
- noble.removeAllListeners('discover');
603
- noble.removeAllListeners('stateChange');
604
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] All Noble listeners cleaned up');
749
+ if (persistentDiscoverListener) {
750
+ noble.removeListener('discover', persistentDiscoverListener);
751
+ persistentDiscoverListener = null;
752
+ }
753
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Owned Noble listeners cleaned up');
605
754
  }
606
755
  catch (error) {
607
756
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to clean up some listeners:', error);
@@ -612,22 +761,12 @@ function getDevice(deviceId) {
612
761
  const peripheral = discoveredDevices.get(deviceId);
613
762
  if (peripheral) {
614
763
  const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
615
- return {
616
- commType: 'electron-ble',
617
- id: peripheral.id,
618
- name: deviceName,
619
- state: peripheral.state || 'disconnected',
620
- };
764
+ return Object.assign({ commType: 'electron-ble', id: peripheral.id, name: deviceName, state: peripheral.state || 'disconnected' }, (typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}));
621
765
  }
622
766
  const connectedPeripheral = connectedDevices.get(deviceId);
623
767
  if (connectedPeripheral) {
624
768
  const deviceName = ((_b = connectedPeripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device';
625
- return {
626
- commType: 'electron-ble',
627
- id: connectedPeripheral.id,
628
- name: deviceName,
629
- state: connectedPeripheral.state || 'connected',
630
- };
769
+ return Object.assign({ commType: 'electron-ble', id: connectedPeripheral.id, name: deviceName, state: connectedPeripheral.state || 'connected' }, (typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}));
631
770
  }
632
771
  return {
633
772
  commType: 'electron-ble',
@@ -678,14 +817,7 @@ function discoverServicesAndCharacteristics(peripheral) {
678
817
  deviceId: peripheral.id,
679
818
  serviceUUIDs: services.map(service => service.uuid),
680
819
  });
681
- let service = services.find(s => NORMALIZED_ONEKEY_SERVICE_UUIDS.has(getBleUuidKey(s.uuid)));
682
- if (!service) {
683
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Known OneKey service UUID not found, trying first vendor service');
684
- service =
685
- services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
686
- services.find(s => !isGenericBleService(s.uuid)) ||
687
- services[0];
688
- }
820
+ const service = services.find(s => hdShared.matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
689
821
  if (!service) {
690
822
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound);
691
823
  }
@@ -708,12 +840,10 @@ function discoverServicesAndCharacteristics(peripheral) {
708
840
  let writeCharacteristic = null;
709
841
  let notifyCharacteristic = null;
710
842
  for (const characteristic of characteristics) {
711
- const uuid = characteristic.uuid.replace(/-/g, '').toLowerCase();
712
- const uuidKey = uuid.length >= 8 ? uuid.substring(4, 8) : uuid;
713
- if (uuidKey === NORMALIZED_WRITE_UUID) {
843
+ if (hdShared.matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
714
844
  writeCharacteristic = characteristic;
715
845
  }
716
- else if (uuidKey === NORMALIZED_NOTIFY_UUID) {
846
+ else if (hdShared.matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
717
847
  notifyCharacteristic = characteristic;
718
848
  }
719
849
  }
@@ -747,27 +877,17 @@ function forceReconnectPeripheral(peripheral, deviceId) {
747
877
  reason: 'force-reconnect',
748
878
  });
749
879
  if (peripheral.state === 'connected') {
750
- peripheral.removeAllListeners('disconnect');
751
- yield new Promise(resolve => {
752
- peripheral.disconnect(() => {
753
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force disconnect completed');
754
- resolve();
755
- });
756
- });
880
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => {
881
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force disconnect completed');
882
+ callback();
883
+ }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
757
884
  }
758
- yield new Promise((resolve, reject) => {
759
- peripheral.connect((error) => {
760
- if (error) {
761
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Force reconnect failed:', error);
762
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
763
- }
764
- else {
765
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
766
- connectedDevices.set(deviceId, peripheral);
767
- resolve();
768
- }
769
- });
885
+ yield runBleCallbackOperation(callback => peripheral.connect(callback), {
886
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
887
+ timeoutBehavior: 'reject',
770
888
  });
889
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
890
+ connectedDevices.set(deviceId, peripheral);
771
891
  yield hdShared.wait(500);
772
892
  });
773
893
  }
@@ -844,18 +964,83 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
844
964
  }
845
965
  function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
846
966
  return index.__awaiter(this, void 0, void 0, function* () {
967
+ setupDisconnectListener(peripheral, deviceId, webContents);
968
+ try {
969
+ return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
970
+ }
971
+ catch (initialError) {
972
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
973
+ deviceId,
974
+ error: initialError,
975
+ });
976
+ }
847
977
  yield forceReconnectPeripheral(peripheral, deviceId);
848
978
  setupDisconnectListener(peripheral, deviceId, webContents);
849
979
  try {
850
980
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
851
981
  }
852
- catch (error) {
853
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', error);
982
+ catch (reconnectError) {
983
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
854
984
  return freshScanAndDiscover(deviceId, webContents);
855
985
  }
856
986
  });
857
987
  }
988
+ const DIRECT_CONNECT_TIMEOUT_MS = 2000;
989
+ const DIRECT_CONNECT_COOLDOWN_MS = 15000;
990
+ const directConnectCooldownUntil = new Map();
991
+ function tryDirectConnectById(deviceId) {
992
+ var _a;
993
+ return index.__awaiter(this, void 0, void 0, function* () {
994
+ const nobleInstance = noble;
995
+ if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
996
+ return undefined;
997
+ const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
998
+ if (Date.now() < cooldownUntil)
999
+ return undefined;
1000
+ try {
1001
+ const directPromise = nobleInstance.connectAsync(deviceId);
1002
+ const raced = yield Promise.race([
1003
+ directPromise,
1004
+ new Promise(resolve => {
1005
+ setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
1006
+ }),
1007
+ ]);
1008
+ if (raced === 'timeout') {
1009
+ directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1010
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1011
+ deviceId,
1012
+ });
1013
+ directPromise
1014
+ .then(late => {
1015
+ const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1016
+ if (latePeripheral &&
1017
+ latePeripheral.state === 'connected' &&
1018
+ !connectedDevices.has(deviceId)) {
1019
+ latePeripheral.removeAllListeners('disconnect');
1020
+ latePeripheral.disconnect(() => undefined);
1021
+ }
1022
+ })
1023
+ .catch(() => undefined);
1024
+ return undefined;
1025
+ }
1026
+ const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
1027
+ if (!peripheral || peripheral.state !== 'connected')
1028
+ return undefined;
1029
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id succeeded', { deviceId });
1030
+ return peripheral;
1031
+ }
1032
+ catch (error) {
1033
+ directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1034
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
1035
+ deviceId,
1036
+ error: String(error),
1037
+ });
1038
+ return undefined;
1039
+ }
1040
+ });
1041
+ }
858
1042
  function connectDevice(deviceId, webContents) {
1043
+ var _a;
859
1044
  return index.__awaiter(this, void 0, void 0, function* () {
860
1045
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
861
1046
  deviceId,
@@ -865,7 +1050,7 @@ function connectDevice(deviceId, webContents) {
865
1050
  totalDiscovered: discoveredDevices.size,
866
1051
  totalConnected: connectedDevices.size,
867
1052
  });
868
- let peripheral = discoveredDevices.get(deviceId);
1053
+ let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
869
1054
  if (!peripheral) {
870
1055
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
871
1056
  if (!noble) {
@@ -874,6 +1059,12 @@ function connectDevice(deviceId, webContents) {
874
1059
  if (!noble) {
875
1060
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
876
1061
  }
1062
+ peripheral = yield tryDirectConnectById(deviceId);
1063
+ if (peripheral) {
1064
+ discoveredDevices.set(deviceId, peripheral);
1065
+ }
1066
+ }
1067
+ if (!peripheral) {
877
1068
  try {
878
1069
  const foundPeripheral = yield performTargetedScan(deviceId);
879
1070
  if (!foundPeripheral) {
@@ -894,8 +1085,8 @@ function connectDevice(deviceId, webContents) {
894
1085
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
895
1086
  if (!connectedDevices.has(deviceId)) {
896
1087
  connectedDevices.set(deviceId, peripheral);
897
- setupDisconnectListener(peripheral, deviceId, webContents);
898
1088
  }
1089
+ setupDisconnectListener(peripheral, deviceId, webContents);
899
1090
  if (deviceCharacteristics.has(deviceId)) {
900
1091
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
901
1092
  const ongoingOperation = subscriptionOperations.get(deviceId);
@@ -917,18 +1108,37 @@ function connectDevice(deviceId, webContents) {
917
1108
  notificationCallbacks.delete(deviceId);
918
1109
  subscribedDevices.delete(deviceId);
919
1110
  }
920
- const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
921
- deviceCharacteristics.set(deviceId, characteristics);
922
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1111
+ try {
1112
+ const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
1113
+ deviceCharacteristics.set(deviceId, characteristics);
1114
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1115
+ }
1116
+ catch (setupError) {
1117
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed on kept-alive link:', setupError);
1118
+ yield disconnectDevice(deviceId).catch(() => undefined);
1119
+ throw setupError;
1120
+ }
923
1121
  return;
924
1122
  }
925
1123
  return new Promise((resolve, reject) => {
1124
+ let connectionTimedOut = false;
926
1125
  const timeout = setTimeout(() => {
1126
+ connectionTimedOut = true;
927
1127
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
928
- }, CONNECTION_TIMEOUT);
1128
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
929
1129
  const connectedPeripheral = peripheral;
930
1130
  connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
931
1131
  clearTimeout(timeout);
1132
+ if (connectionTimedOut) {
1133
+ if (!error) {
1134
+ try {
1135
+ connectedPeripheral.disconnect(() => undefined);
1136
+ }
1137
+ catch (_a) {
1138
+ }
1139
+ }
1140
+ return;
1141
+ }
932
1142
  if (error) {
933
1143
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
934
1144
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
@@ -958,17 +1168,20 @@ function disconnectDevice(deviceId) {
958
1168
  if (!peripheral) {
959
1169
  return;
960
1170
  }
961
- return new Promise(resolve => {
962
- peripheral.removeAllListeners('disconnect');
963
- peripheral.disconnect(() => {
964
- cleanupDevice(deviceId, undefined, {
965
- cleanupConnection: true,
966
- sendDisconnectEvent: false,
967
- cancelOperations: true,
968
- reason: 'manual-disconnect',
969
- });
970
- resolve();
971
- });
1171
+ const disconnectEntry = deviceDisconnectListeners.get(deviceId);
1172
+ if (disconnectEntry) {
1173
+ disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
1174
+ deviceDisconnectListeners.delete(deviceId);
1175
+ }
1176
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1177
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1178
+ timeoutBehavior: 'resolve',
1179
+ });
1180
+ cleanupDevice(deviceId, undefined, {
1181
+ cleanupConnection: true,
1182
+ sendDisconnectEvent: false,
1183
+ cancelOperations: true,
1184
+ reason: 'manual-disconnect',
972
1185
  });
973
1186
  });
974
1187
  }
@@ -983,18 +1196,11 @@ function unsubscribeNotifications(deviceId) {
983
1196
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Unsubscribing from notifications for device:', deviceId);
984
1197
  subscriptionOperations.set(deviceId, 'unsubscribing');
985
1198
  try {
986
- yield new Promise((resolve, reject) => {
987
- notifyCharacteristic.unsubscribe((error) => {
988
- if (error) {
989
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Notification unsubscription failed:', error);
990
- reject(error);
991
- }
992
- else {
993
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification unsubscription successful');
994
- resolve();
995
- }
996
- });
1199
+ yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
1200
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1201
+ timeoutBehavior: 'resolve',
997
1202
  });
1203
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification unsubscription completed');
998
1204
  notifyCharacteristic.removeAllListeners('data');
999
1205
  notificationCallbacks.delete(deviceId);
1000
1206
  subscribedDevices.delete(deviceId);
@@ -1046,19 +1252,13 @@ function subscribeNotifications(deviceId, callback) {
1046
1252
  notificationCallbacks.set(deviceId, callback);
1047
1253
  function rebuildAppSubscription(deviceId, notifyCharacteristic) {
1048
1254
  return index.__awaiter(this, void 0, void 0, function* () {
1049
- yield new Promise(resolve => {
1050
- notifyCharacteristic.unsubscribe(() => {
1051
- resolve();
1052
- });
1255
+ yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
1256
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1257
+ timeoutBehavior: 'resolve',
1053
1258
  });
1054
- yield new Promise((resolve, reject) => {
1055
- notifyCharacteristic.subscribe((error) => {
1056
- if (error) {
1057
- reject(error);
1058
- return;
1059
- }
1060
- resolve();
1061
- });
1259
+ yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1260
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1261
+ timeoutBehavior: 'reject',
1062
1262
  });
1063
1263
  notifyCharacteristic.on('data', (data) => {
1064
1264
  if (!pairedDevices.has(deviceId)) {
@@ -1082,8 +1282,12 @@ function setupNobleBleHandlers(webContents) {
1082
1282
  try {
1083
1283
  logger = require('electron-log');
1084
1284
  const { ipcMain } = require('electron');
1285
+ const handle = (channel, listener) => {
1286
+ ipcMain.removeHandler(channel);
1287
+ ipcMain.handle(channel, listener);
1288
+ };
1085
1289
  safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers');
1086
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
1290
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
1087
1291
  try {
1088
1292
  const devices = yield enumerateDevices();
1089
1293
  safeLog(logger, 'debug', 'Enumeration completed', {
@@ -1097,34 +1301,54 @@ function setupNobleBleHandlers(webContents) {
1097
1301
  throw error;
1098
1302
  }
1099
1303
  }));
1100
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, () => index.__awaiter(this, void 0, void 0, function* () {
1304
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, () => index.__awaiter(this, void 0, void 0, function* () {
1101
1305
  yield stopScanning();
1102
1306
  }));
1103
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE, (_event, deviceId) => getDevice(deviceId));
1104
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1307
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE, (_event, deviceId) => getDevice(deviceId));
1308
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1105
1309
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] IPC CONNECT request received:', {
1106
1310
  deviceId,
1107
1311
  hasPeripheral: connectedDevices.has(deviceId),
1108
1312
  hasCharacteristics: deviceCharacteristics.has(deviceId),
1109
1313
  totalConnectedDevices: connectedDevices.size,
1110
1314
  });
1111
- yield connectDevice(deviceId, webContents);
1315
+ clearIdleDisconnect(deviceId);
1316
+ yield awaitIdleDisconnect(deviceId);
1317
+ try {
1318
+ yield connectDevice(deviceId, webContents);
1319
+ }
1320
+ finally {
1321
+ if (connectedDevices.has(deviceId)) {
1322
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
1323
+ }
1324
+ }
1112
1325
  }));
1113
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1326
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE, (_event, deviceId, keepSession) => {
1327
+ if (!connectedDevices.has(deviceId))
1328
+ return;
1329
+ if (keepSession) {
1330
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
1331
+ return;
1332
+ }
1333
+ armIdleDisconnect(deviceId);
1334
+ });
1335
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1114
1336
  yield disconnectDevice(deviceId);
1115
1337
  }));
1116
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, (_event, deviceId, hexData) => index.__awaiter(this, void 0, void 0, function* () {
1117
- yield transmitHexDataToDevice(deviceId, hexData);
1338
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, (_event, deviceId, hexData, options) => index.__awaiter(this, void 0, void 0, function* () {
1339
+ yield transmitHexDataToDevice(deviceId, hexData, options);
1118
1340
  }));
1119
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1341
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1120
1342
  yield subscribeNotifications(deviceId, (data) => {
1121
1343
  webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
1122
1344
  });
1345
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
1123
1346
  }));
1124
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1347
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1125
1348
  yield unsubscribeNotifications(deviceId);
1349
+ armIdleDisconnect(deviceId);
1126
1350
  }));
1127
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, () => index.__awaiter(this, void 0, void 0, function* () {
1351
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, () => index.__awaiter(this, void 0, void 0, function* () {
1128
1352
  const deviceIds = Array.from(connectedDevices.keys());
1129
1353
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Cancel pairing invoked', {
1130
1354
  platform: process.platform,
@@ -1140,7 +1364,7 @@ function setupNobleBleHandlers(webContents) {
1140
1364
  }
1141
1365
  }
1142
1366
  }));
1143
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, () => index.__awaiter(this, void 0, void 0, function* () {
1367
+ handle(hdShared.EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, () => index.__awaiter(this, void 0, void 0, function* () {
1144
1368
  try {
1145
1369
  const bluetoothStatus = yield checkBluetoothAvailability();
1146
1370
  safeLog(logger, 'info', 'Bluetooth availability check completed:', bluetoothStatus);
@@ -1158,23 +1382,23 @@ function setupNobleBleHandlers(webContents) {
1158
1382
  }));
1159
1383
  webContents.on('destroyed', () => {
1160
1384
  safeLog(logger, 'info', 'Cleaning up Noble BLE handlers');
1161
- const deviceIds = Array.from(connectedDevices.keys());
1162
- deviceIds.forEach(deviceId => {
1163
- cleanupDevice(deviceId, undefined, {
1164
- cleanupConnection: true,
1165
- sendDisconnectEvent: false,
1166
- cancelOperations: true,
1167
- reason: 'app-quit',
1168
- });
1385
+ (() => index.__awaiter(this, void 0, void 0, function* () {
1386
+ const deviceIds = Array.from(connectedDevices.keys());
1387
+ for (const deviceId of deviceIds) {
1388
+ yield unsubscribeNotifications(deviceId).catch(() => undefined);
1389
+ yield disconnectDevice(deviceId).catch(() => undefined);
1390
+ }
1391
+ yield stopScanning().catch(() => undefined);
1392
+ if (noble && persistentStateListener) {
1393
+ noble.removeListener('stateChange', persistentStateListener);
1394
+ persistentStateListener = null;
1395
+ }
1396
+ cleanupNobleListeners();
1397
+ discoveredDevices.clear();
1398
+ safeLog(logger, 'info', 'Noble BLE cleanup completed');
1399
+ }))().catch(error => {
1400
+ safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
1169
1401
  });
1170
- stopScanning();
1171
- if (noble && persistentStateListener) {
1172
- noble.removeListener('stateChange', persistentStateListener);
1173
- persistentStateListener = null;
1174
- }
1175
- cleanupNobleListeners();
1176
- discoveredDevices.clear();
1177
- safeLog(logger, 'info', 'Noble BLE cleanup completed');
1178
1402
  });
1179
1403
  safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
1180
1404
  }
@@ -1184,4 +1408,5 @@ function setupNobleBleHandlers(webContents) {
1184
1408
  }
1185
1409
  }
1186
1410
 
1411
+ exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
1187
1412
  exports.setupNobleBleHandlers = setupNobleBleHandlers;