@onekeyfe/hd-transport-electron 1.2.0-alpha.162 → 1.2.0-alpha.164

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.
@@ -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-e8b87173.js'); });
35
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-255383f1.js'); });
36
36
  setupNobleBleHandlers(webContents);
37
37
  });
38
38
  }
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ interface NobleModule {
25
25
  interface Logger {
26
26
  info(message: string, ...args: any[]): void;
27
27
  debug(message: string, ...args: any[]): void;
28
+ warn(message: string, ...args: any[]): void;
28
29
  error(message: string, ...args: any[]): void;
29
30
  }
30
31
 
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-b37d926c.js');
5
+ var index = require('./index-5478256a.js');
6
6
 
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-b37d926c.js');
3
+ var index = require('./index-5478256a.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -116,7 +116,8 @@ const DEVICE_SCAN_TIMEOUT = 5000;
116
116
  const DEVICE_CHECK_INTERVAL = 500;
117
117
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
118
118
  const BLE_CLEANUP_TIMEOUT = 250;
119
- const BLE_IDLE_DISCONNECT_MS = 3 * 60000;
119
+ const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
120
+ const BLE_IDLE_DISCONNECT_MS = 20000;
120
121
  const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
121
122
  const BLE_PACKET_SIZE_FALLBACK = 192;
122
123
  const BLE_PACKET_SIZE_MAXIMUM = 244;
@@ -242,6 +243,7 @@ function initializeNoble() {
242
243
  try {
243
244
  noble = require('@stoprocent/noble');
244
245
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
246
+ setupPersistentStateListener();
245
247
  yield new Promise((resolve, reject) => {
246
248
  if (!noble) {
247
249
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
@@ -251,7 +253,6 @@ function initializeNoble() {
251
253
  resolve();
252
254
  return;
253
255
  }
254
- setupPersistentStateListener();
255
256
  const timeout = setTimeout(() => {
256
257
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
257
258
  }, BLUETOOTH_INIT_TIMEOUT);
@@ -336,7 +337,14 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
336
337
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
337
338
  const peripheral = connectedDevices.get(deviceId);
338
339
  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
+ const pending = unsubscribeNotifications(deviceId)
341
+ .catch(error => {
342
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
343
+ deviceId,
344
+ error: error instanceof Error ? error.message : String(error),
345
+ });
346
+ })
347
+ .then(() => disconnectDevice(deviceId))
340
348
  .then(() => {
341
349
  broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
342
350
  id: deviceId,
@@ -409,6 +417,7 @@ function handleDeviceDisconnect(deviceId, webContents) {
409
417
  });
410
418
  cleanupDevice(deviceId, webContents, {
411
419
  cleanupConnection: true,
420
+ cleanupDiscoveredCache: true,
412
421
  sendDisconnectEvent: true,
413
422
  cancelOperations: true,
414
423
  reason: 'auto-disconnect',
@@ -590,18 +599,36 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
590
599
  }
591
600
  });
592
601
  }
602
+ const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
603
+ const bleNamesById = new Map();
604
+ function rememberBleName(deviceId, name) {
605
+ const trimmed = name === null || name === void 0 ? void 0 : name.trim();
606
+ if (trimmed) {
607
+ bleNamesById.set(deviceId, trimmed);
608
+ }
609
+ }
610
+ function resolveBleName(deviceId) {
611
+ var _a, _b, _c, _d, _e, _f;
612
+ const live = ((_c = (_b = (_a = connectedDevices.get(deviceId)) === null || _a === void 0 ? void 0 : _a.advertisement) === null || _b === void 0 ? void 0 : _b.localName) === null || _c === void 0 ? void 0 : _c.trim()) ||
613
+ ((_f = (_e = (_d = discoveredDevices.get(deviceId)) === null || _d === void 0 ? void 0 : _d.advertisement) === null || _e === void 0 ? void 0 : _e.localName) === null || _f === void 0 ? void 0 : _f.trim());
614
+ return live || bleNamesById.get(deviceId);
615
+ }
616
+ function shouldConnectByIdFirst(deviceId) {
617
+ return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
618
+ }
593
619
  function handleDeviceDiscovered(peripheral) {
594
- var _a, _b;
620
+ var _a, _b, _c;
595
621
  if (!isOneKeyPeripheral(peripheral)) {
596
622
  return;
597
623
  }
598
624
  const isNewDevice = !discoveredDevices.has(peripheral.id);
599
625
  discoveredDevices.set(peripheral.id, peripheral);
626
+ rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
600
627
  if (isNewDevice) {
601
628
  logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
602
629
  deviceId: peripheral.id,
603
- name: ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device',
604
- serviceUUIDs: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.serviceUuids) || [],
630
+ name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
631
+ serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
605
632
  });
606
633
  }
607
634
  }
@@ -627,7 +654,7 @@ function waitForNobleScanStop(nobleInstance) {
627
654
  });
628
655
  });
629
656
  }
630
- function performTargetedScan(targetDeviceId) {
657
+ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
631
658
  return index.__awaiter(this, void 0, void 0, function* () {
632
659
  if (!noble) {
633
660
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
@@ -667,7 +694,7 @@ function performTargetedScan(targetDeviceId) {
667
694
  const timeoutId = setTimeout(() => {
668
695
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
669
696
  finish(null).catch(reject);
670
- }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
697
+ }, timeoutMs);
671
698
  nobleInstance.on('discover', onDiscover);
672
699
  nobleInstance.startScanning([], true, (error) => {
673
700
  if (error) {
@@ -966,23 +993,18 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
966
993
  }
967
994
  function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
968
995
  return index.__awaiter(this, void 0, void 0, function* () {
969
- setupDisconnectListener(peripheral, deviceId, webContents);
970
996
  try {
971
- return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
997
+ yield forceReconnectPeripheral(peripheral, deviceId);
972
998
  }
973
- catch (initialError) {
974
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
975
- deviceId,
976
- error: initialError,
977
- });
999
+ catch (resetError) {
1000
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
978
1001
  }
979
- yield forceReconnectPeripheral(peripheral, deviceId);
980
1002
  setupDisconnectListener(peripheral, deviceId, webContents);
981
1003
  try {
982
1004
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
983
1005
  }
984
- catch (reconnectError) {
985
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
1006
+ catch (discoveryError) {
1007
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
986
1008
  return freshScanAndDiscover(deviceId, webContents);
987
1009
  }
988
1010
  });
@@ -1042,7 +1064,7 @@ function tryDirectConnectById(deviceId) {
1042
1064
  });
1043
1065
  }
1044
1066
  function connectDevice(deviceId, webContents) {
1045
- var _a;
1067
+ var _a, _b, _c;
1046
1068
  return index.__awaiter(this, void 0, void 0, function* () {
1047
1069
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
1048
1070
  deviceId,
@@ -1054,34 +1076,47 @@ function connectDevice(deviceId, webContents) {
1054
1076
  });
1055
1077
  let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
1056
1078
  if (!peripheral) {
1057
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
1058
1079
  if (!noble) {
1059
1080
  yield initializeNoble();
1060
1081
  }
1061
1082
  if (!noble) {
1062
1083
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
1063
1084
  }
1064
- peripheral = yield tryDirectConnectById(deviceId);
1065
- if (peripheral) {
1066
- discoveredDevices.set(deviceId, peripheral);
1067
- }
1068
- }
1069
- if (!peripheral) {
1070
- try {
1071
- const foundPeripheral = yield performTargetedScan(deviceId);
1072
- if (!foundPeripheral) {
1073
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found even after targeted scan`);
1085
+ const byIdFirst = shouldConnectByIdFirst(deviceId);
1086
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
1087
+ deviceId,
1088
+ name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
1089
+ strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
1090
+ });
1091
+ const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
1092
+ var _d;
1093
+ try {
1094
+ return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
1074
1095
  }
1075
- peripheral = foundPeripheral;
1076
- }
1077
- catch (error) {
1078
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Targeted scan failed:', error);
1079
- throw error;
1096
+ catch (scanError) {
1097
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
1098
+ deviceId,
1099
+ error: scanError instanceof Error ? scanError.message : String(scanError),
1100
+ });
1101
+ return undefined;
1102
+ }
1103
+ });
1104
+ const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
1105
+ const found = yield tryDirectConnectById(deviceId);
1106
+ if (found) {
1107
+ discoveredDevices.set(deviceId, found);
1108
+ }
1109
+ return found;
1110
+ });
1111
+ peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
1112
+ if (!peripheral) {
1113
+ peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
1080
1114
  }
1081
1115
  }
1082
1116
  if (!peripheral) {
1083
1117
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1084
1118
  }
1119
+ rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
1085
1120
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
1086
1121
  if (peripheral.state === 'connected') {
1087
1122
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
@@ -1176,11 +1211,15 @@ function disconnectDevice(deviceId) {
1176
1211
  deviceDisconnectListeners.delete(deviceId);
1177
1212
  }
1178
1213
  yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1179
- timeoutMs: BLE_CLEANUP_TIMEOUT,
1214
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1180
1215
  timeoutBehavior: 'resolve',
1181
1216
  });
1217
+ if (peripheral.state === 'connected') {
1218
+ logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
1219
+ }
1182
1220
  cleanupDevice(deviceId, undefined, {
1183
1221
  cleanupConnection: true,
1222
+ cleanupDiscoveredCache: true,
1184
1223
  sendDisconnectEvent: false,
1185
1224
  cancelOperations: true,
1186
1225
  reason: 'manual-disconnect',
@@ -1391,10 +1430,6 @@ function setupNobleBleHandlers(webContents) {
1391
1430
  yield disconnectDevice(deviceId).catch(() => undefined);
1392
1431
  }
1393
1432
  yield stopScanning().catch(() => undefined);
1394
- if (noble && persistentStateListener) {
1395
- noble.removeListener('stateChange', persistentStateListener);
1396
- persistentStateListener = null;
1397
- }
1398
1433
  cleanupNobleListeners();
1399
1434
  discoveredDevices.clear();
1400
1435
  safeLog(logger, 'info', 'Noble BLE cleanup completed');
@@ -1 +1 @@
1
- {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AA0EhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA2mDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAgMpE"}
1
+ {"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAoFhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAksDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkMpE"}
@@ -22,6 +22,7 @@ export interface NobleModule {
22
22
  export interface Logger {
23
23
  info(message: string, ...args: any[]): void;
24
24
  debug(message: string, ...args: any[]): void;
25
+ warn(message: string, ...args: any[]): void;
25
26
  error(message: string, ...args: any[]): void;
26
27
  }
27
28
  export declare function safeLog(logger: Logger | null, level: 'info' | 'debug' | 'error', message: string, ...args: any[]): void;
@@ -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;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"}
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,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;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.162",
3
+ "version": "1.2.0-alpha.164",
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.162",
29
- "@onekeyfe/hd-shared": "1.2.0-alpha.162",
30
- "@onekeyfe/hd-transport": "1.2.0-alpha.162",
28
+ "@onekeyfe/hd-core": "1.2.0-alpha.164",
29
+ "@onekeyfe/hd-shared": "1.2.0-alpha.164",
30
+ "@onekeyfe/hd-transport": "1.2.0-alpha.164",
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": "f4aa654b350931d68ed9ba3f77c4cc8faa161218"
39
+ "gitHead": "8a6f829b0ea1544d2c5f7a1fd0cc0f08d6df7dcd"
40
40
  }
@@ -16,6 +16,7 @@ import {
16
16
  createKnownBleUuidAliases,
17
17
  hasOnekeyCommunicationService,
18
18
  isOnekeyBluetoothDevice,
19
+ isPro2FamilyBleName,
19
20
  matchesKnownBleUuid,
20
21
  wait,
21
22
  } from '@onekeyfe/hd-shared';
@@ -90,8 +91,18 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
90
91
  const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
91
92
  const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
92
93
  const BLE_CLEANUP_TIMEOUT = 250;
94
+ // A physical teardown must actually reach the OS before the device can return
95
+ // to a clean state; 250ms routinely declared success while CoreBluetooth was
96
+ // still disconnecting, leaving no trace when the teardown never completed.
97
+ const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
93
98
  // Renderer release is logical only; this timer physically frees the device.
94
- const BLE_IDLE_DISCONNECT_MS = 3 * 60_000;
99
+ // Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped
100
+ // after sitting idle for minutes (field logs 2026-08-19: every deaf window
101
+ // followed a 3-minute-idle disconnect, while disconnects right after traffic
102
+ // have never produced one across 6.5.0's per-call teardown history). A hot
103
+ // disconnect ~20s after the last operation stays inside the proven-safe
104
+ // pattern and also shrinks the window in which phones cannot see the device.
105
+ const BLE_IDLE_DISCONNECT_MS = 20_000;
95
106
  // Ceiling while a call is in flight: no outstanding write, but not forever.
96
107
  const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
97
108
 
@@ -257,6 +268,11 @@ async function initializeNoble(): Promise<void> {
257
268
  noble = require('@stoprocent/noble') as NobleModule;
258
269
  logger?.info('[NobleBLE] Noble library loaded');
259
270
 
271
+ // Register the process-lifetime state listener before any early return:
272
+ // the poweredOn fast path below would otherwise skip it for the whole
273
+ // session, leaving poweredOff cache/state reconciliation dead.
274
+ setupPersistentStateListener();
275
+
260
276
  // Wait for Bluetooth to be ready
261
277
  await new Promise<void>((resolve, reject) => {
262
278
  if (!noble) {
@@ -269,9 +285,6 @@ async function initializeNoble(): Promise<void> {
269
285
  return;
270
286
  }
271
287
 
272
- // Setup persistent state listener before initialization
273
- setupPersistentStateListener();
274
-
275
288
  const timeout = setTimeout(() => {
276
289
  reject(
277
290
  ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
@@ -398,7 +411,21 @@ function armIdleDisconnect(
398
411
  logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
399
412
  const peripheral = connectedDevices.get(deviceId);
400
413
  const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
401
- const pending = disconnectDevice(deviceId)
414
+ // Unsubscribe CCCD before dropping the link, matching every other
415
+ // teardown path: the 1S leaves its notify session half-open when the
416
+ // link drops without an unsubscribe and then ignores application
417
+ // protocol traffic on NEW links for tens of minutes (field log
418
+ // 2026-08-19: the lone unsubscribe-skipping idle teardown caused a
419
+ // 6-attempt reconnect loop; unsubscribe-first teardowns reconnected
420
+ // instantly).
421
+ const pending = unsubscribeNotifications(deviceId)
422
+ .catch(error => {
423
+ logger?.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
424
+ deviceId,
425
+ error: error instanceof Error ? error.message : String(error),
426
+ });
427
+ })
428
+ .then(() => disconnectDevice(deviceId))
402
429
  .then(() => {
403
430
  // A call is still in flight here; it must reject, not hang.
404
431
  // Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
@@ -508,6 +535,9 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
508
535
 
509
536
  cleanupDevice(deviceId, webContents, {
510
537
  cleanupConnection: true,
538
+ // Same stale-object hazard as manual disconnect: an externally dropped
539
+ // link invalidates the cached peripheral for the next reconnect.
540
+ cleanupDiscoveredCache: true,
511
541
  sendDisconnectEvent: true,
512
542
  cancelOperations: true,
513
543
  reason: 'auto-disconnect',
@@ -761,6 +791,41 @@ async function transmitHexDataToDevice(
761
791
  }
762
792
 
763
793
  // Handle discovered device (for general enumeration only)
794
+ // A scan that finds nothing costs this much before the direct-connect
795
+ // fallback runs. Every advertisement in the 6.5.0 control logs arrived within
796
+ // 631ms, so this leaves ~2x headroom while keeping a miss cheap.
797
+ const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
798
+
799
+ // Advertised names, kept for the life of the process: device caches are purged
800
+ // on every disconnect, but the family a device belongs to never changes and
801
+ // decides which connection strategy is safe for it.
802
+ const bleNamesById = new Map<string, string>();
803
+
804
+ function rememberBleName(deviceId: string, name?: string | null): void {
805
+ const trimmed = name?.trim();
806
+ if (trimmed) {
807
+ bleNamesById.set(deviceId, trimmed);
808
+ }
809
+ }
810
+
811
+ function resolveBleName(deviceId: string): string | undefined {
812
+ const live =
813
+ connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
814
+ discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
815
+ return live || bleNamesById.get(deviceId);
816
+ }
817
+
818
+ /**
819
+ * Pro2/Neo connect by id first: they are unaffected by the stale-session
820
+ * problem and advertise under Find My names a OneKey-filtered scan cannot
821
+ * match. Everything else — Classic family, and any device whose name is not
822
+ * known yet — scans first, because connecting by id can resolve a session the
823
+ * device no longer serves (see setupConnectionAndDiscoverServices).
824
+ */
825
+ function shouldConnectByIdFirst(deviceId: string): boolean {
826
+ return isPro2FamilyBleName(resolveBleName(deviceId));
827
+ }
828
+
764
829
  function handleDeviceDiscovered(peripheral: Peripheral): void {
765
830
  // Only process OneKey candidates for general discovery. Avoid logging every
766
831
  // ambient BLE peripheral; it makes Pro2 debugging hard to read.
@@ -770,6 +835,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
770
835
 
771
836
  const isNewDevice = !discoveredDevices.has(peripheral.id);
772
837
  discoveredDevices.set(peripheral.id, peripheral);
838
+ rememberBleName(peripheral.id, peripheral.advertisement?.localName);
773
839
  if (isNewDevice) {
774
840
  logger?.debug('[NobleBLE] OneKey BLE device discovered', {
775
841
  deviceId: peripheral.id,
@@ -804,7 +870,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
804
870
 
805
871
  // Perform targeted scan for a specific device ID
806
872
  // Uses self-contained local listener pattern - no global state needed
807
- async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
873
+ async function performTargetedScan(
874
+ targetDeviceId: string,
875
+ timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
876
+ ): Promise<Peripheral | null> {
808
877
  if (!noble) {
809
878
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
810
879
  }
@@ -849,7 +918,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
849
918
  const timeoutId = setTimeout(() => {
850
919
  logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
851
920
  finish(null).catch(reject);
852
- }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
921
+ }, timeoutMs);
853
922
 
854
923
  // Add local listener for this scan
855
924
  nobleInstance.on('discover', onDiscover);
@@ -1305,26 +1374,28 @@ async function setupConnectionAndDiscoverServices(
1305
1374
  deviceId: string,
1306
1375
  webContents: WebContents
1307
1376
  ): Promise<CharacteristicPair> {
1308
- setupDisconnectListener(peripheral, deviceId, webContents);
1309
-
1377
+ // Reset the link before any protocol traffic reaches the device. 6.5.0 did
1378
+ // this on every cold setup and never produced a protocol-deaf Classic 1S
1379
+ // (control log 2026-08-20: 16/16 links answered within ~1s, including after
1380
+ // a 15-minute idle). Demoting it to a discovery-failure fallback removed the
1381
+ // reset entirely for this device, because discovery always succeeds: the link
1382
+ // comes up, GATT resolves from cache, and the device then answers nothing on
1383
+ // a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
1384
+ // link with an intact subscription returns before reaching here, so reuse
1385
+ // inside a workflow is unaffected.
1310
1386
  try {
1311
- return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
1312
- } catch (initialError) {
1313
- logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', {
1314
- deviceId,
1315
- error: initialError,
1316
- });
1387
+ await forceReconnectPeripheral(peripheral, deviceId);
1388
+ } catch (resetError) {
1389
+ // A failed reset must not abort the attempt: discovery on the existing
1390
+ // link, then the fresh-scan fallback, still have a chance to recover.
1391
+ logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
1317
1392
  }
1318
-
1319
- await forceReconnectPeripheral(peripheral, deviceId);
1320
1393
  setupDisconnectListener(peripheral, deviceId, webContents);
1394
+
1321
1395
  try {
1322
1396
  return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
1323
- } catch (reconnectError) {
1324
- logger?.error(
1325
- '[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
1326
- reconnectError
1327
- );
1397
+ } catch (discoveryError) {
1398
+ logger?.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
1328
1399
  return freshScanAndDiscover(deviceId, webContents);
1329
1400
  }
1330
1401
  }
@@ -1413,10 +1484,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1413
1484
  // enumerate clears the discovery map; a kept-alive link outlives it.
1414
1485
  let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
1415
1486
 
1416
- // If device not discovered, try a targeted scan for this specific device
1417
1487
  if (!peripheral) {
1418
- logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
1419
-
1420
1488
  // Initialize Noble if not already done
1421
1489
  if (!noble) {
1422
1490
  await initializeNoble();
@@ -1426,27 +1494,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1426
1494
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
1427
1495
  }
1428
1496
 
1429
- // Reaches a bonded device that is not advertising; else falls to the scan.
1430
- peripheral = await tryDirectConnectById(deviceId);
1431
- if (peripheral) {
1432
- discoveredDevices.set(deviceId, peripheral);
1433
- }
1434
- }
1497
+ // A live advertisement proves the device holds no link and will open a
1498
+ // fresh session; connecting by id resolves through the OS cache instead
1499
+ // and can hand back a session the device no longer serves (see
1500
+ // setupConnectionAndDiscoverServices). Scanning is also the faster of the
1501
+ // two when the device does advertise: ~96ms median against ~784ms for
1502
+ // connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
1503
+ // because they are unaffected and may advertise under an unmatchable Find
1504
+ // My name.
1505
+ const byIdFirst = shouldConnectByIdFirst(deviceId);
1506
+ logger?.info('[NobleBLE] Resolving device for cold connect', {
1507
+ deviceId,
1508
+ name: resolveBleName(deviceId) ?? 'unknown',
1509
+ strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
1510
+ });
1435
1511
 
1436
- if (!peripheral) {
1437
- // Perform a targeted scan to find the specific device
1438
- try {
1439
- const foundPeripheral = await performTargetedScan(deviceId);
1440
- if (!foundPeripheral) {
1441
- throw ERRORS.TypedError(
1442
- HardwareErrorCode.DeviceNotFound,
1443
- `Device ${deviceId} not found even after targeted scan`
1444
- );
1512
+ const scanForPeripheral = async () => {
1513
+ try {
1514
+ return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
1515
+ } catch (scanError) {
1516
+ logger?.info('[NobleBLE] Targeted scan failed', {
1517
+ deviceId,
1518
+ error: scanError instanceof Error ? scanError.message : String(scanError),
1519
+ });
1520
+ return undefined;
1521
+ }
1522
+ };
1523
+
1524
+ const connectById = async () => {
1525
+ const found = await tryDirectConnectById(deviceId);
1526
+ if (found) {
1527
+ discoveredDevices.set(deviceId, found);
1445
1528
  }
1446
- peripheral = foundPeripheral;
1447
- } catch (error) {
1448
- logger?.error('[NobleBLE] Targeted scan failed:', error);
1449
- throw error;
1529
+ return found;
1530
+ };
1531
+
1532
+ peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
1533
+ if (!peripheral) {
1534
+ // The preferred route came up empty: not advertising (held elsewhere, or
1535
+ // silent), or not reachable by id. Try the other one before giving up.
1536
+ peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
1450
1537
  }
1451
1538
  }
1452
1539
 
@@ -1455,6 +1542,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1455
1542
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1456
1543
  }
1457
1544
 
1545
+ // Also covers the connect-by-id route, whose simulated discovery carries no
1546
+ // service UUIDs and therefore never reaches handleDeviceDiscovered.
1547
+ rememberBleName(deviceId, peripheral.advertisement?.localName);
1548
+
1458
1549
  logger?.info('[NobleBLE] Connecting to device:', deviceId);
1459
1550
 
1460
1551
  // Check if device is already connected
@@ -1597,11 +1688,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
1597
1688
  }
1598
1689
 
1599
1690
  await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1600
- timeoutMs: BLE_CLEANUP_TIMEOUT,
1691
+ timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1601
1692
  timeoutBehavior: 'resolve',
1602
1693
  });
1694
+ if (peripheral.state === 'connected') {
1695
+ logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
1696
+ }
1603
1697
  cleanupDevice(deviceId, undefined, {
1604
1698
  cleanupConnection: true,
1699
+ // macOS returns zero GATT services when a previously-disconnected
1700
+ // peripheral object is reconnected; drop the discovery cache so the next
1701
+ // connect resolves a fresh peripheral (direct connect by id, or scan).
1702
+ cleanupDiscoveredCache: true,
1605
1703
  sendDisconnectEvent: false,
1606
1704
  cancelOperations: true,
1607
1705
  reason: 'manual-disconnect',
@@ -1927,10 +2025,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
1927
2025
  }
1928
2026
 
1929
2027
  await stopScanning().catch(() => undefined);
1930
- if (noble && persistentStateListener) {
1931
- noble.removeListener('stateChange', persistentStateListener);
1932
- persistentStateListener = null;
1933
- }
2028
+ // persistentStateListener is process-lifetime, NOT per-window: this
2029
+ // destroy handler also fires on a renderer soft restart, and removing
2030
+ // the listener here permanently killed poweredOff/poweredOn
2031
+ // reconciliation for the rest of the process (nothing re-registers it:
2032
+ // initializeNoble early-returns once noble is loaded). The null-guard
2033
+ // in setupPersistentStateListener keeps it deduped to one instance.
1934
2034
  cleanupNobleListeners();
1935
2035
  discoveredDevices.clear();
1936
2036
  safeLog(logger, 'info', 'Noble BLE cleanup completed');
@@ -39,6 +39,7 @@ export interface NobleModule {
39
39
  export interface Logger {
40
40
  info(message: string, ...args: any[]): void;
41
41
  debug(message: string, ...args: any[]): void;
42
+ warn(message: string, ...args: any[]): void;
42
43
  error(message: string, ...args: any[]): void;
43
44
  }
44
45