@onekeyfe/hd-transport-react-native 1.2.2-alpha.119 → 1.2.2-alpha.120

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/src/index.ts CHANGED
@@ -242,13 +242,13 @@ function getDeviceDisplayName(device?: Device | null) {
242
242
 
243
243
  const IOS_REQUEST_MTU = 247;
244
244
  const ANDROID_REQUEST_MTU = 517;
245
+ const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
245
246
  const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
246
247
 
247
248
  const getRequestedBleMtu = () =>
248
249
  Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
249
250
 
250
251
  const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
251
- const BLE_MTU_REQUEST_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS;
252
252
 
253
253
  const connectOptions: Record<string, unknown> = {
254
254
  requestMTU: getRequestedBleMtu(),
@@ -256,63 +256,11 @@ const connectOptions: Record<string, unknown> = {
256
256
  refreshGatt: 'OnConnected',
257
257
  };
258
258
 
259
- /** Connect options without requestMTU: the iOS fallback and every bare Android connect. */
259
+ /** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
260
260
  const fallbackConnectOptions: Record<string, unknown> = {
261
261
  timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
262
262
  };
263
263
 
264
- /**
265
- * Android never requests the MTU inside the native connect budget: refreshGatt makes the stack
266
- * rediscover first, and a budget that expires with the MTU request unsent parks every later MTU
267
- * request on that LE link. refreshGatt itself is only added after a firmware install or a
268
- * stale-table symptom, and discovery finishes before the MTU exchange.
269
- */
270
- const androidRefreshGattConnectOptions: Record<string, unknown> = {
271
- timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
272
- refreshGatt: 'OnConnected',
273
- };
274
-
275
- /**
276
- * With no cached GATT table the stack runs its own discovery (up to ~8s) before the MTU
277
- * exchange, so the bound sits above that; a stuck exchange never completes.
278
- */
279
- export const ANDROID_MTU_EXCHANGE_TIMEOUT_MS = 12_000;
280
-
281
- /**
282
- * Android keeps an LE link, with per-link ATT state such as a pending MTU exchange, for its
283
- * 4s GATT link idle timer after the last client closes; a reconnect inside it reuses the link.
284
- */
285
- export const ANDROID_LINK_DROP_QUIET_MS = 5000;
286
- const ANDROID_LINK_DROP_POLL_MS = 250;
287
-
288
- /**
289
- * Android cannot use a link at the default 23-byte ATT MTU: Protocol V1 writes 192-byte
290
- * packets regardless, and a Pro 2 sends only the first ATT_MTU-3 bytes of a V2 reply.
291
- * An unknown MTU is not treated as default.
292
- */
293
- const isKnownDefaultMtu = (mtu: unknown): boolean =>
294
- typeof mtu === 'number' && Number.isFinite(mtu) && mtu <= 23;
295
-
296
- /** Discovery found no OneKey service, or a characteristic of the wrong shape: the cached GATT table may be stale. */
297
- const isMissingGattShapeError = (error: unknown): boolean => {
298
- const code = (error as { errorCode?: unknown })?.errorCode;
299
- const message = (error as { message?: unknown })?.message;
300
- return (
301
- code === HardwareErrorCode.BleServiceNotFound ||
302
- code === HardwareErrorCode.BleCharacteristicNotFound ||
303
- (typeof message === 'string' &&
304
- (message.includes('BLECharacteristicNotFound') ||
305
- message.includes('BLECharacteristicNotWritable') ||
306
- message.includes('BLECharacteristicNotNotifiable')))
307
- );
308
- };
309
- const isStaleGattTableNotifyReason = (reason: string | null | undefined): boolean =>
310
- !!reason &&
311
- (reason.includes('Cannot write client characteristic config descriptor') ||
312
- reason.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
313
- reason.includes('The handle is invalid') ||
314
- reason.includes('Writing is not permitted')); // pro firmware 2.3.4 upgrade
315
-
316
264
  /**
317
265
  * JS backstop for connect. The native adapter applies its own 3s budget, but it
318
266
  * schedules that timeout on its serial queue, so a busy queue (e.g. right after a
@@ -352,15 +300,6 @@ const shouldRethrowBleSetupError = (error: unknown): boolean =>
352
300
  isConnectTimeoutError(error) || isWedgedBleSetupError(error);
353
301
  const isNativeOperationTimeoutError = (error: unknown): boolean =>
354
302
  (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
355
- const isMtuOrCancelledConnectError = (error: unknown): boolean => {
356
- const errorCode = (error as { errorCode?: unknown })?.errorCode;
357
- return (
358
- errorCode === BleErrorCode.DeviceMTUChangeFailed ||
359
- errorCode === BleErrorCode.OperationCancelled
360
- );
361
- };
362
-
363
- type NegotiatedMtuResult = { device: Device; timedOut: boolean };
364
303
 
365
304
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
366
305
 
@@ -375,75 +314,29 @@ const tryToGetConfiguration = (device: Device) => {
375
314
 
376
315
  const requestNegotiatedMtu = async (
377
316
  device: Device,
378
- stage: 'connected' | 'highThroughput',
379
- attempt: number,
380
- cancelTransaction?: (transactionId: string) => Promise<void> | void
381
- ): Promise<NegotiatedMtuResult> => {
382
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') return { device, timedOut: false };
383
-
384
- const transactionId = `${device.id}:mtu:${stage}:${attempt}:${Date.now()}`;
385
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
386
- let timedOut = false;
317
+ stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
318
+ attempt: number
319
+ ) => {
320
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
387
321
 
388
322
  try {
389
323
  // iOS ignores the requested value but react-native-ble-plx returns a fresh
390
324
  // Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
391
- const request = device.requestMTU(getRequestedBleMtu(), transactionId);
392
- // The timeout race may settle before the native request does. Attach a
393
- // rejection handler so a late native cancellation cannot become an
394
- // unhandled rejection after we continue with the current MTU.
395
- request.catch(() => undefined);
396
- const mtuDevice = await Promise.race([
397
- request,
398
- new Promise<never>((_, reject) => {
399
- timeoutId = setTimeout(() => {
400
- timedOut = true;
401
- reject(new Error(`BLE MTU request timeout after ${BLE_MTU_REQUEST_TIMEOUT_MS}ms`));
402
- }, BLE_MTU_REQUEST_TIMEOUT_MS);
403
- }),
404
- ]);
405
- return { device: mtuDevice, timedOut: false };
325
+ const mtuDevice = await device.requestMTU(getRequestedBleMtu());
326
+ return mtuDevice;
406
327
  } catch (error) {
407
- if (timedOut && cancelTransaction) {
408
- try {
409
- Promise.resolve(cancelTransaction(transactionId)).catch(cancelError => {
410
- Log?.debug('[ReactNativeBleTransport] MTU cancellation failed', {
411
- platform: Platform.OS,
412
- stage,
413
- attempt,
414
- error: cancelError instanceof Error ? cancelError.message : String(cancelError),
415
- });
416
- });
417
- } catch (cancelError) {
418
- Log?.debug('[ReactNativeBleTransport] MTU cancellation failed', {
419
- platform: Platform.OS,
420
- stage,
421
- attempt,
422
- error: cancelError instanceof Error ? cancelError.message : String(cancelError),
423
- });
424
- }
425
- }
426
328
  Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
427
329
  platform: Platform.OS,
428
330
  stage,
429
331
  attempt,
430
332
  actual: device.mtu,
431
- timedOut,
432
333
  error: error instanceof Error ? error.message : String(error),
433
334
  });
434
- return { device, timedOut };
435
- } finally {
436
- if (timeoutId) clearTimeout(timeoutId);
335
+ return device;
437
336
  }
438
337
  };
439
338
 
440
- const resolveNegotiatedMtu = (
441
- device: Device,
442
- cancelTransaction?: (transactionId: string) => Promise<void> | void
443
- ): Promise<NegotiatedMtuResult> =>
444
- shouldRefreshNegotiatedMtu(device.mtu)
445
- ? requestNegotiatedMtu(device, 'connected', 0, cancelTransaction)
446
- : Promise.resolve({ device, timedOut: false });
339
+ const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
447
340
 
448
341
  type IOBleErrorRemap = Error | BleError | null | undefined;
449
342
 
@@ -527,12 +420,6 @@ export default class ReactNativeBleTransport {
527
420
  /** Consecutive detections that failed while trusting sessionProtocols. */
528
421
  private protocolReprobeFailures: Map<string, number> = new Map();
529
422
 
530
- /** Endpoints whose last detection got no answer; 'woken' once their Initialize wake is spent. */
531
- private silentDetections = new Map<string, 'silent' | 'woken'>();
532
-
533
- /** Android endpoints whose cached GATT table is suspect; the next connect refreshes it. */
534
- private androidGattCacheRefreshes = new Set<string>();
535
-
536
423
  /**
537
424
  * Native encryption/pairing failures seen before Protocol V2 probe starts.
538
425
  * Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
@@ -1039,10 +926,41 @@ export default class ReactNativeBleTransport {
1039
926
  }
1040
927
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1041
928
 
929
+ const initialMtu = transport.mtuSize;
930
+ let refreshAttempts = 0;
931
+ if (
932
+ (Platform.OS === 'ios' || Platform.OS === 'android') &&
933
+ shouldRefreshNegotiatedMtu(transport.mtuSize)
934
+ ) {
935
+ refreshAttempts += 1;
936
+ let refreshedDevice = await requestNegotiatedMtu(
937
+ transport.device,
938
+ 'servicesAndNotifyReady',
939
+ 1
940
+ );
941
+ if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
942
+ transport.device = refreshedDevice;
943
+ transport.mtuSize =
944
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
945
+
946
+ if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
947
+ await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
948
+ if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
949
+ refreshAttempts += 1;
950
+ refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
951
+ transport.device = refreshedDevice;
952
+ transport.mtuSize =
953
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
954
+ }
955
+ }
956
+
957
+ if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1042
958
  Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
1043
959
  platform: Platform.OS,
1044
960
  requested: getRequestedBleMtu(),
961
+ initial: initialMtu,
1045
962
  actual: transport.mtuSize,
963
+ refreshAttempts,
1046
964
  });
1047
965
 
1048
966
  return transport;
@@ -1110,12 +1028,8 @@ export default class ReactNativeBleTransport {
1110
1028
  const isCachedDeviceConnected = await cachedTransport.device
1111
1029
  .isConnected()
1112
1030
  .catch(() => false);
1113
- // A suspect GATT table is only refreshed through a new connect.
1114
- const isCachedAndroidLinkUsable =
1115
- Platform.OS !== 'android' || !this.androidGattCacheRefreshes.has(uuid);
1116
1031
  if (
1117
1032
  isCachedDeviceConnected &&
1118
- isCachedAndroidLinkUsable &&
1119
1033
  cachedProtocol &&
1120
1034
  (!expectedProtocol || cachedProtocol === expectedProtocol)
1121
1035
  ) {
@@ -1134,18 +1048,6 @@ export default class ReactNativeBleTransport {
1134
1048
  }
1135
1049
 
1136
1050
  let device: Device | null = null;
1137
- const isAndroid = Platform.OS === 'android';
1138
- // A firmware-install reconnect always refreshes: the new firmware may expose a different table.
1139
- const refreshAndroidGattCache =
1140
- isAndroid && (!!skipProtocolProbe || this.androidGattCacheRefreshes.has(uuid));
1141
- let nativeConnectOptions = connectOptions;
1142
- if (isAndroid) {
1143
- nativeConnectOptions = refreshAndroidGattCache
1144
- ? androidRefreshGattConnectOptions
1145
- : fallbackConnectOptions;
1146
- }
1147
- // Only a connect that carried refreshGatt clears the marker; the fallback connects drop it.
1148
- let androidRefreshConnectRan = false;
1149
1051
 
1150
1052
  if (forceCleanRunPromise && this.runPromise) {
1151
1053
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
@@ -1156,7 +1058,6 @@ export default class ReactNativeBleTransport {
1156
1058
  }
1157
1059
 
1158
1060
  const blePlxManager = await this.getPlxManager();
1159
- let skipPostConnectMtu = false;
1160
1061
  try {
1161
1062
  await subscribeBleOn(blePlxManager);
1162
1063
  } catch (error) {
@@ -1201,16 +1102,17 @@ export default class ReactNativeBleTransport {
1201
1102
  Log?.debug('try to connect to device: ', uuid);
1202
1103
  try {
1203
1104
  device = await this.connectWithTimeout(uuid, () =>
1204
- blePlxManager.connectToDevice(uuid, nativeConnectOptions)
1105
+ blePlxManager.connectToDevice(uuid, connectOptions)
1205
1106
  );
1206
- androidRefreshConnectRan = refreshAndroidGattCache;
1207
1107
  } catch (e) {
1208
1108
  Log?.debug('try to connect to device has error: ', e);
1209
1109
  if (shouldRethrowBleSetupError(e)) {
1210
1110
  throw e;
1211
1111
  }
1212
- if (isMtuOrCancelledConnectError(e)) {
1213
- skipPostConnectMtu = true;
1112
+ if (
1113
+ e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
1114
+ e.errorCode === BleErrorCode.OperationCancelled
1115
+ ) {
1214
1116
  Log?.debug('first try to reconnect without params');
1215
1117
  device = await this.connectWithTimeout(uuid, () =>
1216
1118
  blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
@@ -1228,33 +1130,23 @@ export default class ReactNativeBleTransport {
1228
1130
  throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'unable to connect to device');
1229
1131
  }
1230
1132
 
1231
- if (
1232
- refreshAndroidGattCache &&
1233
- !androidRefreshConnectRan &&
1234
- (await device.isConnected().catch(() => false))
1235
- ) {
1236
- // refreshGatt only reaches the stack through a connect. A link that is still up would
1237
- // skip the connect below and keep serving the stale table, so it is dropped first.
1238
- await this.dropAndroidLink(uuid, blePlxManager, device, 'gatt cache refresh');
1239
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1240
- }
1241
-
1242
1133
  if (!(await device.isConnected())) {
1243
1134
  Log?.debug('not connected, try to connect to device: ', uuid);
1244
1135
  const disconnectedDevice = device;
1245
1136
 
1246
1137
  try {
1247
1138
  device = await this.connectWithTimeout(uuid, () =>
1248
- disconnectedDevice.connect(nativeConnectOptions)
1139
+ disconnectedDevice.connect(connectOptions)
1249
1140
  );
1250
- androidRefreshConnectRan = refreshAndroidGattCache;
1251
1141
  } catch (e) {
1252
1142
  Log?.debug('not connected, try to connect to device has error: ', e);
1253
1143
  if (shouldRethrowBleSetupError(e)) {
1254
1144
  throw e;
1255
1145
  }
1256
- if (isMtuOrCancelledConnectError(e)) {
1257
- skipPostConnectMtu = true;
1146
+ if (
1147
+ e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
1148
+ e.errorCode === BleErrorCode.OperationCancelled
1149
+ ) {
1258
1150
  Log?.debug('second try to reconnect without params');
1259
1151
  try {
1260
1152
  device = await this.connectWithTimeout(uuid, () =>
@@ -1296,63 +1188,11 @@ export default class ReactNativeBleTransport {
1296
1188
  throw ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'device is not connected');
1297
1189
  }
1298
1190
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1299
- let characteristics: ResolvedBleCharacteristics | undefined;
1300
- if (isAndroid) {
1301
- if (refreshAndroidGattCache) {
1302
- // refreshGatt has already started a full rediscovery; let it finish before the MTU
1303
- // exchange so the request is not queued behind it.
1304
- characteristics = await this.resolveCharacteristicsWithTimeout(uuid, device);
1305
- if (androidRefreshConnectRan) this.androidGattCacheRefreshes.delete(uuid);
1306
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1307
- }
1308
- device = await this.negotiateAndroidMtu(uuid, blePlxManager, device);
1309
- } else if (!skipPostConnectMtu) {
1310
- // Match 1.1.31: MTU is a connect() best-effort. If connect already fell back
1311
- // without requestMTU, do not put another requestMTU on the native serial
1312
- // queue — that is what wedges GATT after Account#2 reconnect.
1313
- const mtuResult = await resolveNegotiatedMtu(device, transactionId =>
1314
- blePlxManager.cancelTransaction(transactionId)
1315
- );
1316
- device = mtuResult.device;
1317
- if (mtuResult.timedOut) {
1318
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1319
- Log?.debug(
1320
- '[ReactNativeBleTransport] post-connect MTU timed out, reconnecting without requesting MTU'
1321
- );
1322
- const timedOutDevice = device;
1323
- let mtuTeardownSettled = false;
1324
- await this.runNativeTeardown(uuid, blePlxManager, async () => {
1325
- await this.runBestEffortNativeOperation('mtu timeout: cancel device connection', () =>
1326
- timedOutDevice.cancelConnection()
1327
- );
1328
- mtuTeardownSettled = true;
1329
- });
1330
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1331
- if (!mtuTeardownSettled || this.blePlxManager !== blePlxManager) {
1332
- // The teardown budget expired and the manager that owned timedOutDevice was reset.
1333
- throw ERRORS.TypedError(HardwareErrorCode.BleTimeoutError, 'BLE MTU cleanup timed out');
1334
- }
1335
- try {
1336
- device = await this.connectWithTimeout(uuid, () =>
1337
- timedOutDevice.connect(fallbackConnectOptions)
1338
- );
1339
- } catch (error) {
1340
- if (shouldRethrowBleSetupError(error)) throw error;
1341
- if (
1342
- (error as { errorCode?: unknown })?.errorCode === BleErrorCode.DeviceAlreadyConnected
1343
- ) {
1344
- // GATT resolution and the protocol probe below still validate the retained link.
1345
- device = timedOutDevice;
1346
- } else {
1347
- remapError(error);
1348
- }
1349
- }
1350
- }
1351
- }
1191
+ device = await resolveNegotiatedMtu(device);
1352
1192
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1353
1193
  const acquiredDevice = device;
1354
1194
  const { writeCharacteristic, notifyCharacteristic } =
1355
- characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice));
1195
+ await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
1356
1196
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
1357
1197
 
1358
1198
  const protocolHint = expectedProtocol
@@ -1463,9 +1303,6 @@ export default class ReactNativeBleTransport {
1463
1303
  this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
1464
1304
  return;
1465
1305
  }
1466
- if (Platform.OS === 'android' && isStaleGattTableNotifyReason(error.reason)) {
1467
- this.androidGattCacheRefreshes.add(uuid);
1468
- }
1469
1306
  if (this.getActiveProtocol(uuid) === 'V2') {
1470
1307
  let errorCode:
1471
1308
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -1475,7 +1312,10 @@ export default class ReactNativeBleTransport {
1475
1312
  if (error.reason?.includes('The connection has timed out unexpectedly')) {
1476
1313
  errorCode = HardwareErrorCode.BleTimeoutError;
1477
1314
  } else if (
1478
- isStaleGattTableNotifyReason(error.reason) ||
1315
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
1316
+ error.reason?.includes('Cannot find client characteristic config descriptor') ||
1317
+ error.reason?.includes('The handle is invalid') ||
1318
+ error.reason?.includes('Writing is not permitted') ||
1479
1319
  error.reason?.includes('notify change failed for device')
1480
1320
  ) {
1481
1321
  errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
@@ -1492,7 +1332,10 @@ export default class ReactNativeBleTransport {
1492
1332
  ERROR = HardwareErrorCode.BleTimeoutError;
1493
1333
  }
1494
1334
  if (
1495
- isStaleGattTableNotifyReason(error.reason) ||
1335
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
1336
+ error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
1337
+ error.reason?.includes('The handle is invalid') ||
1338
+ error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
1496
1339
  error.reason?.includes('notify change failed for device')
1497
1340
  ) {
1498
1341
  const notifyError = ERRORS.TypedError(
@@ -1922,14 +1765,13 @@ export default class ReactNativeBleTransport {
1922
1765
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1923
1766
  return check.call(jsonData);
1924
1767
  } catch (e) {
1925
- const isProbeTimeout =
1926
- options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS &&
1927
- (name === 'GetFeatures' || name === 'Initialize');
1928
- if (isProbeTimeout) {
1929
- Log?.debug(`[ReactNativeBleTransport] Protocol V1 ${name} probe call failed:`, e);
1768
+ if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1769
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
1930
1770
  } else {
1931
1771
  Log?.error('call error: ', e);
1932
1772
  }
1773
+ const isProbeTimeout =
1774
+ name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1933
1775
  // A call that has been superseded (forceRun) or cleaned up no longer owns the
1934
1776
  // transport; its late timeout must not tear down the connection the current
1935
1777
  // call is actively using.
@@ -2162,98 +2004,6 @@ export default class ReactNativeBleTransport {
2162
2004
  }
2163
2005
  }
2164
2006
 
2165
- /**
2166
- * Android: request the MTU with nothing queued ahead and never close the client while it is
2167
- * outstanding; a link that times out or stays at MTU 23 is dropped.
2168
- */
2169
- private async negotiateAndroidMtu(
2170
- uuid: string,
2171
- manager: BlePlxManager,
2172
- device: Device
2173
- ): Promise<Device> {
2174
- if (!shouldRefreshNegotiatedMtu(device.mtu)) return device;
2175
-
2176
- const startedAt = Date.now();
2177
- let timer: ReturnType<typeof setTimeout> | undefined;
2178
- let timedOut = false;
2179
- let negotiated = device;
2180
- let failure: string | undefined;
2181
- try {
2182
- negotiated = await Promise.race([
2183
- device.requestMTU(ANDROID_REQUEST_MTU, `${device.id}:mtu:connected:0:${startedAt}`),
2184
- new Promise<never>((_, reject) => {
2185
- timer = setTimeout(() => {
2186
- timedOut = true;
2187
- reject(
2188
- new Error(`BLE MTU exchange timeout after ${ANDROID_MTU_EXCHANGE_TIMEOUT_MS}ms`)
2189
- );
2190
- }, ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
2191
- }),
2192
- ]);
2193
- } catch (error) {
2194
- failure = error instanceof Error ? error.message : String(error);
2195
- } finally {
2196
- if (timer) clearTimeout(timer);
2197
- }
2198
- Log?.debug(`[ReactNativeBleTransport] BLE MTU exchange ${failure ? 'failed' : 'completed'}`, {
2199
- connectIdSuffix: uuid.slice(-8),
2200
- elapsedMs: Date.now() - startedAt,
2201
- timedOut,
2202
- actual: negotiated.mtu,
2203
- error: failure,
2204
- });
2205
- if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
2206
- if (!timedOut && !isKnownDefaultMtu(negotiated.mtu)) return negotiated;
2207
-
2208
- // Counted like a setup timeout so a link that keeps failing reaches the wedged-link guard.
2209
- const resetManager = this.abandonStalledConnection(
2210
- uuid,
2211
- timedOut ? 'mtu-backstop' : 'mtu-default'
2212
- );
2213
- await this.dropAndroidLink(
2214
- uuid,
2215
- manager,
2216
- negotiated,
2217
- timedOut ? 'mtu exchange timeout' : 'default mtu'
2218
- );
2219
- if (resetManager) throw this.createWedgedBleSetupError();
2220
- throw ERRORS.TypedError(
2221
- HardwareErrorCode.BleConnectedError,
2222
- timedOut
2223
- ? 'BLE MTU exchange did not complete, reconnecting on a fresh link'
2224
- : `BLE link stayed at the default MTU ${negotiated.mtu}, reconnecting on a fresh link`
2225
- );
2226
- }
2227
-
2228
- /** Close the client and wait out the link idle timer so the next connect gets a fresh link. */
2229
- private async dropAndroidLink(
2230
- uuid: string,
2231
- manager: BlePlxManager,
2232
- device: Device,
2233
- reason: string
2234
- ) {
2235
- await this.runNativeTeardown(uuid, manager, async () => {
2236
- await Promise.all([
2237
- this.runBestEffortNativeOperation(`${reason}: cancel manager connection`, () =>
2238
- manager.cancelDeviceConnection(uuid)
2239
- ),
2240
- this.runBestEffortNativeOperation(`${reason}: cancel device connection`, () =>
2241
- device.cancelConnection()
2242
- ),
2243
- ]);
2244
- });
2245
-
2246
- const startedAt = Date.now();
2247
- while (!this.stopped && Date.now() - startedAt < ANDROID_LINK_DROP_QUIET_MS) {
2248
- await delay(ANDROID_LINK_DROP_POLL_MS);
2249
- }
2250
- Log?.debug('[ReactNativeBleTransport] Android BLE link drop', {
2251
- connectIdSuffix: uuid.slice(-8),
2252
- reason,
2253
- stopped: this.stopped,
2254
- });
2255
- }
2256
-
2257
2007
  /** Run a native connect under the JS backstop budget. */
2258
2008
  private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
2259
2009
  if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
@@ -2343,9 +2093,6 @@ export default class ReactNativeBleTransport {
2343
2093
  throw this.createWedgedBleSetupError();
2344
2094
  }
2345
2095
  }
2346
- if (Platform.OS === 'android' && isMissingGattShapeError(error)) {
2347
- this.androidGattCacheRefreshes.add(uuid);
2348
- }
2349
2096
  throw error;
2350
2097
  } finally {
2351
2098
  if (timer) clearTimeout(timer);
@@ -2366,13 +2113,7 @@ export default class ReactNativeBleTransport {
2366
2113
  */
2367
2114
  private abandonStalledConnection(
2368
2115
  uuid: string,
2369
- stage:
2370
- | 'connect-backstop'
2371
- | 'connect-native'
2372
- | 'gatt-backstop'
2373
- | 'gatt-native'
2374
- | 'mtu-backstop'
2375
- | 'mtu-default'
2116
+ stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
2376
2117
  ): boolean {
2377
2118
  const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
2378
2119
  this.connectionSetupTimeoutCounts.set(uuid, timeouts);
@@ -2547,7 +2288,6 @@ export default class ReactNativeBleTransport {
2547
2288
  // Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
2548
2289
  // firmware reconnect after the native BLE manager is recreated.
2549
2290
  this.protocolReprobeFailures.clear();
2550
- this.silentDetections.clear();
2551
2291
  this.writeTimeoutCounts.clear();
2552
2292
  this.connectionSetupTimeoutCounts.clear();
2553
2293
  this.monitorTokens.clear();
@@ -2659,8 +2399,6 @@ export default class ReactNativeBleTransport {
2659
2399
  reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
2660
2400
  const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
2661
2401
 
2662
- await this.wakeSilentProtocolV1Device(uuid, probeOrder);
2663
-
2664
2402
  for (let i = 0; i < probeOrder.length; i += 1) {
2665
2403
  const protocol = probeOrder[i];
2666
2404
  if (i > 0) {
@@ -2682,7 +2420,6 @@ export default class ReactNativeBleTransport {
2682
2420
  this.confirmedProtocolV2.add(uuid);
2683
2421
  }
2684
2422
  this.protocolReprobeFailures.delete(uuid);
2685
- this.silentDetections.delete(uuid);
2686
2423
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
2687
2424
  deviceId: uuid,
2688
2425
  protocol,
@@ -2692,9 +2429,6 @@ export default class ReactNativeBleTransport {
2692
2429
  }
2693
2430
  }
2694
2431
 
2695
- // Arms the wake for the next detection.
2696
- if (!this.silentDetections.has(uuid)) this.silentDetections.set(uuid, 'silent');
2697
-
2698
2432
  if (trustSessionProtocol) {
2699
2433
  // Still silent on its own protocol: count it, and let the streak expire the
2700
2434
  // shortcut so a device that genuinely switched protocols is found again.
@@ -2708,34 +2442,6 @@ export default class ReactNativeBleTransport {
2708
2442
  throw this.createProtocolDetectionError();
2709
2443
  }
2710
2444
 
2711
- /**
2712
- * A sleeping Classic drops GetFeatures/Ping and only leaves its screensaver on Initialize, which
2713
- * resets the wallet session, so it is sent once after a fully silent detection. The firmware does
2714
- * not reliably answer it, so its timeout must not drop the link. It only runs when V1 is probed
2715
- * first, so a late reply lands on the V1 probe rather than on a V2 one.
2716
- */
2717
- private async wakeSilentProtocolV1Device(uuid: string, probeOrder: ProtocolType[]) {
2718
- if (
2719
- Platform.OS !== 'android' ||
2720
- probeOrder[0] !== 'V1' ||
2721
- this.silentDetections.get(uuid) !== 'silent'
2722
- ) {
2723
- return;
2724
- }
2725
- this.silentDetections.set(uuid, 'woken');
2726
- Log?.debug('[ReactNativeBleTransport] sending Protocol V1 Initialize wake', {
2727
- connectIdSuffix: uuid.slice(-8),
2728
- });
2729
- try {
2730
- this.probingProtocols.set(uuid, 'V1');
2731
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2732
- } catch (error) {
2733
- if (shouldRethrowProtocolProbeError(error)) throw error;
2734
- } finally {
2735
- this.clearProbeProtocol(uuid, 'V1');
2736
- }
2737
- }
2738
-
2739
2445
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
2740
2446
  const transport = transportCache[uuid];
2741
2447
  await this.protocolV2Links.invalidateLink(
@@ -3115,12 +2821,7 @@ export default class ReactNativeBleTransport {
3115
2821
  const transport = this.getCachedTransport(uuid);
3116
2822
  if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
3117
2823
 
3118
- const { device: refreshedDevice } = await requestNegotiatedMtu(
3119
- transport.device,
3120
- 'highThroughput',
3121
- 1,
3122
- transactionId => this.blePlxManager?.cancelTransaction(transactionId)
3123
- );
2824
+ const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
3124
2825
  transport.device = refreshedDevice;
3125
2826
  transport.mtuSize =
3126
2827
  typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;