@onekeyfe/hd-transport-electron 1.2.0-alpha.15 → 1.2.0-alpha.151

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-70b4a97d.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);
@@ -205,6 +242,7 @@ function initializeNoble() {
205
242
  try {
206
243
  noble = require('@stoprocent/noble');
207
244
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
245
+ setupPersistentStateListener();
208
246
  yield new Promise((resolve, reject) => {
209
247
  if (!noble) {
210
248
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
@@ -214,7 +252,6 @@ function initializeNoble() {
214
252
  resolve();
215
253
  return;
216
254
  }
217
- setupPersistentStateListener();
218
255
  const timeout = setTimeout(() => {
219
256
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
220
257
  }, BLUETOOTH_INIT_TIMEOUT);
@@ -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
  });
@@ -300,18 +407,43 @@ function handleDeviceDisconnect(deviceId, webContents) {
300
407
  });
301
408
  cleanupDevice(deviceId, webContents, {
302
409
  cleanupConnection: true,
410
+ cleanupDiscoveredCache: true,
303
411
  sendDisconnectEvent: true,
304
412
  cancelOperations: true,
305
413
  reason: 'auto-disconnect',
306
414
  });
307
415
  }
308
416
  function setupDisconnectListener(peripheral, deviceId, webContents) {
309
- peripheral.removeAllListeners('disconnect');
310
- peripheral.on('disconnect', () => {
417
+ const existing = deviceDisconnectListeners.get(deviceId);
418
+ if (existing) {
419
+ existing.peripheral.removeListener('disconnect', existing.listener);
420
+ }
421
+ const listener = () => {
311
422
  handleDeviceDisconnect(deviceId, webContents);
312
- });
423
+ };
424
+ deviceDisconnectListeners.set(deviceId, { peripheral, listener });
425
+ peripheral.on('disconnect', listener);
426
+ setupMtuListener(peripheral, deviceId, webContents);
313
427
  }
314
- function writeCharacteristicWithAck(deviceId, writeCharacteristic, buffer) {
428
+ function setupMtuListener(peripheral, deviceId, webContents) {
429
+ const existing = deviceMtuListeners.get(deviceId);
430
+ if (existing) {
431
+ existing.peripheral.removeListener('mtu', existing.listener);
432
+ }
433
+ const listener = (mtu) => {
434
+ if (!Number.isFinite(mtu) || mtu <= 0)
435
+ return;
436
+ if (webContents.isDestroyed())
437
+ return;
438
+ webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
439
+ id: deviceId,
440
+ mtu,
441
+ });
442
+ };
443
+ deviceMtuListeners.set(deviceId, { peripheral, listener });
444
+ peripheral.on('mtu', listener);
445
+ }
446
+ function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
315
447
  return index.__awaiter(this, void 0, void 0, function* () {
316
448
  return new Promise((resolve, reject) => {
317
449
  writeCharacteristic.write(buffer, true, (error) => {
@@ -343,7 +475,7 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
343
475
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Write characteristic not available for ${deviceId}`);
344
476
  }
345
477
  try {
346
- yield writeCharacteristicWithAck(deviceId, latestWrite, payload);
478
+ yield writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
347
479
  }
348
480
  catch (e) {
349
481
  const errorMessage = e instanceof Error ? e.message : String(e);
@@ -402,52 +534,57 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
402
534
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `No response observed after ${RETRY_CONFIG.MAX_ATTEMPTS} writes: ${deviceId}`);
403
535
  });
404
536
  }
405
- function transmitHexDataToDevice(deviceId, hexData) {
537
+ function transmitHexDataToDevice(deviceId, hexData, options) {
406
538
  return index.__awaiter(this, void 0, void 0, function* () {
407
539
  const characteristics = deviceCharacteristics.get(deviceId);
408
540
  const peripheral = connectedDevices.get(deviceId);
409
541
  if (!peripheral || !characteristics) {
410
542
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Device ${deviceId} not connected or characteristics not available`);
411
543
  }
544
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
412
545
  const toBuffer = Buffer.from(hexData, 'hex');
413
546
  const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
547
+ const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
548
+ const pacingDelayMs = resolveNobleBleWritePacingDelay(options);
414
549
  if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
415
550
  const writeCharacteristic = doGetWriteCharacteristic();
416
551
  if (!writeCharacteristic) {
417
552
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
418
553
  }
419
- if (toBuffer.length <= BLE_PACKET_SIZE) {
420
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
421
- yield writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
554
+ if (toBuffer.length <= packetCapacity) {
555
+ if (pacingDelayMs > 0)
556
+ yield hdShared.wait(pacingDelayMs);
557
+ yield writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
422
558
  return;
423
559
  }
424
560
  for (let offset = 0; offset < toBuffer.length;) {
425
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
561
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
426
562
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
427
563
  offset += chunkSize;
428
564
  const latest = doGetWriteCharacteristic();
429
565
  if (!latest) {
430
566
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
431
567
  }
432
- yield writeCharacteristicWithAck(deviceId, latest, chunk);
433
- if (offset < toBuffer.length) {
434
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
568
+ yield writeCharacteristicWithoutResponse(deviceId, latest, chunk);
569
+ if (offset < toBuffer.length && pacingDelayMs > 0) {
570
+ yield hdShared.wait(pacingDelayMs);
435
571
  }
436
572
  }
437
573
  return;
438
574
  }
439
- if (toBuffer.length <= BLE_PACKET_SIZE) {
440
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
575
+ if (toBuffer.length <= packetCapacity) {
576
+ if (pacingDelayMs > 0)
577
+ yield hdShared.wait(pacingDelayMs);
441
578
  yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
442
579
  return;
443
580
  }
444
581
  for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
445
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
582
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
446
583
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
447
584
  offset += chunkSize;
448
585
  yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, chunk, `chunk-${idx + 1}`);
449
- if (offset < toBuffer.length) {
450
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
586
+ if (offset < toBuffer.length && pacingDelayMs > 0) {
587
+ yield hdShared.wait(pacingDelayMs);
451
588
  }
452
589
  }
453
590
  });
@@ -470,17 +607,25 @@ function handleDeviceDiscovered(peripheral) {
470
607
  function ensureDiscoverListener() {
471
608
  if (!noble)
472
609
  return;
473
- const listenerCount = noble.listenerCount('discover');
474
- if (listenerCount === 0) {
610
+ if (!persistentDiscoverListener) {
475
611
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Discover listener missing, re-adding it');
476
- noble.on('discover', (peripheral) => {
612
+ persistentDiscoverListener = (peripheral) => {
477
613
  handleDeviceDiscovered(peripheral);
478
- });
614
+ };
615
+ noble.on('discover', persistentDiscoverListener);
479
616
  }
480
617
  else {
481
- logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already exists, count:', listenerCount);
618
+ logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
482
619
  }
483
620
  }
621
+ function waitForNobleScanStop(nobleInstance) {
622
+ return index.__awaiter(this, void 0, void 0, function* () {
623
+ yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
624
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
625
+ timeoutBehavior: 'resolve',
626
+ });
627
+ });
628
+ }
484
629
  function performTargetedScan(targetDeviceId) {
485
630
  return index.__awaiter(this, void 0, void 0, function* () {
486
631
  if (!noble) {
@@ -489,33 +634,43 @@ function performTargetedScan(targetDeviceId) {
489
634
  const nobleInstance = noble;
490
635
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
491
636
  return new Promise((resolve, reject) => {
637
+ let settled = false;
638
+ const finish = (peripheral, error) => index.__awaiter(this, void 0, void 0, function* () {
639
+ if (settled)
640
+ return;
641
+ settled = true;
642
+ if (timeoutId)
643
+ clearTimeout(timeoutId);
644
+ nobleInstance.removeListener('discover', onDiscover);
645
+ yield waitForNobleScanStop(nobleInstance);
646
+ if (error) {
647
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
648
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
649
+ return;
650
+ }
651
+ if (peripheral) {
652
+ discoveredDevices.set(peripheral.id, peripheral);
653
+ }
654
+ resolve(peripheral);
655
+ });
492
656
  const onDiscover = (peripheral) => {
493
657
  var _a;
494
- if (peripheral.id === targetDeviceId) {
658
+ if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
495
659
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Target device found during targeted scan:', {
496
660
  id: peripheral.id,
497
661
  name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
498
662
  });
499
- clearTimeout(timeoutId);
500
- nobleInstance.removeListener('discover', onDiscover);
501
- nobleInstance.stopScanning();
502
- discoveredDevices.set(peripheral.id, peripheral);
503
- resolve(peripheral);
663
+ finish(peripheral).catch(reject);
504
664
  }
505
665
  };
506
666
  const timeoutId = setTimeout(() => {
507
- nobleInstance.removeListener('discover', onDiscover);
508
- nobleInstance.stopScanning();
509
667
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
510
- resolve(null);
511
- }, FAST_SCAN_TIMEOUT);
668
+ finish(null).catch(reject);
669
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
512
670
  nobleInstance.on('discover', onDiscover);
513
- nobleInstance.startScanning([], false, (error) => {
671
+ nobleInstance.startScanning([], true, (error) => {
514
672
  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));
673
+ finish(null, error).catch(reject);
519
674
  return;
520
675
  }
521
676
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -538,44 +693,44 @@ function enumerateDevices() {
538
693
  return new Promise((resolve, reject) => {
539
694
  const devices = [];
540
695
  let intervalId;
541
- const cleanup = () => {
696
+ const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
542
697
  clearTimeout(timeoutId);
543
698
  if (intervalId)
544
699
  clearInterval(intervalId);
545
- nobleInstance.stopScanning();
700
+ yield waitForNobleScanStop(nobleInstance);
701
+ });
702
+ const pushDevice = (peripheral, id) => {
703
+ var _a;
704
+ if (devices.some(d => d.id === id))
705
+ return;
706
+ devices.push({
707
+ commType: 'electron-ble',
708
+ id,
709
+ name: ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device',
710
+ state: peripheral.state || 'disconnected',
711
+ });
546
712
  };
547
713
  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
- });
714
+ discoveredDevices.forEach(pushDevice);
715
+ connectedDevices.forEach(pushDevice);
561
716
  };
562
- const timeoutId = setTimeout(() => {
717
+ const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
563
718
  checkDevices();
564
- cleanup();
719
+ yield cleanup();
565
720
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
566
721
  resolve(devices);
567
- }, DEVICE_SCAN_TIMEOUT);
722
+ }), DEVICE_SCAN_TIMEOUT);
568
723
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
569
- nobleInstance.startScanning([], false, (error) => {
724
+ nobleInstance.startScanning([], true, (error) => index.__awaiter(this, void 0, void 0, function* () {
570
725
  if (error) {
571
- cleanup();
726
+ yield cleanup();
572
727
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
573
728
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
574
729
  return;
575
730
  }
576
731
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning started for OneKey devices');
577
732
  intervalId = setInterval(checkDevices, DEVICE_CHECK_INTERVAL);
578
- });
733
+ }));
579
734
  });
580
735
  });
581
736
  }
@@ -583,25 +738,20 @@ function stopScanning() {
583
738
  return index.__awaiter(this, void 0, void 0, function* () {
584
739
  if (!noble)
585
740
  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
- });
741
+ const nobleInstance = noble;
742
+ yield waitForNobleScanStop(nobleInstance);
743
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
596
744
  });
597
745
  }
598
746
  function cleanupNobleListeners() {
599
747
  if (!noble)
600
748
  return;
601
749
  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');
750
+ if (persistentDiscoverListener) {
751
+ noble.removeListener('discover', persistentDiscoverListener);
752
+ persistentDiscoverListener = null;
753
+ }
754
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Owned Noble listeners cleaned up');
605
755
  }
606
756
  catch (error) {
607
757
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to clean up some listeners:', error);
@@ -612,22 +762,12 @@ function getDevice(deviceId) {
612
762
  const peripheral = discoveredDevices.get(deviceId);
613
763
  if (peripheral) {
614
764
  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
- };
765
+ return Object.assign({ commType: 'electron-ble', id: peripheral.id, name: deviceName, state: peripheral.state || 'disconnected' }, (typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}));
621
766
  }
622
767
  const connectedPeripheral = connectedDevices.get(deviceId);
623
768
  if (connectedPeripheral) {
624
769
  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
- };
770
+ return Object.assign({ commType: 'electron-ble', id: connectedPeripheral.id, name: deviceName, state: connectedPeripheral.state || 'connected' }, (typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}));
631
771
  }
632
772
  return {
633
773
  commType: 'electron-ble',
@@ -678,14 +818,7 @@ function discoverServicesAndCharacteristics(peripheral) {
678
818
  deviceId: peripheral.id,
679
819
  serviceUUIDs: services.map(service => service.uuid),
680
820
  });
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
- }
821
+ const service = services.find(s => hdShared.matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
689
822
  if (!service) {
690
823
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound);
691
824
  }
@@ -708,12 +841,10 @@ function discoverServicesAndCharacteristics(peripheral) {
708
841
  let writeCharacteristic = null;
709
842
  let notifyCharacteristic = null;
710
843
  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) {
844
+ if (hdShared.matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
714
845
  writeCharacteristic = characteristic;
715
846
  }
716
- else if (uuidKey === NORMALIZED_NOTIFY_UUID) {
847
+ else if (hdShared.matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
717
848
  notifyCharacteristic = characteristic;
718
849
  }
719
850
  }
@@ -747,27 +878,17 @@ function forceReconnectPeripheral(peripheral, deviceId) {
747
878
  reason: 'force-reconnect',
748
879
  });
749
880
  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
- });
881
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => {
882
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force disconnect completed');
883
+ callback();
884
+ }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
757
885
  }
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
- });
886
+ yield runBleCallbackOperation(callback => peripheral.connect(callback), {
887
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
888
+ timeoutBehavior: 'reject',
770
889
  });
890
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
891
+ connectedDevices.set(deviceId, peripheral);
771
892
  yield hdShared.wait(500);
772
893
  });
773
894
  }
@@ -844,18 +965,83 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
844
965
  }
845
966
  function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
846
967
  return index.__awaiter(this, void 0, void 0, function* () {
968
+ setupDisconnectListener(peripheral, deviceId, webContents);
969
+ try {
970
+ return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
971
+ }
972
+ catch (initialError) {
973
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
974
+ deviceId,
975
+ error: initialError,
976
+ });
977
+ }
847
978
  yield forceReconnectPeripheral(peripheral, deviceId);
848
979
  setupDisconnectListener(peripheral, deviceId, webContents);
849
980
  try {
850
981
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
851
982
  }
852
- catch (error) {
853
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', error);
983
+ catch (reconnectError) {
984
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
854
985
  return freshScanAndDiscover(deviceId, webContents);
855
986
  }
856
987
  });
857
988
  }
989
+ const DIRECT_CONNECT_TIMEOUT_MS = 2000;
990
+ const DIRECT_CONNECT_COOLDOWN_MS = 15000;
991
+ const directConnectCooldownUntil = new Map();
992
+ function tryDirectConnectById(deviceId) {
993
+ var _a;
994
+ return index.__awaiter(this, void 0, void 0, function* () {
995
+ const nobleInstance = noble;
996
+ if (!(nobleInstance === null || nobleInstance === void 0 ? void 0 : nobleInstance.connectAsync))
997
+ return undefined;
998
+ const cooldownUntil = (_a = directConnectCooldownUntil.get(deviceId)) !== null && _a !== void 0 ? _a : 0;
999
+ if (Date.now() < cooldownUntil)
1000
+ return undefined;
1001
+ try {
1002
+ const directPromise = nobleInstance.connectAsync(deviceId);
1003
+ const raced = yield Promise.race([
1004
+ directPromise,
1005
+ new Promise(resolve => {
1006
+ setTimeout(() => resolve('timeout'), DIRECT_CONNECT_TIMEOUT_MS);
1007
+ }),
1008
+ ]);
1009
+ if (raced === 'timeout') {
1010
+ directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1011
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id timed out, falling back to scan', {
1012
+ deviceId,
1013
+ });
1014
+ directPromise
1015
+ .then(late => {
1016
+ const latePeripheral = late !== null && late !== void 0 ? late : discoveredDevices.get(deviceId);
1017
+ if (latePeripheral &&
1018
+ latePeripheral.state === 'connected' &&
1019
+ !connectedDevices.has(deviceId)) {
1020
+ latePeripheral.removeAllListeners('disconnect');
1021
+ latePeripheral.disconnect(() => undefined);
1022
+ }
1023
+ })
1024
+ .catch(() => undefined);
1025
+ return undefined;
1026
+ }
1027
+ const peripheral = raced !== null && raced !== void 0 ? raced : discoveredDevices.get(deviceId);
1028
+ if (!peripheral || peripheral.state !== 'connected')
1029
+ return undefined;
1030
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id succeeded', { deviceId });
1031
+ return peripheral;
1032
+ }
1033
+ catch (error) {
1034
+ directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
1035
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
1036
+ deviceId,
1037
+ error: String(error),
1038
+ });
1039
+ return undefined;
1040
+ }
1041
+ });
1042
+ }
858
1043
  function connectDevice(deviceId, webContents) {
1044
+ var _a;
859
1045
  return index.__awaiter(this, void 0, void 0, function* () {
860
1046
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
861
1047
  deviceId,
@@ -865,7 +1051,7 @@ function connectDevice(deviceId, webContents) {
865
1051
  totalDiscovered: discoveredDevices.size,
866
1052
  totalConnected: connectedDevices.size,
867
1053
  });
868
- let peripheral = discoveredDevices.get(deviceId);
1054
+ let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
869
1055
  if (!peripheral) {
870
1056
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
871
1057
  if (!noble) {
@@ -874,6 +1060,12 @@ function connectDevice(deviceId, webContents) {
874
1060
  if (!noble) {
875
1061
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
876
1062
  }
1063
+ peripheral = yield tryDirectConnectById(deviceId);
1064
+ if (peripheral) {
1065
+ discoveredDevices.set(deviceId, peripheral);
1066
+ }
1067
+ }
1068
+ if (!peripheral) {
877
1069
  try {
878
1070
  const foundPeripheral = yield performTargetedScan(deviceId);
879
1071
  if (!foundPeripheral) {
@@ -894,8 +1086,8 @@ function connectDevice(deviceId, webContents) {
894
1086
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
895
1087
  if (!connectedDevices.has(deviceId)) {
896
1088
  connectedDevices.set(deviceId, peripheral);
897
- setupDisconnectListener(peripheral, deviceId, webContents);
898
1089
  }
1090
+ setupDisconnectListener(peripheral, deviceId, webContents);
899
1091
  if (deviceCharacteristics.has(deviceId)) {
900
1092
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device characteristics already available');
901
1093
  const ongoingOperation = subscriptionOperations.get(deviceId);
@@ -917,18 +1109,37 @@ function connectDevice(deviceId, webContents) {
917
1109
  notificationCallbacks.delete(deviceId);
918
1110
  subscribedDevices.delete(deviceId);
919
1111
  }
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);
1112
+ try {
1113
+ const characteristics = yield setupConnectionAndDiscoverServices(peripheral, deviceId, webContents);
1114
+ deviceCharacteristics.set(deviceId, characteristics);
1115
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device ready for communication:', deviceId);
1116
+ }
1117
+ catch (setupError) {
1118
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed on kept-alive link:', setupError);
1119
+ yield disconnectDevice(deviceId).catch(() => undefined);
1120
+ throw setupError;
1121
+ }
923
1122
  return;
924
1123
  }
925
1124
  return new Promise((resolve, reject) => {
1125
+ let connectionTimedOut = false;
926
1126
  const timeout = setTimeout(() => {
1127
+ connectionTimedOut = true;
927
1128
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
928
- }, CONNECTION_TIMEOUT);
1129
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
929
1130
  const connectedPeripheral = peripheral;
930
1131
  connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
931
1132
  clearTimeout(timeout);
1133
+ if (connectionTimedOut) {
1134
+ if (!error) {
1135
+ try {
1136
+ connectedPeripheral.disconnect(() => undefined);
1137
+ }
1138
+ catch (_a) {
1139
+ }
1140
+ }
1141
+ return;
1142
+ }
932
1143
  if (error) {
933
1144
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
934
1145
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
@@ -958,17 +1169,21 @@ function disconnectDevice(deviceId) {
958
1169
  if (!peripheral) {
959
1170
  return;
960
1171
  }
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
- });
1172
+ const disconnectEntry = deviceDisconnectListeners.get(deviceId);
1173
+ if (disconnectEntry) {
1174
+ disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
1175
+ deviceDisconnectListeners.delete(deviceId);
1176
+ }
1177
+ yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1178
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1179
+ timeoutBehavior: 'resolve',
1180
+ });
1181
+ cleanupDevice(deviceId, undefined, {
1182
+ cleanupConnection: true,
1183
+ cleanupDiscoveredCache: true,
1184
+ sendDisconnectEvent: false,
1185
+ cancelOperations: true,
1186
+ reason: 'manual-disconnect',
972
1187
  });
973
1188
  });
974
1189
  }
@@ -983,18 +1198,11 @@ function unsubscribeNotifications(deviceId) {
983
1198
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Unsubscribing from notifications for device:', deviceId);
984
1199
  subscriptionOperations.set(deviceId, 'unsubscribing');
985
1200
  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
- });
1201
+ yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
1202
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1203
+ timeoutBehavior: 'resolve',
997
1204
  });
1205
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification unsubscription completed');
998
1206
  notifyCharacteristic.removeAllListeners('data');
999
1207
  notificationCallbacks.delete(deviceId);
1000
1208
  subscribedDevices.delete(deviceId);
@@ -1046,19 +1254,13 @@ function subscribeNotifications(deviceId, callback) {
1046
1254
  notificationCallbacks.set(deviceId, callback);
1047
1255
  function rebuildAppSubscription(deviceId, notifyCharacteristic) {
1048
1256
  return index.__awaiter(this, void 0, void 0, function* () {
1049
- yield new Promise(resolve => {
1050
- notifyCharacteristic.unsubscribe(() => {
1051
- resolve();
1052
- });
1257
+ yield runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
1258
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1259
+ timeoutBehavior: 'resolve',
1053
1260
  });
1054
- yield new Promise((resolve, reject) => {
1055
- notifyCharacteristic.subscribe((error) => {
1056
- if (error) {
1057
- reject(error);
1058
- return;
1059
- }
1060
- resolve();
1061
- });
1261
+ yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1262
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1263
+ timeoutBehavior: 'reject',
1062
1264
  });
1063
1265
  notifyCharacteristic.on('data', (data) => {
1064
1266
  if (!pairedDevices.has(deviceId)) {
@@ -1082,8 +1284,12 @@ function setupNobleBleHandlers(webContents) {
1082
1284
  try {
1083
1285
  logger = require('electron-log');
1084
1286
  const { ipcMain } = require('electron');
1287
+ const handle = (channel, listener) => {
1288
+ ipcMain.removeHandler(channel);
1289
+ ipcMain.handle(channel, listener);
1290
+ };
1085
1291
  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* () {
1292
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, () => index.__awaiter(this, void 0, void 0, function* () {
1087
1293
  try {
1088
1294
  const devices = yield enumerateDevices();
1089
1295
  safeLog(logger, 'debug', 'Enumeration completed', {
@@ -1097,34 +1303,54 @@ function setupNobleBleHandlers(webContents) {
1097
1303
  throw error;
1098
1304
  }
1099
1305
  }));
1100
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, () => index.__awaiter(this, void 0, void 0, function* () {
1306
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_STOP_SCAN, () => index.__awaiter(this, void 0, void 0, function* () {
1101
1307
  yield stopScanning();
1102
1308
  }));
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* () {
1309
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_GET_DEVICE, (_event, deviceId) => getDevice(deviceId));
1310
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1105
1311
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] IPC CONNECT request received:', {
1106
1312
  deviceId,
1107
1313
  hasPeripheral: connectedDevices.has(deviceId),
1108
1314
  hasCharacteristics: deviceCharacteristics.has(deviceId),
1109
1315
  totalConnectedDevices: connectedDevices.size,
1110
1316
  });
1111
- yield connectDevice(deviceId, webContents);
1317
+ clearIdleDisconnect(deviceId);
1318
+ yield awaitIdleDisconnect(deviceId);
1319
+ try {
1320
+ yield connectDevice(deviceId, webContents);
1321
+ }
1322
+ finally {
1323
+ if (connectedDevices.has(deviceId)) {
1324
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
1325
+ }
1326
+ }
1112
1327
  }));
1113
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1328
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_RELEASE, (_event, deviceId, keepSession) => {
1329
+ if (!connectedDevices.has(deviceId))
1330
+ return;
1331
+ if (keepSession) {
1332
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
1333
+ return;
1334
+ }
1335
+ armIdleDisconnect(deviceId);
1336
+ });
1337
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_DISCONNECT, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1114
1338
  yield disconnectDevice(deviceId);
1115
1339
  }));
1116
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, (_event, deviceId, hexData) => index.__awaiter(this, void 0, void 0, function* () {
1117
- yield transmitHexDataToDevice(deviceId, hexData);
1340
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, (_event, deviceId, hexData, options) => index.__awaiter(this, void 0, void 0, function* () {
1341
+ yield transmitHexDataToDevice(deviceId, hexData, options);
1118
1342
  }));
1119
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1343
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_SUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1120
1344
  yield subscribeNotifications(deviceId, (data) => {
1121
1345
  webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_NOTIFICATION, deviceId, data);
1122
1346
  });
1347
+ armIdleDisconnect(deviceId, BLE_BUSY_BACKSTOP_MS, 'busy-backstop');
1123
1348
  }));
1124
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1349
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_UNSUBSCRIBE, (_event, deviceId) => index.__awaiter(this, void 0, void 0, function* () {
1125
1350
  yield unsubscribeNotifications(deviceId);
1351
+ armIdleDisconnect(deviceId);
1126
1352
  }));
1127
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, () => index.__awaiter(this, void 0, void 0, function* () {
1353
+ handle(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_CANCEL_PAIRING, () => index.__awaiter(this, void 0, void 0, function* () {
1128
1354
  const deviceIds = Array.from(connectedDevices.keys());
1129
1355
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Cancel pairing invoked', {
1130
1356
  platform: process.platform,
@@ -1140,7 +1366,7 @@ function setupNobleBleHandlers(webContents) {
1140
1366
  }
1141
1367
  }
1142
1368
  }));
1143
- ipcMain.handle(hdShared.EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, () => index.__awaiter(this, void 0, void 0, function* () {
1369
+ handle(hdShared.EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK, () => index.__awaiter(this, void 0, void 0, function* () {
1144
1370
  try {
1145
1371
  const bluetoothStatus = yield checkBluetoothAvailability();
1146
1372
  safeLog(logger, 'info', 'Bluetooth availability check completed:', bluetoothStatus);
@@ -1158,23 +1384,19 @@ function setupNobleBleHandlers(webContents) {
1158
1384
  }));
1159
1385
  webContents.on('destroyed', () => {
1160
1386
  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
- });
1387
+ (() => index.__awaiter(this, void 0, void 0, function* () {
1388
+ const deviceIds = Array.from(connectedDevices.keys());
1389
+ for (const deviceId of deviceIds) {
1390
+ yield unsubscribeNotifications(deviceId).catch(() => undefined);
1391
+ yield disconnectDevice(deviceId).catch(() => undefined);
1392
+ }
1393
+ yield stopScanning().catch(() => undefined);
1394
+ cleanupNobleListeners();
1395
+ discoveredDevices.clear();
1396
+ safeLog(logger, 'info', 'Noble BLE cleanup completed');
1397
+ }))().catch(error => {
1398
+ safeLog(logger, 'error', 'Noble BLE cleanup failed', error);
1169
1399
  });
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
1400
  });
1179
1401
  safeLog(logger, 'info', 'Noble BLE IPC handlers setup completed');
1180
1402
  }
@@ -1184,4 +1406,5 @@ function setupNobleBleHandlers(webContents) {
1184
1406
  }
1185
1407
  }
1186
1408
 
1409
+ exports.resolveNobleBleWritePacingDelay = resolveNobleBleWritePacingDelay;
1187
1410
  exports.setupNobleBleHandlers = setupNobleBleHandlers;