@onekeyfe/hd-transport-react-native 1.2.2-alpha.100 → 1.2.2-alpha.101

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
@@ -1,7 +1,6 @@
1
1
  import { PermissionsAndroid, Platform } from 'react-native';
2
2
  import { Buffer } from 'buffer';
3
3
  import {
4
- BleATTErrorCode,
5
4
  BleError,
6
5
  BleErrorCode,
7
6
  BleManager as BlePlxManager,
@@ -48,6 +47,11 @@ import {
48
47
  getInfosForServiceUuid,
49
48
  isSameBleUuid,
50
49
  } from './constants';
50
+ import {
51
+ isBleStaleBondHardwareError,
52
+ isNativeBleStaleBondError,
53
+ toBleStaleBondHardwareError,
54
+ } from './bleStaleBond';
51
55
  import { isHeaderChunk } from './utils/validateNotify';
52
56
  import BleTransport from './BleTransport';
53
57
  import timer from './utils/timer';
@@ -58,6 +62,15 @@ import type { Characteristic, Device, Subscription } from 'react-native-ble-plx'
58
62
  import type EventEmitter from 'events';
59
63
  import type { BleAcquireInput, TransportOptions } from './types';
60
64
 
65
+ type FirmwareInstallBleAcquireInput = BleAcquireInput & {
66
+ /**
67
+ * Reuse the already-verified protocol after an expected firmware-install
68
+ * disconnect. The install loader accepts status requests but may not answer
69
+ * the generic protocol probe used by a normal acquire.
70
+ */
71
+ skipProtocolProbe?: boolean;
72
+ };
73
+
61
74
  const { check, ProtocolV1, parseConfigure } = transport;
62
75
 
63
76
  const Log = bleLogger;
@@ -309,13 +322,8 @@ type IOBleErrorRemap = Error | BleError | null | undefined;
309
322
 
310
323
  function remapError(error: IOBleErrorRemap) {
311
324
  if (error instanceof BleError) {
312
- if (
313
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
314
- // @ts-expect-error
315
- error.iosErrorCode === BleATTErrorCode.UnlikelyError ||
316
- error.reason === 'Peer removed pairing information'
317
- ) {
318
- throw ERRORS.TypedError(HardwareErrorCode.BlePeerRemovedPairingInformation);
325
+ if (isNativeBleStaleBondError(error)) {
326
+ throw toBleStaleBondHardwareError(error);
319
327
  }
320
328
 
321
329
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -390,6 +398,16 @@ export default class ReactNativeBleTransport {
390
398
  /** Consecutive detections that failed while trusting sessionProtocols. */
391
399
  private protocolReprobeFailures: Map<string, number> = new Map();
392
400
 
401
+ /**
402
+ * Native encryption/pairing failures seen before Protocol V2 probe starts.
403
+ * Pro2/Neo GATT connect can succeed on a stale iOS bond; the CCCD write then
404
+ * fails with ATT 5/15. Remember it so detectProtocol fails immediately.
405
+ */
406
+ private staleBondErrors: Map<string, Error> = new Map();
407
+
408
+ /** Strict or previously confirmed V2 target while acquire installs notifications. */
409
+ private acquiringProtocolV2 = new Set<string>();
410
+
393
411
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
394
412
 
395
413
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
@@ -865,7 +883,7 @@ export default class ReactNativeBleTransport {
865
883
  return transport;
866
884
  }
867
885
 
868
- async acquire(input: BleAcquireInput) {
886
+ async acquire(input: FirmwareInstallBleAcquireInput) {
869
887
  const { uuid } = input;
870
888
 
871
889
  if (!uuid) {
@@ -875,28 +893,73 @@ export default class ReactNativeBleTransport {
875
893
  return this.runLifecycleOperation(uuid, () => this.acquireUnlocked(input));
876
894
  }
877
895
 
878
- private async acquireUnlocked(input: BleAcquireInput) {
879
- const { uuid, forceCleanRunPromise, expectedProtocol } = input;
896
+ private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
897
+ const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
898
+ const shouldMapProtocolV2StaleBond = expectedProtocol
899
+ ? expectedProtocol === 'V2'
900
+ : this.confirmedProtocolV2.has(uuid);
880
901
 
881
902
  const cachedTransport = transportCache[uuid];
903
+ if (skipProtocolProbe && !cachedTransport && this.blePlxManager) {
904
+ Log?.debug(
905
+ '[ReactNativeBleTransport] refresh uncached BLE connection for firmware install:',
906
+ uuid
907
+ );
908
+ const manager = this.blePlxManager;
909
+ await this.runNativeTeardown(uuid, manager, async () => {
910
+ await this.runBestEffortNativeOperation(
911
+ 'firmware install reconnect: cancel uncached device connection',
912
+ () => manager.cancelDeviceConnection(uuid)
913
+ );
914
+ });
915
+ }
882
916
  if (cachedTransport) {
883
- const cachedProtocol = this.deviceProtocol.get(uuid);
884
- const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false);
885
- if (
886
- isCachedDeviceConnected &&
887
- cachedProtocol &&
888
- (!expectedProtocol || cachedProtocol === expectedProtocol)
889
- ) {
890
- Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
891
- return { uuid, protocolType: cachedProtocol };
892
- }
917
+ if (skipProtocolProbe) {
918
+ Log?.debug(
919
+ '[ReactNativeBleTransport] refresh cached BLE connection for firmware install:',
920
+ uuid
921
+ );
922
+ const manager = this.blePlxManager;
923
+ await this.releaseUnlocked(uuid, true);
924
+ await this.runNativeTeardown(uuid, manager, async () => {
925
+ const operations: Promise<unknown>[] = [];
926
+ if (manager) {
927
+ operations.push(
928
+ this.runBestEffortNativeOperation(
929
+ 'firmware install reconnect: cancel device connection',
930
+ () => manager.cancelDeviceConnection(uuid)
931
+ )
932
+ );
933
+ }
934
+ operations.push(
935
+ this.runBestEffortNativeOperation(
936
+ 'firmware install reconnect: device cancel connection',
937
+ () => cachedTransport.device.cancelConnection()
938
+ )
939
+ );
940
+ await Promise.all(operations);
941
+ });
942
+ } else {
943
+ const cachedProtocol = this.deviceProtocol.get(uuid);
944
+ const isCachedDeviceConnected = await cachedTransport.device
945
+ .isConnected()
946
+ .catch(() => false);
947
+ if (
948
+ isCachedDeviceConnected &&
949
+ cachedProtocol &&
950
+ (!expectedProtocol || cachedProtocol === expectedProtocol)
951
+ ) {
952
+ Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
953
+ return { uuid, protocolType: cachedProtocol };
954
+ }
893
955
 
894
- /**
895
- * If the transport is not reusable due to a protocol mismatch or stale
896
- * connection, clean it up before creating a new transport instance.
897
- */
898
- Log?.debug('transport not reusable, will release: ', uuid);
899
- await this.releaseUnlocked(uuid, true);
956
+ /**
957
+ * If the transport is not reusable due to a protocol mismatch or stale
958
+ * connection, clean it up before creating a new transport instance.
959
+ */
960
+ Log?.debug('transport not reusable, will release: ', uuid);
961
+ await this.releaseUnlocked(uuid, true);
962
+ }
900
963
  }
901
964
 
902
965
  let device: Device | null = null;
@@ -917,15 +980,6 @@ export default class ReactNativeBleTransport {
917
980
  throw error;
918
981
  }
919
982
 
920
- if (Platform.OS === 'android') {
921
- const bondState = await pairDevice(uuid);
922
- if (bondState.bonding) {
923
- await onDeviceBondState(uuid);
924
- } else if (!bondState.bonded) {
925
- throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
926
- }
927
- }
928
-
929
983
  if (!device) {
930
984
  const devices = await blePlxManager.devices([uuid]);
931
985
  [device] = devices;
@@ -1010,6 +1064,32 @@ export default class ReactNativeBleTransport {
1010
1064
  }
1011
1065
  }
1012
1066
 
1067
+ if (Platform.OS === 'android') {
1068
+ // Establish the LE link before createBond(). Without an existing LE ACL,
1069
+ // Android TRANSPORT_AUTO can choose BR/EDR for a BLE-only device.
1070
+ const connectedDevice = device;
1071
+ try {
1072
+ const bondState = await pairDevice(uuid);
1073
+ if (bondState.bonding) {
1074
+ await onDeviceBondState(uuid);
1075
+ } else if (!bondState.bonded) {
1076
+ throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
1077
+ }
1078
+ } catch (error) {
1079
+ await this.runNativeTeardown(uuid, blePlxManager, async () => {
1080
+ await Promise.all([
1081
+ this.runBestEffortNativeOperation('bond failure: cancel manager connection', () =>
1082
+ blePlxManager.cancelDeviceConnection(uuid)
1083
+ ),
1084
+ this.runBestEffortNativeOperation('bond failure: cancel device connection', () =>
1085
+ connectedDevice.cancelConnection()
1086
+ ),
1087
+ ]);
1088
+ });
1089
+ throw error;
1090
+ }
1091
+ }
1092
+
1013
1093
  device = await resolveNegotiatedMtu(device);
1014
1094
  const acquiredDevice = device;
1015
1095
  const { writeCharacteristic, notifyCharacteristic } =
@@ -1025,12 +1105,48 @@ export default class ReactNativeBleTransport {
1025
1105
  this.deviceProtocolHints.set(uuid, protocolHint);
1026
1106
  }
1027
1107
 
1028
- await this.installTransportForAcquire(uuid, acquiredDevice, {
1029
- writeCharacteristic,
1030
- notifyCharacteristic,
1031
- });
1108
+ if (shouldMapProtocolV2StaleBond) {
1109
+ this.acquiringProtocolV2.add(uuid);
1110
+ }
1032
1111
 
1033
1112
  try {
1113
+ await this.installTransportForAcquire(uuid, acquiredDevice, {
1114
+ writeCharacteristic,
1115
+ notifyCharacteristic,
1116
+ });
1117
+
1118
+ if (skipProtocolProbe) {
1119
+ if (!expectedProtocol) {
1120
+ throw ERRORS.TypedError(
1121
+ HardwareErrorCode.RuntimeError,
1122
+ 'skipProtocolProbe requires an expected BLE protocol'
1123
+ );
1124
+ }
1125
+ const hasConfirmedProtocol =
1126
+ this.sessionProtocols.get(uuid) === expectedProtocol ||
1127
+ (expectedProtocol === 'V2' && this.confirmedProtocolV2.has(uuid));
1128
+ if (!hasConfirmedProtocol) {
1129
+ throw ERRORS.TypedError(
1130
+ HardwareErrorCode.RuntimeError,
1131
+ 'skipProtocolProbe requires a previously confirmed protocol for this BLE endpoint'
1132
+ );
1133
+ }
1134
+ this.deviceProtocol.set(uuid, expectedProtocol);
1135
+ this.sessionProtocols.set(uuid, expectedProtocol);
1136
+ this.protocolReprobeFailures.delete(uuid);
1137
+ Log?.debug('[ReactNativeBleTransport] protocol selected without probe', {
1138
+ deviceId: uuid,
1139
+ protocol: expectedProtocol,
1140
+ source: 'firmware-install-reconnect',
1141
+ });
1142
+ const currentTransport = transportCache[uuid];
1143
+ if (!currentTransport) {
1144
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1145
+ }
1146
+ this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
1147
+ return { uuid, protocolType: expectedProtocol };
1148
+ }
1149
+
1034
1150
  const protocolType = await this.detectProtocol(
1035
1151
  uuid,
1036
1152
  expectedProtocol,
@@ -1046,12 +1162,14 @@ export default class ReactNativeBleTransport {
1046
1162
  this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
1047
1163
  return { uuid, protocolType };
1048
1164
  } catch (error) {
1049
- if ((error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceBondError) {
1165
+ if (isBleStaleBondHardwareError(error)) {
1050
1166
  await this.disconnectUnlocked(uuid);
1051
1167
  } else {
1052
1168
  await this.releaseUnlocked(uuid, true);
1053
1169
  }
1054
1170
  throw error;
1171
+ } finally {
1172
+ this.acquiringProtocolV2.delete(uuid);
1055
1173
  }
1056
1174
  }
1057
1175
 
@@ -1079,17 +1197,21 @@ export default class ReactNativeBleTransport {
1079
1197
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
1080
1198
  return;
1081
1199
  }
1200
+ if (
1201
+ (this.getActiveProtocol(uuid) === 'V2' || this.acquiringProtocolV2.has(uuid)) &&
1202
+ isNativeBleStaleBondError(error)
1203
+ ) {
1204
+ this.rememberStaleBondError(uuid, toBleStaleBondHardwareError(error));
1205
+ return;
1206
+ }
1082
1207
  if (this.getActiveProtocol(uuid) === 'V2') {
1083
1208
  let errorCode:
1084
- | typeof HardwareErrorCode.BleDeviceBondError
1085
1209
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
1086
1210
  | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
1087
1211
  | typeof HardwareErrorCode.BleTimeoutError =
1088
1212
  HardwareErrorCode.BleCharacteristicNotifyError;
1089
1213
  if (error.reason?.includes('The connection has timed out unexpectedly')) {
1090
1214
  errorCode = HardwareErrorCode.BleTimeoutError;
1091
- } else if (error.reason?.includes('Encryption is insufficient')) {
1092
- errorCode = HardwareErrorCode.BleDeviceBondError;
1093
1215
  } else if (
1094
1216
  error.reason?.includes('Cannot write client characteristic config descriptor') ||
1095
1217
  error.reason?.includes('Cannot find client characteristic config descriptor') ||
@@ -1104,16 +1226,12 @@ export default class ReactNativeBleTransport {
1104
1226
  }
1105
1227
  if (this.runPromise && this.runPromiseDeviceId === uuid) {
1106
1228
  let ERROR:
1107
- | typeof HardwareErrorCode.BleDeviceBondError
1108
1229
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
1109
1230
  | typeof HardwareErrorCode.BleTimeoutError =
1110
1231
  HardwareErrorCode.BleCharacteristicNotifyError;
1111
1232
  if (error.reason?.includes('The connection has timed out unexpectedly')) {
1112
1233
  ERROR = HardwareErrorCode.BleTimeoutError;
1113
1234
  }
1114
- if (error.reason?.includes('Encryption is insufficient')) {
1115
- ERROR = HardwareErrorCode.BleDeviceBondError;
1116
- }
1117
1235
  if (
1118
1236
  error.reason?.includes('Cannot write client characteristic config descriptor') ||
1119
1237
  error.reason?.includes('Cannot find client characteristic config descriptor') || // pro firmware 2.3.0 upgrade
@@ -1248,6 +1366,8 @@ export default class ReactNativeBleTransport {
1248
1366
 
1249
1367
  this.deviceProtocol.delete(uuid);
1250
1368
  this.probingProtocols.delete(uuid);
1369
+ this.staleBondErrors.delete(uuid);
1370
+ this.acquiringProtocolV2.delete(uuid);
1251
1371
  // Confirmed protocol and caller hints stay in deviceProtocol / protocolHint.
1252
1372
  this.protocolV2Assemblers.get(uuid)?.reset();
1253
1373
  this.protocolV2Assemblers.delete(uuid);
@@ -1620,6 +1740,7 @@ export default class ReactNativeBleTransport {
1620
1740
  }
1621
1741
  this.deviceProtocol.delete(session);
1622
1742
  this.probingProtocols.delete(session);
1743
+ this.staleBondErrors.delete(session);
1623
1744
  this.deviceProtocolHints.delete(session);
1624
1745
  this.sessionProtocols.delete(session);
1625
1746
  this.protocolReprobeFailures.delete(session);
@@ -1843,6 +1964,8 @@ export default class ReactNativeBleTransport {
1843
1964
  }
1844
1965
  this.deviceProtocol.delete(uuid);
1845
1966
  this.probingProtocols.delete(uuid);
1967
+ this.staleBondErrors.delete(uuid);
1968
+ this.acquiringProtocolV2.delete(uuid);
1846
1969
  this.protocolV2Assemblers.delete(uuid);
1847
1970
  this.resetProtocolV2Frames(uuid);
1848
1971
 
@@ -1939,6 +2062,8 @@ export default class ReactNativeBleTransport {
1939
2062
  }
1940
2063
  this.deviceProtocol.delete(uuid);
1941
2064
  this.probingProtocols.delete(uuid);
2065
+ this.staleBondErrors.delete(uuid);
2066
+ this.acquiringProtocolV2.delete(uuid);
1942
2067
  this.protocolV2Assemblers.delete(uuid);
1943
2068
  this.resetProtocolV2Frames(uuid);
1944
2069
 
@@ -1989,8 +2114,11 @@ export default class ReactNativeBleTransport {
1989
2114
  });
1990
2115
  this.deviceProtocol.clear();
1991
2116
  this.probingProtocols.clear();
2117
+ this.staleBondErrors.clear();
2118
+ this.acquiringProtocolV2.clear();
1992
2119
  this.sessionProtocols.clear();
1993
- this.confirmedProtocolV2.clear();
2120
+ // Keep transport-lifetime V2 proof so the same endpoint can finish a no-probe
2121
+ // firmware reconnect after the native BLE manager is recreated.
1994
2122
  this.protocolReprobeFailures.clear();
1995
2123
  this.writeTimeoutCounts.clear();
1996
2124
  this.connectionSetupTimeoutCounts.clear();
@@ -2003,12 +2131,11 @@ export default class ReactNativeBleTransport {
2003
2131
  }
2004
2132
  }
2005
2133
 
2006
- private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
2007
- // A generic Ping miss is not a bond failure. Only a later miss after this
2008
- // endpoint already answered V2, or a native encryption/pairing error, is.
2009
- const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
2134
+ private createProtocolMismatchError(expected: ProtocolType) {
2135
+ // A protocol probe miss alone does not prove that the OS bond is stale.
2136
+ // Native authentication/encryption failures are mapped separately.
2010
2137
  return ERRORS.TypedError(
2011
- isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
2138
+ HardwareErrorCode.RuntimeError,
2012
2139
  `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
2013
2140
  );
2014
2141
  }
@@ -2040,31 +2167,28 @@ export default class ReactNativeBleTransport {
2040
2167
  protocolHint?: ProtocolType,
2041
2168
  rebuildTransport?: () => Promise<void>
2042
2169
  ): Promise<ProtocolType> {
2043
- // iOS still skips an extra V1 Initialize during acquire. Expected V2 must
2044
- // Ping so USB-priority `link disabled` can surface instead of a later
2045
- // unmapped RuntimeError.
2046
- if (Platform.OS === 'ios' && expectedProtocol === 'V1') {
2047
- this.deviceProtocol.set(uuid, expectedProtocol);
2170
+ // A declared V1 is taken at face value on every platform, as iOS has done
2171
+ // since protocol probing arrived: the caller reads the protocol off its own
2172
+ // device record, so the probe re-asks a question that is already answered
2173
+ // and costs a round trip on every acquire. Expected V2 must still Ping so
2174
+ // USB-priority `link disabled` surfaces here instead of as a later unmapped
2175
+ // RuntimeError. sessionProtocols is deliberately NOT stamped here: that map
2176
+ // records protocols the device actually answered on (it gates the
2177
+ // trustSessionProtocol narrowing in forced detection), and this branch has
2178
+ // received no response. skipProtocolProbe does not need it either — its only
2179
+ // caller is the V2 firmware-install reconnect.
2180
+ if (expectedProtocol === 'V1') {
2181
+ this.deviceProtocol.set(uuid, 'V1');
2048
2182
  Log?.debug('[ReactNativeBleTransport] protocol selected', {
2049
2183
  deviceId: uuid,
2050
- protocol: expectedProtocol,
2184
+ protocol: 'V1',
2051
2185
  source: 'expected',
2052
2186
  });
2053
- return expectedProtocol;
2187
+ return 'V1';
2054
2188
  }
2055
2189
 
2056
- if (expectedProtocol === 'V1') {
2057
- if (await this.probeProtocolV1(uuid)) {
2058
- this.deviceProtocol.set(uuid, 'V1');
2059
- this.sessionProtocols.set(uuid, 'V1');
2060
- Log?.debug('[ReactNativeBleTransport] protocol detected', {
2061
- deviceId: uuid,
2062
- protocol: 'V1',
2063
- source: 'expected',
2064
- });
2065
- return 'V1';
2066
- }
2067
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
2190
+ if (expectedProtocol === 'V2' || this.acquiringProtocolV2.has(uuid)) {
2191
+ this.throwIfStaleBondError(uuid);
2068
2192
  }
2069
2193
 
2070
2194
  if (expectedProtocol === 'V2') {
@@ -2079,7 +2203,7 @@ export default class ReactNativeBleTransport {
2079
2203
  });
2080
2204
  return 'V2';
2081
2205
  }
2082
- throw this.createProtocolMismatchError(expectedProtocol, uuid);
2206
+ throw this.createProtocolMismatchError(expectedProtocol);
2083
2207
  }
2084
2208
 
2085
2209
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
@@ -2223,6 +2347,7 @@ export default class ReactNativeBleTransport {
2223
2347
 
2224
2348
  this.probingProtocols.set(uuid, 'V2');
2225
2349
  this.protocolV2Assemblers.get(uuid)?.reset();
2350
+ this.throwIfStaleBondError(uuid);
2226
2351
  const detected = await probeProtocolV2Helper({
2227
2352
  call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
2228
2353
  this.callProtocolV2(uuid, name, data, options),
@@ -2233,6 +2358,7 @@ export default class ReactNativeBleTransport {
2233
2358
  this.protocolV2Assemblers.get(uuid)?.reset();
2234
2359
  this.resetProtocolV2Frames(uuid);
2235
2360
  },
2361
+ shouldRethrow: isBleStaleBondHardwareError,
2236
2362
  });
2237
2363
  if (!detected) {
2238
2364
  this.clearProbeProtocol(uuid, 'V2');
@@ -2301,6 +2427,21 @@ export default class ReactNativeBleTransport {
2301
2427
  }
2302
2428
  }
2303
2429
 
2430
+ private rememberStaleBondError(uuid: string, error: Error) {
2431
+ this.staleBondErrors.set(uuid, error);
2432
+ this.rejectProtocolV2Frames(uuid, error);
2433
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
2434
+ this.runPromise.reject(error);
2435
+ }
2436
+ }
2437
+
2438
+ private throwIfStaleBondError(uuid: string) {
2439
+ const error = this.staleBondErrors.get(uuid);
2440
+ if (error) {
2441
+ throw error;
2442
+ }
2443
+ }
2444
+
2304
2445
  private async readProtocolV2Frame(uuid: string) {
2305
2446
  const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
2306
2447
  if (queuedFrame) {
@@ -2359,6 +2500,11 @@ export default class ReactNativeBleTransport {
2359
2500
  assertCurrentGeneration();
2360
2501
  return;
2361
2502
  } catch (error) {
2503
+ if (isNativeBleStaleBondError(error) || isBleStaleBondHardwareError(error)) {
2504
+ const bondError = toBleStaleBondHardwareError(error);
2505
+ this.rememberStaleBondError(uuid, bondError);
2506
+ throw bondError;
2507
+ }
2362
2508
  if (
2363
2509
  getFirmwareUploadWriteRetryType(error) !== 'congested' ||
2364
2510
  attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES