@onekeyfe/hd-transport-react-native 1.2.2-alpha.113 → 1.2.2-alpha.115

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
@@ -135,6 +135,15 @@ declare class ReactNativeBleTransport {
135
135
  private confirmedProtocolV2;
136
136
  /** Consecutive detections that failed while trusting sessionProtocols. */
137
137
  private protocolReprobeFailures;
138
+ /** Endpoints whose last detection got no answer on any protocol. */
139
+ private silentDetections;
140
+ /**
141
+ * Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
142
+ * on the device, and a failed detection tears the link down and reconnects on every
143
+ * poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
144
+ * detection succeeds, which re-arms it for the next time the device sleeps.
145
+ */
146
+ private protocolWakeAttempts;
138
147
  /**
139
148
  * Native encryption/pairing failures seen before Protocol V2 probe starts.
140
149
  * Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
@@ -228,6 +237,23 @@ declare class ReactNativeBleTransport {
228
237
  /** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
229
238
  private getActiveProtocol;
230
239
  private detectProtocol;
240
+ /**
241
+ * Sent once before probing when the previous detection on this endpoint got no answer
242
+ * at all. On Classic-family firmware that silence is also what a sleeping device looks
243
+ * like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
244
+ * sits in the screensaver loop, whose host-message filter accepts only Initialize and
245
+ * the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
246
+ * amount of probing can bring the device back.
247
+ *
248
+ * Initialize is the one message that breaks that loop, but it also starts a fresh
249
+ * wallet session, which is why it is not the probe itself — a bare Initialize on every
250
+ * acquire would drop a hidden-wallet session before Core can restore it. Requiring a
251
+ * fully silent previous detection keeps it to devices with no session left to protect.
252
+ *
253
+ * The firmware consumes the wake to leave its loop and does not reliably answer it, so
254
+ * the result is ignored: the probes that follow are what decide the protocol.
255
+ */
256
+ private wakeSilentProtocolV1Device;
231
257
  private resetProbeStateAfterProtocolProbe;
232
258
  private probeProtocolV1;
233
259
  private probeProtocolV2;
@@ -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
@@ -446,6 +446,10 @@ const isWedgedBleSetupError = (error) => (error === null || error === void 0 ? v
446
446
  error.message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
447
447
  const shouldRethrowBleSetupError = (error) => isConnectTimeoutError(error) || isWedgedBleSetupError(error);
448
448
  const isNativeOperationTimeoutError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === reactNativeBlePlx.BleErrorCode.OperationTimedOut;
449
+ const isAbandonedNativeOperationError = (error) => {
450
+ const errorCode = error === null || error === void 0 ? void 0 : error.errorCode;
451
+ return (errorCode === reactNativeBlePlx.BleErrorCode.OperationTimedOut || errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled);
452
+ };
449
453
  const tryToGetConfiguration = (device) => {
450
454
  if (!device || !device.serviceUUIDs)
451
455
  return null;
@@ -459,12 +463,13 @@ const tryToGetConfiguration = (device) => {
459
463
  };
460
464
  const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __awaiter(void 0, void 0, void 0, function* () {
461
465
  if (reactNative.Platform.OS !== 'ios' && reactNative.Platform.OS !== 'android')
462
- return device;
466
+ return { device, abandoned: false };
463
467
  const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
464
468
  let timeoutId;
465
469
  let timedOut = false;
466
470
  try {
467
471
  const request = device.requestMTU(getRequestedBleMtu(), transactionId);
472
+ request.catch(() => undefined);
468
473
  const mtuDevice = yield Promise.race([
469
474
  request,
470
475
  new Promise((_, reject) => {
@@ -474,7 +479,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
474
479
  }, BLE_MTU_REQUEST_TIMEOUT_MS);
475
480
  }),
476
481
  ]);
477
- return mtuDevice;
482
+ return { device: mtuDevice, abandoned: false };
478
483
  }
479
484
  catch (error) {
480
485
  if (timedOut && cancelTransaction) {
@@ -504,7 +509,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
504
509
  actual: device.mtu,
505
510
  error: error instanceof Error ? error.message : String(error),
506
511
  });
507
- return device;
512
+ return { device, abandoned: timedOut };
508
513
  }
509
514
  finally {
510
515
  if (timeoutId)
@@ -513,7 +518,7 @@ const requestNegotiatedMtu = (device, stage, attempt, cancelTransaction) => __aw
513
518
  });
514
519
  const resolveNegotiatedMtu = (device, cancelTransaction) => shouldRefreshNegotiatedMtu(device.mtu)
515
520
  ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
516
- : Promise.resolve(device);
521
+ : Promise.resolve({ device, abandoned: false });
517
522
  function remapError(error) {
518
523
  var _a;
519
524
  if (error instanceof reactNativeBlePlx.BleError) {
@@ -550,6 +555,8 @@ class ReactNativeBleTransport {
550
555
  this.sessionProtocols = new Map();
551
556
  this.confirmedProtocolV2 = new Set();
552
557
  this.protocolReprobeFailures = new Map();
558
+ this.silentDetections = new Set();
559
+ this.protocolWakeAttempts = new Set();
553
560
  this.staleBondErrors = new Map();
554
561
  this.acquiringProtocolV2 = new Set();
555
562
  this.protocolV2Assemblers = new Map();
@@ -1036,6 +1043,7 @@ class ReactNativeBleTransport {
1036
1043
  }
1037
1044
  }
1038
1045
  let device = null;
1046
+ let mtuHandshakeRefused = false;
1039
1047
  if (forceCleanRunPromise && this.runPromise) {
1040
1048
  const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise);
1041
1049
  this.runPromise.reject(error);
@@ -1095,6 +1103,8 @@ class ReactNativeBleTransport {
1095
1103
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
1096
1104
  e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
1097
1105
  Log === null || Log === void 0 ? void 0 : Log.debug('first try to reconnect without params');
1106
+ mtuHandshakeRefused = true;
1107
+ yield this.runBestEffortNativeOperation('connect cancelled: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
1098
1108
  device = yield this.connectWithTimeout(uuid, () => blePlxManager.connectToDevice(uuid, fallbackConnectOptions));
1099
1109
  }
1100
1110
  else if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceAlreadyConnected) {
@@ -1123,6 +1133,8 @@ class ReactNativeBleTransport {
1123
1133
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceMTUChangeFailed ||
1124
1134
  e.errorCode === reactNativeBlePlx.BleErrorCode.OperationCancelled) {
1125
1135
  Log === null || Log === void 0 ? void 0 : Log.debug('second try to reconnect without params');
1136
+ mtuHandshakeRefused = true;
1137
+ yield this.runBestEffortNativeOperation('connect cancelled: cancel device connection', () => disconnectedDevice.cancelConnection());
1126
1138
  try {
1127
1139
  device = yield this.connectWithTimeout(uuid, () => disconnectedDevice.connect(fallbackConnectOptions));
1128
1140
  }
@@ -1157,7 +1169,22 @@ class ReactNativeBleTransport {
1157
1169
  }
1158
1170
  if (this.stopped)
1159
1171
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1160
- device = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
1172
+ if (mtuHandshakeRefused) {
1173
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] skipping MTU refresh after cancelled MTU connect', {
1174
+ connectIdSuffix: uuid.slice(-8),
1175
+ actual: device.mtu,
1176
+ });
1177
+ }
1178
+ else {
1179
+ const negotiatedMtu = yield resolveNegotiatedMtu(device, transactionId => blePlxManager.cancelTransaction(transactionId));
1180
+ device = negotiatedMtu.device;
1181
+ if (negotiatedMtu.abandoned) {
1182
+ yield this.runNativeTeardown(uuid, blePlxManager, () => __awaiter(this, void 0, void 0, function* () {
1183
+ yield this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () => blePlxManager.cancelDeviceConnection(uuid));
1184
+ }));
1185
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'BLE MTU negotiation abandoned, reconnecting');
1186
+ }
1187
+ }
1161
1188
  if (this.stopped)
1162
1189
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
1163
1190
  const acquiredDevice = device;
@@ -1872,7 +1899,7 @@ class ReactNativeBleTransport {
1872
1899
  return result;
1873
1900
  }
1874
1901
  catch (error) {
1875
- if (timedOut || isNativeOperationTimeoutError(error)) {
1902
+ if (timedOut || isAbandonedNativeOperationError(error)) {
1876
1903
  const resetManager = this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1877
1904
  if (resetManager) {
1878
1905
  throw this.createWedgedBleSetupError();
@@ -2029,6 +2056,8 @@ class ReactNativeBleTransport {
2029
2056
  this.acquiringProtocolV2.clear();
2030
2057
  this.sessionProtocols.clear();
2031
2058
  this.protocolReprobeFailures.clear();
2059
+ this.silentDetections.clear();
2060
+ this.protocolWakeAttempts.clear();
2032
2061
  this.writeTimeoutCounts.clear();
2033
2062
  this.connectionSetupTimeoutCounts.clear();
2034
2063
  this.monitorTokens.clear();
@@ -2102,6 +2131,7 @@ class ReactNativeBleTransport {
2102
2131
  !protocolHint &&
2103
2132
  reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
2104
2133
  const probeOrder = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
2134
+ yield this.wakeSilentProtocolV1Device(uuid, probeOrder);
2105
2135
  for (let i = 0; i < probeOrder.length; i += 1) {
2106
2136
  const protocol = probeOrder[i];
2107
2137
  if (i > 0) {
@@ -2121,6 +2151,8 @@ class ReactNativeBleTransport {
2121
2151
  this.confirmedProtocolV2.add(uuid);
2122
2152
  }
2123
2153
  this.protocolReprobeFailures.delete(uuid);
2154
+ this.silentDetections.delete(uuid);
2155
+ this.protocolWakeAttempts.delete(uuid);
2124
2156
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol detected', {
2125
2157
  deviceId: uuid,
2126
2158
  protocol,
@@ -2129,6 +2161,7 @@ class ReactNativeBleTransport {
2129
2161
  return protocol;
2130
2162
  }
2131
2163
  }
2164
+ this.silentDetections.add(uuid);
2132
2165
  if (trustSessionProtocol) {
2133
2166
  this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
2134
2167
  }
@@ -2140,6 +2173,38 @@ class ReactNativeBleTransport {
2140
2173
  throw this.createProtocolDetectionError();
2141
2174
  });
2142
2175
  }
2176
+ wakeSilentProtocolV1Device(uuid, probeOrder) {
2177
+ return __awaiter(this, void 0, void 0, function* () {
2178
+ if (!this._messages)
2179
+ return;
2180
+ if (!probeOrder.includes('V1'))
2181
+ return;
2182
+ if (!this.silentDetections.has(uuid))
2183
+ return;
2184
+ if (this.protocolWakeAttempts.has(uuid))
2185
+ return;
2186
+ if (!transportCache[uuid])
2187
+ return;
2188
+ this.protocolWakeAttempts.add(uuid);
2189
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
2190
+ connectIdSuffix: uuid.slice(-8),
2191
+ });
2192
+ try {
2193
+ this.probingProtocols.set(uuid, 'V1');
2194
+ yield this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2195
+ }
2196
+ catch (error) {
2197
+ if (shouldRethrowProtocolProbeError(error)) {
2198
+ this.clearProbeProtocol(uuid, 'V1');
2199
+ throw error;
2200
+ }
2201
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
2202
+ }
2203
+ finally {
2204
+ this.clearProbeProtocol(uuid, 'V1');
2205
+ }
2206
+ });
2207
+ }
2143
2208
  resetProbeStateAfterProtocolProbe(uuid, protocol) {
2144
2209
  var _a, _b, _c;
2145
2210
  return __awaiter(this, void 0, void 0, function* () {
@@ -2457,11 +2522,11 @@ class ReactNativeBleTransport {
2457
2522
  const transport = this.getCachedTransport(uuid);
2458
2523
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize))
2459
2524
  return;
2460
- const refreshedDevice = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
2525
+ const { device: refreshedDevice, abandoned } = yield requestNegotiatedMtu(transport.device, 'highThroughput', 1, transactionId => { var _a; return (_a = this.blePlxManager) === null || _a === void 0 ? void 0 : _a.cancelTransaction(transactionId); });
2461
2526
  transport.device = refreshedDevice;
2462
2527
  transport.mtuSize =
2463
2528
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
2464
- if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2529
+ if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2465
2530
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`);
2466
2531
  }
2467
2532
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.2-alpha.113",
3
+ "version": "1.2.2-alpha.115",
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.2-alpha.113",
24
- "@onekeyfe/hd-shared": "1.2.2-alpha.113",
25
- "@onekeyfe/hd-transport": "1.2.2-alpha.113",
23
+ "@onekeyfe/hd-core": "1.2.2-alpha.115",
24
+ "@onekeyfe/hd-shared": "1.2.2-alpha.115",
25
+ "@onekeyfe/hd-transport": "1.2.2-alpha.115",
26
26
  "@onekeyfe/react-native-ble-utils": "0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "32601b82b280b9ec97161cb4206a15cf9cd72881"
29
+ "gitHead": "a115ca6998586c05c22996f278091778fd5471f1"
30
30
  }
@@ -1126,21 +1126,85 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
1126
1126
  await transport.release(uuid, true);
1127
1127
  });
1128
1128
 
1129
- test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
1129
+ test('drops the link when a stalled MTU negotiation is abandoned', async () => {
1130
1130
  const { transport, uuid, device, bleManager } = createHarness();
1131
1131
  device.mtu = 23;
1132
1132
  device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
1133
+ const { resolveCharacteristics } = transport as any;
1133
1134
 
1134
- await expect(transport.acquire({ uuid })).resolves.toEqual({
1135
- uuid,
1136
- protocolType: 'V2',
1135
+ // cancelTransaction only disposes the JS subscription: RxAndroidBle releases the
1136
+ // connection's serial queue from onComplete/onError alone, so the abandoned exchange
1137
+ // keeps it until its own native timeout. Service discovery must not queue behind it.
1138
+ await expect(transport.acquire({ uuid })).rejects.toMatchObject({
1139
+ errorCode: HardwareErrorCode.BleConnectedError,
1137
1140
  });
1138
1141
  const transactionId = device.requestMTU.mock.calls[0]?.[1];
1139
1142
  expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
1140
1143
  expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
1144
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
1145
+ expect(resolveCharacteristics).not.toHaveBeenCalled();
1146
+ }, 10_000);
1147
+
1148
+ test('skips the MTU refresh after an MTU-bearing connect was cancelled', async () => {
1149
+ const { transport, uuid, device } = createHarness();
1150
+ device.mtu = 23;
1151
+ let connected = false;
1152
+ device.isConnected.mockImplementation(() => Promise.resolve(connected));
1153
+ const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
1154
+ device.connect = jest
1155
+ .fn()
1156
+ .mockRejectedValueOnce(cancelled)
1157
+ .mockImplementation(() => {
1158
+ connected = true;
1159
+ return Promise.resolve(device);
1160
+ });
1161
+
1162
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
1163
+ uuid,
1164
+ protocolType: 'V2',
1165
+ });
1166
+
1167
+ // ble-plx applies the connect timeout to establishConnection -> refreshGatt ->
1168
+ // requestMtu as one chain, so the fallback means this peripheral did not finish the
1169
+ // MTU exchange. Asking again would only park a second abandoned exchange.
1170
+ expect(device.connect).toHaveBeenCalledTimes(2);
1171
+ expect(device.requestMTU).not.toHaveBeenCalled();
1141
1172
  expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
1142
1173
  await transport.release(uuid, true);
1143
- }, 10_000);
1174
+ });
1175
+
1176
+ test('cleans up a cancelled native GATT setup, not only a timed-out one', async () => {
1177
+ const { transport, uuid, bleManager } = createHarness();
1178
+ const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
1179
+ (transport as any).resolveCharacteristics = jest.fn(() => Promise.reject(cancelled));
1180
+
1181
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1182
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
1183
+ });
1184
+
1185
+ test('spends one Initialize wake on the detection after a fully silent one', async () => {
1186
+ const { transport, uuid } = createHarness();
1187
+ const probes = transport as any;
1188
+ jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
1189
+ jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
1190
+ const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
1191
+
1192
+ // Nothing has gone silent yet, so the device keeps whatever session it has.
1193
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1194
+ expect(callProtocolV1).not.toHaveBeenCalled();
1195
+
1196
+ // The previous detection answered on no protocol, which is what a sleeping Classic
1197
+ // looks like, so this one wakes on the fresh link before probing.
1198
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1199
+ expect(callProtocolV1).toHaveBeenCalledTimes(1);
1200
+ expect(callProtocolV1.mock.calls[0]?.[1]).toBe('Initialize');
1201
+
1202
+ // Still silent: the wake is not repeated, so a device that is simply away is not
1203
+ // pushed into a fresh wallet session on every poll.
1204
+ callProtocolV1.mockClear();
1205
+ await expect(transport.acquire({ uuid })).rejects.toBeDefined();
1206
+ expect(callProtocolV1).not.toHaveBeenCalled();
1207
+ });
1144
1208
 
1145
1209
  test('accepts a stable low MTU without the delayed refresh loop', async () => {
1146
1210
  const { transport, uuid, device } = createHarness();
package/src/index.ts CHANGED
@@ -300,6 +300,21 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
300
300
  isConnectTimeoutError(error) || isWedgedBleSetupError(error);
301
301
  const isNativeOperationTimeoutError = (error: unknown): boolean =>
302
302
  (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
303
+ /**
304
+ * ble-plx reports its own expired budget as OperationCancelled, not OperationTimedOut:
305
+ * safeConnectToDevice disposes the operation chain and its doFinally reports a
306
+ * cancellation. Either code means the native layer abandoned work that still owned
307
+ * connection/GATT state, so both have to go through the stalled-connection cleanup.
308
+ * Only used where nothing else recovers the link — the connect path does its own
309
+ * ordered teardown before retrying, and routing it here too would both duplicate that
310
+ * and spend the manager-reset budget on a connect that is about to be retried.
311
+ */
312
+ const isAbandonedNativeOperationError = (error: unknown): boolean => {
313
+ const errorCode = (error as { errorCode?: unknown })?.errorCode;
314
+ return (
315
+ errorCode === BleErrorCode.OperationTimedOut || errorCode === BleErrorCode.OperationCancelled
316
+ );
317
+ };
303
318
 
304
319
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
305
320
 
@@ -312,13 +327,26 @@ const tryToGetConfiguration = (device: Device) => {
312
327
  return infos;
313
328
  };
314
329
 
330
+ type NegotiatedMtuResult = {
331
+ device: Device;
332
+ /**
333
+ * True when the bounded wait gave up while the native MTU exchange was still
334
+ * outstanding. `cancelTransaction` only disposes the JS-side subscription:
335
+ * RxAndroidBle releases a connection's serial operation queue from onComplete /
336
+ * onError alone, so an abandoned exchange keeps that queue until its own native
337
+ * timeout. Any GATT work issued before then — service discovery above all —
338
+ * queues behind it and cannot settle inside its own budget.
339
+ */
340
+ abandoned: boolean;
341
+ };
342
+
315
343
  const requestNegotiatedMtu = async (
316
344
  device: Device,
317
345
  stage: 'connected' | 'highThroughput',
318
346
  attempt: number,
319
347
  cancelTransaction?: (transactionId: string) => Promise<void> | void
320
- ) => {
321
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
348
+ ): Promise<NegotiatedMtuResult> => {
349
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, abandoned: false };
322
350
 
323
351
  const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
324
352
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
@@ -328,6 +356,10 @@ const requestNegotiatedMtu = async (
328
356
  // iOS ignores the requested value but react-native-ble-plx returns a fresh
329
357
  // Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
330
358
  const request = device.requestMTU(getRequestedBleMtu(), transactionId);
359
+ // The timeout race may settle before the native request does. Attach a
360
+ // rejection handler so a late native cancellation cannot become an
361
+ // unhandled rejection after we continue with the current MTU.
362
+ request.catch(() => undefined);
331
363
  const mtuDevice = await Promise.race([
332
364
  request,
333
365
  new Promise<never>((_, reject) => {
@@ -337,7 +369,7 @@ const requestNegotiatedMtu = async (
337
369
  }, BLE_MTU_REQUEST_TIMEOUT_MS);
338
370
  }),
339
371
  ]);
340
- return mtuDevice;
372
+ return { device: mtuDevice, abandoned: false };
341
373
  } catch (error) {
342
374
  if (timedOut && cancelTransaction) {
343
375
  try {
@@ -365,7 +397,7 @@ const requestNegotiatedMtu = async (
365
397
  actual: device.mtu,
366
398
  error: error instanceof Error ? error.message : String(error),
367
399
  });
368
- return device;
400
+ return { device, abandoned: timedOut };
369
401
  } finally {
370
402
  if (timeoutId) clearTimeout(timeoutId);
371
403
  }
@@ -374,10 +406,10 @@ const requestNegotiatedMtu = async (
374
406
  const resolveNegotiatedMtu = (
375
407
  device: Device,
376
408
  cancelTransaction?: (transactionId: string) => Promise<void> | void
377
- ) =>
409
+ ): Promise<NegotiatedMtuResult> =>
378
410
  shouldRefreshNegotiatedMtu(device.mtu)
379
411
  ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
380
- : Promise.resolve(device);
412
+ : Promise.resolve({ device, abandoned: false });
381
413
 
382
414
  type IOBleErrorRemap = Error | BleError | null | undefined;
383
415
 
@@ -461,6 +493,17 @@ export default class ReactNativeBleTransport {
461
493
  /** Consecutive detections that failed while trusting sessionProtocols. */
462
494
  private protocolReprobeFailures: Map<string, number> = new Map();
463
495
 
496
+ /** Endpoints whose last detection got no answer on any protocol. */
497
+ private silentDetections = new Set<string>();
498
+
499
+ /**
500
+ * Endpoints already sent a Protocol V1 wake. The wake starts a fresh wallet session
501
+ * on the device, and a failed detection tears the link down and reconnects on every
502
+ * poll, so neither set is cleared on teardown: one wake is spent per endpoint until a
503
+ * detection succeeds, which re-arms it for the next time the device sleeps.
504
+ */
505
+ private protocolWakeAttempts = new Set<string>();
506
+
464
507
  /**
465
508
  * Native encryption/pairing failures seen before Protocol V2 probe starts.
466
509
  * Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
@@ -1058,6 +1101,14 @@ export default class ReactNativeBleTransport {
1058
1101
  }
1059
1102
 
1060
1103
  let device: Device | null = null;
1104
+ /**
1105
+ * Set when an MTU-bearing connect had to fall back to `fallbackConnectOptions`.
1106
+ * ble-plx applies `connectOptions.timeout` to the whole establishConnection ->
1107
+ * refreshGatt -> requestMtu chain, so that fallback means the peripheral did not
1108
+ * finish the MTU exchange on this link. Asking again below would only park a
1109
+ * second abandoned exchange on the connection's serial queue.
1110
+ */
1111
+ let mtuHandshakeRefused = false;
1061
1112
 
1062
1113
  if (forceCleanRunPromise && this.runPromise) {
1063
1114
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
@@ -1124,6 +1175,13 @@ export default class ReactNativeBleTransport {
1124
1175
  e.errorCode === BleErrorCode.OperationCancelled
1125
1176
  ) {
1126
1177
  Log?.debug('first try to reconnect without params');
1178
+ mtuHandshakeRefused = true;
1179
+ // The disposed chain can still own native connection state, and the retry
1180
+ // would otherwise run on top of a half-open GATT client.
1181
+ await this.runBestEffortNativeOperation(
1182
+ 'connect cancelled: cancel manager connection',
1183
+ () => blePlxManager.cancelDeviceConnection(uuid)
1184
+ );
1127
1185
  device = await this.connectWithTimeout(uuid, () =>
1128
1186
  blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
1129
1187
  );
@@ -1158,6 +1216,13 @@ export default class ReactNativeBleTransport {
1158
1216
  e.errorCode === BleErrorCode.OperationCancelled
1159
1217
  ) {
1160
1218
  Log?.debug('second try to reconnect without params');
1219
+ mtuHandshakeRefused = true;
1220
+ // The disposed chain can still own native connection state, and the retry
1221
+ // would otherwise run on top of a half-open GATT client.
1222
+ await this.runBestEffortNativeOperation(
1223
+ 'connect cancelled: cancel device connection',
1224
+ () => disconnectedDevice.cancelConnection()
1225
+ );
1161
1226
  try {
1162
1227
  device = await this.connectWithTimeout(uuid, () =>
1163
1228
  disconnectedDevice.connect(fallbackConnectOptions)
@@ -1198,9 +1263,30 @@ export default class ReactNativeBleTransport {
1198
1263
  throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
1199
1264
  }
1200
1265
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1201
- device = await resolveNegotiatedMtu(device, transactionId =>
1202
- blePlxManager.cancelTransaction(transactionId)
1203
- );
1266
+ if (mtuHandshakeRefused) {
1267
+ Log?.debug('[ReactNativeBleTransport] skipping MTU refresh after cancelled MTU connect', {
1268
+ connectIdSuffix: uuid.slice(-8),
1269
+ actual: device.mtu,
1270
+ });
1271
+ } else {
1272
+ const negotiatedMtu = await resolveNegotiatedMtu(device, transactionId =>
1273
+ blePlxManager.cancelTransaction(transactionId)
1274
+ );
1275
+ device = negotiatedMtu.device;
1276
+ if (negotiatedMtu.abandoned) {
1277
+ // Service discovery would queue behind the abandoned exchange and miss its own
1278
+ // budget. Drop the link so the next attempt starts on a queue nothing holds.
1279
+ await this.runNativeTeardown(uuid, blePlxManager, async () => {
1280
+ await this.runBestEffortNativeOperation('mtu abandoned: cancel manager connection', () =>
1281
+ blePlxManager.cancelDeviceConnection(uuid)
1282
+ );
1283
+ });
1284
+ throw ERRORS.TypedError(
1285
+ HardwareErrorCode.BleConnectedError,
1286
+ 'BLE MTU negotiation abandoned, reconnecting'
1287
+ );
1288
+ }
1289
+ }
1204
1290
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1205
1291
  const acquiredDevice = device;
1206
1292
  const { writeCharacteristic, notifyCharacteristic } =
@@ -2096,7 +2182,7 @@ export default class ReactNativeBleTransport {
2096
2182
  succeeded = true;
2097
2183
  return result;
2098
2184
  } catch (error) {
2099
- if (timedOut || isNativeOperationTimeoutError(error)) {
2185
+ if (timedOut || isAbandonedNativeOperationError(error)) {
2100
2186
  const resetManager = this.abandonStalledConnection(
2101
2187
  uuid,
2102
2188
  timedOut ? 'gatt-backstop' : 'gatt-native'
@@ -2300,6 +2386,8 @@ export default class ReactNativeBleTransport {
2300
2386
  // Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
2301
2387
  // firmware reconnect after the native BLE manager is recreated.
2302
2388
  this.protocolReprobeFailures.clear();
2389
+ this.silentDetections.clear();
2390
+ this.protocolWakeAttempts.clear();
2303
2391
  this.writeTimeoutCounts.clear();
2304
2392
  this.connectionSetupTimeoutCounts.clear();
2305
2393
  this.monitorTokens.clear();
@@ -2411,6 +2499,8 @@ export default class ReactNativeBleTransport {
2411
2499
  reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
2412
2500
  const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
2413
2501
 
2502
+ await this.wakeSilentProtocolV1Device(uuid, probeOrder);
2503
+
2414
2504
  for (let i = 0; i < probeOrder.length; i += 1) {
2415
2505
  const protocol = probeOrder[i];
2416
2506
  if (i > 0) {
@@ -2432,6 +2522,8 @@ export default class ReactNativeBleTransport {
2432
2522
  this.confirmedProtocolV2.add(uuid);
2433
2523
  }
2434
2524
  this.protocolReprobeFailures.delete(uuid);
2525
+ this.silentDetections.delete(uuid);
2526
+ this.protocolWakeAttempts.delete(uuid);
2435
2527
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
2436
2528
  deviceId: uuid,
2437
2529
  protocol,
@@ -2441,6 +2533,10 @@ export default class ReactNativeBleTransport {
2441
2533
  }
2442
2534
  }
2443
2535
 
2536
+ // Nothing answered on any probed protocol. Remember it so the next detection, which
2537
+ // the caller reaches on a freshly reconnected link, can spend a wake first.
2538
+ this.silentDetections.add(uuid);
2539
+
2444
2540
  if (trustSessionProtocol) {
2445
2541
  // Still silent on its own protocol: count it, and let the streak expire the
2446
2542
  // shortcut so a device that genuinely switched protocols is found again.
@@ -2454,6 +2550,47 @@ export default class ReactNativeBleTransport {
2454
2550
  throw this.createProtocolDetectionError();
2455
2551
  }
2456
2552
 
2553
+ /**
2554
+ * Sent once before probing when the previous detection on this endpoint got no answer
2555
+ * at all. On Classic-family firmware that silence is also what a sleeping device looks
2556
+ * like: its BLE co-processor keeps serving connect, GATT and MTU while the main MCU
2557
+ * sits in the screensaver loop, whose host-message filter accepts only Initialize and
2558
+ * the *Ack messages. GetFeatures and Ping are dropped there without a reply, so no
2559
+ * amount of probing can bring the device back.
2560
+ *
2561
+ * Initialize is the one message that breaks that loop, but it also starts a fresh
2562
+ * wallet session, which is why it is not the probe itself — a bare Initialize on every
2563
+ * acquire would drop a hidden-wallet session before Core can restore it. Requiring a
2564
+ * fully silent previous detection keeps it to devices with no session left to protect.
2565
+ *
2566
+ * The firmware consumes the wake to leave its loop and does not reliably answer it, so
2567
+ * the result is ignored: the probes that follow are what decide the protocol.
2568
+ */
2569
+ private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
2570
+ if (!this._messages) return;
2571
+ if (!probeOrder.includes('V1')) return;
2572
+ if (!this.silentDetections.has(uuid)) return;
2573
+ if (this.protocolWakeAttempts.has(uuid)) return;
2574
+ if (!transportCache[uuid]) return;
2575
+ this.protocolWakeAttempts.add(uuid);
2576
+
2577
+ Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
2578
+ connectIdSuffix: uuid.slice(-8),
2579
+ });
2580
+ try {
2581
+ this.probingProtocols.set(uuid, 'V1');
2582
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2583
+ } catch (error) {
2584
+ if (shouldRethrowProtocolProbeError(error)) {
2585
+ this.clearProbeProtocol(uuid, 'V1');
2586
+ throw error;
2587
+ }
2588
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize wake did not answer:', error);
2589
+ } finally {
2590
+ this.clearProbeProtocol(uuid, 'V1');
2591
+ }
2592
+ }
2593
+
2457
2594
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
2458
2595
  const transport = transportCache[uuid];
2459
2596
  await this.protocolV2Links.invalidateLink(
@@ -2833,7 +2970,7 @@ export default class ReactNativeBleTransport {
2833
2970
  const transport = this.getCachedTransport(uuid);
2834
2971
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
2835
2972
 
2836
- const refreshedDevice = await requestNegotiatedMtu(
2973
+ const { device: refreshedDevice, abandoned } = await requestNegotiatedMtu(
2837
2974
  transport.device,
2838
2975
  'highThroughput',
2839
2976
  1,
@@ -2843,7 +2980,7 @@ export default class ReactNativeBleTransport {
2843
2980
  transport.mtuSize =
2844
2981
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
2845
2982
 
2846
- if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2983
+ if (abandoned || shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2847
2984
  throw ERRORS.TypedError(
2848
2985
  HardwareErrorCode.BleConnectedError,
2849
2986
  `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`