@onekeyfe/hd-transport-react-native 1.2.0-alpha.68 → 1.2.0-alpha.69

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
@@ -5,6 +5,7 @@ import {
5
5
  BleError,
6
6
  BleErrorCode,
7
7
  BleManager as BlePlxManager,
8
+ ConnectionPriority,
8
9
  ScanMode,
9
10
  } from 'react-native-ble-plx';
10
11
  import ByteBuffer from 'bytebuffer';
@@ -32,11 +33,17 @@ import {
32
33
  } from '@onekeyfe/hd-shared';
33
34
 
34
35
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
35
- import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
36
+ import {
37
+ hasWritableCapability,
38
+ resolveProtocolV2PacketCapacity,
39
+ shouldWriteProtocolV2WithResponse,
40
+ } from './bleStrategy';
36
41
  import { subscribeBleOn } from './subscribeBleOn';
37
42
  import {
38
43
  ANDROID_PACKET_LENGTH,
44
+ ANDROID_PROTOCOL_V2_PACKET_LENGTH,
39
45
  IOS_PACKET_LENGTH,
46
+ IOS_PROTOCOL_V2_PACKET_LENGTH,
40
47
  getBluetoothServiceUuids,
41
48
  getInfosForServiceUuid,
42
49
  isSameBleUuid,
@@ -61,7 +68,6 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
61
68
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
62
69
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
63
70
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
64
- const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
65
71
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
66
72
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
67
73
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
@@ -135,22 +141,6 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
135
141
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
136
142
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
137
143
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
138
- /**
139
- * Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
140
- * reports the peripheral ready again; a peripheral wedged by its own firmware reboot
141
- * stops reporting ready while staying connected, so the write promise never settles.
142
- * Response timeouts cannot cover that — they are armed after the writes complete —
143
- * and an unbounded write leaves the whole transport unusable until the process dies.
144
- * A healthy packet completes in milliseconds, so this only fires on a dead link.
145
- */
146
- export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
147
- const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
148
- const isWedgedWriteError = (error: unknown): boolean =>
149
- (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
150
- typeof (error as { message?: unknown })?.message === 'string' &&
151
- (error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
152
- /** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
153
- export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
154
144
  const DEVICE_SCAN_TIMEOUT_MS = 3000;
155
145
  const IOS_NOTIFY_READY_DELAY_MS = 150;
156
146
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
@@ -162,8 +152,8 @@ export type ProtocolV2BleTuning = {
162
152
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
163
153
 
164
154
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
165
- iosPacketLength: IOS_PACKET_LENGTH,
166
- androidPacketLength: ANDROID_PACKET_LENGTH,
155
+ iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
156
+ androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
167
157
  };
168
158
 
169
159
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -205,54 +195,21 @@ function getDeviceDisplayName(device?: Device | null) {
205
195
  return device?.name || device?.localName || null;
206
196
  }
207
197
 
208
- const ANDROID_REQUEST_MTU = 256;
198
+ const IOS_REQUEST_MTU = 247;
199
+ const ANDROID_REQUEST_MTU = 517;
200
+ const BLE_MTU_REFRESH_THRESHOLD = 247;
201
+ const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
202
+ const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
209
203
 
210
- const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
204
+ const getRequestedBleMtu = () =>
205
+ Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
211
206
 
212
207
  const connectOptions: Record<string, unknown> = {
213
- requestMTU: ANDROID_REQUEST_MTU,
214
- timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
208
+ requestMTU: getRequestedBleMtu(),
209
+ timeout: 3000,
215
210
  refreshGatt: 'OnConnected',
216
211
  };
217
212
 
218
- /** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
219
- const fallbackConnectOptions: Record<string, unknown> = {
220
- timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
221
- };
222
-
223
- /**
224
- * JS backstop for connect. The native adapter applies its own 3s budget, but it
225
- * schedules that timeout on its serial queue, so a busy queue (e.g. right after a
226
- * firmware install tears the link down) can leave the promise unsettled — observed
227
- * blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
228
- * inside the native budget, so this only fires when the native timeout did not.
229
- */
230
- export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
231
- /**
232
- * Service discovery and characteristic resolution run after connect() succeeds, but
233
- * CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
234
- * device reboot, these calls can remain pending forever unless they have their own
235
- * budget.
236
- */
237
- export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
238
- /**
239
- * How many times a known device may fail its own protocol before we probe the others
240
- * again. Reconnect polling during a device reboot repeats this every few seconds, and
241
- * probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
242
- * device we just spoke V1 to dominates the wait. A firmware update can legitimately
243
- * change a device's protocol, so the shortcut has to expire rather than stick.
244
- */
245
- export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
246
- /** BLE setup timeouts since the last successful setup before the manager is recreated. */
247
- export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
248
- const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
249
- const isConnectTimeoutError = (error: unknown): boolean =>
250
- (error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
251
- typeof (error as { message?: unknown })?.message === 'string' &&
252
- (error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
253
- const isNativeOperationTimeoutError = (error: unknown): boolean =>
254
- (error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
255
-
256
213
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
257
214
 
258
215
  const tryToGetConfiguration = (device: Device) => {
@@ -264,23 +221,32 @@ const tryToGetConfiguration = (device: Device) => {
264
221
  return infos;
265
222
  };
266
223
 
267
- const requestAndroidMtu = async (device: Device) => {
268
- if (Platform.OS !== 'android') return device;
224
+ const requestNegotiatedMtu = async (
225
+ device: Device,
226
+ stage: 'connected' | 'servicesAndNotifyReady',
227
+ attempt: number
228
+ ) => {
229
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
269
230
 
270
231
  try {
271
- const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
272
- Log?.debug('[ReactNativeBleTransport] MTU configured', {
273
- deviceId: device.id,
274
- requested: ANDROID_REQUEST_MTU,
275
- actual: mtuDevice.mtu,
276
- });
232
+ // iOS ignores the requested value but react-native-ble-plx returns a fresh
233
+ // Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
234
+ const mtuDevice = await device.requestMTU(getRequestedBleMtu());
277
235
  return mtuDevice;
278
236
  } catch (error) {
279
- Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
237
+ Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
238
+ platform: Platform.OS,
239
+ stage,
240
+ attempt,
241
+ actual: device.mtu,
242
+ error: error instanceof Error ? error.message : String(error),
243
+ });
280
244
  return device;
281
245
  }
282
246
  };
283
247
 
248
+ const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
249
+
284
250
  type IOBleErrorRemap = Error | BleError | null | undefined;
285
251
 
286
252
  function remapError(error: IOBleErrorRemap) {
@@ -341,28 +307,8 @@ export default class ReactNativeBleTransport {
341
307
  /** Per-device protocol type detected by active wire-level probe after connect. */
342
308
  private deviceProtocol: Map<string, ProtocolType> = new Map();
343
309
 
344
- /**
345
- * Protocol a probe is currently trying, before the device has confirmed it. Calls
346
- * must route with it, but acquire() must not treat it as a detected protocol: a
347
- * probe that never answers would otherwise leave the reuse fast path handing out a
348
- * transport that was never validated.
349
- */
350
- private probingProtocols: Map<string, ProtocolType> = new Map();
351
-
352
- /** Consecutive write timeouts per device; reset by any write that completes. */
353
- private writeTimeoutCounts: Map<string, number> = new Map();
354
-
355
- /** BLE setup timeouts per device since the last complete characteristic resolution. */
356
- private connectionSetupTimeoutCounts: Map<string, number> = new Map();
357
-
358
310
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
359
311
 
360
- /** Protocol this device actually answered on, kept across reconnects of one session. */
361
- private sessionProtocols: Map<string, ProtocolType> = new Map();
362
-
363
- /** Consecutive detections that failed while trusting sessionProtocols. */
364
- private protocolReprobeFailures: Map<string, number> = new Map();
365
-
366
312
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
367
313
 
368
314
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
@@ -394,6 +340,12 @@ export default class ReactNativeBleTransport {
394
340
 
395
341
  private disconnectEventTokens: Map<string, number> = new Map();
396
342
 
343
+ private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
344
+
345
+ private androidHighPriorityDevices: Set<string> = new Set();
346
+
347
+ private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
348
+
397
349
  private nextMonitorToken = 1;
398
350
 
399
351
  constructor(options: TransportOptions) {
@@ -580,21 +532,22 @@ export default class ReactNativeBleTransport {
580
532
  const isConnected = await device.isConnected().catch(() => false);
581
533
  if (!isConnected) {
582
534
  try {
583
- device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
535
+ device = await device.connect(connectOptions);
584
536
  } catch (e) {
585
537
  if (
586
538
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
587
539
  e.errorCode === BleErrorCode.OperationCancelled
588
540
  ) {
589
- device = await this.connectWithTimeout(uuid, () => device.connect());
541
+ device = await device.connect();
590
542
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
591
543
  throw e;
592
544
  }
593
545
  }
594
546
  }
595
547
 
596
- const { writeCharacteristic, notifyCharacteristic } =
597
- await this.resolveCharacteristicsWithTimeout(uuid, device);
548
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
549
+ device
550
+ );
598
551
 
599
552
  transport.device = device;
600
553
  transport.writeCharacteristic = writeCharacteristic;
@@ -773,11 +726,9 @@ export default class ReactNativeBleTransport {
773
726
  characteristics?: ResolvedBleCharacteristics
774
727
  ) {
775
728
  const { writeCharacteristic, notifyCharacteristic } =
776
- characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
729
+ characteristics ?? (await this.resolveCharacteristics(device));
777
730
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
778
- if (Platform.OS === 'android') {
779
- transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
780
- }
731
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
781
732
  const monitorToken = this.nextMonitorToken;
782
733
  this.nextMonitorToken += 1;
783
734
  const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
@@ -791,6 +742,7 @@ export default class ReactNativeBleTransport {
791
742
  notifyTransactionId
792
743
  );
793
744
  transportCache[uuid] = transport;
745
+ this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
794
746
  this.protocolV2Assemblers.set(
795
747
  uuid,
796
748
  new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
@@ -804,6 +756,40 @@ export default class ReactNativeBleTransport {
804
756
  await delay(ANDROID_NOTIFY_READY_DELAY_MS);
805
757
  }
806
758
 
759
+ const initialMtu = transport.mtuSize;
760
+ let refreshAttempts = 0;
761
+ if (
762
+ (Platform.OS === 'ios' || Platform.OS === 'android') &&
763
+ (typeof transport.mtuSize !== 'number' || transport.mtuSize < BLE_MTU_REFRESH_THRESHOLD)
764
+ ) {
765
+ refreshAttempts += 1;
766
+ let refreshedDevice = await requestNegotiatedMtu(
767
+ transport.device,
768
+ 'servicesAndNotifyReady',
769
+ 1
770
+ );
771
+ transport.device = refreshedDevice;
772
+ transport.mtuSize =
773
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
774
+
775
+ if (typeof transport.mtuSize !== 'number' || transport.mtuSize < BLE_MTU_REFRESH_THRESHOLD) {
776
+ await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
777
+ refreshAttempts += 1;
778
+ refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
779
+ transport.device = refreshedDevice;
780
+ transport.mtuSize =
781
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
782
+ }
783
+ }
784
+
785
+ Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
786
+ platform: Platform.OS,
787
+ requested: getRequestedBleMtu(),
788
+ initial: initialMtu,
789
+ actual: transport.mtuSize,
790
+ refreshAttempts,
791
+ });
792
+
807
793
  return transport;
808
794
  }
809
795
 
@@ -877,22 +863,15 @@ export default class ReactNativeBleTransport {
877
863
  if (!device) {
878
864
  Log?.debug('try to connect to device: ', uuid);
879
865
  try {
880
- device = await this.connectWithTimeout(uuid, () =>
881
- blePlxManager.connectToDevice(uuid, connectOptions)
882
- );
866
+ device = await blePlxManager.connectToDevice(uuid, connectOptions);
883
867
  } catch (e) {
884
868
  Log?.debug('try to connect to device has error: ', e);
885
- if (isConnectTimeoutError(e)) {
886
- throw e;
887
- }
888
869
  if (
889
870
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
890
871
  e.errorCode === BleErrorCode.OperationCancelled
891
872
  ) {
892
873
  Log?.debug('first try to reconnect without params');
893
- device = await this.connectWithTimeout(uuid, () =>
894
- blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
895
- );
874
+ device = await blePlxManager.connectToDevice(uuid);
896
875
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
897
876
  Log?.debug('device already connected');
898
877
  throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
@@ -908,36 +887,26 @@ export default class ReactNativeBleTransport {
908
887
 
909
888
  if (!(await device.isConnected())) {
910
889
  Log?.debug('not connected, try to connect to device: ', uuid);
911
- const disconnectedDevice = device;
912
890
 
913
891
  try {
914
- device = await this.connectWithTimeout(uuid, () =>
915
- disconnectedDevice.connect(connectOptions)
916
- );
892
+ device = await device.connect(connectOptions);
917
893
  } catch (e) {
918
894
  Log?.debug('not connected, try to connect to device has error: ', e);
919
- if (isConnectTimeoutError(e)) {
920
- throw e;
921
- }
922
895
  if (
923
896
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
924
897
  e.errorCode === BleErrorCode.OperationCancelled
925
898
  ) {
926
899
  Log?.debug('second try to reconnect without params');
927
900
  try {
928
- device = await this.connectWithTimeout(uuid, () =>
929
- disconnectedDevice.connect(fallbackConnectOptions)
930
- );
901
+ device = await device.connect();
931
902
  } catch (e) {
932
903
  Log?.debug('last try to reconnect error: ', e);
933
904
  // last try to reconnect device if this issue exists
934
905
  // https://github.com/dotintent/react-native-ble-plx/issues/426
935
906
  if (e.errorCode === BleErrorCode.OperationCancelled) {
936
907
  Log?.debug('last try to reconnect');
937
- await disconnectedDevice.cancelConnection();
938
- device = await this.connectWithTimeout(uuid, () =>
939
- disconnectedDevice.connect(fallbackConnectOptions)
940
- );
908
+ await device.cancelConnection();
909
+ device = await device.connect();
941
910
  }
942
911
  }
943
912
  } else {
@@ -946,10 +915,11 @@ export default class ReactNativeBleTransport {
946
915
  }
947
916
  }
948
917
 
949
- device = await requestAndroidMtu(device);
918
+ device = await resolveNegotiatedMtu(device);
950
919
  const acquiredDevice = device;
951
- const { writeCharacteristic, notifyCharacteristic } =
952
- await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
920
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
921
+ acquiredDevice
922
+ );
953
923
 
954
924
  const protocolHint = expectedProtocol
955
925
  ? undefined
@@ -981,7 +951,7 @@ export default class ReactNativeBleTransport {
981
951
  if (!currentTransport) {
982
952
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
983
953
  }
984
- this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
954
+ this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
985
955
  return { uuid, protocolType };
986
956
  } catch (error) {
987
957
  await this.release(uuid, true);
@@ -1013,7 +983,7 @@ export default class ReactNativeBleTransport {
1013
983
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
1014
984
  return;
1015
985
  }
1016
- if (this.getActiveProtocol(uuid) === 'V2') {
986
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1017
987
  let errorCode:
1018
988
  | typeof HardwareErrorCode.BleDeviceBondError
1019
989
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -1083,7 +1053,7 @@ export default class ReactNativeBleTransport {
1083
1053
 
1084
1054
  try {
1085
1055
  const data = Buffer.from(c.value as string, 'base64');
1086
- const protocol = this.getActiveProtocol(uuid);
1056
+ const protocol = this.deviceProtocol.get(uuid);
1087
1057
  if (!protocol) {
1088
1058
  Log?.debug('monitor data ignored before protocol detection: ', uuid);
1089
1059
  return;
@@ -1117,7 +1087,7 @@ export default class ReactNativeBleTransport {
1117
1087
  } catch (error) {
1118
1088
  Log?.debug('monitor data error: ', error);
1119
1089
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1120
- if (this.getActiveProtocol(uuid) === 'V2') {
1090
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1121
1091
  this.rejectProtocolV2Frames(uuid, notifyError);
1122
1092
  } else if (this.runPromiseDeviceId === uuid) {
1123
1093
  this.runPromise?.reject(notifyError);
@@ -1151,6 +1121,8 @@ export default class ReactNativeBleTransport {
1151
1121
  return Promise.resolve(true);
1152
1122
  }
1153
1123
 
1124
+ await this.restoreAndroidConnectionPriority(uuid, transport);
1125
+
1154
1126
  if (transport) {
1155
1127
  if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1156
1128
  this.monitorTokens.delete(uuid);
@@ -1180,8 +1152,9 @@ export default class ReactNativeBleTransport {
1180
1152
  delete transportCache[uuid];
1181
1153
  }
1182
1154
 
1155
+ this.protocolV2HighVolumeLogSignatures.delete(uuid);
1156
+
1183
1157
  this.deviceProtocol.delete(uuid);
1184
- this.probingProtocols.delete(uuid);
1185
1158
  // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
1186
1159
  this.protocolV2Assemblers.get(uuid)?.reset();
1187
1160
  this.protocolV2Assemblers.delete(uuid);
@@ -1248,25 +1221,8 @@ export default class ReactNativeBleTransport {
1248
1221
  const transport = this.getCachedTransport(uuid);
1249
1222
  const runPromise = createDeferred<string>();
1250
1223
  runPromise.promise.catch(() => undefined);
1251
- const supersededRunPromise = this.runPromise;
1252
- if (supersededRunPromise) {
1253
- // Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
1254
- // the superseded deferred now so its response race resolves and its finally block
1255
- // clears its timeout timer; an orphaned timer would otherwise fire much later and
1256
- // tear down the shared connection while another call is using it.
1257
- supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
1258
- }
1259
1224
  this.runPromise = runPromise;
1260
1225
  this.runPromiseDeviceId = uuid;
1261
- // A superseded call's late write failure must not clear the successor's ownership;
1262
- // only the call that still owns the slot may release it.
1263
- const releaseOwnershipIfCurrent = () => {
1264
- if (this.runPromise === runPromise) {
1265
- this.runPromise = null;
1266
- this.runPromiseDeviceId = null;
1267
- }
1268
- };
1269
- const isCurrentOwner = () => this.runPromise === runPromise;
1270
1226
  const messages = this._messages;
1271
1227
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1272
1228
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1292,9 +1248,6 @@ export default class ReactNativeBleTransport {
1292
1248
  chunk = ByteBuffer.allocate(packetCapacity);
1293
1249
  } catch (e) {
1294
1250
  onError(e);
1295
- if (isWedgedWriteError(e)) {
1296
- throw e;
1297
- }
1298
1251
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1299
1252
  }
1300
1253
  }
@@ -1326,9 +1279,6 @@ export default class ReactNativeBleTransport {
1326
1279
  }
1327
1280
  } catch (e) {
1328
1281
  onError(e);
1329
- if (isWedgedWriteError(e)) {
1330
- throw e;
1331
- }
1332
1282
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1333
1283
  }
1334
1284
  }
@@ -1342,15 +1292,9 @@ export default class ReactNativeBleTransport {
1342
1292
  if (name === 'EmmcFileWrite') {
1343
1293
  await writeChunkedData(
1344
1294
  buffers,
1345
- data =>
1346
- this.writeBlePacket(
1347
- uuid,
1348
- data,
1349
- payload => transport.writeWithRetry(payload),
1350
- isCurrentOwner
1351
- ),
1295
+ data => transport.writeWithRetry(data),
1352
1296
  e => {
1353
- releaseOwnershipIfCurrent();
1297
+ this.runPromise = null;
1354
1298
  Log?.error('writeCharacteristic write error: ', e);
1355
1299
  }
1356
1300
  );
@@ -1371,12 +1315,7 @@ export default class ReactNativeBleTransport {
1371
1315
  // eslint-disable-next-line no-constant-condition
1372
1316
  while (true) {
1373
1317
  try {
1374
- await this.writeBlePacket(
1375
- uuid,
1376
- data,
1377
- payload => transport.writeWithRetry(payload),
1378
- isCurrentOwner
1379
- );
1318
+ await transport.writeWithRetry(data);
1380
1319
  return;
1381
1320
  } catch (error) {
1382
1321
  const retryType = getFirmwareUploadWriteRetryType(error);
@@ -1395,7 +1334,7 @@ export default class ReactNativeBleTransport {
1395
1334
  }
1396
1335
  },
1397
1336
  e => {
1398
- releaseOwnershipIfCurrent();
1337
+ this.runPromise = null;
1399
1338
  Log?.error('writeCharacteristic write error: ', e);
1400
1339
  }
1401
1340
  );
@@ -1406,21 +1345,14 @@ export default class ReactNativeBleTransport {
1406
1345
  try {
1407
1346
  const shouldUseWriteWithResponse =
1408
1347
  Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1409
- await this.writeBlePacket(
1410
- uuid,
1411
- outData,
1412
- payload =>
1413
- shouldUseWriteWithResponse
1414
- ? transport.writeCharacteristic.writeWithResponse(payload)
1415
- : transport.writeCharacteristic.writeWithoutResponse(payload),
1416
- isCurrentOwner
1417
- );
1348
+ if (shouldUseWriteWithResponse) {
1349
+ await transport.writeCharacteristic.writeWithResponse(outData);
1350
+ } else {
1351
+ await transport.writeCharacteristic.writeWithoutResponse(outData);
1352
+ }
1418
1353
  } catch (e) {
1419
1354
  Log?.debug('writeCharacteristic write error: ', e);
1420
- releaseOwnershipIfCurrent();
1421
- if (isWedgedWriteError(e)) {
1422
- throw e;
1423
- }
1355
+ this.runPromise = null;
1424
1356
  if (e.errorCode === BleErrorCode.DeviceDisconnected) {
1425
1357
  throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
1426
1358
  } else if (e.errorCode === BleErrorCode.OperationStartFailed) {
@@ -1463,13 +1395,8 @@ export default class ReactNativeBleTransport {
1463
1395
  }
1464
1396
  const isProbeTimeout =
1465
1397
  name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1466
- // A call that has been superseded (forceRun) or cleaned up no longer owns the
1467
- // transport; its late timeout must not tear down the connection the current
1468
- // call is actively using.
1469
- const isStaleCall = this.runPromise !== runPromise;
1470
1398
  if (
1471
1399
  !isProbeTimeout &&
1472
- !isStaleCall &&
1473
1400
  (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1474
1401
  ) {
1475
1402
  await this.disconnect(uuid);
@@ -1549,10 +1476,7 @@ export default class ReactNativeBleTransport {
1549
1476
  delete transportCache[session];
1550
1477
  }
1551
1478
  this.deviceProtocol.delete(session);
1552
- this.probingProtocols.delete(session);
1553
1479
  this.deviceProtocolHints.delete(session);
1554
- this.sessionProtocols.delete(session);
1555
- this.protocolReprobeFailures.delete(session);
1556
1480
  this.protocolV2Assemblers.delete(session);
1557
1481
  this.resetProtocolV2Frames(session);
1558
1482
 
@@ -1578,113 +1502,6 @@ export default class ReactNativeBleTransport {
1578
1502
  this.runPromiseDeviceId = null;
1579
1503
  }
1580
1504
 
1581
- /** Run a native connect under the JS backstop budget. */
1582
- private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
1583
- let timer: ReturnType<typeof setTimeout> | undefined;
1584
- let timedOut = false;
1585
- const pending = connect();
1586
- // The abandoned attempt keeps running; swallow its late outcome so it cannot
1587
- // surface as an unhandled rejection after we have already given up on it.
1588
- pending.catch(() => undefined);
1589
- try {
1590
- const result = await Promise.race([
1591
- pending,
1592
- new Promise<never>((_, reject) => {
1593
- timer = setTimeout(() => {
1594
- timedOut = true;
1595
- reject(
1596
- ERRORS.TypedError(
1597
- HardwareErrorCode.BleConnectedError,
1598
- `BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
1599
- )
1600
- );
1601
- }, BLE_CONNECT_TIMEOUT_MS);
1602
- }),
1603
- ]);
1604
- return result;
1605
- } catch (error) {
1606
- if (timedOut || isNativeOperationTimeoutError(error)) {
1607
- this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
1608
- }
1609
- throw error;
1610
- } finally {
1611
- if (timer) clearTimeout(timer);
1612
- }
1613
- }
1614
-
1615
- /** Resolve the complete GATT shape under a budget so acquire() always settles. */
1616
- private async resolveCharacteristicsWithTimeout(
1617
- uuid: string,
1618
- device: Device
1619
- ): Promise<ResolvedBleCharacteristics> {
1620
- let timer: ReturnType<typeof setTimeout> | undefined;
1621
- let timedOut = false;
1622
- const pending = this.resolveCharacteristics(device);
1623
- pending.catch(() => undefined);
1624
- try {
1625
- const result = await Promise.race([
1626
- pending,
1627
- new Promise<never>((_, reject) => {
1628
- timer = setTimeout(() => {
1629
- timedOut = true;
1630
- reject(
1631
- ERRORS.TypedError(
1632
- HardwareErrorCode.BleConnectedError,
1633
- `BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
1634
- )
1635
- );
1636
- }, BLE_GATT_SETUP_TIMEOUT_MS);
1637
- }),
1638
- ]);
1639
- this.connectionSetupTimeoutCounts.delete(uuid);
1640
- return result;
1641
- } catch (error) {
1642
- if (timedOut || isNativeOperationTimeoutError(error)) {
1643
- this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
1644
- }
1645
- throw error;
1646
- } finally {
1647
- if (timer) clearTimeout(timer);
1648
- }
1649
- }
1650
-
1651
- /**
1652
- * Give up on a BLE setup operation the native layer did not settle. The abandoned
1653
- * operation still owns native connection/GATT state that can poison the next attempt,
1654
- * so it is cleared here without awaiting the same queue that stopped responding.
1655
- */
1656
- private abandonStalledConnection(
1657
- uuid: string,
1658
- stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
1659
- ) {
1660
- const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
1661
- this.connectionSetupTimeoutCounts.set(uuid, timeouts);
1662
- Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
1663
- stage,
1664
- setupTimeoutsSinceSuccess: timeouts,
1665
- });
1666
-
1667
- this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
1668
- // Rejects with "Operation was cancelled" while merely connecting — expected.
1669
- });
1670
- const stalled = transportCache[uuid];
1671
- if (stalled) {
1672
- delete transportCache[uuid];
1673
- }
1674
- this.deviceProtocol.delete(uuid);
1675
- this.probingProtocols.delete(uuid);
1676
- this.protocolV2Assemblers.delete(uuid);
1677
- this.resetProtocolV2Frames(uuid);
1678
-
1679
- if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1680
- // BleManager.destroy() force-rejects every promise the native queue abandoned —
1681
- // the only JS-reachable way to settle them — and drops all cached peripherals.
1682
- Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
1683
- this.resetPlxManager();
1684
- this.connectionSetupTimeoutCounts.delete(uuid);
1685
- }
1686
- }
1687
-
1688
1505
  private getCachedTransport(uuid: string) {
1689
1506
  const transport = transportCache[uuid];
1690
1507
  if (!transport) {
@@ -1693,107 +1510,6 @@ export default class ReactNativeBleTransport {
1693
1510
  return transport;
1694
1511
  }
1695
1512
 
1696
- /**
1697
- * Write one packet under a bounded budget. A write that never settles means the
1698
- * peripheral is wedged even though the GATT link still reports connected, so the
1699
- * link is torn down: releasing JS state alone would leave the poisoned peripheral
1700
- * cached and every later call would hang on it again.
1701
- */
1702
- private async writeBlePacket(
1703
- uuid: string,
1704
- data: string,
1705
- write: (payload: string) => Promise<unknown>,
1706
- isCurrentOwner?: () => boolean
1707
- ) {
1708
- let timer: ReturnType<typeof setTimeout> | undefined;
1709
- let timedOut = false;
1710
- try {
1711
- await Promise.race([
1712
- write(data),
1713
- new Promise<never>((_, reject) => {
1714
- timer = setTimeout(() => {
1715
- timedOut = true;
1716
- reject(
1717
- ERRORS.TypedError(
1718
- HardwareErrorCode.BleWriteCharacteristicError,
1719
- `BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
1720
- )
1721
- );
1722
- }, BLE_WRITE_PACKET_TIMEOUT_MS);
1723
- }),
1724
- ]);
1725
- this.writeTimeoutCounts.delete(uuid);
1726
- } catch (error) {
1727
- if (timedOut) {
1728
- // A superseded call's late write must not tear down the link the current
1729
- // call is using; only the owner of the transport may declare it dead.
1730
- if (isCurrentOwner && !isCurrentOwner()) {
1731
- Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
1732
- } else {
1733
- this.tearDownWedgedLink(uuid);
1734
- }
1735
- }
1736
- throw error;
1737
- } finally {
1738
- if (timer) clearTimeout(timer);
1739
- }
1740
- }
1741
-
1742
- /**
1743
- * Drop a link whose writes stopped completing. The JS state is purged synchronously
1744
- * so the next acquire() cannot reuse the dead transport, while the native teardown is
1745
- * intentionally NOT awaited: it talks to the very layer that just stopped settling
1746
- * promises, so awaiting it could hang exactly like the write it is recovering from.
1747
- */
1748
- private tearDownWedgedLink(uuid: string) {
1749
- const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
1750
- this.writeTimeoutCounts.set(uuid, timeouts);
1751
- Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
1752
- consecutiveWriteTimeouts: timeouts,
1753
- });
1754
-
1755
- const wedged = transportCache[uuid];
1756
- this.disconnect(uuid).catch(error => {
1757
- Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
1758
- });
1759
- if (wedged && transportCache[uuid] === wedged) {
1760
- delete transportCache[uuid];
1761
- }
1762
- this.deviceProtocol.delete(uuid);
1763
- this.probingProtocols.delete(uuid);
1764
- this.protocolV2Assemblers.delete(uuid);
1765
- this.resetProtocolV2Frames(uuid);
1766
-
1767
- if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
1768
- // Reconnecting reuses the same native peripheral object. When it stays wedged
1769
- // across attempts the poison lives in the BLE manager itself, and only a fresh
1770
- // manager drops every cached peripheral — the JS equivalent of restarting the app.
1771
- Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
1772
- this.resetPlxManager();
1773
- this.writeTimeoutCounts.delete(uuid);
1774
- }
1775
- }
1776
-
1777
- private resetPlxManager() {
1778
- const manager = this.blePlxManager;
1779
- this.blePlxManager = undefined;
1780
- // Every cached transport belongs to the destroyed manager's peripherals.
1781
- Object.keys(transportCache).forEach(key => {
1782
- delete transportCache[key];
1783
- });
1784
- this.deviceProtocol.clear();
1785
- this.probingProtocols.clear();
1786
- this.sessionProtocols.clear();
1787
- this.protocolReprobeFailures.clear();
1788
- this.monitorTokens.clear();
1789
- this.protocolV2Assemblers.clear();
1790
- try {
1791
- manager?.destroy();
1792
- } catch (error) {
1793
- Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
1794
- }
1795
- }
1796
-
1797
1513
  private createProtocolMismatchError(expected: ProtocolType) {
1798
1514
  return ERRORS.TypedError(
1799
1515
  HardwareErrorCode.RuntimeError,
@@ -1809,19 +1525,11 @@ export default class ReactNativeBleTransport {
1809
1525
  }
1810
1526
 
1811
1527
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1812
- if (this.probingProtocols.get(uuid) === protocol) {
1813
- this.probingProtocols.delete(uuid);
1814
- }
1815
1528
  if (this.deviceProtocol.get(uuid) === protocol) {
1816
1529
  this.deviceProtocol.delete(uuid);
1817
1530
  }
1818
1531
  }
1819
1532
 
1820
- /** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
1821
- private getActiveProtocol(uuid: string): ProtocolType | undefined {
1822
- return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
1823
- }
1824
-
1825
1533
  private async detectProtocol(
1826
1534
  uuid: string,
1827
1535
  expectedProtocol?: ProtocolType,
@@ -1841,7 +1549,6 @@ export default class ReactNativeBleTransport {
1841
1549
  if (expectedProtocol === 'V1') {
1842
1550
  if (await this.probeProtocolV1(uuid)) {
1843
1551
  this.deviceProtocol.set(uuid, 'V1');
1844
- this.sessionProtocols.set(uuid, 'V1');
1845
1552
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1846
1553
  deviceId: uuid,
1847
1554
  protocol: 'V1',
@@ -1855,7 +1562,6 @@ export default class ReactNativeBleTransport {
1855
1562
  if (expectedProtocol === 'V2') {
1856
1563
  if (await this.probeProtocolV2(uuid)) {
1857
1564
  this.deviceProtocol.set(uuid, 'V2');
1858
- this.sessionProtocols.set(uuid, 'V2');
1859
1565
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1860
1566
  deviceId: uuid,
1861
1567
  protocol: 'V2',
@@ -1868,18 +1574,8 @@ export default class ReactNativeBleTransport {
1868
1574
 
1869
1575
  // Protocol must be actively probed after connection. Name, PID, and descriptors only
1870
1576
  // influence probe order; a V2 hint probes V2 first and falls back to V1.
1871
- const sessionProtocol = this.sessionProtocols.get(uuid);
1872
- const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
1873
- const fullProbeOrder: ProtocolType[] =
1577
+ const probeOrder: ProtocolType[] =
1874
1578
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1875
- // A device that already answered on a protocol in this session keeps answering on
1876
- // it; while it is rebooting nothing answers at all, so probing the other protocol
1877
- // only adds its timeout to every poll.
1878
- const trustSessionProtocol =
1879
- sessionProtocol !== undefined &&
1880
- !protocolHint &&
1881
- reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1882
- const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1883
1579
 
1884
1580
  for (let i = 0; i < probeOrder.length; i += 1) {
1885
1581
  const protocol = probeOrder[i];
@@ -1897,8 +1593,6 @@ export default class ReactNativeBleTransport {
1897
1593
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1898
1594
  if (detected) {
1899
1595
  this.deviceProtocol.set(uuid, protocol);
1900
- this.sessionProtocols.set(uuid, protocol);
1901
- this.protocolReprobeFailures.delete(uuid);
1902
1596
  Log?.debug('[ReactNativeBleTransport] protocol detected', {
1903
1597
  deviceId: uuid,
1904
1598
  protocol,
@@ -1908,16 +1602,7 @@ export default class ReactNativeBleTransport {
1908
1602
  }
1909
1603
  }
1910
1604
 
1911
- if (trustSessionProtocol) {
1912
- // Still silent on its own protocol: count it, and let the streak expire the
1913
- // shortcut so a device that genuinely switched protocols is found again.
1914
- this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1915
- } else {
1916
- this.protocolReprobeFailures.delete(uuid);
1917
- }
1918
-
1919
1605
  this.deviceProtocol.delete(uuid);
1920
- this.probingProtocols.delete(uuid);
1921
1606
  throw this.createProtocolDetectionError();
1922
1607
  }
1923
1608
 
@@ -1979,20 +1664,14 @@ export default class ReactNativeBleTransport {
1979
1664
  }
1980
1665
 
1981
1666
  try {
1982
- this.probingProtocols.set(uuid, 'V1');
1667
+ this.deviceProtocol.set(uuid, 'V1');
1983
1668
  // GetFeatures identifies Protocol V1 without resetting an existing wallet
1984
1669
  // session before Core has a chance to restore a hidden wallet.
1985
1670
  await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1986
- this.probingProtocols.delete(uuid);
1987
1671
  return true;
1988
1672
  } catch (error) {
1989
1673
  this.clearProbeProtocol(uuid, 'V1');
1990
1674
  Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
1991
- // A wedged write already dropped the link, so probing another protocol on it
1992
- // would only fail against a torn-down transport: surface the real cause.
1993
- if (isWedgedWriteError(error)) {
1994
- throw error;
1995
- }
1996
1675
  return false;
1997
1676
  }
1998
1677
  }
@@ -2002,7 +1681,7 @@ export default class ReactNativeBleTransport {
2002
1681
  return false;
2003
1682
  }
2004
1683
 
2005
- this.probingProtocols.set(uuid, 'V2');
1684
+ this.deviceProtocol.set(uuid, 'V2');
2006
1685
  this.protocolV2Assemblers.get(uuid)?.reset();
2007
1686
  const detected = await probeProtocolV2Helper({
2008
1687
  call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
@@ -2017,8 +1696,6 @@ export default class ReactNativeBleTransport {
2017
1696
  });
2018
1697
  if (!detected) {
2019
1698
  this.clearProbeProtocol(uuid, 'V2');
2020
- } else {
2021
- this.probingProtocols.delete(uuid);
2022
1699
  }
2023
1700
  return detected;
2024
1701
  }
@@ -2100,15 +1777,17 @@ export default class ReactNativeBleTransport {
2100
1777
  }
2101
1778
 
2102
1779
  private async writeProtocolV2Packet(
2103
- uuid: string,
2104
1780
  transport: BleTransport,
2105
1781
  base64: string,
2106
1782
  context: ProtocolV2CallContext,
2107
1783
  assertCurrentGeneration: () => void
2108
1784
  ) {
2109
- const shouldUseWriteWithResponse =
2110
- transport.writeCharacteristic.isWritableWithResponse &&
2111
- (context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
1785
+ const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
1786
+ platform: Platform.OS,
1787
+ highVolume: context.highVolume,
1788
+ requestedWithResponse: context.writeWithResponse,
1789
+ characteristic: transport.writeCharacteristic,
1790
+ });
2112
1791
  let attempt = 0;
2113
1792
  for (;;) {
2114
1793
  assertCurrentGeneration();
@@ -2116,24 +1795,11 @@ export default class ReactNativeBleTransport {
2116
1795
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
2117
1796
  }
2118
1797
  try {
2119
- await this.writeBlePacket(
2120
- uuid,
2121
- base64,
2122
- payload =>
2123
- shouldUseWriteWithResponse
2124
- ? transport.writeCharacteristic.writeWithResponse(payload)
2125
- : transport.writeCharacteristic.writeWithoutResponse(payload),
2126
- // Same rule as Protocol V1: a write from a superseded generation must not
2127
- // tear down the link that the current generation is using.
2128
- () => {
2129
- try {
2130
- assertCurrentGeneration();
2131
- return !context.signal.aborted;
2132
- } catch {
2133
- return false;
2134
- }
2135
- }
2136
- );
1798
+ if (shouldUseWriteWithResponse) {
1799
+ await transport.writeCharacteristic.writeWithResponse(base64);
1800
+ } else {
1801
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1802
+ }
2137
1803
  assertCurrentGeneration();
2138
1804
  return;
2139
1805
  } catch (error) {
@@ -2156,7 +1822,6 @@ export default class ReactNativeBleTransport {
2156
1822
  }
2157
1823
 
2158
1824
  private async writeProtocolV2Frame(
2159
- uuid: string,
2160
1825
  transport: BleTransport,
2161
1826
  frame: Uint8Array,
2162
1827
  context: ProtocolV2CallContext,
@@ -2167,28 +1832,17 @@ export default class ReactNativeBleTransport {
2167
1832
  platform: Platform.OS,
2168
1833
  iosPacketLength: tuning.iosPacketLength,
2169
1834
  androidPacketLength: tuning.androidPacketLength,
2170
- mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1835
+ mtu: transport.mtuSize,
2171
1836
  });
2172
- // Match Desktop BLE pacing so Pro2 firmware can finish the previous response
2173
- // before the next single-packet control command is written.
2174
- const initialDelayMs =
2175
- Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
2176
- ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
2177
- : 0;
2178
1837
  await writeProtocolV2BleFrame({
2179
1838
  frame,
2180
1839
  packetCapacity,
2181
1840
  assertActive: assertCurrentGeneration,
2182
1841
  signal: context.signal,
2183
1842
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
2184
- initialDelayMs,
2185
- burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
2186
- burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
2187
- flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
2188
1843
  wait: delay,
2189
1844
  writePacket: packet =>
2190
1845
  this.writeProtocolV2Packet(
2191
- uuid,
2192
1846
  transport,
2193
1847
  Buffer.from(packet).toString('base64'),
2194
1848
  context,
@@ -2212,11 +1866,39 @@ export default class ReactNativeBleTransport {
2212
1866
 
2213
1867
  if (highVolumeWrite) {
2214
1868
  const tuning = getProtocolV2BleTuning();
2215
- Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
2216
- name,
2217
- writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
2218
- packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1869
+ const currentTransport = this.getCachedTransport(uuid);
1870
+ const writeWithResponse = shouldWriteProtocolV2WithResponse({
1871
+ platform: Platform.OS,
1872
+ highVolume: true,
1873
+ requestedWithResponse: options?.writeWithResponse,
1874
+ characteristic: currentTransport.writeCharacteristic,
1875
+ });
1876
+ const packetCapacity = resolveProtocolV2PacketCapacity({
1877
+ platform: Platform.OS,
1878
+ iosPacketLength: tuning.iosPacketLength,
1879
+ androidPacketLength: tuning.androidPacketLength,
1880
+ mtu: currentTransport.mtuSize,
2219
1881
  });
1882
+ const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
1883
+ const logSignature = `${name}:${writeMode}:${String(
1884
+ currentTransport.mtuSize
1885
+ )}:${packetCapacity}`;
1886
+ const loggedSignatures =
1887
+ this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
1888
+ if (!loggedSignatures.has(logSignature)) {
1889
+ loggedSignatures.add(logSignature);
1890
+ this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
1891
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1892
+ name,
1893
+ writeMode,
1894
+ reportedMtu: currentTransport.mtuSize,
1895
+ packetCapacity,
1896
+ });
1897
+ }
1898
+ }
1899
+
1900
+ if (highVolumeWrite) {
1901
+ await this.enableAndroidHighConnectionPriority(uuid);
2220
1902
  }
2221
1903
 
2222
1904
  try {
@@ -2230,6 +1912,73 @@ export default class ReactNativeBleTransport {
2230
1912
  } catch (e) {
2231
1913
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
2232
1914
  throw e;
1915
+ } finally {
1916
+ if (highVolumeWrite) {
1917
+ this.scheduleAndroidBalancedConnectionPriority(uuid);
1918
+ }
1919
+ }
1920
+ }
1921
+
1922
+ private clearAndroidPriorityResetTimer(uuid: string) {
1923
+ const timerId = this.androidPriorityResetTimers.get(uuid);
1924
+ if (timerId !== undefined) {
1925
+ clearTimeout(timerId);
1926
+ this.androidPriorityResetTimers.delete(uuid);
1927
+ }
1928
+ }
1929
+
1930
+ private async enableAndroidHighConnectionPriority(uuid: string) {
1931
+ if (Platform.OS !== 'android') return;
1932
+
1933
+ this.clearAndroidPriorityResetTimer(uuid);
1934
+ if (this.androidHighPriorityDevices.has(uuid)) return;
1935
+
1936
+ const transport = transportCache[uuid];
1937
+ if (!transport) return;
1938
+
1939
+ try {
1940
+ transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
1941
+ this.androidHighPriorityDevices.add(uuid);
1942
+ Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
1943
+ priority: 'high',
1944
+ });
1945
+ } catch (error) {
1946
+ Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
1947
+ error: error instanceof Error ? error.message : String(error),
1948
+ });
1949
+ }
1950
+ }
1951
+
1952
+ private scheduleAndroidBalancedConnectionPriority(uuid: string) {
1953
+ if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
1954
+
1955
+ this.clearAndroidPriorityResetTimer(uuid);
1956
+ const timerId = setTimeout(() => {
1957
+ this.androidPriorityResetTimers.delete(uuid);
1958
+ this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
1959
+ Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
1960
+ );
1961
+ }, ANDROID_HIGH_PRIORITY_IDLE_MS);
1962
+ this.androidPriorityResetTimers.set(uuid, timerId);
1963
+ }
1964
+
1965
+ private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
1966
+ this.clearAndroidPriorityResetTimer(uuid);
1967
+ if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
1968
+ return;
1969
+ }
1970
+
1971
+ try {
1972
+ transport.device = await transport.device.requestConnectionPriority(
1973
+ ConnectionPriority.Balanced
1974
+ );
1975
+ Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
1976
+ priority: 'balanced',
1977
+ });
1978
+ } catch (error) {
1979
+ Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
1980
+ error: error instanceof Error ? error.message : String(error),
1981
+ });
2233
1982
  }
2234
1983
  }
2235
1984
 
@@ -2253,13 +2002,7 @@ export default class ReactNativeBleTransport {
2253
2002
  writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
2254
2003
  assertCurrentGeneration();
2255
2004
  const currentTransport = this.getCachedTransport(uuid);
2256
- await this.writeProtocolV2Frame(
2257
- uuid,
2258
- currentTransport,
2259
- frame,
2260
- context,
2261
- assertCurrentGeneration
2262
- );
2005
+ await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
2263
2006
  },
2264
2007
  readFrame: async () => {
2265
2008
  assertCurrentGeneration();
@@ -2284,6 +2027,6 @@ export default class ReactNativeBleTransport {
2284
2027
  }
2285
2028
 
2286
2029
  getProtocolType(path: string): ProtocolType | undefined {
2287
- return this.getActiveProtocol(path);
2030
+ return this.deviceProtocol.get(path);
2288
2031
  }
2289
2032
  }