@onekeyfe/hd-transport-react-native 1.2.0-alpha.63 → 1.2.0-alpha.65

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.
package/dist/index.d.ts CHANGED
@@ -64,7 +64,22 @@ declare function getProtocolV2BleTuning(): {
64
64
  * inside the native budget, so this only fires when the native timeout did not.
65
65
  */
66
66
  declare const BLE_CONNECT_TIMEOUT_MS: number;
67
- /** Consecutive connect timeouts on one device before the BLE manager itself is recreated. */
67
+ /**
68
+ * Service discovery and characteristic resolution run after connect() succeeds, but
69
+ * CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
70
+ * device reboot, these calls can remain pending forever unless they have their own
71
+ * budget.
72
+ */
73
+ declare const BLE_GATT_SETUP_TIMEOUT_MS = 10000;
74
+ /**
75
+ * How many times a known device may fail its own protocol before we probe the others
76
+ * again. Reconnect polling during a device reboot repeats this every few seconds, and
77
+ * probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
78
+ * device we just spoke V1 to dominates the wait. A firmware update can legitimately
79
+ * change a device's protocol, so the shortcut has to expire rather than stick.
80
+ */
81
+ declare const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
82
+ /** BLE setup timeouts since the last successful setup before the manager is recreated. */
68
83
  declare const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
69
84
  type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
70
85
  declare class ReactNativeBleTransport {
@@ -91,9 +106,13 @@ declare class ReactNativeBleTransport {
91
106
  private probingProtocols;
92
107
  /** Consecutive write timeouts per device; reset by any write that completes. */
93
108
  private writeTimeoutCounts;
94
- /** Consecutive connect timeouts per device; reset by any connect that settles. */
95
- private connectTimeoutCounts;
109
+ /** BLE setup timeouts per device since the last complete characteristic resolution. */
110
+ private connectionSetupTimeoutCounts;
96
111
  private deviceProtocolHints;
112
+ /** Protocol this device actually answered on, kept across reconnects of one session. */
113
+ private sessionProtocols;
114
+ /** Consecutive detections that failed while trusting sessionProtocols. */
115
+ private protocolReprobeFailures;
97
116
  private protocolV2Assemblers;
98
117
  private protocolV2FrameQueues;
99
118
  private protocolV2FramePromises;
@@ -133,13 +152,14 @@ declare class ReactNativeBleTransport {
133
152
  cancel(): void;
134
153
  /** Run a native connect under the JS backstop budget. */
135
154
  private connectWithTimeout;
155
+ /** Resolve the complete GATT shape under a budget so acquire() always settles. */
156
+ private resolveCharacteristicsWithTimeout;
136
157
  /**
137
- * Give up on a connect the native layer never settled. The abandoned attempt still
138
- * holds a native "connecting" entry that would cancel the NEXT attempt out from under
139
- * itself, so it is cleared here fire and forget, because that call talks to the very
140
- * queue that just stopped responding.
158
+ * Give up on a BLE setup operation the native layer did not settle. The abandoned
159
+ * operation still owns native connection/GATT state that can poison the next attempt,
160
+ * so it is cleared here without awaiting the same queue that stopped responding.
141
161
  */
142
- private abandonStalledConnect;
162
+ private abandonStalledConnection;
143
163
  private getCachedTransport;
144
164
  /**
145
165
  * Write one packet under a bounded budget. A write that never settles means the
@@ -178,4 +198,4 @@ declare class ReactNativeBleTransport {
178
198
  getProtocolType(path: string): ProtocolType | undefined;
179
199
  }
180
200
 
181
- export { BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD, BLE_CONNECT_TIMEOUT_MS, BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
201
+ export { BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD, BLE_CONNECT_TIMEOUT_MS, BLE_GATT_SETUP_TIMEOUT_MS, BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, PROTOCOL_REPROBE_FALLBACK_ATTEMPTS, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -295,11 +295,14 @@ const fallbackConnectOptions = {
295
295
  timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
296
296
  };
297
297
  const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
298
+ const BLE_GATT_SETUP_TIMEOUT_MS = 10000;
299
+ const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
298
300
  const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
299
301
  const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
300
302
  const isConnectTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleConnectedError &&
301
303
  typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' &&
302
304
  error.message.startsWith(CONNECT_TIMEOUT_MESSAGE);
305
+ const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
303
306
  const tryToGetConfiguration = (device) => {
304
307
  if (!device || !device.serviceUUIDs)
305
308
  return null;
@@ -359,8 +362,10 @@ class ReactNativeBleTransport {
359
362
  this.deviceProtocol = new Map();
360
363
  this.probingProtocols = new Map();
361
364
  this.writeTimeoutCounts = new Map();
362
- this.connectTimeoutCounts = new Map();
365
+ this.connectionSetupTimeoutCounts = new Map();
363
366
  this.deviceProtocolHints = new Map();
367
+ this.sessionProtocols = new Map();
368
+ this.protocolReprobeFailures = new Map();
364
369
  this.protocolV2Assemblers = new Map();
365
370
  this.protocolV2FrameQueues = new Map();
366
371
  this.protocolV2FramePromises = new Map();
@@ -564,7 +569,7 @@ class ReactNativeBleTransport {
564
569
  }
565
570
  }
566
571
  }
567
- const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristics(device);
572
+ const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, device);
568
573
  transport.device = device;
569
574
  transport.writeCharacteristic = writeCharacteristic;
570
575
  transport.notifyCharacteristic = notifyCharacteristic;
@@ -693,7 +698,7 @@ class ReactNativeBleTransport {
693
698
  }
694
699
  installTransportForAcquire(uuid, device, characteristics) {
695
700
  return __awaiter(this, void 0, void 0, function* () {
696
- const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristics(device));
701
+ const { writeCharacteristic, notifyCharacteristic } = characteristics !== null && characteristics !== void 0 ? characteristics : (yield this.resolveCharacteristicsWithTimeout(uuid, device));
697
702
  const transport$1 = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
698
703
  if (reactNative.Platform.OS === 'android') {
699
704
  transport$1.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport$1.mtuSize;
@@ -833,7 +838,7 @@ class ReactNativeBleTransport {
833
838
  }
834
839
  device = yield requestAndroidMtu(device);
835
840
  const acquiredDevice = device;
836
- const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristics(acquiredDevice);
841
+ const { writeCharacteristic, notifyCharacteristic } = yield this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
837
842
  const protocolHint = expectedProtocol
838
843
  ? undefined
839
844
  : (_b = (_a = input.protocolHint) !== null && _a !== void 0 ? _a : this.deviceProtocolHints.get(uuid)) !== null && _b !== void 0 ? _b : inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
@@ -1304,6 +1309,8 @@ class ReactNativeBleTransport {
1304
1309
  this.deviceProtocol.delete(session);
1305
1310
  this.probingProtocols.delete(session);
1306
1311
  this.deviceProtocolHints.delete(session);
1312
+ this.sessionProtocols.delete(session);
1313
+ this.protocolReprobeFailures.delete(session);
1307
1314
  this.protocolV2Assemblers.delete(session);
1308
1315
  this.resetProtocolV2Frames(session);
1309
1316
  try {
@@ -1340,12 +1347,11 @@ class ReactNativeBleTransport {
1340
1347
  }, BLE_CONNECT_TIMEOUT_MS);
1341
1348
  }),
1342
1349
  ]);
1343
- this.connectTimeoutCounts.delete(uuid);
1344
1350
  return result;
1345
1351
  }
1346
1352
  catch (error) {
1347
- if (timedOut) {
1348
- this.abandonStalledConnect(uuid);
1353
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1354
+ this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
1349
1355
  }
1350
1356
  throw error;
1351
1357
  }
@@ -1355,12 +1361,44 @@ class ReactNativeBleTransport {
1355
1361
  }
1356
1362
  });
1357
1363
  }
1358
- abandonStalledConnect(uuid) {
1364
+ resolveCharacteristicsWithTimeout(uuid, device) {
1365
+ return __awaiter(this, void 0, void 0, function* () {
1366
+ let timer;
1367
+ let timedOut = false;
1368
+ const pending = this.resolveCharacteristics(device);
1369
+ pending.catch(() => undefined);
1370
+ try {
1371
+ const result = yield Promise.race([
1372
+ pending,
1373
+ new Promise((_, reject) => {
1374
+ timer = setTimeout(() => {
1375
+ timedOut = true;
1376
+ reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`));
1377
+ }, BLE_GATT_SETUP_TIMEOUT_MS);
1378
+ }),
1379
+ ]);
1380
+ this.connectionSetupTimeoutCounts.delete(uuid);
1381
+ return result;
1382
+ }
1383
+ catch (error) {
1384
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1385
+ this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1386
+ }
1387
+ throw error;
1388
+ }
1389
+ finally {
1390
+ if (timer)
1391
+ clearTimeout(timer);
1392
+ }
1393
+ });
1394
+ }
1395
+ abandonStalledConnection(uuid, stage) {
1359
1396
  var _a, _b;
1360
- const timeouts = ((_a = this.connectTimeoutCounts.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
1361
- this.connectTimeoutCounts.set(uuid, timeouts);
1362
- Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE connect timed out:', uuid, {
1363
- consecutiveConnectTimeouts: timeouts,
1397
+ const timeouts = ((_a = this.connectionSetupTimeoutCounts.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
1398
+ this.connectionSetupTimeoutCounts.set(uuid, timeouts);
1399
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
1400
+ stage,
1401
+ setupTimeoutsSinceSuccess: timeouts,
1364
1402
  });
1365
1403
  (_b = this.blePlxManager) === null || _b === void 0 ? void 0 : _b.cancelDeviceConnection(uuid).catch(() => {
1366
1404
  });
@@ -1373,9 +1411,9 @@ class ReactNativeBleTransport {
1373
1411
  this.protocolV2Assemblers.delete(uuid);
1374
1412
  this.resetProtocolV2Frames(uuid);
1375
1413
  if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1376
- Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE connects wedged repeatedly, resetting BLE manager');
1414
+ Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
1377
1415
  this.resetPlxManager();
1378
- this.connectTimeoutCounts.delete(uuid);
1416
+ this.connectionSetupTimeoutCounts.delete(uuid);
1379
1417
  }
1380
1418
  }
1381
1419
  getCachedTransport(uuid) {
@@ -1450,6 +1488,8 @@ class ReactNativeBleTransport {
1450
1488
  });
1451
1489
  this.deviceProtocol.clear();
1452
1490
  this.probingProtocols.clear();
1491
+ this.sessionProtocols.clear();
1492
+ this.protocolReprobeFailures.clear();
1453
1493
  this.monitorTokens.clear();
1454
1494
  this.protocolV2Assemblers.clear();
1455
1495
  try {
@@ -1478,10 +1518,12 @@ class ReactNativeBleTransport {
1478
1518
  return (_a = this.deviceProtocol.get(uuid)) !== null && _a !== void 0 ? _a : this.probingProtocols.get(uuid);
1479
1519
  }
1480
1520
  detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
1521
+ var _a;
1481
1522
  return __awaiter(this, void 0, void 0, function* () {
1482
1523
  if (expectedProtocol === 'V1') {
1483
1524
  if (yield this.probeProtocolV1(uuid)) {
1484
1525
  this.deviceProtocol.set(uuid, 'V1');
1526
+ this.sessionProtocols.set(uuid, 'V1');
1485
1527
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1486
1528
  deviceId: uuid,
1487
1529
  protocol: 'V1',
@@ -1494,6 +1536,7 @@ class ReactNativeBleTransport {
1494
1536
  if (expectedProtocol === 'V2') {
1495
1537
  if (yield this.probeProtocolV2(uuid)) {
1496
1538
  this.deviceProtocol.set(uuid, 'V2');
1539
+ this.sessionProtocols.set(uuid, 'V2');
1497
1540
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1498
1541
  deviceId: uuid,
1499
1542
  protocol: 'V2',
@@ -1503,7 +1546,13 @@ class ReactNativeBleTransport {
1503
1546
  }
1504
1547
  throw this.createProtocolMismatchError(expectedProtocol);
1505
1548
  }
1506
- const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1549
+ const sessionProtocol = this.sessionProtocols.get(uuid);
1550
+ const reprobeFailures = (_a = this.protocolReprobeFailures.get(uuid)) !== null && _a !== void 0 ? _a : 0;
1551
+ const fullProbeOrder = protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1552
+ const trustSessionProtocol = sessionProtocol !== undefined &&
1553
+ !protocolHint &&
1554
+ reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1555
+ const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1507
1556
  for (let i = 0; i < probeOrder.length; i += 1) {
1508
1557
  const protocol = probeOrder[i];
1509
1558
  if (i > 0) {
@@ -1518,6 +1567,8 @@ class ReactNativeBleTransport {
1518
1567
  const detected = protocol === 'V1' ? yield this.probeProtocolV1(uuid) : yield this.probeProtocolV2(uuid);
1519
1568
  if (detected) {
1520
1569
  this.deviceProtocol.set(uuid, protocol);
1570
+ this.sessionProtocols.set(uuid, protocol);
1571
+ this.protocolReprobeFailures.delete(uuid);
1521
1572
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
1522
1573
  deviceId: uuid,
1523
1574
  protocol,
@@ -1526,6 +1577,12 @@ class ReactNativeBleTransport {
1526
1577
  return protocol;
1527
1578
  }
1528
1579
  }
1580
+ if (trustSessionProtocol) {
1581
+ this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1582
+ }
1583
+ else {
1584
+ this.protocolReprobeFailures.delete(uuid);
1585
+ }
1529
1586
  this.deviceProtocol.delete(uuid);
1530
1587
  this.probingProtocols.delete(uuid);
1531
1588
  throw this.createProtocolDetectionError();
@@ -1823,8 +1880,10 @@ class ReactNativeBleTransport {
1823
1880
 
1824
1881
  exports.BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD;
1825
1882
  exports.BLE_CONNECT_TIMEOUT_MS = BLE_CONNECT_TIMEOUT_MS;
1883
+ exports.BLE_GATT_SETUP_TIMEOUT_MS = BLE_GATT_SETUP_TIMEOUT_MS;
1826
1884
  exports.BLE_WRITE_PACKET_TIMEOUT_MS = BLE_WRITE_PACKET_TIMEOUT_MS;
1827
1885
  exports.BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD;
1886
+ exports.PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1828
1887
  exports.configureProtocolV2BleTuning = configureProtocolV2BleTuning;
1829
1888
  exports["default"] = ReactNativeBleTransport;
1830
1889
  exports.getFirmwareUploadWriteRetryType = getFirmwareUploadWriteRetryType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.0-alpha.63",
3
+ "version": "1.2.0-alpha.65",
4
4
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,11 +20,11 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-core": "1.2.0-alpha.63",
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.63",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.63",
23
+ "@onekeyfe/hd-core": "1.2.0-alpha.65",
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.65",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.65",
26
26
  "@onekeyfe/react-native-ble-utils": "^0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "c5f9c9e47a1198a4ff1022ffafeeb6ca7af83fa0"
29
+ "gitHead": "2850b5b34935d4e97a1fe8a6459eb5a96a5cf0f6"
30
30
  }
@@ -1,10 +1,12 @@
1
1
  import { EventEmitter } from 'events';
2
+ import { BleErrorCode } from 'react-native-ble-plx';
2
3
 
3
4
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
4
5
 
5
6
  import ReactNativeBleTransport, {
6
7
  BLE_CONNECT_TIMEOUT_MS,
7
8
  BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
9
+ BLE_GATT_SETUP_TIMEOUT_MS,
8
10
  } from '../index';
9
11
 
10
12
  import messages from '@onekeyfe/hd-transport/messages.json';
@@ -31,6 +33,7 @@ jest.mock('react-native-ble-plx', () => ({
31
33
  OperationStartFailed: 601,
32
34
  DeviceMTUChangeFailed: 401,
33
35
  OperationCancelled: 2,
36
+ OperationTimedOut: 3,
34
37
  DeviceAlreadyConnected: 203,
35
38
  },
36
39
  BleManager: jest.fn(),
@@ -66,6 +69,14 @@ async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 25
66
69
  /** A device whose native connect() never settles — the observed iOS failure mode. */
67
70
  function createHarness(connectImpl: () => Promise<unknown>) {
68
71
  const connect = jest.fn(connectImpl);
72
+ const writeCharacteristic = {
73
+ uuid: '00000002-0000-1000-8000-00805f9b34fb',
74
+ isWritableWithResponse: true,
75
+ };
76
+ const notifyCharacteristic = {
77
+ uuid: '00000003-0000-1000-8000-00805f9b34fb',
78
+ isNotifiable: true,
79
+ };
69
80
  const device = {
70
81
  id: UUID,
71
82
  name: 'OneKey Classic',
@@ -74,6 +85,11 @@ function createHarness(connectImpl: () => Promise<unknown>) {
74
85
  isConnected: jest.fn(() => Promise.resolve(false)),
75
86
  cancelConnection: jest.fn(() => Promise.resolve()),
76
87
  connect,
88
+ discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
89
+ characteristicsForService: jest.fn(() =>
90
+ Promise.resolve([writeCharacteristic, notifyCharacteristic])
91
+ ),
92
+ services: jest.fn(() => Promise.resolve([])),
77
93
  onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
78
94
  };
79
95
  const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
@@ -189,4 +205,78 @@ describe('BLE connect timeout', () => {
189
205
  expect(destroy).toHaveBeenCalledTimes(1);
190
206
  expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
191
207
  });
208
+
209
+ test('native connect timeouts contribute to the same manager reset budget', async () => {
210
+ const { transport, bleManager } = createHarness(() => Promise.resolve());
211
+ const destroy = jest.fn();
212
+ (bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
213
+ const nativeTimeout = Object.assign(new Error('Operation timed out'), {
214
+ errorCode: BleErrorCode.OperationTimedOut,
215
+ });
216
+
217
+ for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
218
+ // eslint-disable-next-line no-await-in-loop
219
+ await expect(
220
+ (transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
221
+ ).rejects.toBe(nativeTimeout);
222
+ }
223
+
224
+ expect(destroy).toHaveBeenCalledTimes(1);
225
+ });
226
+
227
+ test('GATT discovery is bounded and abandons the native connection', async () => {
228
+ const { transport, device, bleManager } = createHarness(() => Promise.resolve());
229
+ device.discoverAllServicesAndCharacteristics.mockImplementation(
230
+ () =>
231
+ new Promise(() => {
232
+ // never settles
233
+ })
234
+ );
235
+
236
+ const errors: Array<{ errorCode?: unknown }> = [];
237
+ let settled = false;
238
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch((error: unknown) => {
239
+ errors.push(error as { errorCode?: unknown });
240
+ settled = true;
241
+ });
242
+ await flush();
243
+ await advanceUntil(() => settled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
244
+
245
+ expect(errors).toHaveLength(1);
246
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
247
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
248
+ });
249
+
250
+ test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
251
+ const { transport, device, bleManager } = createHarness(() => Promise.resolve());
252
+ let resolveAbandonedDiscovery: (() => void) | undefined;
253
+ device.discoverAllServicesAndCharacteristics
254
+ .mockImplementationOnce(
255
+ () =>
256
+ new Promise<void>(resolve => {
257
+ resolveAbandonedDiscovery = resolve;
258
+ })
259
+ )
260
+ .mockResolvedValueOnce(undefined);
261
+
262
+ let firstSettled = false;
263
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch(() => {
264
+ firstSettled = true;
265
+ });
266
+ await flush();
267
+ await advanceUntil(() => firstSettled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
268
+
269
+ await expect(
270
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device)
271
+ ).resolves.toMatchObject({
272
+ writeCharacteristic: expect.any(Object),
273
+ notifyCharacteristic: expect.any(Object),
274
+ });
275
+
276
+ resolveAbandonedDiscovery?.();
277
+ await flush();
278
+
279
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
280
+ expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
281
+ });
192
282
  });
@@ -0,0 +1,133 @@
1
+ import ReactNativeBleTransport, { PROTOCOL_REPROBE_FALLBACK_ATTEMPTS } from '../index';
2
+
3
+ import messages from '@onekeyfe/hd-transport/messages.json';
4
+
5
+ jest.mock(
6
+ 'react-native',
7
+ () => ({
8
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
9
+ PermissionsAndroid: {
10
+ PERMISSIONS: {},
11
+ RESULTS: {},
12
+ request: jest.fn(),
13
+ requestMultiple: jest.fn(),
14
+ },
15
+ }),
16
+ { virtual: true }
17
+ );
18
+
19
+ jest.mock('react-native-ble-plx', () => ({
20
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
21
+ BleError: Error,
22
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
23
+ BleManager: jest.fn(),
24
+ ScanMode: { LowLatency: 2 },
25
+ }));
26
+
27
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
28
+ __esModule: true,
29
+ default: {
30
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
31
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
32
+ pairDevice: jest.fn(() => Promise.resolve()),
33
+ },
34
+ }));
35
+
36
+ const UUID = 'reprobe-device';
37
+
38
+ /** Drive detectProtocol with stubbed probes so we can observe which are attempted. */
39
+ function createHarness({ v1, v2 }: { v1: boolean; v2: boolean }) {
40
+ const transport = new ReactNativeBleTransport({});
41
+ transport.configure(messages);
42
+ const probeV1 = jest.fn(() => {
43
+ if (v1) {
44
+ (transport as any).deviceProtocol.set(UUID, 'V1');
45
+ }
46
+ return Promise.resolve(v1);
47
+ });
48
+ const probeV2 = jest.fn(() => {
49
+ if (v2) {
50
+ (transport as any).deviceProtocol.set(UUID, 'V2');
51
+ }
52
+ return Promise.resolve(v2);
53
+ });
54
+ (transport as any).probeProtocolV1 = probeV1;
55
+ (transport as any).probeProtocolV2 = probeV2;
56
+ (transport as any).resetProbeStateAfterProtocolProbe = jest.fn(() => Promise.resolve());
57
+ return { transport, probeV1, probeV2 };
58
+ }
59
+
60
+ const detect = (transport: ReactNativeBleTransport) =>
61
+ (transport as any).detectProtocol(UUID, undefined, undefined, () =>
62
+ Promise.resolve()
63
+ ) as Promise<string>;
64
+
65
+ describe('protocol re-probe after a known device goes away', () => {
66
+ test('an unknown device still probes both protocols', async () => {
67
+ const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
68
+
69
+ await expect(detect(transport)).rejects.toBeDefined();
70
+
71
+ expect(probeV1).toHaveBeenCalledTimes(1);
72
+ expect(probeV2).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ test('a device already known to speak V1 does not pay the V2 probe while it is away', async () => {
76
+ const known = createHarness({ v1: true, v2: false });
77
+ // First detection confirms V1 (this is the session that just talked to the device).
78
+ await expect(detect(known.transport)).resolves.toBe('V1');
79
+
80
+ // The device now reboots after a firmware install: V1 stops answering.
81
+ (known.transport as any).deviceProtocol.delete(UUID);
82
+ const probeV1 = jest.fn(() => Promise.resolve(false));
83
+ const probeV2 = jest.fn(() => Promise.resolve(false));
84
+ (known.transport as any).probeProtocolV1 = probeV1;
85
+ (known.transport as any).probeProtocolV2 = probeV2;
86
+
87
+ await expect(detect(known.transport)).rejects.toBeDefined();
88
+
89
+ expect(probeV1).toHaveBeenCalledTimes(1);
90
+ // The 10s Protocol V2 ping is pure waste for a device we just spoke V1 to.
91
+ expect(probeV2).not.toHaveBeenCalled();
92
+ });
93
+
94
+ test('after repeated failures it re-probes every protocol again', async () => {
95
+ const known = createHarness({ v1: true, v2: false });
96
+ await expect(detect(known.transport)).resolves.toBe('V1');
97
+
98
+ (known.transport as any).deviceProtocol.delete(UUID);
99
+ const probeV1 = jest.fn(() => Promise.resolve(false));
100
+ const probeV2 = jest.fn(() => Promise.resolve(false));
101
+ (known.transport as any).probeProtocolV1 = probeV1;
102
+ (known.transport as any).probeProtocolV2 = probeV2;
103
+
104
+ for (let attempt = 0; attempt < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS; attempt += 1) {
105
+ // eslint-disable-next-line no-await-in-loop
106
+ await expect(detect(known.transport)).rejects.toBeDefined();
107
+ }
108
+ expect(probeV2).not.toHaveBeenCalled();
109
+
110
+ // The device may genuinely have changed protocol, so the shortcut must expire.
111
+ await expect(detect(known.transport)).rejects.toBeDefined();
112
+ expect(probeV2).toHaveBeenCalled();
113
+ });
114
+
115
+ test('a successful detection clears the failure streak', async () => {
116
+ const known = createHarness({ v1: true, v2: false });
117
+ await expect(detect(known.transport)).resolves.toBe('V1');
118
+ (known.transport as any).deviceProtocol.delete(UUID);
119
+
120
+ const failingV1 = jest.fn(() => Promise.resolve(false));
121
+ (known.transport as any).probeProtocolV1 = failingV1;
122
+ await expect(detect(known.transport)).rejects.toBeDefined();
123
+
124
+ // Device comes back.
125
+ (known.transport as any).probeProtocolV1 = jest.fn(() => {
126
+ (known.transport as any).deviceProtocol.set(UUID, 'V1');
127
+ return Promise.resolve(true);
128
+ });
129
+ await expect(detect(known.transport)).resolves.toBe('V1');
130
+
131
+ expect((known.transport as any).protocolReprobeFailures.get(UUID)).toBeUndefined();
132
+ });
133
+ });
package/src/index.ts CHANGED
@@ -199,13 +199,30 @@ const fallbackConnectOptions: Record<string, unknown> = {
199
199
  * inside the native budget, so this only fires when the native timeout did not.
200
200
  */
201
201
  export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
202
- /** Consecutive connect timeouts on one device before the BLE manager itself is recreated. */
202
+ /**
203
+ * Service discovery and characteristic resolution run after connect() succeeds, but
204
+ * CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
205
+ * device reboot, these calls can remain pending forever unless they have their own
206
+ * budget.
207
+ */
208
+ export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
209
+ /**
210
+ * How many times a known device may fail its own protocol before we probe the others
211
+ * again. Reconnect polling during a device reboot repeats this every few seconds, and
212
+ * probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
213
+ * device we just spoke V1 to dominates the wait. A firmware update can legitimately
214
+ * change a device's protocol, so the shortcut has to expire rather than stick.
215
+ */
216
+ export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
217
+ /** BLE setup timeouts since the last successful setup before the manager is recreated. */
203
218
  export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
204
219
  const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
205
220
  const isConnectTimeoutError = (error: unknown): boolean =>
206
221
  (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
207
222
  typeof (error as { message?: unknown })?.message === 'string' &&
208
223
  (error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
224
+ const isNativeOperationTimeoutError = (error: unknown): boolean =>
225
+ (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
209
226
 
210
227
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
211
228
 
@@ -306,11 +323,17 @@ export default class ReactNativeBleTransport {
306
323
  /** Consecutive write timeouts per device; reset by any write that completes. */
307
324
  private writeTimeoutCounts: Map<string, number> = new Map();
308
325
 
309
- /** Consecutive connect timeouts per device; reset by any connect that settles. */
310
- private connectTimeoutCounts: Map<string, number> = new Map();
326
+ /** BLE setup timeouts per device since the last complete characteristic resolution. */
327
+ private connectionSetupTimeoutCounts: Map<string, number> = new Map();
311
328
 
312
329
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
313
330
 
331
+ /** Protocol this device actually answered on, kept across reconnects of one session. */
332
+ private sessionProtocols: Map<string, ProtocolType> = new Map();
333
+
334
+ /** Consecutive detections that failed while trusting sessionProtocols. */
335
+ private protocolReprobeFailures: Map<string, number> = new Map();
336
+
314
337
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
315
338
 
316
339
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
@@ -541,9 +564,8 @@ export default class ReactNativeBleTransport {
541
564
  }
542
565
  }
543
566
 
544
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
545
- device
546
- );
567
+ const { writeCharacteristic, notifyCharacteristic } =
568
+ await this.resolveCharacteristicsWithTimeout(uuid, device);
547
569
 
548
570
  transport.device = device;
549
571
  transport.writeCharacteristic = writeCharacteristic;
@@ -708,7 +730,7 @@ export default class ReactNativeBleTransport {
708
730
  characteristics?: ResolvedBleCharacteristics
709
731
  ) {
710
732
  const { writeCharacteristic, notifyCharacteristic } =
711
- characteristics ?? (await this.resolveCharacteristics(device));
733
+ characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
712
734
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
713
735
  if (Platform.OS === 'android') {
714
736
  transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
@@ -883,9 +905,8 @@ export default class ReactNativeBleTransport {
883
905
 
884
906
  device = await requestAndroidMtu(device);
885
907
  const acquiredDevice = device;
886
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
887
- acquiredDevice
888
- );
908
+ const { writeCharacteristic, notifyCharacteristic } =
909
+ await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
889
910
 
890
911
  const protocolHint = expectedProtocol
891
912
  ? undefined
@@ -1482,6 +1503,8 @@ export default class ReactNativeBleTransport {
1482
1503
  this.deviceProtocol.delete(session);
1483
1504
  this.probingProtocols.delete(session);
1484
1505
  this.deviceProtocolHints.delete(session);
1506
+ this.sessionProtocols.delete(session);
1507
+ this.protocolReprobeFailures.delete(session);
1485
1508
  this.protocolV2Assemblers.delete(session);
1486
1509
  this.resetProtocolV2Frames(session);
1487
1510
 
@@ -1530,11 +1553,46 @@ export default class ReactNativeBleTransport {
1530
1553
  }, BLE_CONNECT_TIMEOUT_MS);
1531
1554
  }),
1532
1555
  ]);
1533
- this.connectTimeoutCounts.delete(uuid);
1534
1556
  return result;
1535
1557
  } catch (error) {
1536
- if (timedOut) {
1537
- this.abandonStalledConnect(uuid);
1558
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1559
+ this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
1560
+ }
1561
+ throw error;
1562
+ } finally {
1563
+ if (timer) clearTimeout(timer);
1564
+ }
1565
+ }
1566
+
1567
+ /** Resolve the complete GATT shape under a budget so acquire() always settles. */
1568
+ private async resolveCharacteristicsWithTimeout(
1569
+ uuid: string,
1570
+ device: Device
1571
+ ): Promise<ResolvedBleCharacteristics> {
1572
+ let timer: ReturnType<typeof setTimeout> | undefined;
1573
+ let timedOut = false;
1574
+ const pending = this.resolveCharacteristics(device);
1575
+ pending.catch(() => undefined);
1576
+ try {
1577
+ const result = await Promise.race([
1578
+ pending,
1579
+ new Promise<never>((_, reject) => {
1580
+ timer = setTimeout(() => {
1581
+ timedOut = true;
1582
+ reject(
1583
+ ERRORS.TypedError(
1584
+ HardwareErrorCode.BleConnectedError,
1585
+ `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
1586
+ )
1587
+ );
1588
+ }, BLE_GATT_SETUP_TIMEOUT_MS);
1589
+ }),
1590
+ ]);
1591
+ this.connectionSetupTimeoutCounts.delete(uuid);
1592
+ return result;
1593
+ } catch (error) {
1594
+ if (timedOut || isNativeOperationTimeoutError(error)) {
1595
+ this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1538
1596
  }
1539
1597
  throw error;
1540
1598
  } finally {
@@ -1543,16 +1601,19 @@ export default class ReactNativeBleTransport {
1543
1601
  }
1544
1602
 
1545
1603
  /**
1546
- * Give up on a connect the native layer never settled. The abandoned attempt still
1547
- * holds a native "connecting" entry that would cancel the NEXT attempt out from under
1548
- * itself, so it is cleared here fire and forget, because that call talks to the very
1549
- * queue that just stopped responding.
1604
+ * Give up on a BLE setup operation the native layer did not settle. The abandoned
1605
+ * operation still owns native connection/GATT state that can poison the next attempt,
1606
+ * so it is cleared here without awaiting the same queue that stopped responding.
1550
1607
  */
1551
- private abandonStalledConnect(uuid: string) {
1552
- const timeouts = (this.connectTimeoutCounts.get(uuid) ?? 0) + 1;
1553
- this.connectTimeoutCounts.set(uuid, timeouts);
1554
- Log?.error('[ReactNativeBleTransport] BLE connect timed out:', uuid, {
1555
- consecutiveConnectTimeouts: timeouts,
1608
+ private abandonStalledConnection(
1609
+ uuid: string,
1610
+ stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
1611
+ ) {
1612
+ const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
1613
+ this.connectionSetupTimeoutCounts.set(uuid, timeouts);
1614
+ Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
1615
+ stage,
1616
+ setupTimeoutsSinceSuccess: timeouts,
1556
1617
  });
1557
1618
 
1558
1619
  this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
@@ -1570,9 +1631,9 @@ export default class ReactNativeBleTransport {
1570
1631
  if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1571
1632
  // BleManager.destroy() force-rejects every promise the native queue abandoned —
1572
1633
  // the only JS-reachable way to settle them — and drops all cached peripherals.
1573
- Log?.error('[ReactNativeBleTransport] BLE connects wedged repeatedly, resetting BLE manager');
1634
+ Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
1574
1635
  this.resetPlxManager();
1575
- this.connectTimeoutCounts.delete(uuid);
1636
+ this.connectionSetupTimeoutCounts.delete(uuid);
1576
1637
  }
1577
1638
  }
1578
1639
 
@@ -1674,6 +1735,8 @@ export default class ReactNativeBleTransport {
1674
1735
  });
1675
1736
  this.deviceProtocol.clear();
1676
1737
  this.probingProtocols.clear();
1738
+ this.sessionProtocols.clear();
1739
+ this.protocolReprobeFailures.clear();
1677
1740
  this.monitorTokens.clear();
1678
1741
  this.protocolV2Assemblers.clear();
1679
1742
  try {
@@ -1720,6 +1783,7 @@ export default class ReactNativeBleTransport {
1720
1783
  if (expectedProtocol === 'V1') {
1721
1784
  if (await this.probeProtocolV1(uuid)) {
1722
1785
  this.deviceProtocol.set(uuid, 'V1');
1786
+ this.sessionProtocols.set(uuid, 'V1');
1723
1787
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1724
1788
  deviceId: uuid,
1725
1789
  protocol: 'V1',
@@ -1733,6 +1797,7 @@ export default class ReactNativeBleTransport {
1733
1797
  if (expectedProtocol === 'V2') {
1734
1798
  if (await this.probeProtocolV2(uuid)) {
1735
1799
  this.deviceProtocol.set(uuid, 'V2');
1800
+ this.sessionProtocols.set(uuid, 'V2');
1736
1801
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1737
1802
  deviceId: uuid,
1738
1803
  protocol: 'V2',
@@ -1745,8 +1810,18 @@ export default class ReactNativeBleTransport {
1745
1810
 
1746
1811
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
1747
1812
  // influence probe order; a V2 hint probes V2 first and falls back to V1.
1748
- const probeOrder: ProtocolType[] =
1813
+ const sessionProtocol = this.sessionProtocols.get(uuid);
1814
+ const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
1815
+ const fullProbeOrder: ProtocolType[] =
1749
1816
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1817
+ // A device that already answered on a protocol in this session keeps answering on
1818
+ // it; while it is rebooting nothing answers at all, so probing the other protocol
1819
+ // only adds its timeout to every poll.
1820
+ const trustSessionProtocol =
1821
+ sessionProtocol !== undefined &&
1822
+ !protocolHint &&
1823
+ reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1824
+ const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1750
1825
 
1751
1826
  for (let i = 0; i < probeOrder.length; i += 1) {
1752
1827
  const protocol = probeOrder[i];
@@ -1764,6 +1839,8 @@ export default class ReactNativeBleTransport {
1764
1839
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1765
1840
  if (detected) {
1766
1841
  this.deviceProtocol.set(uuid, protocol);
1842
+ this.sessionProtocols.set(uuid, protocol);
1843
+ this.protocolReprobeFailures.delete(uuid);
1767
1844
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1768
1845
  deviceId: uuid,
1769
1846
  protocol,
@@ -1773,6 +1850,14 @@ export default class ReactNativeBleTransport {
1773
1850
  }
1774
1851
  }
1775
1852
 
1853
+ if (trustSessionProtocol) {
1854
+ // Still silent on its own protocol: count it, and let the streak expire the
1855
+ // shortcut so a device that genuinely switched protocols is found again.
1856
+ this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1857
+ } else {
1858
+ this.protocolReprobeFailures.delete(uuid);
1859
+ }
1860
+
1776
1861
  this.deviceProtocol.delete(uuid);
1777
1862
  this.probingProtocols.delete(uuid);
1778
1863
  throw this.createProtocolDetectionError();