@onekeyfe/hd-transport-react-native 1.2.0-alpha.73 → 1.2.0-alpha.75

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
@@ -142,22 +142,6 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
142
142
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
143
143
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
144
144
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
145
- /**
146
- * Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
147
- * reports the peripheral ready again; a peripheral wedged by its own firmware reboot
148
- * stops reporting ready while staying connected, so the write promise never settles.
149
- * Response timeouts cannot cover that — they are armed after the writes complete —
150
- * and an unbounded write leaves the whole transport unusable until the process dies.
151
- * A healthy packet completes in milliseconds, so this only fires on a dead link.
152
- */
153
- export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
154
- const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
155
- const isWedgedWriteError = (error: unknown): boolean =>
156
- (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
157
- typeof (error as { message?: unknown })?.message === 'string' &&
158
- (error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
159
- /** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
160
- export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
161
145
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
162
146
  const IOS_NOTIFY_READY_DELAY_MS = 150;
163
147
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
@@ -220,52 +204,12 @@ const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
220
204
  const getRequestedBleMtu = () =>
221
205
  Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
222
206
 
223
- const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
224
-
225
207
  const connectOptions: Record<string, unknown> = {
226
208
  requestMTU: getRequestedBleMtu(),
227
- timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
209
+ timeout: 3000,
228
210
  refreshGatt: 'OnConnected',
229
211
  };
230
212
 
231
- /** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
232
- const fallbackConnectOptions: Record<string, unknown> = {
233
- timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
234
- };
235
-
236
- /**
237
- * JS backstop for connect. The native adapter applies its own 3s budget, but it
238
- * schedules that timeout on its serial queue, so a busy queue (e.g. right after a
239
- * firmware install tears the link down) can leave the promise unsettled — observed
240
- * blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
241
- * inside the native budget, so this only fires when the native timeout did not.
242
- */
243
- export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
244
- /**
245
- * Service discovery and characteristic resolution run after connect() succeeds, but
246
- * CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
247
- * device reboot, these calls can remain pending forever unless they have their own
248
- * budget.
249
- */
250
- export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
251
- /**
252
- * How many times a known device may fail its own protocol before we probe the others
253
- * again. Reconnect polling during a device reboot repeats this every few seconds, and
254
- * probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
255
- * device we just spoke V1 to dominates the wait. A firmware update can legitimately
256
- * change a device's protocol, so the shortcut has to expire rather than stick.
257
- */
258
- export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
259
- /** BLE setup timeouts since the last successful setup before the manager is recreated. */
260
- export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
261
- const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
262
- const isConnectTimeoutError = (error: unknown): boolean =>
263
- (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
264
- typeof (error as { message?: unknown })?.message === 'string' &&
265
- (error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
266
- const isNativeOperationTimeoutError = (error: unknown): boolean =>
267
- (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
268
-
269
213
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
270
214
 
271
215
  const tryToGetConfiguration = (device: Device) => {
@@ -363,28 +307,8 @@ export default class ReactNativeBleTransport {
363
307
  /** Per-device protocol type detected by active wire-level probe after connect. */
364
308
  private deviceProtocol: Map<string, ProtocolType> = new Map();
365
309
 
366
- /**
367
- * Protocol a probe is currently trying, before the device has confirmed it. Calls
368
- * must route with it, but acquire() must not treat it as a detected protocol: a
369
- * probe that never answers would otherwise leave the reuse fast path handing out a
370
- * transport that was never validated.
371
- */
372
- private probingProtocols: Map<string, ProtocolType> = new Map();
373
-
374
- /** Consecutive write timeouts per device; reset by any write that completes. */
375
- private writeTimeoutCounts: Map<string, number> = new Map();
376
-
377
- /** BLE setup timeouts per device since the last complete characteristic resolution. */
378
- private connectionSetupTimeoutCounts: Map<string, number> = new Map();
379
-
380
310
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
381
311
 
382
- /** Protocol this device actually answered on, kept across reconnects of one session. */
383
- private sessionProtocols: Map<string, ProtocolType> = new Map();
384
-
385
- /** Consecutive detections that failed while trusting sessionProtocols. */
386
- private protocolReprobeFailures: Map<string, number> = new Map();
387
-
388
312
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
389
313
 
390
314
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
@@ -608,21 +532,22 @@ export default class ReactNativeBleTransport {
608
532
  const isConnected = await device.isConnected().catch(() => false);
609
533
  if (!isConnected) {
610
534
  try {
611
- device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
535
+ device = await device.connect(connectOptions);
612
536
  } catch (e) {
613
537
  if (
614
538
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
615
539
  e.errorCode === BleErrorCode.OperationCancelled
616
540
  ) {
617
- device = await this.connectWithTimeout(uuid, () => device.connect());
541
+ device = await device.connect();
618
542
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
619
543
  throw e;
620
544
  }
621
545
  }
622
546
  }
623
547
 
624
- const { writeCharacteristic, notifyCharacteristic } =
625
- await this.resolveCharacteristicsWithTimeout(uuid, device);
548
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
549
+ device
550
+ );
626
551
 
627
552
  transport.device = device;
628
553
  transport.writeCharacteristic = writeCharacteristic;
@@ -801,7 +726,7 @@ export default class ReactNativeBleTransport {
801
726
  characteristics?: ResolvedBleCharacteristics
802
727
  ) {
803
728
  const { writeCharacteristic, notifyCharacteristic } =
804
- characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
729
+ characteristics ?? (await this.resolveCharacteristics(device));
805
730
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
806
731
  transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
807
732
  const monitorToken = this.nextMonitorToken;
@@ -938,22 +863,15 @@ export default class ReactNativeBleTransport {
938
863
  if (!device) {
939
864
  Log?.debug('try to connect to device: ', uuid);
940
865
  try {
941
- device = await this.connectWithTimeout(uuid, () =>
942
- blePlxManager.connectToDevice(uuid, connectOptions)
943
- );
866
+ device = await blePlxManager.connectToDevice(uuid, connectOptions);
944
867
  } catch (e) {
945
868
  Log?.debug('try to connect to device has error: ', e);
946
- if (isConnectTimeoutError(e)) {
947
- throw e;
948
- }
949
869
  if (
950
870
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
951
871
  e.errorCode === BleErrorCode.OperationCancelled
952
872
  ) {
953
873
  Log?.debug('first try to reconnect without params');
954
- device = await this.connectWithTimeout(uuid, () =>
955
- blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
956
- );
874
+ device = await blePlxManager.connectToDevice(uuid);
957
875
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
958
876
  Log?.debug('device already connected');
959
877
  throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
@@ -969,36 +887,26 @@ export default class ReactNativeBleTransport {
969
887
 
970
888
  if (!(await device.isConnected())) {
971
889
  Log?.debug('not connected, try to connect to device: ', uuid);
972
- const disconnectedDevice = device;
973
890
 
974
891
  try {
975
- device = await this.connectWithTimeout(uuid, () =>
976
- disconnectedDevice.connect(connectOptions)
977
- );
892
+ device = await device.connect(connectOptions);
978
893
  } catch (e) {
979
894
  Log?.debug('not connected, try to connect to device has error: ', e);
980
- if (isConnectTimeoutError(e)) {
981
- throw e;
982
- }
983
895
  if (
984
896
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
985
897
  e.errorCode === BleErrorCode.OperationCancelled
986
898
  ) {
987
899
  Log?.debug('second try to reconnect without params');
988
900
  try {
989
- device = await this.connectWithTimeout(uuid, () =>
990
- disconnectedDevice.connect(fallbackConnectOptions)
991
- );
901
+ device = await device.connect();
992
902
  } catch (e) {
993
903
  Log?.debug('last try to reconnect error: ', e);
994
904
  // last try to reconnect device if this issue exists
995
905
  // https://github.com/dotintent/react-native-ble-plx/issues/426
996
906
  if (e.errorCode === BleErrorCode.OperationCancelled) {
997
907
  Log?.debug('last try to reconnect');
998
- await disconnectedDevice.cancelConnection();
999
- device = await this.connectWithTimeout(uuid, () =>
1000
- disconnectedDevice.connect(fallbackConnectOptions)
1001
- );
908
+ await device.cancelConnection();
909
+ device = await device.connect();
1002
910
  }
1003
911
  }
1004
912
  } else {
@@ -1009,8 +917,9 @@ export default class ReactNativeBleTransport {
1009
917
 
1010
918
  device = await resolveNegotiatedMtu(device);
1011
919
  const acquiredDevice = device;
1012
- const { writeCharacteristic, notifyCharacteristic } =
1013
- await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
920
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
921
+ acquiredDevice
922
+ );
1014
923
 
1015
924
  const protocolHint = expectedProtocol
1016
925
  ? undefined
@@ -1074,7 +983,7 @@ export default class ReactNativeBleTransport {
1074
983
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
1075
984
  return;
1076
985
  }
1077
- if (this.getActiveProtocol(uuid) === 'V2') {
986
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1078
987
  let errorCode:
1079
988
  | typeof HardwareErrorCode.BleDeviceBondError
1080
989
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -1144,7 +1053,7 @@ export default class ReactNativeBleTransport {
1144
1053
 
1145
1054
  try {
1146
1055
  const data = Buffer.from(c.value as string, 'base64');
1147
- const protocol = this.getActiveProtocol(uuid);
1056
+ const protocol = this.deviceProtocol.get(uuid);
1148
1057
  if (!protocol) {
1149
1058
  Log?.debug('monitor data ignored before protocol detection: ', uuid);
1150
1059
  return;
@@ -1178,7 +1087,7 @@ export default class ReactNativeBleTransport {
1178
1087
  } catch (error) {
1179
1088
  Log?.debug('monitor data error: ', error);
1180
1089
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1181
- if (this.getActiveProtocol(uuid) === 'V2') {
1090
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1182
1091
  this.rejectProtocolV2Frames(uuid, notifyError);
1183
1092
  } else if (this.runPromiseDeviceId === uuid) {
1184
1093
  this.runPromise?.reject(notifyError);
@@ -1246,7 +1155,6 @@ export default class ReactNativeBleTransport {
1246
1155
  this.protocolV2HighVolumeLogSignatures.delete(uuid);
1247
1156
 
1248
1157
  this.deviceProtocol.delete(uuid);
1249
- this.probingProtocols.delete(uuid);
1250
1158
  // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
1251
1159
  this.protocolV2Assemblers.get(uuid)?.reset();
1252
1160
  this.protocolV2Assemblers.delete(uuid);
@@ -1313,25 +1221,8 @@ export default class ReactNativeBleTransport {
1313
1221
  const transport = this.getCachedTransport(uuid);
1314
1222
  const runPromise = createDeferred<string>();
1315
1223
  runPromise.promise.catch(() => undefined);
1316
- const supersededRunPromise = this.runPromise;
1317
- if (supersededRunPromise) {
1318
- // Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
1319
- // the superseded deferred now so its response race resolves and its finally block
1320
- // clears its timeout timer; an orphaned timer would otherwise fire much later and
1321
- // tear down the shared connection while another call is using it.
1322
- supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
1323
- }
1324
1224
  this.runPromise = runPromise;
1325
1225
  this.runPromiseDeviceId = uuid;
1326
- // A superseded call's late write failure must not clear the successor's ownership;
1327
- // only the call that still owns the slot may release it.
1328
- const releaseOwnershipIfCurrent = () => {
1329
- if (this.runPromise === runPromise) {
1330
- this.runPromise = null;
1331
- this.runPromiseDeviceId = null;
1332
- }
1333
- };
1334
- const isCurrentOwner = () => this.runPromise === runPromise;
1335
1226
  const messages = this._messages;
1336
1227
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1337
1228
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1357,9 +1248,6 @@ export default class ReactNativeBleTransport {
1357
1248
  chunk = ByteBuffer.allocate(packetCapacity);
1358
1249
  } catch (e) {
1359
1250
  onError(e);
1360
- if (isWedgedWriteError(e)) {
1361
- throw e;
1362
- }
1363
1251
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1364
1252
  }
1365
1253
  }
@@ -1391,9 +1279,6 @@ export default class ReactNativeBleTransport {
1391
1279
  }
1392
1280
  } catch (e) {
1393
1281
  onError(e);
1394
- if (isWedgedWriteError(e)) {
1395
- throw e;
1396
- }
1397
1282
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1398
1283
  }
1399
1284
  }
@@ -1407,15 +1292,9 @@ export default class ReactNativeBleTransport {
1407
1292
  if (name === 'EmmcFileWrite') {
1408
1293
  await writeChunkedData(
1409
1294
  buffers,
1410
- data =>
1411
- this.writeBlePacket(
1412
- uuid,
1413
- data,
1414
- payload => transport.writeWithRetry(payload),
1415
- isCurrentOwner
1416
- ),
1295
+ data => transport.writeWithRetry(data),
1417
1296
  e => {
1418
- releaseOwnershipIfCurrent();
1297
+ this.runPromise = null;
1419
1298
  Log?.error('writeCharacteristic write error: ', e);
1420
1299
  }
1421
1300
  );
@@ -1436,12 +1315,7 @@ export default class ReactNativeBleTransport {
1436
1315
  // eslint-disable-next-line no-constant-condition
1437
1316
  while (true) {
1438
1317
  try {
1439
- await this.writeBlePacket(
1440
- uuid,
1441
- data,
1442
- payload => transport.writeWithRetry(payload),
1443
- isCurrentOwner
1444
- );
1318
+ await transport.writeWithRetry(data);
1445
1319
  return;
1446
1320
  } catch (error) {
1447
1321
  const retryType = getFirmwareUploadWriteRetryType(error);
@@ -1460,7 +1334,7 @@ export default class ReactNativeBleTransport {
1460
1334
  }
1461
1335
  },
1462
1336
  e => {
1463
- releaseOwnershipIfCurrent();
1337
+ this.runPromise = null;
1464
1338
  Log?.error('writeCharacteristic write error: ', e);
1465
1339
  }
1466
1340
  );
@@ -1471,21 +1345,14 @@ export default class ReactNativeBleTransport {
1471
1345
  try {
1472
1346
  const shouldUseWriteWithResponse =
1473
1347
  Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1474
- await this.writeBlePacket(
1475
- uuid,
1476
- outData,
1477
- payload =>
1478
- shouldUseWriteWithResponse
1479
- ? transport.writeCharacteristic.writeWithResponse(payload)
1480
- : transport.writeCharacteristic.writeWithoutResponse(payload),
1481
- isCurrentOwner
1482
- );
1348
+ if (shouldUseWriteWithResponse) {
1349
+ await transport.writeCharacteristic.writeWithResponse(outData);
1350
+ } else {
1351
+ await transport.writeCharacteristic.writeWithoutResponse(outData);
1352
+ }
1483
1353
  } catch (e) {
1484
1354
  Log?.debug('writeCharacteristic write error: ', e);
1485
- releaseOwnershipIfCurrent();
1486
- if (isWedgedWriteError(e)) {
1487
- throw e;
1488
- }
1355
+ this.runPromise = null;
1489
1356
  if (e.errorCode === BleErrorCode.DeviceDisconnected) {
1490
1357
  throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
1491
1358
  } else if (e.errorCode === BleErrorCode.OperationStartFailed) {
@@ -1528,13 +1395,8 @@ export default class ReactNativeBleTransport {
1528
1395
  }
1529
1396
  const isProbeTimeout =
1530
1397
  name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1531
- // A call that has been superseded (forceRun) or cleaned up no longer owns the
1532
- // transport; its late timeout must not tear down the connection the current
1533
- // call is actively using.
1534
- const isStaleCall = this.runPromise !== runPromise;
1535
1398
  if (
1536
1399
  !isProbeTimeout &&
1537
- !isStaleCall &&
1538
1400
  (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1539
1401
  ) {
1540
1402
  await this.disconnect(uuid);
@@ -1614,10 +1476,7 @@ export default class ReactNativeBleTransport {
1614
1476
  delete transportCache[session];
1615
1477
  }
1616
1478
  this.deviceProtocol.delete(session);
1617
- this.probingProtocols.delete(session);
1618
1479
  this.deviceProtocolHints.delete(session);
1619
- this.sessionProtocols.delete(session);
1620
- this.protocolReprobeFailures.delete(session);
1621
1480
  this.protocolV2Assemblers.delete(session);
1622
1481
  this.resetProtocolV2Frames(session);
1623
1482
 
@@ -1643,113 +1502,6 @@ export default class ReactNativeBleTransport {
1643
1502
  this.runPromiseDeviceId = null;
1644
1503
  }
1645
1504
 
1646
- /** Run a native connect under the JS backstop budget. */
1647
- private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
1648
- let timer: ReturnType<typeof setTimeout> | undefined;
1649
- let timedOut = false;
1650
- const pending = connect();
1651
- // The abandoned attempt keeps running; swallow its late outcome so it cannot
1652
- // surface as an unhandled rejection after we have already given up on it.
1653
- pending.catch(() => undefined);
1654
- try {
1655
- const result = await Promise.race([
1656
- pending,
1657
- new Promise<never>((_, reject) => {
1658
- timer = setTimeout(() => {
1659
- timedOut = true;
1660
- reject(
1661
- ERRORS.TypedError(
1662
- HardwareErrorCode.BleConnectedError,
1663
- `BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
1664
- )
1665
- );
1666
- }, BLE_CONNECT_TIMEOUT_MS);
1667
- }),
1668
- ]);
1669
- return result;
1670
- } catch (error) {
1671
- if (timedOut || isNativeOperationTimeoutError(error)) {
1672
- this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
1673
- }
1674
- throw error;
1675
- } finally {
1676
- if (timer) clearTimeout(timer);
1677
- }
1678
- }
1679
-
1680
- /** Resolve the complete GATT shape under a budget so acquire() always settles. */
1681
- private async resolveCharacteristicsWithTimeout(
1682
- uuid: string,
1683
- device: Device
1684
- ): Promise<ResolvedBleCharacteristics> {
1685
- let timer: ReturnType<typeof setTimeout> | undefined;
1686
- let timedOut = false;
1687
- const pending = this.resolveCharacteristics(device);
1688
- pending.catch(() => undefined);
1689
- try {
1690
- const result = await Promise.race([
1691
- pending,
1692
- new Promise<never>((_, reject) => {
1693
- timer = setTimeout(() => {
1694
- timedOut = true;
1695
- reject(
1696
- ERRORS.TypedError(
1697
- HardwareErrorCode.BleConnectedError,
1698
- `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
1699
- )
1700
- );
1701
- }, BLE_GATT_SETUP_TIMEOUT_MS);
1702
- }),
1703
- ]);
1704
- this.connectionSetupTimeoutCounts.delete(uuid);
1705
- return result;
1706
- } catch (error) {
1707
- if (timedOut || isNativeOperationTimeoutError(error)) {
1708
- this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1709
- }
1710
- throw error;
1711
- } finally {
1712
- if (timer) clearTimeout(timer);
1713
- }
1714
- }
1715
-
1716
- /**
1717
- * Give up on a BLE setup operation the native layer did not settle. The abandoned
1718
- * operation still owns native connection/GATT state that can poison the next attempt,
1719
- * so it is cleared here without awaiting the same queue that stopped responding.
1720
- */
1721
- private abandonStalledConnection(
1722
- uuid: string,
1723
- stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
1724
- ) {
1725
- const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
1726
- this.connectionSetupTimeoutCounts.set(uuid, timeouts);
1727
- Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
1728
- stage,
1729
- setupTimeoutsSinceSuccess: timeouts,
1730
- });
1731
-
1732
- this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
1733
- // Rejects with "Operation was cancelled" while merely connecting — expected.
1734
- });
1735
- const stalled = transportCache[uuid];
1736
- if (stalled) {
1737
- delete transportCache[uuid];
1738
- }
1739
- this.deviceProtocol.delete(uuid);
1740
- this.probingProtocols.delete(uuid);
1741
- this.protocolV2Assemblers.delete(uuid);
1742
- this.resetProtocolV2Frames(uuid);
1743
-
1744
- if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1745
- // BleManager.destroy() force-rejects every promise the native queue abandoned —
1746
- // the only JS-reachable way to settle them — and drops all cached peripherals.
1747
- Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
1748
- this.resetPlxManager();
1749
- this.connectionSetupTimeoutCounts.delete(uuid);
1750
- }
1751
- }
1752
-
1753
1505
  private getCachedTransport(uuid: string) {
1754
1506
  const transport = transportCache[uuid];
1755
1507
  if (!transport) {
@@ -1758,107 +1510,6 @@ export default class ReactNativeBleTransport {
1758
1510
  return transport;
1759
1511
  }
1760
1512
 
1761
- /**
1762
- * Write one packet under a bounded budget. A write that never settles means the
1763
- * peripheral is wedged even though the GATT link still reports connected, so the
1764
- * link is torn down: releasing JS state alone would leave the poisoned peripheral
1765
- * cached and every later call would hang on it again.
1766
- */
1767
- private async writeBlePacket(
1768
- uuid: string,
1769
- data: string,
1770
- write: (payload: string) => Promise<unknown>,
1771
- isCurrentOwner?: () => boolean
1772
- ) {
1773
- let timer: ReturnType<typeof setTimeout> | undefined;
1774
- let timedOut = false;
1775
- try {
1776
- await Promise.race([
1777
- write(data),
1778
- new Promise<never>((_, reject) => {
1779
- timer = setTimeout(() => {
1780
- timedOut = true;
1781
- reject(
1782
- ERRORS.TypedError(
1783
- HardwareErrorCode.BleWriteCharacteristicError,
1784
- `BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
1785
- )
1786
- );
1787
- }, BLE_WRITE_PACKET_TIMEOUT_MS);
1788
- }),
1789
- ]);
1790
- this.writeTimeoutCounts.delete(uuid);
1791
- } catch (error) {
1792
- if (timedOut) {
1793
- // A superseded call's late write must not tear down the link the current
1794
- // call is using; only the owner of the transport may declare it dead.
1795
- if (isCurrentOwner && !isCurrentOwner()) {
1796
- Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
1797
- } else {
1798
- this.tearDownWedgedLink(uuid);
1799
- }
1800
- }
1801
- throw error;
1802
- } finally {
1803
- if (timer) clearTimeout(timer);
1804
- }
1805
- }
1806
-
1807
- /**
1808
- * Drop a link whose writes stopped completing. The JS state is purged synchronously
1809
- * so the next acquire() cannot reuse the dead transport, while the native teardown is
1810
- * intentionally NOT awaited: it talks to the very layer that just stopped settling
1811
- * promises, so awaiting it could hang exactly like the write it is recovering from.
1812
- */
1813
- private tearDownWedgedLink(uuid: string) {
1814
- const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
1815
- this.writeTimeoutCounts.set(uuid, timeouts);
1816
- Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
1817
- consecutiveWriteTimeouts: timeouts,
1818
- });
1819
-
1820
- const wedged = transportCache[uuid];
1821
- this.disconnect(uuid).catch(error => {
1822
- Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
1823
- });
1824
- if (wedged && transportCache[uuid] === wedged) {
1825
- delete transportCache[uuid];
1826
- }
1827
- this.deviceProtocol.delete(uuid);
1828
- this.probingProtocols.delete(uuid);
1829
- this.protocolV2Assemblers.delete(uuid);
1830
- this.resetProtocolV2Frames(uuid);
1831
-
1832
- if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1833
- // Reconnecting reuses the same native peripheral object. When it stays wedged
1834
- // across attempts the poison lives in the BLE manager itself, and only a fresh
1835
- // manager drops every cached peripheral — the JS equivalent of restarting the app.
1836
- Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
1837
- this.resetPlxManager();
1838
- this.writeTimeoutCounts.delete(uuid);
1839
- }
1840
- }
1841
-
1842
- private resetPlxManager() {
1843
- const manager = this.blePlxManager;
1844
- this.blePlxManager = undefined;
1845
- // Every cached transport belongs to the destroyed manager's peripherals.
1846
- Object.keys(transportCache).forEach(key => {
1847
- delete transportCache[key];
1848
- });
1849
- this.deviceProtocol.clear();
1850
- this.probingProtocols.clear();
1851
- this.sessionProtocols.clear();
1852
- this.protocolReprobeFailures.clear();
1853
- this.monitorTokens.clear();
1854
- this.protocolV2Assemblers.clear();
1855
- try {
1856
- manager?.destroy();
1857
- } catch (error) {
1858
- Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
1859
- }
1860
- }
1861
-
1862
1513
  private createProtocolMismatchError(expected: ProtocolType) {
1863
1514
  return ERRORS.TypedError(
1864
1515
  HardwareErrorCode.RuntimeError,
@@ -1874,19 +1525,11 @@ export default class ReactNativeBleTransport {
1874
1525
  }
1875
1526
 
1876
1527
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1877
- if (this.probingProtocols.get(uuid) === protocol) {
1878
- this.probingProtocols.delete(uuid);
1879
- }
1880
1528
  if (this.deviceProtocol.get(uuid) === protocol) {
1881
1529
  this.deviceProtocol.delete(uuid);
1882
1530
  }
1883
1531
  }
1884
1532
 
1885
- /** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
1886
- private getActiveProtocol(uuid: string): ProtocolType | undefined {
1887
- return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
1888
- }
1889
-
1890
1533
  private async detectProtocol(
1891
1534
  uuid: string,
1892
1535
  expectedProtocol?: ProtocolType,
@@ -1906,7 +1549,6 @@ export default class ReactNativeBleTransport {
1906
1549
  if (expectedProtocol === 'V1') {
1907
1550
  if (await this.probeProtocolV1(uuid)) {
1908
1551
  this.deviceProtocol.set(uuid, 'V1');
1909
- this.sessionProtocols.set(uuid, 'V1');
1910
1552
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1911
1553
  deviceId: uuid,
1912
1554
  protocol: 'V1',
@@ -1920,7 +1562,6 @@ export default class ReactNativeBleTransport {
1920
1562
  if (expectedProtocol === 'V2') {
1921
1563
  if (await this.probeProtocolV2(uuid)) {
1922
1564
  this.deviceProtocol.set(uuid, 'V2');
1923
- this.sessionProtocols.set(uuid, 'V2');
1924
1565
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1925
1566
  deviceId: uuid,
1926
1567
  protocol: 'V2',
@@ -1933,18 +1574,8 @@ export default class ReactNativeBleTransport {
1933
1574
 
1934
1575
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
1935
1576
  // influence probe order; a V2 hint probes V2 first and falls back to V1.
1936
- const sessionProtocol = this.sessionProtocols.get(uuid);
1937
- const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
1938
- const fullProbeOrder: ProtocolType[] =
1577
+ const probeOrder: ProtocolType[] =
1939
1578
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1940
- // A device that already answered on a protocol in this session keeps answering on
1941
- // it; while it is rebooting nothing answers at all, so probing the other protocol
1942
- // only adds its timeout to every poll.
1943
- const trustSessionProtocol =
1944
- sessionProtocol !== undefined &&
1945
- !protocolHint &&
1946
- reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1947
- const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1948
1579
 
1949
1580
  for (let i = 0; i < probeOrder.length; i += 1) {
1950
1581
  const protocol = probeOrder[i];
@@ -1962,8 +1593,6 @@ export default class ReactNativeBleTransport {
1962
1593
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1963
1594
  if (detected) {
1964
1595
  this.deviceProtocol.set(uuid, protocol);
1965
- this.sessionProtocols.set(uuid, protocol);
1966
- this.protocolReprobeFailures.delete(uuid);
1967
1596
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1968
1597
  deviceId: uuid,
1969
1598
  protocol,
@@ -1973,16 +1602,7 @@ export default class ReactNativeBleTransport {
1973
1602
  }
1974
1603
  }
1975
1604
 
1976
- if (trustSessionProtocol) {
1977
- // Still silent on its own protocol: count it, and let the streak expire the
1978
- // shortcut so a device that genuinely switched protocols is found again.
1979
- this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1980
- } else {
1981
- this.protocolReprobeFailures.delete(uuid);
1982
- }
1983
-
1984
1605
  this.deviceProtocol.delete(uuid);
1985
- this.probingProtocols.delete(uuid);
1986
1606
  throw this.createProtocolDetectionError();
1987
1607
  }
1988
1608
 
@@ -2044,20 +1664,14 @@ export default class ReactNativeBleTransport {
2044
1664
  }
2045
1665
 
2046
1666
  try {
2047
- this.probingProtocols.set(uuid, 'V1');
1667
+ this.deviceProtocol.set(uuid, 'V1');
2048
1668
  // GetFeatures identifies Protocol V1 without resetting an existing wallet
2049
1669
  // session before Core has a chance to restore a hidden wallet.
2050
1670
  await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2051
- this.probingProtocols.delete(uuid);
2052
1671
  return true;
2053
1672
  } catch (error) {
2054
1673
  this.clearProbeProtocol(uuid, 'V1');
2055
1674
  Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
2056
- // A wedged write already dropped the link, so probing another protocol on it
2057
- // would only fail against a torn-down transport: surface the real cause.
2058
- if (isWedgedWriteError(error)) {
2059
- throw error;
2060
- }
2061
1675
  return false;
2062
1676
  }
2063
1677
  }
@@ -2067,7 +1681,7 @@ export default class ReactNativeBleTransport {
2067
1681
  return false;
2068
1682
  }
2069
1683
 
2070
- this.probingProtocols.set(uuid, 'V2');
1684
+ this.deviceProtocol.set(uuid, 'V2');
2071
1685
  this.protocolV2Assemblers.get(uuid)?.reset();
2072
1686
  const detected = await probeProtocolV2Helper({
2073
1687
  call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
@@ -2082,8 +1696,6 @@ export default class ReactNativeBleTransport {
2082
1696
  });
2083
1697
  if (!detected) {
2084
1698
  this.clearProbeProtocol(uuid, 'V2');
2085
- } else {
2086
- this.probingProtocols.delete(uuid);
2087
1699
  }
2088
1700
  return detected;
2089
1701
  }
@@ -2165,7 +1777,6 @@ export default class ReactNativeBleTransport {
2165
1777
  }
2166
1778
 
2167
1779
  private async writeProtocolV2Packet(
2168
- uuid: string,
2169
1780
  transport: BleTransport,
2170
1781
  base64: string,
2171
1782
  context: ProtocolV2CallContext,
@@ -2184,24 +1795,11 @@ export default class ReactNativeBleTransport {
2184
1795
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
2185
1796
  }
2186
1797
  try {
2187
- await this.writeBlePacket(
2188
- uuid,
2189
- base64,
2190
- payload =>
2191
- shouldUseWriteWithResponse
2192
- ? transport.writeCharacteristic.writeWithResponse(payload)
2193
- : transport.writeCharacteristic.writeWithoutResponse(payload),
2194
- // Same rule as Protocol V1: a write from a superseded generation must not
2195
- // tear down the link that the current generation is using.
2196
- () => {
2197
- try {
2198
- assertCurrentGeneration();
2199
- return !context.signal.aborted;
2200
- } catch {
2201
- return false;
2202
- }
2203
- }
2204
- );
1798
+ if (shouldUseWriteWithResponse) {
1799
+ await transport.writeCharacteristic.writeWithResponse(base64);
1800
+ } else {
1801
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1802
+ }
2205
1803
  assertCurrentGeneration();
2206
1804
  return;
2207
1805
  } catch (error) {
@@ -2224,7 +1822,6 @@ export default class ReactNativeBleTransport {
2224
1822
  }
2225
1823
 
2226
1824
  private async writeProtocolV2Frame(
2227
- uuid: string,
2228
1825
  transport: BleTransport,
2229
1826
  frame: Uint8Array,
2230
1827
  context: ProtocolV2CallContext,
@@ -2246,7 +1843,6 @@ export default class ReactNativeBleTransport {
2246
1843
  wait: delay,
2247
1844
  writePacket: packet =>
2248
1845
  this.writeProtocolV2Packet(
2249
- uuid,
2250
1846
  transport,
2251
1847
  Buffer.from(packet).toString('base64'),
2252
1848
  context,
@@ -2424,13 +2020,7 @@ export default class ReactNativeBleTransport {
2424
2020
  writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
2425
2021
  assertCurrentGeneration();
2426
2022
  const currentTransport = this.getCachedTransport(uuid);
2427
- await this.writeProtocolV2Frame(
2428
- uuid,
2429
- currentTransport,
2430
- frame,
2431
- context,
2432
- assertCurrentGeneration
2433
- );
2023
+ await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
2434
2024
  },
2435
2025
  readFrame: async () => {
2436
2026
  assertCurrentGeneration();
@@ -2455,6 +2045,6 @@ export default class ReactNativeBleTransport {
2455
2045
  }
2456
2046
 
2457
2047
  getProtocolType(path: string): ProtocolType | undefined {
2458
- return this.getActiveProtocol(path);
2048
+ return this.deviceProtocol.get(path);
2459
2049
  }
2460
2050
  }