@onekeyfe/hd-transport-electron 1.2.2-alpha.0 → 1.2.2-alpha.100

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-56d2eab3.js'); });
35
+ const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-e8b87173.js'); });
36
36
  setupNobleBleHandlers(webContents);
37
37
  });
38
38
  }
package/dist/index.d.ts CHANGED
@@ -25,7 +25,6 @@ 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;
29
28
  error(message: string, ...args: any[]): void;
30
29
  }
31
30
 
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-a518d449.js');
5
+ var index = require('./index-b37d926c.js');
6
6
 
7
7
 
8
8
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var index = require('./index-a518d449.js');
3
+ var index = require('./index-b37d926c.js');
4
4
  var hdShared = require('@onekeyfe/hd-shared');
5
5
  var pRetry = require('p-retry');
6
6
 
@@ -116,8 +116,7 @@ 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_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
120
- const BLE_IDLE_DISCONNECT_MS = 20000;
119
+ const BLE_IDLE_DISCONNECT_MS = 3 * 60000;
121
120
  const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
122
121
  const BLE_PACKET_SIZE_FALLBACK = 192;
123
122
  const BLE_PACKET_SIZE_MAXIMUM = 244;
@@ -243,7 +242,6 @@ function initializeNoble() {
243
242
  try {
244
243
  noble = require('@stoprocent/noble');
245
244
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
246
- setupPersistentStateListener();
247
245
  yield new Promise((resolve, reject) => {
248
246
  if (!noble) {
249
247
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
@@ -253,6 +251,7 @@ function initializeNoble() {
253
251
  resolve();
254
252
  return;
255
253
  }
254
+ setupPersistentStateListener();
256
255
  const timeout = setTimeout(() => {
257
256
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
258
257
  }, BLUETOOTH_INIT_TIMEOUT);
@@ -337,14 +336,7 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
337
336
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
338
337
  const peripheral = connectedDevices.get(deviceId);
339
338
  const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
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))
339
+ const pending = disconnectDevice(deviceId)
348
340
  .then(() => {
349
341
  broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
350
342
  id: deviceId,
@@ -417,7 +409,6 @@ function handleDeviceDisconnect(deviceId, webContents) {
417
409
  });
418
410
  cleanupDevice(deviceId, webContents, {
419
411
  cleanupConnection: true,
420
- cleanupDiscoveredCache: true,
421
412
  sendDisconnectEvent: true,
422
413
  cancelOperations: true,
423
414
  reason: 'auto-disconnect',
@@ -599,37 +590,18 @@ function transmitHexDataToDevice(deviceId, hexData, options) {
599
590
  }
600
591
  });
601
592
  }
602
- const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500;
603
- const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
604
- const bleNamesById = new Map();
605
- function rememberBleName(deviceId, name) {
606
- const trimmed = name === null || name === void 0 ? void 0 : name.trim();
607
- if (trimmed) {
608
- bleNamesById.set(deviceId, trimmed);
609
- }
610
- }
611
- function resolveBleName(deviceId) {
612
- var _a, _b, _c, _d, _e, _f;
613
- 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()) ||
614
- ((_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());
615
- return live || bleNamesById.get(deviceId);
616
- }
617
- function shouldConnectByIdFirst(deviceId) {
618
- return hdShared.isPro2FamilyBleName(resolveBleName(deviceId));
619
- }
620
593
  function handleDeviceDiscovered(peripheral) {
621
- var _a, _b, _c;
594
+ var _a, _b;
622
595
  if (!isOneKeyPeripheral(peripheral)) {
623
596
  return;
624
597
  }
625
598
  const isNewDevice = !discoveredDevices.has(peripheral.id);
626
599
  discoveredDevices.set(peripheral.id, peripheral);
627
- rememberBleName(peripheral.id, (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName);
628
600
  if (isNewDevice) {
629
601
  logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] OneKey BLE device discovered', {
630
602
  deviceId: peripheral.id,
631
- name: ((_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device',
632
- serviceUUIDs: ((_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.serviceUuids) || [],
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) || [],
633
605
  });
634
606
  }
635
607
  }
@@ -655,7 +627,7 @@ function waitForNobleScanStop(nobleInstance) {
655
627
  });
656
628
  });
657
629
  }
658
- function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS) {
630
+ function performTargetedScan(targetDeviceId) {
659
631
  return index.__awaiter(this, void 0, void 0, function* () {
660
632
  if (!noble) {
661
633
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
@@ -695,7 +667,7 @@ function performTargetedScan(targetDeviceId, timeoutMs = NOBLE_BLE_TARGETED_SCAN
695
667
  const timeoutId = setTimeout(() => {
696
668
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
697
669
  finish(null).catch(reject);
698
- }, timeoutMs);
670
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
699
671
  nobleInstance.on('discover', onDiscover);
700
672
  nobleInstance.startScanning([], true, (error) => {
701
673
  if (error) {
@@ -830,7 +802,7 @@ function discoverServicesAndCharacteristics(peripheral) {
830
802
  });
831
803
  const discoveryPromise = (() => index.__awaiter(this, void 0, void 0, function* () {
832
804
  const services = yield new Promise((resolve, reject) => {
833
- peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
805
+ peripheral.discoverServices([], (error, svc) => {
834
806
  if (error) {
835
807
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed:', error);
836
808
  reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, error.message));
@@ -841,7 +813,7 @@ function discoverServicesAndCharacteristics(peripheral) {
841
813
  });
842
814
  });
843
815
  if (!services || services.length === 0) {
844
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
816
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleServiceNotFound, 'No services found');
845
817
  }
846
818
  logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] services discovered', {
847
819
  deviceId: peripheral.id,
@@ -958,11 +930,11 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
958
930
  deviceId,
959
931
  peripheralState: peripheral.state,
960
932
  attempt: attemptNumber,
961
- maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
933
+ maxRetries: 5,
962
934
  targetUUIDs: ONEKEY_SERVICE_UUIDS,
963
935
  });
964
936
  if (attemptNumber > 1) {
965
- logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`);
937
+ logger === null || logger === void 0 ? void 0 : logger.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/5`);
966
938
  }
967
939
  if (peripheral.state !== 'connected') {
968
940
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Device not connected: ${peripheral.state}`);
@@ -971,16 +943,16 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
971
943
  return yield discoverServicesAndCharacteristics(peripheral);
972
944
  }
973
945
  catch (error) {
974
- logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
975
- if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
976
- logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`);
946
+ logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] No services found (attempt ${attemptNumber}/5)`);
947
+ if (attemptNumber < 5) {
948
+ logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/5)`);
977
949
  }
978
950
  throw error;
979
951
  }
980
952
  }), {
981
- retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
953
+ retries: 4,
982
954
  factor: 1.5,
983
- minTimeout: 500,
955
+ minTimeout: 1000,
984
956
  maxTimeout: 3000,
985
957
  onFailedAttempt: error => {
986
958
  logger === null || logger === void 0 ? void 0 : logger.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
@@ -994,24 +966,23 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
994
966
  }
995
967
  function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
996
968
  return index.__awaiter(this, void 0, void 0, function* () {
969
+ setupDisconnectListener(peripheral, deviceId, webContents);
997
970
  try {
998
- yield forceReconnectPeripheral(peripheral, deviceId);
971
+ return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
999
972
  }
1000
- catch (resetError) {
1001
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
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
+ });
1002
978
  }
979
+ yield forceReconnectPeripheral(peripheral, deviceId);
1003
980
  setupDisconnectListener(peripheral, deviceId, webContents);
1004
981
  try {
1005
982
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
1006
983
  }
1007
- catch (discoveryError) {
1008
- if (peripheral.state === 'connected') {
1009
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan', discoveryError);
1010
- yield disconnectDevice(deviceId).catch(() => undefined);
1011
- }
1012
- else {
1013
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
1014
- }
984
+ catch (reconnectError) {
985
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
1015
986
  return freshScanAndDiscover(deviceId, webContents);
1016
987
  }
1017
988
  });
@@ -1071,7 +1042,7 @@ function tryDirectConnectById(deviceId) {
1071
1042
  });
1072
1043
  }
1073
1044
  function connectDevice(deviceId, webContents) {
1074
- var _a, _b, _c;
1045
+ var _a;
1075
1046
  return index.__awaiter(this, void 0, void 0, function* () {
1076
1047
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
1077
1048
  deviceId,
@@ -1083,47 +1054,34 @@ function connectDevice(deviceId, webContents) {
1083
1054
  });
1084
1055
  let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
1085
1056
  if (!peripheral) {
1057
+ logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
1086
1058
  if (!noble) {
1087
1059
  yield initializeNoble();
1088
1060
  }
1089
1061
  if (!noble) {
1090
1062
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
1091
1063
  }
1092
- const byIdFirst = shouldConnectByIdFirst(deviceId);
1093
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Resolving device for cold connect', {
1094
- deviceId,
1095
- name: (_b = resolveBleName(deviceId)) !== null && _b !== void 0 ? _b : 'unknown',
1096
- strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
1097
- });
1098
- const scanForPeripheral = () => index.__awaiter(this, void 0, void 0, function* () {
1099
- var _d;
1100
- try {
1101
- return (_d = (yield performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS))) !== null && _d !== void 0 ? _d : undefined;
1102
- }
1103
- catch (scanError) {
1104
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed', {
1105
- deviceId,
1106
- error: scanError instanceof Error ? scanError.message : String(scanError),
1107
- });
1108
- return undefined;
1109
- }
1110
- });
1111
- const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
1112
- const found = yield tryDirectConnectById(deviceId);
1113
- if (found) {
1114
- discoveredDevices.set(deviceId, found);
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`);
1115
1074
  }
1116
- return found;
1117
- });
1118
- peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
1119
- if (!peripheral) {
1120
- peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
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;
1121
1080
  }
1122
1081
  }
1123
1082
  if (!peripheral) {
1124
1083
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1125
1084
  }
1126
- rememberBleName(deviceId, (_c = peripheral.advertisement) === null || _c === void 0 ? void 0 : _c.localName);
1127
1085
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connecting to device:', deviceId);
1128
1086
  if (peripheral.state === 'connected') {
1129
1087
  logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device already connected, skipping connection step');
@@ -1198,9 +1156,9 @@ function connectDevice(deviceId, webContents) {
1198
1156
  }
1199
1157
  catch (setupError) {
1200
1158
  logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection setup failed:', setupError);
1201
- disconnectDevice(deviceId)
1202
- .catch(() => undefined)
1203
- .then(() => reject(setupError));
1159
+ connectedPeripheral.disconnect(() => {
1160
+ reject(setupError);
1161
+ });
1204
1162
  }
1205
1163
  }));
1206
1164
  });
@@ -1218,15 +1176,11 @@ function disconnectDevice(deviceId) {
1218
1176
  deviceDisconnectListeners.delete(deviceId);
1219
1177
  }
1220
1178
  yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1221
- timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1179
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1222
1180
  timeoutBehavior: 'resolve',
1223
1181
  });
1224
- if (peripheral.state === 'connected') {
1225
- logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
1226
- }
1227
1182
  cleanupDevice(deviceId, undefined, {
1228
1183
  cleanupConnection: true,
1229
- cleanupDiscoveredCache: true,
1230
1184
  sendDisconnectEvent: false,
1231
1185
  cancelOperations: true,
1232
1186
  reason: 'manual-disconnect',
@@ -1317,14 +1271,9 @@ function subscribeNotifications(deviceId, callback) {
1317
1271
  });
1318
1272
  });
1319
1273
  }
1320
- const subscribeStartedAt = Date.now();
1321
1274
  try {
1322
1275
  yield rebuildAppSubscription(deviceId, notifyCharacteristic);
1323
1276
  subscribedDevices.set(deviceId, true);
1324
- logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Notification subscription active', {
1325
- deviceId,
1326
- ms: Date.now() - subscribeStartedAt,
1327
- });
1328
1277
  }
1329
1278
  finally {
1330
1279
  subscriptionOperations.set(deviceId, 'idle');
@@ -1442,6 +1391,10 @@ function setupNobleBleHandlers(webContents) {
1442
1391
  yield disconnectDevice(deviceId).catch(() => undefined);
1443
1392
  }
1444
1393
  yield stopScanning().catch(() => undefined);
1394
+ if (noble && persistentStateListener) {
1395
+ noble.removeListener('stateChange', persistentStateListener);
1396
+ persistentStateListener = null;
1397
+ }
1445
1398
  cleanupNobleListeners();
1446
1399
  discoveredDevices.clear();
1447
1400
  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":"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;AAsvDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkMpE"}
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"}
@@ -22,7 +22,6 @@ 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;
26
25
  error(message: string, ...args: any[]): void;
27
26
  }
28
27
  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,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"}
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.2-alpha.0",
3
+ "version": "1.2.2-alpha.100",
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.2-alpha.0",
29
- "@onekeyfe/hd-shared": "1.2.2-alpha.0",
30
- "@onekeyfe/hd-transport": "1.2.2-alpha.0",
28
+ "@onekeyfe/hd-core": "1.2.2-alpha.100",
29
+ "@onekeyfe/hd-shared": "1.2.2-alpha.100",
30
+ "@onekeyfe/hd-transport": "1.2.2-alpha.100",
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": "9c18d7073728621179d58252e07fb3385b865226"
39
+ "gitHead": "c40dad085297b0e3cc2020bd218c146dff6ed3e1"
40
40
  }
@@ -16,7 +16,6 @@ import {
16
16
  createKnownBleUuidAliases,
17
17
  hasOnekeyCommunicationService,
18
18
  isOnekeyBluetoothDevice,
19
- isPro2FamilyBleName,
20
19
  matchesKnownBleUuid,
21
20
  wait,
22
21
  } from '@onekeyfe/hd-shared';
@@ -91,18 +90,8 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
91
90
  const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
92
91
  const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
93
92
  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;
98
93
  // Renderer release is logical only; this timer physically frees the device.
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;
94
+ const BLE_IDLE_DISCONNECT_MS = 3 * 60_000;
106
95
  // Ceiling while a call is in flight: no outstanding write, but not forever.
107
96
  const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
108
97
 
@@ -268,11 +257,6 @@ async function initializeNoble(): Promise<void> {
268
257
  noble = require('@stoprocent/noble') as NobleModule;
269
258
  logger?.info('[NobleBLE] Noble library loaded');
270
259
 
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
-
276
260
  // Wait for Bluetooth to be ready
277
261
  await new Promise<void>((resolve, reject) => {
278
262
  if (!noble) {
@@ -285,6 +269,9 @@ async function initializeNoble(): Promise<void> {
285
269
  return;
286
270
  }
287
271
 
272
+ // Setup persistent state listener before initialization
273
+ setupPersistentStateListener();
274
+
288
275
  const timeout = setTimeout(() => {
289
276
  reject(
290
277
  ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
@@ -411,21 +398,7 @@ function armIdleDisconnect(
411
398
  logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
412
399
  const peripheral = connectedDevices.get(deviceId);
413
400
  const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
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))
401
+ const pending = disconnectDevice(deviceId)
429
402
  .then(() => {
430
403
  // A call is still in flight here; it must reject, not hang.
431
404
  // Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
@@ -535,9 +508,6 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
535
508
 
536
509
  cleanupDevice(deviceId, webContents, {
537
510
  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,
541
511
  sendDisconnectEvent: true,
542
512
  cancelOperations: true,
543
513
  reason: 'auto-disconnect',
@@ -791,44 +761,6 @@ async function transmitHexDataToDevice(
791
761
  }
792
762
 
793
763
  // 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
- // Attempts before a link is written off as unable to serve the OneKey service.
800
- const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2;
801
-
802
- // Advertised names, kept for the life of the process: device caches are purged
803
- // on every disconnect, but the family a device belongs to never changes and
804
- // decides which connection strategy is safe for it.
805
- const bleNamesById = new Map<string, string>();
806
-
807
- function rememberBleName(deviceId: string, name?: string | null): void {
808
- const trimmed = name?.trim();
809
- if (trimmed) {
810
- bleNamesById.set(deviceId, trimmed);
811
- }
812
- }
813
-
814
- function resolveBleName(deviceId: string): string | undefined {
815
- const live =
816
- connectedDevices.get(deviceId)?.advertisement?.localName?.trim() ||
817
- discoveredDevices.get(deviceId)?.advertisement?.localName?.trim();
818
- return live || bleNamesById.get(deviceId);
819
- }
820
-
821
- /**
822
- * Pro2/Neo connect by id first: they are unaffected by the stale-session
823
- * problem and advertise under Find My names a OneKey-filtered scan cannot
824
- * match. Everything else — Classic family, and any device whose name is not
825
- * known yet — scans first, because connecting by id can resolve a session the
826
- * device no longer serves (see setupConnectionAndDiscoverServices).
827
- */
828
- function shouldConnectByIdFirst(deviceId: string): boolean {
829
- return isPro2FamilyBleName(resolveBleName(deviceId));
830
- }
831
-
832
764
  function handleDeviceDiscovered(peripheral: Peripheral): void {
833
765
  // Only process OneKey candidates for general discovery. Avoid logging every
834
766
  // ambient BLE peripheral; it makes Pro2 debugging hard to read.
@@ -838,7 +770,6 @@ function handleDeviceDiscovered(peripheral: Peripheral): void {
838
770
 
839
771
  const isNewDevice = !discoveredDevices.has(peripheral.id);
840
772
  discoveredDevices.set(peripheral.id, peripheral);
841
- rememberBleName(peripheral.id, peripheral.advertisement?.localName);
842
773
  if (isNewDevice) {
843
774
  logger?.debug('[NobleBLE] OneKey BLE device discovered', {
844
775
  deviceId: peripheral.id,
@@ -873,10 +804,7 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
873
804
 
874
805
  // Perform targeted scan for a specific device ID
875
806
  // Uses self-contained local listener pattern - no global state needed
876
- async function performTargetedScan(
877
- targetDeviceId: string,
878
- timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS
879
- ): Promise<Peripheral | null> {
807
+ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
880
808
  if (!noble) {
881
809
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
882
810
  }
@@ -921,7 +849,7 @@ async function performTargetedScan(
921
849
  const timeoutId = setTimeout(() => {
922
850
  logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
923
851
  finish(null).catch(reject);
924
- }, timeoutMs);
852
+ }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
925
853
 
926
854
  // Add local listener for this scan
927
855
  nobleInstance.on('discover', onDiscover);
@@ -1123,17 +1051,9 @@ async function discoverServicesAndCharacteristics(
1123
1051
 
1124
1052
  // Main discovery logic as async function
1125
1053
  const discoveryPromise = (async (): Promise<CharacteristicPair> => {
1126
- // Step 1: Discover the OneKey service by UUID. Filtering is not just a
1127
- // narrowing: an unfiltered discovery is answered from the OS GATT cache, so
1128
- // a device whose stack still advertises and accepts links but no longer
1129
- // serves its application layer still looks healthy — the link comes up, the
1130
- // cached services resolve, the protocol frame goes out and nothing ever
1131
- // answers. A targeted query returns nothing in that state, which is what
1132
- // 6.5.0 relied on to trip the recovery path below (retry, reset, fresh
1133
- // scan) — the sequence that brings such a device back. Pro2/Neo expose the
1134
- // same service UUID, and the selection below only ever accepts that one.
1054
+ // Step 1: Discover ALL services (no filter Pro2 may use different service UUID)
1135
1055
  const services = await new Promise<Service[]>((resolve, reject) => {
1136
- peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => {
1056
+ peripheral.discoverServices([], (error, svc) => {
1137
1057
  if (error) {
1138
1058
  logger?.error('[NobleBLE] Service discovery failed:', error);
1139
1059
  reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
@@ -1144,7 +1064,7 @@ async function discoverServicesAndCharacteristics(
1144
1064
  });
1145
1065
 
1146
1066
  if (!services || services.length === 0) {
1147
- throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found');
1067
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found');
1148
1068
  }
1149
1069
 
1150
1070
  logger?.debug('[NobleBLE] services discovered', {
@@ -1328,14 +1248,12 @@ async function discoverServicesAndCharacteristicsWithRetry(
1328
1248
  deviceId,
1329
1249
  peripheralState: peripheral.state,
1330
1250
  attempt: attemptNumber,
1331
- maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS,
1251
+ maxRetries: 5,
1332
1252
  targetUUIDs: ONEKEY_SERVICE_UUIDS,
1333
1253
  });
1334
1254
 
1335
1255
  if (attemptNumber > 1) {
1336
- logger?.info(
1337
- `[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}`
1338
- );
1256
+ logger?.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/5`);
1339
1257
  }
1340
1258
 
1341
1259
  // Verify connection state before attempting service discovery
@@ -1349,32 +1267,20 @@ async function discoverServicesAndCharacteristicsWithRetry(
1349
1267
  try {
1350
1268
  return await discoverServicesAndCharacteristics(peripheral);
1351
1269
  } catch (error) {
1352
- logger?.error(
1353
- `[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
1354
- );
1270
+ logger?.error(`[NobleBLE] No services found (attempt ${attemptNumber}/5)`);
1355
1271
 
1356
- if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) {
1357
- logger?.error(
1358
- `[NobleBLE] Will retry service discovery (attempt ${
1359
- attemptNumber + 1
1360
- }/${SERVICE_DISCOVERY_MAX_ATTEMPTS})`
1361
- );
1272
+ if (attemptNumber < 5) {
1273
+ logger?.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/5)`);
1362
1274
  }
1363
1275
 
1364
1276
  throw error; // p-retry will handle the retry logic
1365
1277
  }
1366
1278
  },
1367
1279
  {
1368
- // One retry, not four. An empty result here is the signature of a device
1369
- // whose stack no longer serves its application layer, and retrying the
1370
- // same link has never recovered it — every observed run failed all the
1371
- // way through. The recovery is the teardown and cold reconnect the caller
1372
- // performs afterwards, so reaching it sooner is what shortens the wait.
1373
- // The single retry stays for a genuinely transient miss.
1374
- retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1,
1375
- factor: 1.5,
1376
- minTimeout: 500,
1377
- maxTimeout: 3000,
1280
+ retries: 4, // Total 5 attempts (initial + 4 retries)
1281
+ factor: 1.5, // Exponential backoff: 1000ms 1500ms 2250ms 3000ms
1282
+ minTimeout: 1000, // Start with 1 second delay
1283
+ maxTimeout: 3000, // Maximum 3 seconds delay
1378
1284
  onFailedAttempt: error => {
1379
1285
  // This runs after each failed attempt
1380
1286
  logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
@@ -1399,45 +1305,26 @@ async function setupConnectionAndDiscoverServices(
1399
1305
  deviceId: string,
1400
1306
  webContents: WebContents
1401
1307
  ): Promise<CharacteristicPair> {
1402
- // Reset the link before any protocol traffic reaches the device. 6.5.0 did
1403
- // this on every cold setup and never produced a protocol-deaf Classic 1S
1404
- // (control log 2026-08-20: 16/16 links answered within ~1s, including after
1405
- // a 15-minute idle). Demoting it to a discovery-failure fallback removed the
1406
- // reset entirely for this device, because discovery always succeeds: the link
1407
- // comes up, GATT resolves from cache, and the device then answers nothing on
1408
- // a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
1409
- // link with an intact subscription returns before reaching here, so reuse
1410
- // inside a workflow is unaffected.
1308
+ setupDisconnectListener(peripheral, deviceId, webContents);
1309
+
1411
1310
  try {
1412
- await forceReconnectPeripheral(peripheral, deviceId);
1413
- } catch (resetError) {
1414
- // A failed reset must not abort the attempt: discovery on the existing
1415
- // link, then the fresh-scan fallback, still have a chance to recover.
1416
- logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
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
+ });
1417
1317
  }
1418
- setupDisconnectListener(peripheral, deviceId, webContents);
1419
1318
 
1319
+ await forceReconnectPeripheral(peripheral, deviceId);
1320
+ setupDisconnectListener(peripheral, deviceId, webContents);
1420
1321
  try {
1421
1322
  return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
1422
- } catch (discoveryError) {
1423
- // A connected peripheral does not advertise, so the fresh scan below would
1424
- // only run out its timeout. Drop the link first, then rescan: the cold
1425
- // reconnect is what recovers a device whose stack stopped serving its
1426
- // application layer. Recover here rather than throwing to the caller —
1427
- // one acquire pays ~2s for the rescan, while a thrown error costs the
1428
- // renderer a full retry round trip.
1429
- if (peripheral.state === 'connected') {
1430
- logger?.error(
1431
- '[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan',
1432
- discoveryError
1433
- );
1434
- await disconnectDevice(deviceId).catch(() => undefined);
1435
- } else {
1436
- logger?.error(
1437
- '[NobleBLE] Service discovery failed, attempting fresh scan...',
1438
- discoveryError
1439
- );
1440
- }
1323
+ } catch (reconnectError) {
1324
+ logger?.error(
1325
+ '[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
1326
+ reconnectError
1327
+ );
1441
1328
  return freshScanAndDiscover(deviceId, webContents);
1442
1329
  }
1443
1330
  }
@@ -1526,7 +1413,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1526
1413
  // enumerate clears the discovery map; a kept-alive link outlives it.
1527
1414
  let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
1528
1415
 
1416
+ // If device not discovered, try a targeted scan for this specific device
1529
1417
  if (!peripheral) {
1418
+ logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
1419
+
1530
1420
  // Initialize Noble if not already done
1531
1421
  if (!noble) {
1532
1422
  await initializeNoble();
@@ -1536,46 +1426,27 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1536
1426
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
1537
1427
  }
1538
1428
 
1539
- // A live advertisement proves the device holds no link and will open a
1540
- // fresh session; connecting by id resolves through the OS cache instead
1541
- // and can hand back a session the device no longer serves (see
1542
- // setupConnectionAndDiscoverServices). Scanning is also the faster of the
1543
- // two when the device does advertise: ~96ms median against ~784ms for
1544
- // connect-by-id across the field logs. Pro2/Neo keep connect-by-id first
1545
- // because they are unaffected and may advertise under an unmatchable Find
1546
- // My name.
1547
- const byIdFirst = shouldConnectByIdFirst(deviceId);
1548
- logger?.info('[NobleBLE] Resolving device for cold connect', {
1549
- deviceId,
1550
- name: resolveBleName(deviceId) ?? 'unknown',
1551
- strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first',
1552
- });
1553
-
1554
- const scanForPeripheral = async () => {
1555
- try {
1556
- return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined;
1557
- } catch (scanError) {
1558
- logger?.info('[NobleBLE] Targeted scan failed', {
1559
- deviceId,
1560
- error: scanError instanceof Error ? scanError.message : String(scanError),
1561
- });
1562
- return undefined;
1563
- }
1564
- };
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
+ }
1565
1435
 
1566
- const connectById = async () => {
1567
- const found = await tryDirectConnectById(deviceId);
1568
- if (found) {
1569
- discoveredDevices.set(deviceId, found);
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
+ );
1570
1445
  }
1571
- return found;
1572
- };
1573
-
1574
- peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
1575
- if (!peripheral) {
1576
- // The preferred route came up empty: not advertising (held elsewhere, or
1577
- // silent), or not reachable by id. Try the other one before giving up.
1578
- peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
1446
+ peripheral = foundPeripheral;
1447
+ } catch (error) {
1448
+ logger?.error('[NobleBLE] Targeted scan failed:', error);
1449
+ throw error;
1579
1450
  }
1580
1451
  }
1581
1452
 
@@ -1584,10 +1455,6 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1584
1455
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`);
1585
1456
  }
1586
1457
 
1587
- // Also covers the connect-by-id route, whose simulated discovery carries no
1588
- // service UUIDs and therefore never reaches handleDeviceDiscovered.
1589
- rememberBleName(deviceId, peripheral.advertisement?.localName);
1590
-
1591
1458
  logger?.info('[NobleBLE] Connecting to device:', deviceId);
1592
1459
 
1593
1460
  // Check if device is already connected
@@ -1708,14 +1575,9 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
1708
1575
  resolve();
1709
1576
  } catch (setupError) {
1710
1577
  logger?.error('[NobleBLE] Connection setup failed:', setupError);
1711
- // Never reject from inside a raw disconnect callback: noble only fires
1712
- // it on a real 'disconnect' event, so a peripheral that is already
1713
- // down leaves this promise — and the renderer acquire awaiting it —
1714
- // pending forever. disconnectDevice always settles (it no-ops on an
1715
- // unknown peripheral and caps the confirm wait).
1716
- disconnectDevice(deviceId)
1717
- .catch(() => undefined)
1718
- .then(() => reject(setupError));
1578
+ connectedPeripheral.disconnect(() => {
1579
+ reject(setupError);
1580
+ });
1719
1581
  }
1720
1582
  });
1721
1583
  });
@@ -1735,18 +1597,11 @@ async function disconnectDevice(deviceId: string): Promise<void> {
1735
1597
  }
1736
1598
 
1737
1599
  await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1738
- timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1600
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1739
1601
  timeoutBehavior: 'resolve',
1740
1602
  });
1741
- if (peripheral.state === 'connected') {
1742
- logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
1743
- }
1744
1603
  cleanupDevice(deviceId, undefined, {
1745
1604
  cleanupConnection: true,
1746
- // macOS returns zero GATT services when a previously-disconnected
1747
- // peripheral object is reconnected; drop the discovery cache so the next
1748
- // connect resolves a fresh peripheral (direct connect by id, or scan).
1749
- cleanupDiscoveredCache: true,
1750
1605
  sendDisconnectEvent: false,
1751
1606
  cancelOperations: true,
1752
1607
  reason: 'manual-disconnect',
@@ -1887,14 +1742,9 @@ async function subscribeNotifications(
1887
1742
  });
1888
1743
  }
1889
1744
 
1890
- const subscribeStartedAt = Date.now();
1891
1745
  try {
1892
1746
  await rebuildAppSubscription(deviceId, notifyCharacteristic);
1893
1747
  subscribedDevices.set(deviceId, true);
1894
- logger?.info('[NobleBLE] Notification subscription active', {
1895
- deviceId,
1896
- ms: Date.now() - subscribeStartedAt,
1897
- });
1898
1748
  } finally {
1899
1749
  // 🔒 CRITICAL: Always clear operation state (even on error)
1900
1750
  subscriptionOperations.set(deviceId, 'idle');
@@ -2077,12 +1927,10 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
2077
1927
  }
2078
1928
 
2079
1929
  await stopScanning().catch(() => undefined);
2080
- // persistentStateListener is process-lifetime, NOT per-window: this
2081
- // destroy handler also fires on a renderer soft restart, and removing
2082
- // the listener here permanently killed poweredOff/poweredOn
2083
- // reconciliation for the rest of the process (nothing re-registers it:
2084
- // initializeNoble early-returns once noble is loaded). The null-guard
2085
- // in setupPersistentStateListener keeps it deduped to one instance.
1930
+ if (noble && persistentStateListener) {
1931
+ noble.removeListener('stateChange', persistentStateListener);
1932
+ persistentStateListener = null;
1933
+ }
2086
1934
  cleanupNobleListeners();
2087
1935
  discoveredDevices.clear();
2088
1936
  safeLog(logger, 'info', 'Noble BLE cleanup completed');
@@ -39,7 +39,6 @@ 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;
43
42
  error(message: string, ...args: any[]): void;
44
43
  }
45
44