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

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-e87ba8bc.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-fea7f7d5.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-fea7f7d5.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',
@@ -975,18 +966,23 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
975
966
  }
976
967
  function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
977
968
  return index.__awaiter(this, void 0, void 0, function* () {
969
+ setupDisconnectListener(peripheral, deviceId, webContents);
978
970
  try {
979
- yield forceReconnectPeripheral(peripheral, deviceId);
971
+ return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
980
972
  }
981
- catch (resetError) {
982
- 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
+ });
983
978
  }
979
+ yield forceReconnectPeripheral(peripheral, deviceId);
984
980
  setupDisconnectListener(peripheral, deviceId, webContents);
985
981
  try {
986
982
  return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
987
983
  }
988
- catch (discoveryError) {
989
- logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
984
+ catch (reconnectError) {
985
+ logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', reconnectError);
990
986
  return freshScanAndDiscover(deviceId, webContents);
991
987
  }
992
988
  });
@@ -1180,15 +1176,11 @@ function disconnectDevice(deviceId) {
1180
1176
  deviceDisconnectListeners.delete(deviceId);
1181
1177
  }
1182
1178
  yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1183
- timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1179
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1184
1180
  timeoutBehavior: 'resolve',
1185
1181
  });
1186
- if (peripheral.state === 'connected') {
1187
- logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
1188
- }
1189
1182
  cleanupDevice(deviceId, undefined, {
1190
1183
  cleanupConnection: true,
1191
- cleanupDiscoveredCache: true,
1192
1184
  sendDisconnectEvent: false,
1193
1185
  cancelOperations: true,
1194
1186
  reason: 'manual-disconnect',
@@ -1399,6 +1391,10 @@ function setupNobleBleHandlers(webContents) {
1399
1391
  yield disconnectDevice(deviceId).catch(() => undefined);
1400
1392
  }
1401
1393
  yield stopScanning().catch(() => undefined);
1394
+ if (noble && persistentStateListener) {
1395
+ noble.removeListener('stateChange', persistentStateListener);
1396
+ persistentStateListener = null;
1397
+ }
1402
1398
  cleanupNobleListeners();
1403
1399
  discoveredDevices.clear();
1404
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":"AA+BA,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;AAuoDD,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.0-alpha.161",
3
+ "version": "1.2.0-alpha.162",
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.161",
29
- "@onekeyfe/hd-shared": "1.2.0-alpha.161",
30
- "@onekeyfe/hd-transport": "1.2.0-alpha.161",
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",
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": "06316117fc9a7ebaecea1ca583b2d105709361bf"
39
+ "gitHead": "f4aa654b350931d68ed9ba3f77c4cc8faa161218"
40
40
  }
@@ -90,18 +90,8 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
90
90
  const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
91
91
  const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
92
92
  const BLE_CLEANUP_TIMEOUT = 250;
93
- // A physical teardown must actually reach the OS before the device can return
94
- // to a clean state; 250ms routinely declared success while CoreBluetooth was
95
- // still disconnecting, leaving no trace when the teardown never completed.
96
- const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
97
93
  // Renderer release is logical only; this timer physically frees the device.
98
- // Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped
99
- // after sitting idle for minutes (field logs 2026-08-19: every deaf window
100
- // followed a 3-minute-idle disconnect, while disconnects right after traffic
101
- // have never produced one across 6.5.0's per-call teardown history). A hot
102
- // disconnect ~20s after the last operation stays inside the proven-safe
103
- // pattern and also shrinks the window in which phones cannot see the device.
104
- const BLE_IDLE_DISCONNECT_MS = 20_000;
94
+ const BLE_IDLE_DISCONNECT_MS = 3 * 60_000;
105
95
  // Ceiling while a call is in flight: no outstanding write, but not forever.
106
96
  const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
107
97
 
@@ -267,11 +257,6 @@ async function initializeNoble(): Promise<void> {
267
257
  noble = require('@stoprocent/noble') as NobleModule;
268
258
  logger?.info('[NobleBLE] Noble library loaded');
269
259
 
270
- // Register the process-lifetime state listener before any early return:
271
- // the poweredOn fast path below would otherwise skip it for the whole
272
- // session, leaving poweredOff cache/state reconciliation dead.
273
- setupPersistentStateListener();
274
-
275
260
  // Wait for Bluetooth to be ready
276
261
  await new Promise<void>((resolve, reject) => {
277
262
  if (!noble) {
@@ -284,6 +269,9 @@ async function initializeNoble(): Promise<void> {
284
269
  return;
285
270
  }
286
271
 
272
+ // Setup persistent state listener before initialization
273
+ setupPersistentStateListener();
274
+
287
275
  const timeout = setTimeout(() => {
288
276
  reject(
289
277
  ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
@@ -410,21 +398,7 @@ function armIdleDisconnect(
410
398
  logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
411
399
  const peripheral = connectedDevices.get(deviceId);
412
400
  const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
413
- // Unsubscribe CCCD before dropping the link, matching every other
414
- // teardown path: the 1S leaves its notify session half-open when the
415
- // link drops without an unsubscribe and then ignores application
416
- // protocol traffic on NEW links for tens of minutes (field log
417
- // 2026-08-19: the lone unsubscribe-skipping idle teardown caused a
418
- // 6-attempt reconnect loop; unsubscribe-first teardowns reconnected
419
- // instantly).
420
- const pending = unsubscribeNotifications(deviceId)
421
- .catch(error => {
422
- logger?.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
423
- deviceId,
424
- error: error instanceof Error ? error.message : String(error),
425
- });
426
- })
427
- .then(() => disconnectDevice(deviceId))
401
+ const pending = disconnectDevice(deviceId)
428
402
  .then(() => {
429
403
  // A call is still in flight here; it must reject, not hang.
430
404
  // Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
@@ -534,9 +508,6 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
534
508
 
535
509
  cleanupDevice(deviceId, webContents, {
536
510
  cleanupConnection: true,
537
- // Same stale-object hazard as manual disconnect: an externally dropped
538
- // link invalidates the cached peripheral for the next reconnect.
539
- cleanupDiscoveredCache: true,
540
511
  sendDisconnectEvent: true,
541
512
  cancelOperations: true,
542
513
  reason: 'auto-disconnect',
@@ -1334,28 +1305,26 @@ async function setupConnectionAndDiscoverServices(
1334
1305
  deviceId: string,
1335
1306
  webContents: WebContents
1336
1307
  ): Promise<CharacteristicPair> {
1337
- // Reset the link before any protocol traffic reaches the device. 6.5.0 did
1338
- // this on every cold setup and never produced a protocol-deaf Classic 1S
1339
- // (control log 2026-08-20: 16/16 links answered within ~1s, including after
1340
- // a 15-minute idle). Demoting it to a discovery-failure fallback removed the
1341
- // reset entirely for this device, because discovery always succeeds: the link
1342
- // comes up, GATT resolves from cache, and the device then answers nothing on
1343
- // a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
1344
- // link with an intact subscription returns before reaching here, so reuse
1345
- // inside a workflow is unaffected.
1308
+ setupDisconnectListener(peripheral, deviceId, webContents);
1309
+
1346
1310
  try {
1347
- await forceReconnectPeripheral(peripheral, deviceId);
1348
- } catch (resetError) {
1349
- // A failed reset must not abort the attempt: discovery on the existing
1350
- // link, then the fresh-scan fallback, still have a chance to recover.
1351
- 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
+ });
1352
1317
  }
1353
- setupDisconnectListener(peripheral, deviceId, webContents);
1354
1318
 
1319
+ await forceReconnectPeripheral(peripheral, deviceId);
1320
+ setupDisconnectListener(peripheral, deviceId, webContents);
1355
1321
  try {
1356
1322
  return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
1357
- } catch (discoveryError) {
1358
- logger?.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
1323
+ } catch (reconnectError) {
1324
+ logger?.error(
1325
+ '[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
1326
+ reconnectError
1327
+ );
1359
1328
  return freshScanAndDiscover(deviceId, webContents);
1360
1329
  }
1361
1330
  }
@@ -1628,18 +1597,11 @@ async function disconnectDevice(deviceId: string): Promise<void> {
1628
1597
  }
1629
1598
 
1630
1599
  await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
1631
- timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
1600
+ timeoutMs: BLE_CLEANUP_TIMEOUT,
1632
1601
  timeoutBehavior: 'resolve',
1633
1602
  });
1634
- if (peripheral.state === 'connected') {
1635
- logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
1636
- }
1637
1603
  cleanupDevice(deviceId, undefined, {
1638
1604
  cleanupConnection: true,
1639
- // macOS returns zero GATT services when a previously-disconnected
1640
- // peripheral object is reconnected; drop the discovery cache so the next
1641
- // connect resolves a fresh peripheral (direct connect by id, or scan).
1642
- cleanupDiscoveredCache: true,
1643
1605
  sendDisconnectEvent: false,
1644
1606
  cancelOperations: true,
1645
1607
  reason: 'manual-disconnect',
@@ -1965,12 +1927,10 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
1965
1927
  }
1966
1928
 
1967
1929
  await stopScanning().catch(() => undefined);
1968
- // persistentStateListener is process-lifetime, NOT per-window: this
1969
- // destroy handler also fires on a renderer soft restart, and removing
1970
- // the listener here permanently killed poweredOff/poweredOn
1971
- // reconciliation for the rest of the process (nothing re-registers it:
1972
- // initializeNoble early-returns once noble is loaded). The null-guard
1973
- // in setupPersistentStateListener keeps it deduped to one instance.
1930
+ if (noble && persistentStateListener) {
1931
+ noble.removeListener('stateChange', persistentStateListener);
1932
+ persistentStateListener = null;
1933
+ }
1974
1934
  cleanupNobleListeners();
1975
1935
  discoveredDevices.clear();
1976
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