@onekeyfe/hd-transport-electron 1.2.0-alpha.67 → 1.2.0-alpha.69

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.
@@ -0,0 +1,2 @@
1
+ export declare function resolveBlePacketCapacity(mtu: number | null | undefined, maximumPacketCapacity: number, fallbackPacketCapacity: number): number;
2
+ //# sourceMappingURL=ble-packet-capacity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ble-packet-capacity.d.ts","sourceRoot":"","sources":["../src/ble-packet-capacity.ts"],"names":[],"mappings":"AAEA,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9B,qBAAqB,EAAE,MAAM,EAC7B,sBAAsB,EAAE,MAAM,GAC7B,MAAM,CAMR"}
@@ -32,7 +32,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
32
32
 
33
33
  function initNobleBleSupport(webContents) {
34
34
  return __awaiter(this, void 0, void 0, function* () {
35
- const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-d58af248.js'); });
35
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-7ac9c43d.js'); });
36
36
  setupNobleBleHandlers(webContents);
37
37
  });
38
38
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ interface DeviceInfo extends OneKeyDeviceInfoBase {
6
6
  id: string;
7
7
  name: string;
8
8
  state: string;
9
+ mtu?: number;
9
10
  }
10
11
  interface CharacteristicPair {
11
12
  write: Characteristic;
@@ -38,6 +39,7 @@ interface NobleBleAPI {
38
39
  getDevice: (uuid: string) => Promise<{
39
40
  id: string;
40
41
  name: string;
42
+ mtu?: number;
41
43
  } | null>;
42
44
  connect: (uuid: string) => Promise<void>;
43
45
  disconnect: (uuid: string) => Promise<void>;
@@ -45,6 +47,10 @@ interface NobleBleAPI {
45
47
  unsubscribe: (uuid: string) => Promise<void>;
46
48
  write: (uuid: string, data: string) => Promise<void>;
47
49
  onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
50
+ onMtuChanged?: (callback: (device: {
51
+ id: string;
52
+ mtu: number;
53
+ }) => void) => () => void;
48
54
  onDeviceDisconnected: (callback: (device: {
49
55
  id: string;
50
56
  name: string;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('./index-5f728113.js');
5
+ var index = require('./index-bd1cfdb6.js');
6
6
 
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-5f728113.js');
3
+ var index = require('./index-bd1cfdb6.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);
@@ -78,6 +86,9 @@ function softRefreshSubscription(params) {
78
86
  });
79
87
  }
80
88
 
89
+ const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
90
+ const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
91
+
81
92
  let noble = null;
82
93
  let logger = null;
83
94
  const bluetoothState = {
@@ -95,19 +106,18 @@ const notificationCallbacks = new Map();
95
106
  const subscribedDevices = new Map();
96
107
  const subscriptionOperations = new Map();
97
108
  const deviceDisconnectListeners = new Map();
109
+ const deviceMtuListeners = new Map();
98
110
  const ONEKEY_SERVICE_UUIDS = [hdShared.ONEKEY_SERVICE_UUID];
99
111
  const ONEKEY_SERVICE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_SERVICE_UUID);
100
112
  const ONEKEY_WRITE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_WRITE_CHARACTERISTIC_UUID);
101
113
  const ONEKEY_NOTIFY_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
102
114
  const BLUETOOTH_INIT_TIMEOUT = 10000;
103
- const DEVICE_SCAN_TIMEOUT = 8000;
104
- const FAST_SCAN_TIMEOUT = 8000;
115
+ const DEVICE_SCAN_TIMEOUT = 5000;
105
116
  const DEVICE_CHECK_INTERVAL = 500;
106
- const CONNECTION_TIMEOUT = 8000;
107
117
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
108
118
  const BLE_CLEANUP_TIMEOUT = 250;
109
- const BLE_PACKET_SIZE = 192;
110
- const UNIFIED_WRITE_DELAY = 5;
119
+ const BLE_PACKET_SIZE_FALLBACK = 192;
120
+ const BLE_PACKET_SIZE_MAXIMUM = 244;
111
121
  const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 };
112
122
  const IS_WINDOWS = process.platform === 'win32';
113
123
  const ABORTABLE_WRITE_ERROR_PATTERNS = [
@@ -116,10 +126,14 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
116
126
  function isOneKeyPeripheral(peripheral) {
117
127
  var _a, _b;
118
128
  const serviceUuids = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids;
129
+ const localName = (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName;
130
+ if (!(localName === null || localName === void 0 ? void 0 : localName.trim()) || hdShared.isPro2FindMyAdvertisementName(localName)) {
131
+ return false;
132
+ }
119
133
  return (hdShared.hasOnekeyCommunicationService(serviceUuids) &&
120
134
  hdShared.isOnekeyBluetoothDevice({
121
135
  id: peripheral.id,
122
- localName: (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName,
136
+ localName,
123
137
  serviceUuids,
124
138
  }));
125
139
  }
@@ -295,6 +309,11 @@ function cleanupDevice(deviceId, webContents, options = {}) {
295
309
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
296
310
  deviceDisconnectListeners.delete(deviceId);
297
311
  }
312
+ const mtuEntry = deviceMtuListeners.get(deviceId);
313
+ if (mtuEntry) {
314
+ mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
315
+ deviceMtuListeners.delete(deviceId);
316
+ }
298
317
  connectedDevices.delete(deviceId);
299
318
  deviceCharacteristics.delete(deviceId);
300
319
  notificationCallbacks.delete(deviceId);
@@ -338,8 +357,25 @@ function setupDisconnectListener(peripheral, deviceId, webContents) {
338
357
  };
339
358
  deviceDisconnectListeners.set(deviceId, { peripheral, listener });
340
359
  peripheral.on('disconnect', listener);
360
+ setupMtuListener(peripheral, deviceId, webContents);
341
361
  }
342
- function writeCharacteristicWithAck(deviceId, writeCharacteristic, buffer) {
362
+ function setupMtuListener(peripheral, deviceId, webContents) {
363
+ const existing = deviceMtuListeners.get(deviceId);
364
+ if (existing) {
365
+ existing.peripheral.removeListener('mtu', existing.listener);
366
+ }
367
+ const listener = (mtu) => {
368
+ if (!Number.isFinite(mtu) || mtu <= 0)
369
+ return;
370
+ webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
371
+ id: deviceId,
372
+ mtu,
373
+ });
374
+ };
375
+ deviceMtuListeners.set(deviceId, { peripheral, listener });
376
+ peripheral.on('mtu', listener);
377
+ }
378
+ function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
343
379
  return index.__awaiter(this, void 0, void 0, function* () {
344
380
  return new Promise((resolve, reject) => {
345
381
  writeCharacteristic.write(buffer, true, (error) => {
@@ -371,7 +407,7 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
371
407
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Write characteristic not available for ${deviceId}`);
372
408
  }
373
409
  try {
374
- yield writeCharacteristicWithAck(deviceId, latestWrite, payload);
410
+ yield writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
375
411
  }
376
412
  catch (e) {
377
413
  const errorMessage = e instanceof Error ? e.message : String(e);
@@ -439,44 +475,37 @@ function transmitHexDataToDevice(deviceId, hexData) {
439
475
  }
440
476
  const toBuffer = Buffer.from(hexData, 'hex');
441
477
  const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
478
+ const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
442
479
  if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
443
480
  const writeCharacteristic = doGetWriteCharacteristic();
444
481
  if (!writeCharacteristic) {
445
482
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
446
483
  }
447
- if (toBuffer.length <= BLE_PACKET_SIZE) {
448
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
449
- yield writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
484
+ if (toBuffer.length <= packetCapacity) {
485
+ yield writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
450
486
  return;
451
487
  }
452
488
  for (let offset = 0; offset < toBuffer.length;) {
453
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
489
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
454
490
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
455
491
  offset += chunkSize;
456
492
  const latest = doGetWriteCharacteristic();
457
493
  if (!latest) {
458
494
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
459
495
  }
460
- yield writeCharacteristicWithAck(deviceId, latest, chunk);
461
- if (offset < toBuffer.length) {
462
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
463
- }
496
+ yield writeCharacteristicWithoutResponse(deviceId, latest, chunk);
464
497
  }
465
498
  return;
466
499
  }
467
- if (toBuffer.length <= BLE_PACKET_SIZE) {
468
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
500
+ if (toBuffer.length <= packetCapacity) {
469
501
  yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
470
502
  return;
471
503
  }
472
504
  for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
473
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
505
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
474
506
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
475
507
  offset += chunkSize;
476
508
  yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, chunk, `chunk-${idx + 1}`);
477
- if (offset < toBuffer.length) {
478
- yield hdShared.wait(UNIFIED_WRITE_DELAY);
479
- }
480
509
  }
481
510
  });
482
511
  }
@@ -509,6 +538,14 @@ function ensureDiscoverListener() {
509
538
  logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
510
539
  }
511
540
  }
541
+ function waitForNobleScanStop(nobleInstance) {
542
+ return index.__awaiter(this, void 0, void 0, function* () {
543
+ yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
544
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
545
+ timeoutBehavior: 'resolve',
546
+ });
547
+ });
548
+ }
512
549
  function performTargetedScan(targetDeviceId) {
513
550
  return index.__awaiter(this, void 0, void 0, function* () {
514
551
  if (!noble) {
@@ -517,6 +554,25 @@ function performTargetedScan(targetDeviceId) {
517
554
  const nobleInstance = noble;
518
555
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
519
556
  return new Promise((resolve, reject) => {
557
+ let settled = false;
558
+ const finish = (peripheral, error) => index.__awaiter(this, void 0, void 0, function* () {
559
+ if (settled)
560
+ return;
561
+ settled = true;
562
+ if (timeoutId)
563
+ clearTimeout(timeoutId);
564
+ nobleInstance.removeListener('discover', onDiscover);
565
+ yield waitForNobleScanStop(nobleInstance);
566
+ if (error) {
567
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
568
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
569
+ return;
570
+ }
571
+ if (peripheral) {
572
+ discoveredDevices.set(peripheral.id, peripheral);
573
+ }
574
+ resolve(peripheral);
575
+ });
520
576
  const onDiscover = (peripheral) => {
521
577
  var _a;
522
578
  if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
@@ -524,26 +580,17 @@ function performTargetedScan(targetDeviceId) {
524
580
  id: peripheral.id,
525
581
  name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
526
582
  });
527
- clearTimeout(timeoutId);
528
- nobleInstance.removeListener('discover', onDiscover);
529
- nobleInstance.stopScanning();
530
- discoveredDevices.set(peripheral.id, peripheral);
531
- resolve(peripheral);
583
+ finish(peripheral).catch(reject);
532
584
  }
533
585
  };
534
586
  const timeoutId = setTimeout(() => {
535
- nobleInstance.removeListener('discover', onDiscover);
536
- nobleInstance.stopScanning();
537
587
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
538
- resolve(null);
539
- }, FAST_SCAN_TIMEOUT);
588
+ finish(null).catch(reject);
589
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
540
590
  nobleInstance.on('discover', onDiscover);
541
591
  nobleInstance.startScanning([], false, (error) => {
542
592
  if (error) {
543
- clearTimeout(timeoutId);
544
- nobleInstance.removeListener('discover', onDiscover);
545
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
546
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
593
+ finish(null, error).catch(reject);
547
594
  return;
548
595
  }
549
596
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -566,12 +613,12 @@ function enumerateDevices() {
566
613
  return new Promise((resolve, reject) => {
567
614
  const devices = [];
568
615
  let intervalId;
569
- const cleanup = () => {
616
+ const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
570
617
  clearTimeout(timeoutId);
571
618
  if (intervalId)
572
619
  clearInterval(intervalId);
573
- nobleInstance.stopScanning();
574
- };
620
+ yield waitForNobleScanStop(nobleInstance);
621
+ });
575
622
  const checkDevices = () => {
576
623
  discoveredDevices.forEach((peripheral, id) => {
577
624
  var _a;
@@ -587,23 +634,23 @@ function enumerateDevices() {
587
634
  }
588
635
  });
589
636
  };
590
- const timeoutId = setTimeout(() => {
637
+ const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
591
638
  checkDevices();
592
- cleanup();
639
+ yield cleanup();
593
640
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
594
641
  resolve(devices);
595
- }, DEVICE_SCAN_TIMEOUT);
642
+ }), DEVICE_SCAN_TIMEOUT);
596
643
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
597
- nobleInstance.startScanning([], false, (error) => {
644
+ nobleInstance.startScanning([], false, (error) => index.__awaiter(this, void 0, void 0, function* () {
598
645
  if (error) {
599
- cleanup();
646
+ yield cleanup();
600
647
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
601
648
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
602
649
  return;
603
650
  }
604
651
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning started for OneKey devices');
605
652
  intervalId = setInterval(checkDevices, DEVICE_CHECK_INTERVAL);
606
- });
653
+ }));
607
654
  });
608
655
  });
609
656
  }
@@ -612,10 +659,7 @@ function stopScanning() {
612
659
  if (!noble)
613
660
  return;
614
661
  const nobleInstance = noble;
615
- yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
616
- timeoutMs: BLE_CLEANUP_TIMEOUT,
617
- timeoutBehavior: 'resolve',
618
- });
662
+ yield waitForNobleScanStop(nobleInstance);
619
663
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
620
664
  });
621
665
  }
@@ -638,22 +682,12 @@ function getDevice(deviceId) {
638
682
  const peripheral = discoveredDevices.get(deviceId);
639
683
  if (peripheral) {
640
684
  const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
641
- return {
642
- commType: 'electron-ble',
643
- id: peripheral.id,
644
- name: deviceName,
645
- state: peripheral.state || 'disconnected',
646
- };
685
+ return Object.assign({ commType: 'electron-ble', id: peripheral.id, name: deviceName, state: peripheral.state || 'disconnected' }, (typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}));
647
686
  }
648
687
  const connectedPeripheral = connectedDevices.get(deviceId);
649
688
  if (connectedPeripheral) {
650
689
  const deviceName = ((_b = connectedPeripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device';
651
- return {
652
- commType: 'electron-ble',
653
- id: connectedPeripheral.id,
654
- name: deviceName,
655
- state: connectedPeripheral.state || 'connected',
656
- };
690
+ return Object.assign({ commType: 'electron-ble', id: connectedPeripheral.id, name: deviceName, state: connectedPeripheral.state || 'connected' }, (typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}));
657
691
  }
658
692
  return {
659
693
  commType: 'electron-ble',
@@ -770,7 +804,7 @@ function forceReconnectPeripheral(peripheral, deviceId) {
770
804
  }), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
771
805
  }
772
806
  yield runBleCallbackOperation(callback => peripheral.connect(callback), {
773
- timeoutMs: CONNECTION_TIMEOUT,
807
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
774
808
  timeoutBehavior: 'reject',
775
809
  });
776
810
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
@@ -940,12 +974,24 @@ function connectDevice(deviceId, webContents) {
940
974
  return;
941
975
  }
942
976
  return new Promise((resolve, reject) => {
977
+ let connectionTimedOut = false;
943
978
  const timeout = setTimeout(() => {
979
+ connectionTimedOut = true;
944
980
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
945
- }, CONNECTION_TIMEOUT);
981
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
946
982
  const connectedPeripheral = peripheral;
947
983
  connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
948
984
  clearTimeout(timeout);
985
+ if (connectionTimedOut) {
986
+ if (!error) {
987
+ try {
988
+ connectedPeripheral.disconnect(() => undefined);
989
+ }
990
+ catch (_a) {
991
+ }
992
+ }
993
+ return;
994
+ }
949
995
  if (error) {
950
996
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
951
997
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
@@ -1064,7 +1110,7 @@ function subscribeNotifications(deviceId, callback) {
1064
1110
  timeoutBehavior: 'resolve',
1065
1111
  });
1066
1112
  yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1067
- timeoutMs: CONNECTION_TIMEOUT,
1113
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1068
1114
  timeoutBehavior: 'reject',
1069
1115
  });
1070
1116
  notifyCharacteristic.on('data', (data) => {
@@ -1 +1 @@
1
- {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AA46ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAyJpE"}
1
+ {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AA++ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAyJpE"}
@@ -0,0 +1,3 @@
1
+ export declare const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
2
+ export declare const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
3
+ //# sourceMappingURL=noble-ble-timeouts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noble-ble-timeouts.d.ts","sourceRoot":"","sources":["../src/noble-ble-timeouts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kCAAkC,OAAQ,CAAC;AACxD,eAAO,MAAM,+BAA+B,QAAS,CAAC"}
@@ -6,6 +6,7 @@ export interface NobleBleAPI {
6
6
  getDevice: (uuid: string) => Promise<{
7
7
  id: string;
8
8
  name: string;
9
+ mtu?: number;
9
10
  } | null>;
10
11
  connect: (uuid: string) => Promise<void>;
11
12
  disconnect: (uuid: string) => Promise<void>;
@@ -13,6 +14,10 @@ export interface NobleBleAPI {
13
14
  unsubscribe: (uuid: string) => Promise<void>;
14
15
  write: (uuid: string, data: string) => Promise<void>;
15
16
  onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
17
+ onMtuChanged?: (callback: (device: {
18
+ id: string;
19
+ mtu: number;
20
+ }) => void) => () => void;
16
21
  onDeviceDisconnected: (callback: (device: {
17
22
  id: string;
18
23
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,oBAAoB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/F,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
1
+ {"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxF,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACvF,oBAAoB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/F,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
@@ -4,6 +4,7 @@ export interface DeviceInfo extends OneKeyDeviceInfoBase {
4
4
  id: string;
5
5
  name: string;
6
6
  state: string;
7
+ mtu?: number;
7
8
  }
8
9
  export interface CharacteristicPair {
9
10
  write: Characteristic;
@@ -1 +1 @@
1
- {"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
1
+ {"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-electron",
3
- "version": "1.2.0-alpha.67",
3
+ "version": "1.2.0-alpha.69",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -25,9 +25,9 @@
25
25
  "electron-log": ">=4.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-core": "1.2.0-alpha.67",
29
- "@onekeyfe/hd-shared": "1.2.0-alpha.67",
30
- "@onekeyfe/hd-transport": "1.2.0-alpha.67",
28
+ "@onekeyfe/hd-core": "1.2.0-alpha.69",
29
+ "@onekeyfe/hd-shared": "1.2.0-alpha.69",
30
+ "@onekeyfe/hd-transport": "1.2.0-alpha.69",
31
31
  "@stoprocent/noble": "2.3.16",
32
32
  "p-retry": "^4.6.2"
33
33
  },
@@ -36,5 +36,5 @@
36
36
  "electron": "^25.0.0",
37
37
  "typescript": "^5.3.3"
38
38
  },
39
- "gitHead": "5fea66e8c65a1fab2ccc099b2811c8749b2487e0"
39
+ "gitHead": "c6baed25917e3c3f027f83dfc23cde85400de558"
40
40
  }
@@ -0,0 +1,13 @@
1
+ import { resolveBlePacketCapacity } from '../ble-packet-capacity';
2
+
3
+ describe('resolveBlePacketCapacity', () => {
4
+ test('uses ATT MTU payload capacity with an upper bound', () => {
5
+ expect(resolveBlePacketCapacity(247, 244, 192)).toBe(244);
6
+ expect(resolveBlePacketCapacity(185, 244, 192)).toBe(182);
7
+ });
8
+
9
+ test('preserves the compatibility fallback when MTU is unavailable', () => {
10
+ expect(resolveBlePacketCapacity(null, 244, 192)).toBe(192);
11
+ expect(resolveBlePacketCapacity(undefined, 244, 192)).toBe(192);
12
+ });
13
+ });
@@ -0,0 +1,354 @@
1
+ import { EventEmitter } from 'events';
2
+ import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
3
+
4
+ import {
5
+ NOBLE_BLE_CONNECTION_TIMEOUT_MS,
6
+ NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
7
+ } from '../noble-ble-timeouts';
8
+
9
+ import type { WebContents } from 'electron';
10
+
11
+ type IpcHandler = (...args: unknown[]) => Promise<unknown> | unknown;
12
+
13
+ const createPeripheral = (id: string, localName?: string) => ({
14
+ id,
15
+ state: 'disconnected',
16
+ advertisement: {
17
+ localName,
18
+ serviceUuids: ['0001', 'fffd'],
19
+ },
20
+ });
21
+
22
+ describe('Electron Noble BLE device discovery', () => {
23
+ afterEach(() => {
24
+ jest.useRealTimers();
25
+ jest.resetModules();
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ test('allows enough time for a slow targeted scan and connection', () => {
30
+ expect(NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS).toBe(5_000);
31
+ expect(NOBLE_BLE_CONNECTION_TIMEOUT_MS).toBe(10_000);
32
+ });
33
+
34
+ test('waits for Noble to stop scanning before enumeration resolves', async () => {
35
+ jest.useFakeTimers({ doNotFake: ['performance'] });
36
+
37
+ const handlers = new Map<string, IpcHandler>();
38
+ const ipcMain = {
39
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
40
+ handlers.set(channel, handler);
41
+ }),
42
+ };
43
+ const noble = new EventEmitter() as EventEmitter & {
44
+ state: string;
45
+ startScanning: jest.Mock;
46
+ stopScanning: jest.Mock;
47
+ };
48
+ let resolveScanStarted = () => undefined;
49
+ const scanStarted = new Promise<void>(resolve => {
50
+ resolveScanStarted = resolve;
51
+ });
52
+ let stopScanningCallback: (() => void) | undefined;
53
+ noble.state = 'poweredOn';
54
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
55
+ callback?.();
56
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
57
+ resolveScanStarted();
58
+ });
59
+ noble.stopScanning = jest.fn(callback => {
60
+ stopScanningCallback = callback;
61
+ });
62
+
63
+ jest.doMock('@stoprocent/noble', () => noble);
64
+ jest.doMock('electron', () => ({ ipcMain }));
65
+ jest.doMock('electron-log', () => ({
66
+ info: jest.fn(),
67
+ debug: jest.fn(),
68
+ error: jest.fn(),
69
+ }));
70
+
71
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
72
+ setupNobleBleHandlers({
73
+ on: jest.fn(),
74
+ send: jest.fn(),
75
+ } as unknown as WebContents);
76
+
77
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
78
+ if (!enumerate) {
79
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
80
+ }
81
+
82
+ let enumerationResolved = false;
83
+ const devicesPromise = Promise.resolve(enumerate()).then(devices => {
84
+ enumerationResolved = true;
85
+ return devices;
86
+ });
87
+ await scanStarted;
88
+ jest.advanceTimersByTime(5_000);
89
+ await Promise.resolve();
90
+
91
+ expect(noble.stopScanning).toHaveBeenCalledTimes(1);
92
+ expect(enumerationResolved).toBe(false);
93
+
94
+ stopScanningCallback?.();
95
+ await expect(devicesPromise).resolves.toEqual([
96
+ expect.objectContaining({ id: 'onekey-device' }),
97
+ ]);
98
+ });
99
+
100
+ test('waits for a targeted scan to stop before connecting', async () => {
101
+ jest.useFakeTimers({ doNotFake: ['performance'] });
102
+
103
+ const handlers = new Map<string, IpcHandler>();
104
+ const ipcMain = {
105
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
106
+ handlers.set(channel, handler);
107
+ }),
108
+ };
109
+ const noble = new EventEmitter() as EventEmitter & {
110
+ state: string;
111
+ startScanning: jest.Mock;
112
+ stopScanning: jest.Mock;
113
+ };
114
+ let resolveScanStarted = () => undefined;
115
+ const scanStarted = new Promise<void>(resolve => {
116
+ resolveScanStarted = resolve;
117
+ });
118
+ let stopScanningCallback: (() => void) | undefined;
119
+ const peripheral = Object.assign(
120
+ new EventEmitter(),
121
+ createPeripheral('target-device', 'Pro2 A1B2'),
122
+ {
123
+ connect: jest.fn((callback: (error?: Error) => void) => {
124
+ callback(new Error('expected test connection failure'));
125
+ }),
126
+ }
127
+ );
128
+ noble.state = 'poweredOn';
129
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
130
+ callback?.();
131
+ noble.emit('discover', peripheral);
132
+ resolveScanStarted();
133
+ });
134
+ noble.stopScanning = jest.fn(callback => {
135
+ stopScanningCallback = callback;
136
+ });
137
+
138
+ jest.doMock('@stoprocent/noble', () => noble);
139
+ jest.doMock('electron', () => ({ ipcMain }));
140
+ jest.doMock('electron-log', () => ({
141
+ info: jest.fn(),
142
+ debug: jest.fn(),
143
+ error: jest.fn(),
144
+ }));
145
+
146
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
147
+ setupNobleBleHandlers({
148
+ on: jest.fn(),
149
+ send: jest.fn(),
150
+ } as unknown as WebContents);
151
+
152
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
153
+ if (!connect) {
154
+ throw new Error('Electron Noble BLE connect handler was not registered');
155
+ }
156
+
157
+ const connectPromise = Promise.resolve(connect(undefined, 'target-device'));
158
+ await scanStarted;
159
+
160
+ expect(noble.stopScanning).toHaveBeenCalledTimes(1);
161
+ expect(peripheral.connect).not.toHaveBeenCalled();
162
+
163
+ stopScanningCallback?.();
164
+ await expect(connectPromise).rejects.toThrow('expected test connection failure');
165
+ expect(peripheral.connect).toHaveBeenCalledTimes(1);
166
+ });
167
+
168
+ test('disconnects a connection callback that arrives after timeout', async () => {
169
+ jest.useFakeTimers({ doNotFake: ['performance'] });
170
+
171
+ const handlers = new Map<string, IpcHandler>();
172
+ const ipcMain = {
173
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
174
+ handlers.set(channel, handler);
175
+ }),
176
+ };
177
+ const noble = new EventEmitter() as EventEmitter & {
178
+ state: string;
179
+ startScanning: jest.Mock;
180
+ stopScanning: jest.Mock;
181
+ };
182
+ let connectCallback: ((error?: Error) => void) | undefined;
183
+ let resolveConnectStarted = () => undefined;
184
+ const connectStarted = new Promise<void>(resolve => {
185
+ resolveConnectStarted = resolve;
186
+ });
187
+ const peripheral = Object.assign(
188
+ new EventEmitter(),
189
+ createPeripheral('slow-device', 'Pro2 A1B2'),
190
+ {
191
+ connect: jest.fn((callback: (error?: Error) => void) => {
192
+ connectCallback = callback;
193
+ resolveConnectStarted();
194
+ }),
195
+ disconnect: jest.fn((callback: () => void) => callback()),
196
+ }
197
+ );
198
+ noble.state = 'poweredOn';
199
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
200
+ callback?.();
201
+ noble.emit('discover', peripheral);
202
+ });
203
+ noble.stopScanning = jest.fn(callback => callback?.());
204
+
205
+ jest.doMock('@stoprocent/noble', () => noble);
206
+ jest.doMock('electron', () => ({ ipcMain }));
207
+ jest.doMock('electron-log', () => ({
208
+ info: jest.fn(),
209
+ debug: jest.fn(),
210
+ error: jest.fn(),
211
+ }));
212
+
213
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
214
+ setupNobleBleHandlers({
215
+ on: jest.fn(),
216
+ send: jest.fn(),
217
+ } as unknown as WebContents);
218
+
219
+ const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
220
+ if (!connect) {
221
+ throw new Error('Electron Noble BLE connect handler was not registered');
222
+ }
223
+
224
+ const connectPromise = Promise.resolve(connect(undefined, 'slow-device'));
225
+ await connectStarted;
226
+ expect(peripheral.connect).toHaveBeenCalledTimes(1);
227
+
228
+ jest.advanceTimersByTime(NOBLE_BLE_CONNECTION_TIMEOUT_MS);
229
+ await expect(connectPromise).rejects.toThrow('Connection timeout');
230
+
231
+ connectCallback?.();
232
+ await Promise.resolve();
233
+ expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
234
+ });
235
+
236
+ test('does not enumerate a Pro2 Find My advertisement with the communication service', async () => {
237
+ jest.useFakeTimers({ doNotFake: ['performance'] });
238
+
239
+ const handlers = new Map<string, IpcHandler>();
240
+ const ipcMain = {
241
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
242
+ handlers.set(channel, handler);
243
+ }),
244
+ };
245
+ const noble = new EventEmitter() as EventEmitter & {
246
+ state: string;
247
+ startScanning: jest.Mock;
248
+ stopScanning: jest.Mock;
249
+ };
250
+ let resolveScanStarted = () => undefined;
251
+ const scanStarted = new Promise<void>(resolve => {
252
+ resolveScanStarted = resolve;
253
+ });
254
+ noble.state = 'poweredOn';
255
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
256
+ callback?.();
257
+ noble.emit('discover', createPeripheral('find-my-device', 'Pro2 A1B2 - Find My'));
258
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
259
+ resolveScanStarted();
260
+ });
261
+ noble.stopScanning = jest.fn(callback => callback?.());
262
+
263
+ jest.doMock('@stoprocent/noble', () => noble);
264
+ jest.doMock('electron', () => ({ ipcMain }));
265
+ jest.doMock('electron-log', () => ({
266
+ info: jest.fn(),
267
+ debug: jest.fn(),
268
+ error: jest.fn(),
269
+ }));
270
+
271
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
272
+ setupNobleBleHandlers({
273
+ on: jest.fn(),
274
+ send: jest.fn(),
275
+ } as unknown as WebContents);
276
+
277
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
278
+ if (!enumerate) {
279
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
280
+ }
281
+
282
+ const devicesPromise = Promise.resolve(enumerate());
283
+ await scanStarted;
284
+ jest.advanceTimersByTime(5000);
285
+
286
+ await expect(devicesPromise).resolves.toEqual([
287
+ expect.objectContaining({
288
+ id: 'onekey-device',
289
+ name: 'Pro2 A1B2',
290
+ }),
291
+ ]);
292
+ });
293
+
294
+ test('does not enumerate a Find My peripheral first discovered without a name', async () => {
295
+ jest.useFakeTimers({ doNotFake: ['performance'] });
296
+
297
+ const handlers = new Map<string, IpcHandler>();
298
+ const ipcMain = {
299
+ handle: jest.fn((channel: string, handler: IpcHandler) => {
300
+ handlers.set(channel, handler);
301
+ }),
302
+ };
303
+ const noble = new EventEmitter() as EventEmitter & {
304
+ state: string;
305
+ startScanning: jest.Mock;
306
+ stopScanning: jest.Mock;
307
+ };
308
+ let resolveScanStarted = () => undefined;
309
+ const scanStarted = new Promise<void>(resolve => {
310
+ resolveScanStarted = resolve;
311
+ });
312
+ const findMyPeripheral = createPeripheral('find-my-device');
313
+ noble.state = 'poweredOn';
314
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
315
+ callback?.();
316
+ noble.emit('discover', findMyPeripheral);
317
+ findMyPeripheral.advertisement.localName = 'Pro2 A1B2 - Find My';
318
+ noble.emit('discover', findMyPeripheral);
319
+ noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
320
+ resolveScanStarted();
321
+ });
322
+ noble.stopScanning = jest.fn(callback => callback?.());
323
+
324
+ jest.doMock('@stoprocent/noble', () => noble);
325
+ jest.doMock('electron', () => ({ ipcMain }));
326
+ jest.doMock('electron-log', () => ({
327
+ info: jest.fn(),
328
+ debug: jest.fn(),
329
+ error: jest.fn(),
330
+ }));
331
+
332
+ const { setupNobleBleHandlers } = await import('../noble-ble-handler');
333
+ setupNobleBleHandlers({
334
+ on: jest.fn(),
335
+ send: jest.fn(),
336
+ } as unknown as WebContents);
337
+
338
+ const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
339
+ if (!enumerate) {
340
+ throw new Error('Electron Noble BLE enumerate handler was not registered');
341
+ }
342
+
343
+ const devicesPromise = Promise.resolve(enumerate());
344
+ await scanStarted;
345
+ jest.advanceTimersByTime(5000);
346
+
347
+ await expect(devicesPromise).resolves.toEqual([
348
+ expect.objectContaining({
349
+ id: 'onekey-device',
350
+ name: 'Pro2 A1B2',
351
+ }),
352
+ ]);
353
+ });
354
+ });
@@ -0,0 +1,13 @@
1
+ const BLE_ATT_HEADER_BYTES = 3;
2
+
3
+ export function resolveBlePacketCapacity(
4
+ mtu: number | null | undefined,
5
+ maximumPacketCapacity: number,
6
+ fallbackPacketCapacity: number
7
+ ): number {
8
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
9
+ return fallbackPacketCapacity;
10
+ }
11
+
12
+ return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
13
+ }
@@ -15,13 +15,19 @@ import {
15
15
  createKnownBleUuidAliases,
16
16
  hasOnekeyCommunicationService,
17
17
  isOnekeyBluetoothDevice,
18
+ isPro2FindMyAdvertisementName,
18
19
  matchesKnownBleUuid,
19
20
  wait,
20
21
  } from '@onekeyfe/hd-shared';
21
22
  import pRetry from 'p-retry';
22
23
 
24
+ import { resolveBlePacketCapacity } from './ble-packet-capacity';
23
25
  import { safeLog } from './types/noble-extended';
24
26
  import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
27
+ import {
28
+ NOBLE_BLE_CONNECTION_TIMEOUT_MS,
29
+ NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
30
+ } from './noble-ble-timeouts';
25
31
 
26
32
  import type { IpcMainInvokeEvent, WebContents } from 'electron';
27
33
  import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
@@ -60,6 +66,10 @@ const deviceDisconnectListeners = new Map<
60
66
  string,
61
67
  { peripheral: Peripheral; listener: () => void }
62
68
  >();
69
+ const deviceMtuListeners = new Map<
70
+ string,
71
+ { peripheral: Peripheral; listener: (mtu: number) => void }
72
+ >();
63
73
 
64
74
  // Windows-only response watchdog state moved to utils/windows-ble-recovery
65
75
 
@@ -75,16 +85,14 @@ const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARA
75
85
 
76
86
  // Timeout and interval constants
77
87
  const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
78
- const DEVICE_SCAN_TIMEOUT = 8000; // 8 seconds for device scanning (Pro2 has longer advertising interval)
79
- const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has longer advertising interval)
88
+ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
80
89
  const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
81
- const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow)
82
90
  const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
83
91
  const BLE_CLEANUP_TIMEOUT = 250;
84
92
 
85
93
  // Write-related constants
86
- const BLE_PACKET_SIZE = 192;
87
- const UNIFIED_WRITE_DELAY = 5;
94
+ const BLE_PACKET_SIZE_FALLBACK = 192;
95
+ const BLE_PACKET_SIZE_MAXIMUM = 244;
88
96
  const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 } as const;
89
97
  const IS_WINDOWS = process.platform === 'win32';
90
98
  const ABORTABLE_WRITE_ERROR_PATTERNS = [
@@ -93,11 +101,19 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
93
101
 
94
102
  function isOneKeyPeripheral(peripheral: Peripheral) {
95
103
  const serviceUuids = peripheral.advertisement?.serviceUuids;
104
+ const localName = peripheral.advertisement?.localName;
105
+
106
+ // Noble localName is the current advertisement name, so reject the Pro2
107
+ // Find My endpoint before the communication-service fast path accepts it.
108
+ if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
109
+ return false;
110
+ }
111
+
96
112
  return (
97
113
  hasOnekeyCommunicationService(serviceUuids) &&
98
114
  isOnekeyBluetoothDevice({
99
115
  id: peripheral.id,
100
- localName: peripheral.advertisement?.localName,
116
+ localName,
101
117
  serviceUuids,
102
118
  })
103
119
  );
@@ -353,6 +369,11 @@ function cleanupDevice(
353
369
  disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
354
370
  deviceDisconnectListeners.delete(deviceId);
355
371
  }
372
+ const mtuEntry = deviceMtuListeners.get(deviceId);
373
+ if (mtuEntry) {
374
+ mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
375
+ deviceMtuListeners.delete(deviceId);
376
+ }
356
377
  connectedDevices.delete(deviceId);
357
378
  deviceCharacteristics.delete(deviceId);
358
379
  notificationCallbacks.delete(deviceId);
@@ -412,11 +433,33 @@ function setupDisconnectListener(
412
433
  };
413
434
  deviceDisconnectListeners.set(deviceId, { peripheral, listener });
414
435
  peripheral.on('disconnect', listener);
436
+ setupMtuListener(peripheral, deviceId, webContents);
437
+ }
438
+
439
+ function setupMtuListener(
440
+ peripheral: Peripheral,
441
+ deviceId: string,
442
+ webContents: WebContents
443
+ ): void {
444
+ const existing = deviceMtuListeners.get(deviceId);
445
+ if (existing) {
446
+ existing.peripheral.removeListener('mtu', existing.listener);
447
+ }
448
+
449
+ const listener = (mtu: number) => {
450
+ if (!Number.isFinite(mtu) || mtu <= 0) return;
451
+ webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
452
+ id: deviceId,
453
+ mtu,
454
+ });
455
+ };
456
+ deviceMtuListeners.set(deviceId, { peripheral, listener });
457
+ peripheral.on('mtu', listener);
415
458
  }
416
459
 
417
460
  // ===== Write helpers (inline) =====
418
461
 
419
- async function writeCharacteristicWithAck(
462
+ async function writeCharacteristicWithoutResponse(
420
463
  deviceId: string,
421
464
  writeCharacteristic: Characteristic,
422
465
  buffer: Buffer
@@ -464,7 +507,7 @@ async function attemptWindowsWriteUntilPaired(
464
507
  }
465
508
 
466
509
  try {
467
- await writeCharacteristicWithAck(deviceId, latestWrite, payload);
510
+ await writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
468
511
  } catch (e) {
469
512
  const errorMessage = e instanceof Error ? e.message : String(e);
470
513
  logger?.error('[BLE-Write] Windows write error', {
@@ -548,6 +591,11 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
548
591
 
549
592
  const toBuffer = Buffer.from(hexData, 'hex');
550
593
  const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
594
+ const packetCapacity = resolveBlePacketCapacity(
595
+ peripheral.mtu,
596
+ BLE_PACKET_SIZE_MAXIMUM,
597
+ BLE_PACKET_SIZE_FALLBACK
598
+ );
551
599
 
552
600
  if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
553
601
  // macOS / Linux or already paired on Windows: direct write
@@ -558,14 +606,13 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
558
606
  `Write characteristic not available for ${deviceId}`
559
607
  );
560
608
  }
561
- if (toBuffer.length <= BLE_PACKET_SIZE) {
562
- await wait(UNIFIED_WRITE_DELAY);
563
- await writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
609
+ if (toBuffer.length <= packetCapacity) {
610
+ await writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
564
611
  return;
565
612
  }
566
613
  // chunked
567
614
  for (let offset = 0; offset < toBuffer.length; ) {
568
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
615
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
569
616
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
570
617
  offset += chunkSize;
571
618
  const latest = doGetWriteCharacteristic();
@@ -575,23 +622,19 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
575
622
  `Write characteristic not available for ${deviceId}`
576
623
  );
577
624
  }
578
- await writeCharacteristicWithAck(deviceId, latest, chunk);
579
- if (offset < toBuffer.length) {
580
- await wait(UNIFIED_WRITE_DELAY);
581
- }
625
+ await writeCharacteristicWithoutResponse(deviceId, latest, chunk);
582
626
  }
583
627
  return;
584
628
  }
585
629
 
586
630
  // Windows unpaired path: use loop
587
- if (toBuffer.length <= BLE_PACKET_SIZE) {
588
- await wait(UNIFIED_WRITE_DELAY);
631
+ if (toBuffer.length <= packetCapacity) {
589
632
  await attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
590
633
  return;
591
634
  }
592
635
  // chunked loop
593
636
  for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
594
- const chunkSize = Math.min(BLE_PACKET_SIZE, toBuffer.length - offset);
637
+ const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
595
638
  const chunk = toBuffer.subarray(offset, offset + chunkSize);
596
639
  offset += chunkSize;
597
640
  await attemptWindowsWriteUntilPaired(
@@ -600,9 +643,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
600
643
  chunk,
601
644
  `chunk-${idx + 1}`
602
645
  );
603
- if (offset < toBuffer.length) {
604
- await wait(UNIFIED_WRITE_DELAY);
605
- }
606
646
  }
607
647
  }
608
648
 
@@ -641,6 +681,13 @@ function ensureDiscoverListener(): void {
641
681
  }
642
682
  }
643
683
 
684
+ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
685
+ await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
686
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
687
+ timeoutBehavior: 'resolve',
688
+ });
689
+ }
690
+
644
691
  // Perform targeted scan for a specific device ID
645
692
  // Uses self-contained local listener pattern - no global state needed
646
693
  async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
@@ -654,6 +701,26 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
654
701
  logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
655
702
 
656
703
  return new Promise((resolve, reject) => {
704
+ let settled = false;
705
+
706
+ const finish = async (peripheral: Peripheral | null, error?: Error) => {
707
+ if (settled) return;
708
+ settled = true;
709
+ if (timeoutId) clearTimeout(timeoutId);
710
+ nobleInstance.removeListener('discover', onDiscover);
711
+ await waitForNobleScanStop(nobleInstance);
712
+
713
+ if (error) {
714
+ logger?.error('[NobleBLE] Failed to start targeted scan:', error);
715
+ reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
716
+ return;
717
+ }
718
+ if (peripheral) {
719
+ discoveredDevices.set(peripheral.id, peripheral);
720
+ }
721
+ resolve(peripheral);
722
+ };
723
+
657
724
  // Local discover listener - only matches target device
658
725
  const onDiscover = (peripheral: Peripheral) => {
659
726
  if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
@@ -661,21 +728,14 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
661
728
  id: peripheral.id,
662
729
  name: peripheral.advertisement?.localName,
663
730
  });
664
- clearTimeout(timeoutId);
665
- nobleInstance.removeListener('discover', onDiscover);
666
- nobleInstance.stopScanning();
667
- discoveredDevices.set(peripheral.id, peripheral);
668
- resolve(peripheral);
731
+ finish(peripheral).catch(reject);
669
732
  }
670
733
  };
671
734
 
672
- // Timeout handler - must be after onDiscover so it can reference it
673
735
  const timeoutId = setTimeout(() => {
674
- nobleInstance.removeListener('discover', onDiscover);
675
- nobleInstance.stopScanning();
676
736
  logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
677
- resolve(null);
678
- }, FAST_SCAN_TIMEOUT);
737
+ finish(null).catch(reject);
738
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
679
739
 
680
740
  // Add local listener for this scan
681
741
  nobleInstance.on('discover', onDiscover);
@@ -683,10 +743,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
683
743
  // Start scanning — no service UUID filter (Pro2 may use different service UUID)
684
744
  nobleInstance.startScanning([], false, (error?: Error) => {
685
745
  if (error) {
686
- clearTimeout(timeoutId);
687
- nobleInstance.removeListener('discover', onDiscover);
688
- logger?.error('[NobleBLE] Failed to start targeted scan:', error);
689
- reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
746
+ finish(null, error).catch(reject);
690
747
  return;
691
748
  }
692
749
  logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
@@ -720,11 +777,13 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
720
777
  const devices: DeviceInfo[] = [];
721
778
  let intervalId: ReturnType<typeof setInterval> | undefined;
722
779
 
723
- // Cleanup function: clears both timeout and interval
724
- const cleanup = () => {
780
+ // Cleanup function: clears timers and waits until Noble confirms scanning
781
+ // has stopped. Resolving enumerate before this callback creates a race with
782
+ // an immediately-following connection attempt.
783
+ const cleanup = async () => {
725
784
  clearTimeout(timeoutId);
726
785
  if (intervalId) clearInterval(intervalId);
727
- nobleInstance.stopScanning();
786
+ await waitForNobleScanStop(nobleInstance);
728
787
  };
729
788
 
730
789
  // Collect discovered devices into the devices array
@@ -744,10 +803,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
744
803
  };
745
804
 
746
805
  // Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
747
- const timeoutId = setTimeout(() => {
806
+ const timeoutId = setTimeout(async () => {
748
807
  // Final collection before resolving — catches devices discovered near the deadline
749
808
  checkDevices();
750
- cleanup();
809
+ await cleanup();
751
810
  logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
752
811
  resolve(devices);
753
812
  }, DEVICE_SCAN_TIMEOUT);
@@ -755,9 +814,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
755
814
  // Start scanning without a service UUID filter so Pro2 advertisements with
756
815
  // short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
757
816
  logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
758
- nobleInstance.startScanning([], false, (error?: Error) => {
817
+ nobleInstance.startScanning([], false, async (error?: Error) => {
759
818
  if (error) {
760
- cleanup();
819
+ await cleanup();
761
820
  logger?.error('[NobleBLE] Failed to start scanning:', error);
762
821
  reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
763
822
  return;
@@ -775,10 +834,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
775
834
  async function stopScanning(): Promise<void> {
776
835
  if (!noble) return;
777
836
  const nobleInstance = noble;
778
- await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
779
- timeoutMs: BLE_CLEANUP_TIMEOUT,
780
- timeoutBehavior: 'resolve',
781
- });
837
+ await waitForNobleScanStop(nobleInstance);
782
838
  logger?.info('[NobleBLE] Scanning stopped');
783
839
  }
784
840
 
@@ -808,6 +864,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
808
864
  id: peripheral.id,
809
865
  name: deviceName,
810
866
  state: peripheral.state || 'disconnected',
867
+ ...(typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}),
811
868
  };
812
869
  }
813
870
 
@@ -820,6 +877,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
820
877
  id: connectedPeripheral.id,
821
878
  name: deviceName,
822
879
  state: connectedPeripheral.state || 'connected',
880
+ ...(typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}),
823
881
  };
824
882
  }
825
883
 
@@ -996,7 +1054,7 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
996
1054
 
997
1055
  // Step 3: Re-establish connection
998
1056
  await runBleCallbackOperation(callback => peripheral.connect(callback), {
999
- timeoutMs: CONNECTION_TIMEOUT,
1057
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1000
1058
  timeoutBehavior: 'reject',
1001
1059
  });
1002
1060
  logger?.info('[NobleBLE] Force reconnect successful');
@@ -1268,15 +1326,30 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1268
1326
  }
1269
1327
 
1270
1328
  return new Promise((resolve, reject) => {
1329
+ let connectionTimedOut = false;
1271
1330
  const timeout = setTimeout(() => {
1331
+ connectionTimedOut = true;
1272
1332
  reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
1273
- }, CONNECTION_TIMEOUT);
1333
+ }, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
1274
1334
 
1275
1335
  // TypeScript type assertion - peripheral is guaranteed to be defined at this point
1276
1336
  const connectedPeripheral = peripheral as Peripheral;
1277
1337
  connectedPeripheral.connect(async (error: Error | undefined) => {
1278
1338
  clearTimeout(timeout);
1279
1339
 
1340
+ // Noble may invoke the callback after the SDK timed out and released the request.
1341
+ // Ignore it to avoid initializing disposed commands or leaving an orphaned connection.
1342
+ if (connectionTimedOut) {
1343
+ if (!error) {
1344
+ try {
1345
+ connectedPeripheral.disconnect(() => undefined);
1346
+ } catch {
1347
+ // Best-effort cleanup only.
1348
+ }
1349
+ }
1350
+ return;
1351
+ }
1352
+
1280
1353
  if (error) {
1281
1354
  logger?.error('[NobleBLE] Connection failed:', error);
1282
1355
  reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
@@ -1450,7 +1523,7 @@ async function subscribeNotifications(
1450
1523
  timeoutBehavior: 'resolve',
1451
1524
  });
1452
1525
  await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
1453
- timeoutMs: CONNECTION_TIMEOUT,
1526
+ timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
1454
1527
  timeoutBehavior: 'reject',
1455
1528
  });
1456
1529
 
@@ -0,0 +1,2 @@
1
+ export const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5_000;
2
+ export const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10_000;
@@ -6,13 +6,14 @@
6
6
  // Noble BLE API interface - core BLE functionality
7
7
  export interface NobleBleAPI {
8
8
  enumerate: () => Promise<{ id: string; name: string }[]>;
9
- getDevice: (uuid: string) => Promise<{ id: string; name: string } | null>;
9
+ getDevice: (uuid: string) => Promise<{ id: string; name: string; mtu?: number } | null>;
10
10
  connect: (uuid: string) => Promise<void>;
11
11
  disconnect: (uuid: string) => Promise<void>;
12
12
  subscribe: (uuid: string) => Promise<void>;
13
13
  unsubscribe: (uuid: string) => Promise<void>;
14
14
  write: (uuid: string, data: string) => Promise<void>;
15
15
  onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
16
+ onMtuChanged?: (callback: (device: { id: string; mtu: number }) => void) => () => void;
16
17
  onDeviceDisconnected: (callback: (device: { id: string; name: string }) => void) => () => void;
17
18
  checkAvailability: () => Promise<{
18
19
  available: boolean;
@@ -11,6 +11,7 @@ export interface DeviceInfo extends OneKeyDeviceInfoBase {
11
11
  id: string;
12
12
  name: string;
13
13
  state: string;
14
+ mtu?: number;
14
15
  }
15
16
 
16
17
  // Characteristic pair interface