@onekeyfe/hd-transport-react-native 1.2.0-alpha.9 → 1.2.0-alpha.91

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,34 +5,46 @@ 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';
11
12
  import transport, {
12
- LogBlockCommand,
13
13
  type OneKeyDeviceInfoBase,
14
14
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
15
15
  PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
16
16
  PROTOCOL_V2_CHANNEL_BLE_UART,
17
17
  type ProtocolType,
18
+ type ProtocolV2CallContext,
18
19
  ProtocolV2FrameAssembler,
19
20
  ProtocolV2LinkManager,
21
+ TRANSPORT_EVENT,
20
22
  type TransportCallOptions,
23
+ isProtocolV2HighThroughputCall,
21
24
  probeProtocolV2 as probeProtocolV2Helper,
25
+ writeProtocolV2BleFrame,
22
26
  } from '@onekeyfe/hd-transport';
23
- import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
27
+ import {
28
+ ERRORS,
29
+ HardwareErrorCode,
30
+ createDeferred,
31
+ isOnekeyBluetoothDevice,
32
+ isPro2FindMyAdvertisementName,
33
+ } from '@onekeyfe/hd-shared';
24
34
 
25
35
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
26
36
  import {
27
37
  hasWritableCapability,
28
- resolveBleWriteMode,
29
38
  resolveProtocolV2PacketCapacity,
39
+ shouldRefreshNegotiatedMtu,
40
+ shouldWriteProtocolV2WithResponse,
30
41
  } from './bleStrategy';
31
42
  import { subscribeBleOn } from './subscribeBleOn';
32
43
  import {
33
44
  ANDROID_PACKET_LENGTH,
45
+ ANDROID_PROTOCOL_V2_PACKET_LENGTH,
34
46
  IOS_PACKET_LENGTH,
35
- getBleUuidKey,
47
+ IOS_PROTOCOL_V2_PACKET_LENGTH,
36
48
  getBluetoothServiceUuids,
37
49
  getInfosForServiceUuid,
38
50
  isSameBleUuid,
@@ -41,6 +53,7 @@ import { isHeaderChunk } from './utils/validateNotify';
41
53
  import BleTransport from './BleTransport';
42
54
  import timer from './utils/timer';
43
55
  import { bleLogger, setBleLogger } from './logger';
56
+ import { createTransportCallLog } from './transportLog';
44
57
 
45
58
  import type { Deferred } from '@onekeyfe/hd-shared';
46
59
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
@@ -56,24 +69,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
56
69
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
57
70
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
58
71
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
59
- const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
60
72
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
61
73
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
62
74
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
63
75
  const ANDROID_GATT_CONGESTED_STATUS = 143;
64
76
 
65
- type FirmwareUploadWriteRetryType = 'congested' | 'reconnectable';
77
+ type FirmwareUploadWriteRetryType = 'congested';
66
78
  type ResolvedBleCharacteristics = {
67
79
  writeCharacteristic: Characteristic;
68
80
  notifyCharacteristic: Characteristic;
69
81
  };
70
82
 
83
+ const isAsciiWhitespace = (code: number) =>
84
+ code === 0x09 ||
85
+ code === 0x0a ||
86
+ code === 0x0b ||
87
+ code === 0x0c ||
88
+ code === 0x0d ||
89
+ code === 0x20;
90
+
91
+ const hasGattCongestedStatus = (text: string) => {
92
+ let searchFrom = 0;
93
+ while (searchFrom < text.length) {
94
+ const statusIndex = text.indexOf('status', searchFrom);
95
+ if (statusIndex < 0) return false;
96
+
97
+ let cursor = statusIndex + 'status'.length;
98
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
99
+ if (text[cursor] === ':' || text[cursor] === '=') {
100
+ cursor += 1;
101
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
102
+ }
103
+ if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
104
+
105
+ searchFrom = statusIndex + 'status'.length;
106
+ }
107
+ return false;
108
+ };
109
+
71
110
  const delay = (ms: number) =>
72
111
  new Promise<void>(resolve => {
73
112
  setTimeout(resolve, ms);
74
113
  });
75
114
 
76
- const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRetryType | null => {
115
+ export const getFirmwareUploadWriteRetryType = (
116
+ error: unknown
117
+ ): FirmwareUploadWriteRetryType | null => {
77
118
  if (!error || typeof error !== 'object') return null;
78
119
  const bleWriteError = error as {
79
120
  androidErrorCode?: unknown;
@@ -84,13 +125,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
84
125
  name?: unknown;
85
126
  };
86
127
 
87
- if (
88
- bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
89
- bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
90
- ) {
91
- return 'reconnectable';
92
- }
93
-
94
128
  if (
95
129
  bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
96
130
  bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
@@ -101,39 +135,42 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
101
135
  const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
102
136
  .filter(value => typeof value === 'string')
103
137
  .join(' ');
104
- return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
138
+ return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
105
139
  };
106
140
 
107
141
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
108
142
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
109
- const BLE_RESPONSE_TIMEOUT_MS = 30_000;
110
143
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
111
144
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
112
- const DEVICE_SCAN_TIMEOUT_MS = 8000;
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
+ const DEVICE_SCAN_TIMEOUT_MS = 3000;
113
162
  const IOS_NOTIFY_READY_DELAY_MS = 150;
114
163
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
115
- const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
116
- const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
117
- const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
118
-
119
164
  export type ProtocolV2BleTuning = {
120
165
  iosPacketLength?: number;
121
166
  androidPacketLength?: number;
122
- highVolumeWriteBurstSize?: number;
123
- highVolumeWritePauseMs?: number;
124
- highVolumeWriteFlushDelayMs?: number;
125
- highVolumeWriteWithResponse?: boolean;
126
167
  };
127
168
 
128
169
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
129
170
 
130
171
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
131
- iosPacketLength: IOS_PACKET_LENGTH,
132
- androidPacketLength: ANDROID_PACKET_LENGTH,
133
- highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
134
- highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
135
- highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
136
- highVolumeWriteWithResponse: false,
172
+ iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
173
+ androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
137
174
  };
138
175
 
139
176
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -154,27 +191,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
154
191
  tuning.androidPacketLength,
155
192
  protocolV2BleTuning.androidPacketLength
156
193
  ),
157
- highVolumeWriteBurstSize: normalizePositiveInteger(
158
- tuning.highVolumeWriteBurstSize,
159
- protocolV2BleTuning.highVolumeWriteBurstSize
160
- ),
161
- highVolumeWritePauseMs: normalizePositiveInteger(
162
- tuning.highVolumeWritePauseMs,
163
- protocolV2BleTuning.highVolumeWritePauseMs
164
- ),
165
- highVolumeWriteFlushDelayMs: normalizePositiveInteger(
166
- tuning.highVolumeWriteFlushDelayMs,
167
- protocolV2BleTuning.highVolumeWriteFlushDelayMs
168
- ),
169
- highVolumeWriteWithResponse:
170
- tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
171
194
  };
172
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
195
+ Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
173
196
  }
174
197
 
175
198
  export function resetProtocolV2BleTuning() {
176
199
  protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
177
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
200
+ Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
178
201
  }
179
202
 
180
203
  export function getProtocolV2BleTuning() {
@@ -189,24 +212,60 @@ function getDeviceDisplayName(device?: Device | null) {
189
212
  return device?.name || device?.localName || null;
190
213
  }
191
214
 
192
- function isGenericBleService(uuid?: string | null) {
193
- return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
194
- }
215
+ const IOS_REQUEST_MTU = 247;
216
+ const ANDROID_REQUEST_MTU = 517;
217
+ const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
218
+ const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
195
219
 
196
- function hasKnownOneKeyService(device?: Device | null) {
197
- return (device?.serviceUUIDs ?? []).some(serviceUuid =>
198
- getInfosForServiceUuid(serviceUuid, 'classic')
199
- );
200
- }
220
+ const getRequestedBleMtu = () =>
221
+ Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
201
222
 
202
- const ANDROID_REQUEST_MTU = 256;
223
+ const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
203
224
 
204
225
  const connectOptions: Record<string, unknown> = {
205
- requestMTU: ANDROID_REQUEST_MTU,
206
- timeout: 3000,
226
+ requestMTU: getRequestedBleMtu(),
227
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
207
228
  refreshGatt: 'OnConnected',
208
229
  };
209
230
 
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
+
210
269
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
211
270
 
212
271
  const tryToGetConfiguration = (device: Device) => {
@@ -218,22 +277,32 @@ const tryToGetConfiguration = (device: Device) => {
218
277
  return infos;
219
278
  };
220
279
 
221
- const requestAndroidMtu = async (device: Device) => {
222
- if (Platform.OS !== 'android') return device;
280
+ const requestNegotiatedMtu = async (
281
+ device: Device,
282
+ stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
283
+ attempt: number
284
+ ) => {
285
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
223
286
 
224
287
  try {
225
- const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
226
- Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
227
- requested: ANDROID_REQUEST_MTU,
228
- mtu: mtuDevice.mtu,
229
- });
288
+ // iOS ignores the requested value but react-native-ble-plx returns a fresh
289
+ // Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
290
+ const mtuDevice = await device.requestMTU(getRequestedBleMtu());
230
291
  return mtuDevice;
231
292
  } catch (error) {
232
- Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
293
+ Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
294
+ platform: Platform.OS,
295
+ stage,
296
+ attempt,
297
+ actual: device.mtu,
298
+ error: error instanceof Error ? error.message : String(error),
299
+ });
233
300
  return device;
234
301
  }
235
302
  };
236
303
 
304
+ const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
305
+
237
306
  type IOBleErrorRemap = Error | BleError | null | undefined;
238
307
 
239
308
  function remapError(error: IOBleErrorRemap) {
@@ -273,6 +342,8 @@ export default class ReactNativeBleTransport {
273
342
 
274
343
  _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
275
344
 
345
+ private protocolV2SchemaConfiguration: string | undefined;
346
+
276
347
  name = 'ReactNativeBleTransport';
277
348
 
278
349
  configured = false;
@@ -283,6 +354,8 @@ export default class ReactNativeBleTransport {
283
354
 
284
355
  runPromise: Deferred<any> | null = null;
285
356
 
357
+ private runPromiseDeviceId: string | null = null;
358
+
286
359
  emitter?: EventEmitter;
287
360
 
288
361
  firmwareUploadWriteRecoveryIds = new Set<string>();
@@ -290,8 +363,28 @@ export default class ReactNativeBleTransport {
290
363
  /** Per-device protocol type detected by active wire-level probe after connect. */
291
364
  private deviceProtocol: Map<string, ProtocolType> = new Map();
292
365
 
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
+
293
380
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
294
381
 
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
+
295
388
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
296
389
 
297
390
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
@@ -314,13 +407,21 @@ export default class ReactNativeBleTransport {
314
407
  this.rejectProtocolV2Frames(uuid, new Error(reason));
315
408
  Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
316
409
  if (reason.startsWith('Protocol V2 link-fatal error:')) {
317
- await this.release(uuid, true);
410
+ await this.releaseNative(uuid, true);
318
411
  }
319
412
  },
320
413
  });
321
414
 
322
415
  private monitorTokens: Map<string, number> = new Map();
323
416
 
417
+ private disconnectEventTokens: Map<string, number> = new Map();
418
+
419
+ private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
420
+
421
+ private androidHighPriorityDevices: Set<string> = new Set();
422
+
423
+ private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
424
+
324
425
  private nextMonitorToken = 1;
325
426
 
326
427
  constructor(options: TransportOptions) {
@@ -339,11 +440,19 @@ export default class ReactNativeBleTransport {
339
440
  }
340
441
 
341
442
  configureProtocolV2(signedData: any) {
443
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
444
+ if (this.protocolV2SchemaConfiguration === configuration) {
445
+ return;
446
+ }
447
+
448
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
342
449
  this._messagesV2 = parseConfigure(signedData);
343
- this.protocolV2Links
344
- .invalidateAllLinks('Protocol V2 schema reconfigured')
345
- .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
346
- Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
450
+ this.protocolV2SchemaConfiguration = configuration;
451
+ if (isReconfiguration) {
452
+ this.protocolV2Links
453
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
454
+ .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
455
+ }
347
456
  }
348
457
 
349
458
  listen() {
@@ -373,29 +482,15 @@ export default class ReactNativeBleTransport {
373
482
  }
374
483
  }
375
484
 
376
- let fallbackServiceUuid: string | undefined;
377
-
378
485
  if (!infos) {
379
486
  const services = await device.services();
380
487
  Log?.debug(
381
488
  '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
382
489
  services?.map(service => service.uuid)
383
490
  );
384
-
385
- const knownService = services.find(service =>
386
- getInfosForServiceUuid(service.uuid, 'classic')
387
- );
388
- const fallbackService =
389
- knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
390
-
391
- if (fallbackService) {
392
- fallbackServiceUuid = fallbackService.uuid;
393
- characteristics = await device.characteristicsForService(fallbackService.uuid);
394
- Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
395
- }
396
491
  }
397
492
 
398
- if (!infos && !fallbackServiceUuid) {
493
+ if (!infos) {
399
494
  try {
400
495
  Log?.debug('cancel connection when service not found');
401
496
  await device.cancelConnection();
@@ -405,9 +500,7 @@ export default class ReactNativeBleTransport {
405
500
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
406
501
  }
407
502
 
408
- const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
409
- const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
410
- const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
503
+ const { serviceUuid, writeUuid, notifyUuid } = infos;
411
504
 
412
505
  if (!serviceUuid) {
413
506
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
@@ -457,6 +550,7 @@ export default class ReactNativeBleTransport {
457
550
 
458
551
  attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
459
552
  transport.disconnectSubscription?.remove();
553
+ const { monitorToken } = transport;
460
554
  transport.disconnectSubscription = device.onDisconnected(() => {
461
555
  if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
462
556
  Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
@@ -466,18 +560,17 @@ export default class ReactNativeBleTransport {
466
560
  Log?.debug('device disconnect ignored for stale transport: ', device?.id);
467
561
  return;
468
562
  }
563
+ if (this.monitorTokens.get(uuid) !== monitorToken) {
564
+ Log?.debug('device disconnect ignored for stale generation: ', device?.id);
565
+ return;
566
+ }
469
567
 
470
568
  try {
471
569
  Log?.debug('device disconnect: ', device?.id);
472
- this.emitter?.emit('device-disconnect', {
473
- name: device?.name,
474
- id: device?.id,
475
- connectId: device?.id,
476
- });
477
- if (this.runPromise) {
570
+ this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
571
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
478
572
  const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
479
573
  this.runPromise.reject(error);
480
- this.rejectAllProtocolV2Frames(error);
481
574
  }
482
575
  } catch (e) {
483
576
  Log?.debug('device disconnect error: ', e);
@@ -487,6 +580,22 @@ export default class ReactNativeBleTransport {
487
580
  });
488
581
  }
489
582
 
583
+ private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
584
+ if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
585
+ return;
586
+ }
587
+ if (this.monitorTokens.get(uuid) !== token) {
588
+ Log?.debug('device disconnect event ignored for stale generation: ', uuid);
589
+ return;
590
+ }
591
+ this.disconnectEventTokens.set(uuid, token);
592
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
593
+ name,
594
+ id: uuid,
595
+ connectId: uuid,
596
+ });
597
+ }
598
+
490
599
  async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
491
600
  this.firmwareUploadWriteRecoveryIds.add(uuid);
492
601
  try {
@@ -499,22 +608,21 @@ export default class ReactNativeBleTransport {
499
608
  const isConnected = await device.isConnected().catch(() => false);
500
609
  if (!isConnected) {
501
610
  try {
502
- device = await device.connect(connectOptions);
611
+ device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
503
612
  } catch (e) {
504
613
  if (
505
614
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
506
615
  e.errorCode === BleErrorCode.OperationCancelled
507
616
  ) {
508
- device = await device.connect();
617
+ device = await this.connectWithTimeout(uuid, () => device.connect());
509
618
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
510
619
  throw e;
511
620
  }
512
621
  }
513
622
  }
514
623
 
515
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
516
- device
517
- );
624
+ const { writeCharacteristic, notifyCharacteristic } =
625
+ await this.resolveCharacteristicsWithTimeout(uuid, device);
518
626
 
519
627
  transport.device = device;
520
628
  transport.writeCharacteristic = writeCharacteristic;
@@ -574,14 +682,13 @@ export default class ReactNativeBleTransport {
574
682
  }
575
683
 
576
684
  blePlxManager.startDeviceScan(
577
- null,
685
+ getBluetoothServiceUuids(),
578
686
  {
579
687
  allowDuplicates: true,
580
688
  scanMode: ScanMode.LowLatency,
581
689
  },
582
690
  (error, device) => {
583
691
  if (error) {
584
- Log?.debug('ble scan manager: ', blePlxManager);
585
692
  Log?.debug('ble scan error: ', error);
586
693
  if (
587
694
  [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
@@ -605,33 +712,23 @@ export default class ReactNativeBleTransport {
605
712
  }
606
713
 
607
714
  const displayName = getDeviceDisplayName(device);
715
+ // iOS may report a service-only advertisement before the named scan response.
716
+ // Do not cache that incomplete advertisement as an unknown device.
717
+ const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
718
+ const isFindMyPeripheral =
719
+ isPro2FindMyAdvertisementName(device?.name) ||
720
+ isPro2FindMyAdvertisementName(device?.localName);
608
721
  const isOneKey =
609
- isOnekeyDevice(device?.name ?? null, device?.id) ||
610
- isOnekeyDevice(device?.localName ?? null, device?.id) ||
611
- hasKnownOneKeyService(device);
612
- const shouldTraceCandidate =
613
- !!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
614
-
615
- if (shouldTraceCandidate) {
616
- Log?.debug('[ReactNativeBleTransport] scan candidate', {
722
+ !isUnnamedIOSPeripheral &&
723
+ !isFindMyPeripheral &&
724
+ isOnekeyBluetoothDevice({
725
+ id: device?.id,
617
726
  name: device?.name,
618
727
  localName: device?.localName,
619
- id: device?.id,
620
- serviceUUIDs: device?.serviceUUIDs,
621
- accepted: isOneKey,
728
+ serviceUuids: device?.serviceUUIDs,
622
729
  });
623
- }
624
-
625
730
  if (isOneKey) {
626
- Log?.debug('search device start ======================');
627
- const { name, localName, id, serviceUUIDs } = device ?? {};
628
- Log?.debug(
629
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
630
- id ?? ''
631
- }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
632
- );
633
731
  addDevice(device as unknown as Device);
634
- Log?.debug('search device end ======================\n');
635
732
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
636
733
  Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
637
734
  name: device?.name,
@@ -643,12 +740,32 @@ export default class ReactNativeBleTransport {
643
740
  }
644
741
  );
645
742
 
646
- getConnectedDeviceIds(getBluetoothServiceUuids()).then(devices => {
647
- for (const device of devices) {
648
- Log?.debug('search connected peripheral: ', device.id);
649
- addDevice(device as unknown as Device);
743
+ getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
744
+ devices => {
745
+ for (const device of devices) {
746
+ const localName =
747
+ 'localName' in device && typeof device.localName === 'string'
748
+ ? device.localName
749
+ : null;
750
+ const isFindMyPeripheral =
751
+ isPro2FindMyAdvertisementName(device.name) ||
752
+ isPro2FindMyAdvertisementName(localName);
753
+
754
+ if (
755
+ !isFindMyPeripheral &&
756
+ isOnekeyBluetoothDevice({
757
+ id: device.id,
758
+ name: device.name,
759
+ localName,
760
+ serviceUuids: device.serviceUUIDs,
761
+ })
762
+ ) {
763
+ Log?.debug('search connected peripheral: ', device.id);
764
+ addDevice(device as unknown as Device);
765
+ }
766
+ }
650
767
  }
651
- });
768
+ );
652
769
 
653
770
  const addDevice = (device: Device) => {
654
771
  if (deviceList.every(d => d.id !== device.id)) {
@@ -662,6 +779,12 @@ export default class ReactNativeBleTransport {
662
779
  name: displayName,
663
780
  commType: 'ble',
664
781
  } as IOneKeyDevice);
782
+ Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
783
+ deviceId: device.id,
784
+ name: displayName,
785
+ serviceUUIDs: device.serviceUUIDs,
786
+ protocolHint,
787
+ });
665
788
  }
666
789
  };
667
790
 
@@ -672,6 +795,79 @@ export default class ReactNativeBleTransport {
672
795
  });
673
796
  }
674
797
 
798
+ private async installTransportForAcquire(
799
+ uuid: string,
800
+ device: Device,
801
+ characteristics?: ResolvedBleCharacteristics
802
+ ) {
803
+ const { writeCharacteristic, notifyCharacteristic } =
804
+ characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
805
+ const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
806
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
807
+ const monitorToken = this.nextMonitorToken;
808
+ this.nextMonitorToken += 1;
809
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
810
+ transport.monitorToken = monitorToken;
811
+ transport.notifyTransactionId = notifyTransactionId;
812
+ this.monitorTokens.set(uuid, monitorToken);
813
+ transport.notifySubscription = this._monitorCharacteristic(
814
+ transport.notifyCharacteristic,
815
+ uuid,
816
+ monitorToken,
817
+ notifyTransactionId
818
+ );
819
+ transportCache[uuid] = transport;
820
+ this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
821
+ this.protocolV2Assemblers.set(
822
+ uuid,
823
+ new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
824
+ );
825
+
826
+ if (Platform.OS === 'ios') {
827
+ await new Promise<void>(resolve => {
828
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
829
+ });
830
+ } else if (Platform.OS === 'android') {
831
+ await delay(ANDROID_NOTIFY_READY_DELAY_MS);
832
+ }
833
+
834
+ const initialMtu = transport.mtuSize;
835
+ let refreshAttempts = 0;
836
+ if (
837
+ (Platform.OS === 'ios' || Platform.OS === 'android') &&
838
+ shouldRefreshNegotiatedMtu(transport.mtuSize)
839
+ ) {
840
+ refreshAttempts += 1;
841
+ let refreshedDevice = await requestNegotiatedMtu(
842
+ transport.device,
843
+ 'servicesAndNotifyReady',
844
+ 1
845
+ );
846
+ transport.device = refreshedDevice;
847
+ transport.mtuSize =
848
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
849
+
850
+ if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
851
+ await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
852
+ refreshAttempts += 1;
853
+ refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
854
+ transport.device = refreshedDevice;
855
+ transport.mtuSize =
856
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
857
+ }
858
+ }
859
+
860
+ Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
861
+ platform: Platform.OS,
862
+ requested: getRequestedBleMtu(),
863
+ initial: initialMtu,
864
+ actual: transport.mtuSize,
865
+ refreshAttempts,
866
+ });
867
+
868
+ return transport;
869
+ }
870
+
675
871
  async acquire(input: BleAcquireInput) {
676
872
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
677
873
 
@@ -705,8 +901,8 @@ export default class ReactNativeBleTransport {
705
901
  if (forceCleanRunPromise && this.runPromise) {
706
902
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
707
903
  this.runPromise.reject(error);
708
- this.rejectAllProtocolV2Frames(error);
709
904
  this.runPromise = null;
905
+ this.runPromiseDeviceId = null;
710
906
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
711
907
  }
712
908
 
@@ -742,15 +938,22 @@ export default class ReactNativeBleTransport {
742
938
  if (!device) {
743
939
  Log?.debug('try to connect to device: ', uuid);
744
940
  try {
745
- device = await blePlxManager.connectToDevice(uuid, connectOptions);
941
+ device = await this.connectWithTimeout(uuid, () =>
942
+ blePlxManager.connectToDevice(uuid, connectOptions)
943
+ );
746
944
  } catch (e) {
747
945
  Log?.debug('try to connect to device has error: ', e);
946
+ if (isConnectTimeoutError(e)) {
947
+ throw e;
948
+ }
748
949
  if (
749
950
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
750
951
  e.errorCode === BleErrorCode.OperationCancelled
751
952
  ) {
752
953
  Log?.debug('first try to reconnect without params');
753
- device = await blePlxManager.connectToDevice(uuid);
954
+ device = await this.connectWithTimeout(uuid, () =>
955
+ blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
956
+ );
754
957
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
755
958
  Log?.debug('device already connected');
756
959
  throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
@@ -766,26 +969,36 @@ export default class ReactNativeBleTransport {
766
969
 
767
970
  if (!(await device.isConnected())) {
768
971
  Log?.debug('not connected, try to connect to device: ', uuid);
972
+ const disconnectedDevice = device;
769
973
 
770
974
  try {
771
- device = await device.connect(connectOptions);
975
+ device = await this.connectWithTimeout(uuid, () =>
976
+ disconnectedDevice.connect(connectOptions)
977
+ );
772
978
  } catch (e) {
773
979
  Log?.debug('not connected, try to connect to device has error: ', e);
980
+ if (isConnectTimeoutError(e)) {
981
+ throw e;
982
+ }
774
983
  if (
775
984
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
776
985
  e.errorCode === BleErrorCode.OperationCancelled
777
986
  ) {
778
987
  Log?.debug('second try to reconnect without params');
779
988
  try {
780
- device = await device.connect();
989
+ device = await this.connectWithTimeout(uuid, () =>
990
+ disconnectedDevice.connect(fallbackConnectOptions)
991
+ );
781
992
  } catch (e) {
782
993
  Log?.debug('last try to reconnect error: ', e);
783
994
  // last try to reconnect device if this issue exists
784
995
  // https://github.com/dotintent/react-native-ble-plx/issues/426
785
996
  if (e.errorCode === BleErrorCode.OperationCancelled) {
786
997
  Log?.debug('last try to reconnect');
787
- await device.cancelConnection();
788
- device = await device.connect();
998
+ await disconnectedDevice.cancelConnection();
999
+ device = await this.connectWithTimeout(uuid, () =>
1000
+ disconnectedDevice.connect(fallbackConnectOptions)
1001
+ );
789
1002
  }
790
1003
  }
791
1004
  } else {
@@ -794,13 +1007,16 @@ export default class ReactNativeBleTransport {
794
1007
  }
795
1008
  }
796
1009
 
797
- device = await requestAndroidMtu(device);
798
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
1010
+ device = await resolveNegotiatedMtu(device);
1011
+ const acquiredDevice = device;
1012
+ const { writeCharacteristic, notifyCharacteristic } =
1013
+ await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
799
1014
 
800
1015
  const protocolHint = expectedProtocol
801
1016
  ? undefined
802
- : this.deviceProtocolHints.get(uuid) ??
803
- inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
1017
+ : input.protocolHint ??
1018
+ this.deviceProtocolHints.get(uuid) ??
1019
+ inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
804
1020
 
805
1021
  // release transport before new transport instance
806
1022
  await this.release(uuid, true);
@@ -808,45 +1024,30 @@ export default class ReactNativeBleTransport {
808
1024
  this.deviceProtocolHints.set(uuid, protocolHint);
809
1025
  }
810
1026
 
811
- const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
812
- if (Platform.OS === 'android') {
813
- transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
814
- }
815
- const monitorToken = this.nextMonitorToken;
816
- this.nextMonitorToken += 1;
817
- const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
818
- transport.monitorToken = monitorToken;
819
- transport.notifyTransactionId = notifyTransactionId;
820
- this.monitorTokens.set(uuid, monitorToken);
821
- transport.notifySubscription = this._monitorCharacteristic(
822
- transport.notifyCharacteristic,
823
- uuid,
824
- monitorToken,
825
- notifyTransactionId
826
- );
827
- transportCache[uuid] = transport;
828
-
829
- this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
830
-
831
- if (Platform.OS === 'ios') {
832
- await new Promise<void>(resolve => {
833
- setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
834
- });
835
- } else if (Platform.OS === 'android') {
836
- await delay(ANDROID_NOTIFY_READY_DELAY_MS);
837
- }
838
-
839
- const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
840
-
841
- this.emitter?.emit('device-connect', {
842
- name: device.name,
843
- id: device.id,
844
- connectId: device.id,
1027
+ await this.installTransportForAcquire(uuid, acquiredDevice, {
1028
+ writeCharacteristic,
1029
+ notifyCharacteristic,
845
1030
  });
846
1031
 
847
- this.attachDisconnectSubscription(transport, device, uuid);
848
-
849
- return { uuid, protocolType };
1032
+ try {
1033
+ const protocolType = await this.detectProtocol(
1034
+ uuid,
1035
+ expectedProtocol,
1036
+ protocolHint,
1037
+ async () => {
1038
+ await this.installTransportForAcquire(uuid, acquiredDevice);
1039
+ }
1040
+ );
1041
+ const currentTransport = transportCache[uuid];
1042
+ if (!currentTransport) {
1043
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1044
+ }
1045
+ this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
1046
+ return { uuid, protocolType };
1047
+ } catch (error) {
1048
+ await this.release(uuid, true);
1049
+ throw error;
1050
+ }
850
1051
  }
851
1052
 
852
1053
  _monitorCharacteristic(
@@ -873,7 +1074,7 @@ export default class ReactNativeBleTransport {
873
1074
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
874
1075
  return;
875
1076
  }
876
- if (this.deviceProtocol.get(uuid) === 'V2') {
1077
+ if (this.getActiveProtocol(uuid) === 'V2') {
877
1078
  let errorCode:
878
1079
  | typeof HardwareErrorCode.BleDeviceBondError
879
1080
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -896,7 +1097,7 @@ export default class ReactNativeBleTransport {
896
1097
  this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
897
1098
  return;
898
1099
  }
899
- if (this.runPromise) {
1100
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
900
1101
  let ERROR:
901
1102
  | typeof HardwareErrorCode.BleDeviceBondError
902
1103
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -919,7 +1120,6 @@ export default class ReactNativeBleTransport {
919
1120
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure
920
1121
  );
921
1122
  this.runPromise.reject(notifyError);
922
- this.rejectAllProtocolV2Frames(notifyError);
923
1123
  Log?.debug(
924
1124
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
925
1125
  );
@@ -927,7 +1127,6 @@ export default class ReactNativeBleTransport {
927
1127
  }
928
1128
  const notifyError = ERRORS.TypedError(ERROR);
929
1129
  this.runPromise.reject(notifyError);
930
- this.rejectAllProtocolV2Frames(notifyError);
931
1130
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
932
1131
  }
933
1132
 
@@ -945,7 +1144,7 @@ export default class ReactNativeBleTransport {
945
1144
 
946
1145
  try {
947
1146
  const data = Buffer.from(c.value as string, 'base64');
948
- const protocol = this.deviceProtocol.get(uuid);
1147
+ const protocol = this.getActiveProtocol(uuid);
949
1148
  if (!protocol) {
950
1149
  Log?.debug('monitor data ignored before protocol detection: ', uuid);
951
1150
  return;
@@ -972,14 +1171,16 @@ export default class ReactNativeBleTransport {
972
1171
  // );
973
1172
  bufferLength = 0;
974
1173
  buffer = [];
975
- this.runPromise?.resolve(value.toString('hex'));
1174
+ if (this.runPromiseDeviceId === uuid) {
1175
+ this.runPromise?.resolve(value.toString('hex'));
1176
+ }
976
1177
  }
977
1178
  } catch (error) {
978
1179
  Log?.debug('monitor data error: ', error);
979
1180
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
980
- if (this.deviceProtocol.get(uuid) === 'V2') {
1181
+ if (this.getActiveProtocol(uuid) === 'V2') {
981
1182
  this.rejectProtocolV2Frames(uuid, notifyError);
982
- } else {
1183
+ } else if (this.runPromiseDeviceId === uuid) {
983
1184
  this.runPromise?.reject(notifyError);
984
1185
  }
985
1186
  }
@@ -989,13 +1190,18 @@ export default class ReactNativeBleTransport {
989
1190
  }
990
1191
 
991
1192
  async release(uuid: string, onclose = false) {
992
- const transport = transportCache[uuid];
993
1193
  await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
994
- if (this.runPromise) {
1194
+ return this.releaseNative(uuid, onclose);
1195
+ }
1196
+
1197
+ private async releaseNative(uuid: string, onclose = false) {
1198
+ const transport = transportCache[uuid];
1199
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
995
1200
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
996
1201
  this.runPromise.reject(error);
997
1202
  this.runPromise = null;
998
- this.rejectAllProtocolV2Frames(error);
1203
+ this.runPromiseDeviceId = null;
1204
+ this.rejectProtocolV2Frames(uuid, error);
999
1205
  } else {
1000
1206
  this.resetProtocolV2Frames(uuid);
1001
1207
  }
@@ -1006,6 +1212,8 @@ export default class ReactNativeBleTransport {
1006
1212
  return Promise.resolve(true);
1007
1213
  }
1008
1214
 
1215
+ await this.restoreAndroidConnectionPriority(uuid, transport);
1216
+
1009
1217
  if (transport) {
1010
1218
  if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1011
1219
  this.monitorTokens.delete(uuid);
@@ -1035,8 +1243,11 @@ export default class ReactNativeBleTransport {
1035
1243
  delete transportCache[uuid];
1036
1244
  }
1037
1245
 
1246
+ this.protocolV2HighVolumeLogSignatures.delete(uuid);
1247
+
1038
1248
  this.deviceProtocol.delete(uuid);
1039
- // 设备名称提示不依赖当前连接;保留它可让重连优先探测 V2。
1249
+ this.probingProtocols.delete(uuid);
1250
+ // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
1040
1251
  this.protocolV2Assemblers.get(uuid)?.reset();
1041
1252
  this.protocolV2Assemblers.delete(uuid);
1042
1253
  this.resetProtocolV2Frames(uuid);
@@ -1075,33 +1286,13 @@ export default class ReactNativeBleTransport {
1075
1286
  `Device protocol has not been detected for ${uuid}`
1076
1287
  );
1077
1288
  }
1078
- // Upload resources on low-end phones may OOM
1079
- if (name === 'ResourceUpdate' || name === 'ResourceAck') {
1080
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
1081
- file_name: data?.file_name,
1082
- hash: data?.hash,
1083
- });
1084
- } else if (LogBlockCommand.has(name)) {
1085
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
1086
- } else {
1087
- Log?.debug(
1088
- 'transport-react-native',
1089
- 'call-',
1090
- ' name: ',
1091
- name,
1092
- ' data: ',
1093
- data,
1094
- ' protocol: ',
1095
- protocol
1096
- );
1097
- }
1289
+ Log?.debug('transport call', createTransportCallLog(name, protocol, data));
1098
1290
 
1099
1291
  if (protocol === 'V2') {
1100
1292
  return this.callProtocolV2(uuid, name, data, options);
1101
1293
  }
1102
1294
 
1103
1295
  const forceRun = name === 'Initialize' || name === 'Cancel';
1104
- Log?.debug('transport-react-native call this.runPromise', this.runPromise);
1105
1296
  if (this.runPromise && !forceRun) {
1106
1297
  throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1107
1298
  }
@@ -1122,7 +1313,25 @@ export default class ReactNativeBleTransport {
1122
1313
  const transport = this.getCachedTransport(uuid);
1123
1314
  const runPromise = createDeferred<string>();
1124
1315
  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
+ }
1125
1324
  this.runPromise = runPromise;
1325
+ 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;
1126
1335
  const messages = this._messages;
1127
1336
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1128
1337
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1148,6 +1357,9 @@ export default class ReactNativeBleTransport {
1148
1357
  chunk = ByteBuffer.allocate(packetCapacity);
1149
1358
  } catch (e) {
1150
1359
  onError(e);
1360
+ if (isWedgedWriteError(e)) {
1361
+ throw e;
1362
+ }
1151
1363
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1152
1364
  }
1153
1365
  }
@@ -1179,6 +1391,9 @@ export default class ReactNativeBleTransport {
1179
1391
  }
1180
1392
  } catch (e) {
1181
1393
  onError(e);
1394
+ if (isWedgedWriteError(e)) {
1395
+ throw e;
1396
+ }
1182
1397
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1183
1398
  }
1184
1399
  }
@@ -1192,21 +1407,26 @@ export default class ReactNativeBleTransport {
1192
1407
  if (name === 'EmmcFileWrite') {
1193
1408
  await writeChunkedData(
1194
1409
  buffers,
1195
- data => transport.writeWithRetry(data),
1410
+ data =>
1411
+ this.writeBlePacket(
1412
+ uuid,
1413
+ data,
1414
+ payload => transport.writeWithRetry(payload),
1415
+ isCurrentOwner
1416
+ ),
1196
1417
  e => {
1197
- this.runPromise = null;
1418
+ releaseOwnershipIfCurrent();
1198
1419
  Log?.error('writeCharacteristic write error: ', e);
1199
1420
  }
1200
1421
  );
1201
1422
  } else if (name === 'FirmwareUpload') {
1202
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
1423
+ Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
1203
1424
  packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
1204
1425
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1205
1426
  pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1206
1427
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1207
1428
  maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
1208
1429
  });
1209
-
1210
1430
  await writeFirmwareUploadChunkedData(
1211
1431
  buffers,
1212
1432
  async data => {
@@ -1216,43 +1436,31 @@ export default class ReactNativeBleTransport {
1216
1436
  // eslint-disable-next-line no-constant-condition
1217
1437
  while (true) {
1218
1438
  try {
1219
- await transport.writeCharacteristic.writeWithoutResponse(data);
1439
+ await this.writeBlePacket(
1440
+ uuid,
1441
+ data,
1442
+ payload => transport.writeWithRetry(payload),
1443
+ isCurrentOwner
1444
+ );
1220
1445
  return;
1221
1446
  } catch (error) {
1222
1447
  const retryType = getFirmwareUploadWriteRetryType(error);
1223
1448
  if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1224
1449
  throw error;
1225
1450
  }
1226
- const shouldReconnect = retryType === 'reconnectable';
1227
- const delayMs = shouldReconnect
1228
- ? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
1229
- : resolveFirmwareUploadRetryDelay(attempt);
1451
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
1230
1452
  Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
1231
1453
  attempt: attempt + 1,
1232
1454
  delayMs,
1233
- reconnect: shouldReconnect,
1234
1455
  error,
1235
1456
  });
1236
- if (shouldReconnect) {
1237
- this.firmwareUploadWriteRecoveryIds.add(uuid);
1238
- }
1239
1457
  await delay(delayMs);
1240
1458
  attempt += 1;
1241
- if (shouldReconnect) {
1242
- try {
1243
- await this.reconnectFirmwareUploadTransport(uuid, transport);
1244
- } catch (e) {
1245
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
1246
- if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1247
- throw e;
1248
- }
1249
- }
1250
- }
1251
1459
  }
1252
1460
  }
1253
1461
  },
1254
1462
  e => {
1255
- this.runPromise = null;
1463
+ releaseOwnershipIfCurrent();
1256
1464
  Log?.error('writeCharacteristic write error: ', e);
1257
1465
  }
1258
1466
  );
@@ -1260,12 +1468,24 @@ export default class ReactNativeBleTransport {
1260
1468
  for (const o of buffers) {
1261
1469
  const outData = o.toString('base64');
1262
1470
  // Upload resources on low-end phones may OOM
1263
- // this.Log.debug('send hex strting: ', o.toString('hex'));
1264
1471
  try {
1265
- await transport.writeCharacteristic.writeWithoutResponse(outData);
1472
+ const shouldUseWriteWithResponse =
1473
+ 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
+ );
1266
1483
  } catch (e) {
1267
1484
  Log?.debug('writeCharacteristic write error: ', e);
1268
- this.runPromise = null;
1485
+ releaseOwnershipIfCurrent();
1486
+ if (isWedgedWriteError(e)) {
1487
+ throw e;
1488
+ }
1269
1489
  if (e.errorCode === BleErrorCode.DeviceDisconnected) {
1270
1490
  throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
1271
1491
  } else if (e.errorCode === BleErrorCode.OperationStartFailed) {
@@ -1298,20 +1518,33 @@ export default class ReactNativeBleTransport {
1298
1518
  throw new Error('Returning data is not string.');
1299
1519
  }
1300
1520
 
1301
- Log?.debug('receive data: ', response);
1302
1521
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1303
1522
  return check.call(jsonData);
1304
1523
  } catch (e) {
1305
- if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1306
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1524
+ if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1525
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
1307
1526
  } else {
1308
1527
  Log?.error('call error: ', e);
1309
1528
  }
1529
+ const isProbeTimeout =
1530
+ 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
+ if (
1536
+ !isProbeTimeout &&
1537
+ !isStaleCall &&
1538
+ (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1539
+ ) {
1540
+ await this.disconnect(uuid);
1541
+ }
1310
1542
  throw e;
1311
1543
  } finally {
1312
1544
  if (timeout) clearTimeout(timeout);
1313
1545
  if (this.runPromise === runPromise) {
1314
1546
  this.runPromise = null;
1547
+ this.runPromiseDeviceId = null;
1315
1548
  }
1316
1549
  }
1317
1550
  }
@@ -1321,9 +1554,9 @@ export default class ReactNativeBleTransport {
1321
1554
  }
1322
1555
 
1323
1556
  async disconnect(session: string) {
1324
- Log?.debug('transport-react-native transport resetSession: ', session);
1325
1557
  await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
1326
1558
  const transport = transportCache[session];
1559
+ const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
1327
1560
 
1328
1561
  // Clean up disconnect subscription first to prevent onDisconnected callback
1329
1562
  // from being triggered when we cancel the connection below
@@ -1381,20 +1614,22 @@ export default class ReactNativeBleTransport {
1381
1614
  delete transportCache[session];
1382
1615
  }
1383
1616
  this.deviceProtocol.delete(session);
1617
+ this.probingProtocols.delete(session);
1384
1618
  this.deviceProtocolHints.delete(session);
1619
+ this.sessionProtocols.delete(session);
1620
+ this.protocolReprobeFailures.delete(session);
1385
1621
  this.protocolV2Assemblers.delete(session);
1386
1622
  this.resetProtocolV2Frames(session);
1387
1623
 
1388
1624
  // emit the disconnect event
1389
1625
  try {
1390
- this.emitter?.emit('device-disconnect', {
1391
- name: transport?.device?.name,
1392
- id: session,
1393
- connectId: session,
1394
- });
1626
+ this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
1395
1627
  } catch (e) {
1396
1628
  Log?.error('resetSession: emit disconnect event error: ', e);
1397
1629
  }
1630
+ if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
1631
+ this.monitorTokens.delete(session);
1632
+ }
1398
1633
  // eslint-disable-next-line no-promise-executor-return
1399
1634
  await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
1400
1635
  }
@@ -1405,6 +1640,114 @@ export default class ReactNativeBleTransport {
1405
1640
  // this.runPromise.reject(new Error('Transport_CallCanceled'));
1406
1641
  }
1407
1642
  this.runPromise = null;
1643
+ this.runPromiseDeviceId = null;
1644
+ }
1645
+
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
+ }
1408
1751
  }
1409
1752
 
1410
1753
  private getCachedTransport(uuid: string) {
@@ -1415,6 +1758,109 @@ export default class ReactNativeBleTransport {
1415
1758
  return transport;
1416
1759
  }
1417
1760
 
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.writeTimeoutCounts.clear();
1854
+ this.connectionSetupTimeoutCounts.clear();
1855
+ this.monitorTokens.clear();
1856
+ this.protocolV2Assemblers.clear();
1857
+ try {
1858
+ manager?.destroy();
1859
+ } catch (error) {
1860
+ Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
1861
+ }
1862
+ }
1863
+
1418
1864
  private createProtocolMismatchError(expected: ProtocolType) {
1419
1865
  return ERRORS.TypedError(
1420
1866
  HardwareErrorCode.RuntimeError,
@@ -1425,60 +1871,120 @@ export default class ReactNativeBleTransport {
1425
1871
  private createProtocolDetectionError() {
1426
1872
  return ERRORS.TypedError(
1427
1873
  HardwareErrorCode.BleTimeoutError,
1428
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1874
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
1429
1875
  );
1430
1876
  }
1431
1877
 
1432
1878
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1879
+ if (this.probingProtocols.get(uuid) === protocol) {
1880
+ this.probingProtocols.delete(uuid);
1881
+ }
1433
1882
  if (this.deviceProtocol.get(uuid) === protocol) {
1434
1883
  this.deviceProtocol.delete(uuid);
1435
1884
  }
1436
1885
  }
1437
1886
 
1887
+ /** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
1888
+ private getActiveProtocol(uuid: string): ProtocolType | undefined {
1889
+ return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
1890
+ }
1891
+
1438
1892
  private async detectProtocol(
1439
1893
  uuid: string,
1440
1894
  expectedProtocol?: ProtocolType,
1441
- protocolHint?: ProtocolType
1895
+ protocolHint?: ProtocolType,
1896
+ rebuildTransport?: () => Promise<void>
1442
1897
  ): Promise<ProtocolType> {
1898
+ if (Platform.OS === 'ios' && expectedProtocol) {
1899
+ this.deviceProtocol.set(uuid, expectedProtocol);
1900
+ Log?.debug('[ReactNativeBleTransport] protocol selected', {
1901
+ deviceId: uuid,
1902
+ protocol: expectedProtocol,
1903
+ source: 'expected',
1904
+ });
1905
+ return expectedProtocol;
1906
+ }
1907
+
1443
1908
  if (expectedProtocol === 'V1') {
1444
1909
  if (await this.probeProtocolV1(uuid)) {
1445
1910
  this.deviceProtocol.set(uuid, 'V1');
1446
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1911
+ this.sessionProtocols.set(uuid, 'V1');
1912
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1913
+ deviceId: uuid,
1914
+ protocol: 'V1',
1915
+ source: 'expected',
1916
+ });
1447
1917
  return 'V1';
1448
1918
  }
1449
1919
  throw this.createProtocolMismatchError(expectedProtocol);
1450
1920
  }
1451
1921
 
1452
1922
  if (expectedProtocol === 'V2') {
1453
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
1454
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
1455
- this.deviceProtocol.set(uuid, 'V2');
1456
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1457
- return 'V2';
1923
+ if (await this.probeProtocolV2(uuid)) {
1924
+ this.deviceProtocol.set(uuid, 'V2');
1925
+ this.sessionProtocols.set(uuid, 'V2');
1926
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1927
+ deviceId: uuid,
1928
+ protocol: 'V2',
1929
+ source: 'expected',
1930
+ });
1931
+ return 'V2';
1932
+ }
1933
+ throw this.createProtocolMismatchError(expectedProtocol);
1458
1934
  }
1459
1935
 
1460
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
1461
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1
1462
- // 不能作为最终结论。
1463
- const probeOrder: ProtocolType[] =
1936
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
1937
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
1938
+ const sessionProtocol = this.sessionProtocols.get(uuid);
1939
+ const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
1940
+ const fullProbeOrder: ProtocolType[] =
1464
1941
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1942
+ // A device that already answered on a protocol in this session keeps answering on
1943
+ // it; while it is rebooting nothing answers at all, so probing the other protocol
1944
+ // only adds its timeout to every poll.
1945
+ const trustSessionProtocol =
1946
+ sessionProtocol !== undefined &&
1947
+ !protocolHint &&
1948
+ reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
1949
+ const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
1465
1950
 
1466
1951
  for (let i = 0; i < probeOrder.length; i += 1) {
1467
1952
  const protocol = probeOrder[i];
1468
1953
  if (i > 0) {
1469
- // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
1954
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
1470
1955
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1956
+ if (!transportCache[uuid]) {
1957
+ if (!rebuildTransport) {
1958
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1959
+ }
1960
+ await rebuildTransport();
1961
+ }
1471
1962
  }
1472
1963
  const detected =
1473
1964
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1474
1965
  if (detected) {
1475
1966
  this.deviceProtocol.set(uuid, protocol);
1476
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1967
+ this.sessionProtocols.set(uuid, protocol);
1968
+ this.protocolReprobeFailures.delete(uuid);
1969
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1970
+ deviceId: uuid,
1971
+ protocol,
1972
+ source: 'probe',
1973
+ });
1477
1974
  return protocol;
1478
1975
  }
1479
1976
  }
1480
1977
 
1978
+ if (trustSessionProtocol) {
1979
+ // Still silent on its own protocol: count it, and let the streak expire the
1980
+ // shortcut so a device that genuinely switched protocols is found again.
1981
+ this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
1982
+ } else {
1983
+ this.protocolReprobeFailures.delete(uuid);
1984
+ }
1985
+
1481
1986
  this.deviceProtocol.delete(uuid);
1987
+ this.probingProtocols.delete(uuid);
1482
1988
  throw this.createProtocolDetectionError();
1483
1989
  }
1484
1990
 
@@ -1540,12 +2046,20 @@ export default class ReactNativeBleTransport {
1540
2046
  }
1541
2047
 
1542
2048
  try {
1543
- this.deviceProtocol.set(uuid, 'V1');
1544
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2049
+ this.probingProtocols.set(uuid, 'V1');
2050
+ // GetFeatures identifies Protocol V1 without resetting an existing wallet
2051
+ // session before Core has a chance to restore a hidden wallet.
2052
+ await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
2053
+ this.probingProtocols.delete(uuid);
1545
2054
  return true;
1546
2055
  } catch (error) {
1547
2056
  this.clearProbeProtocol(uuid, 'V1');
1548
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
2057
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
2058
+ // A wedged write already dropped the link, so probing another protocol on it
2059
+ // would only fail against a torn-down transport: surface the real cause.
2060
+ if (isWedgedWriteError(error)) {
2061
+ throw error;
2062
+ }
1549
2063
  return false;
1550
2064
  }
1551
2065
  }
@@ -1555,7 +2069,7 @@ export default class ReactNativeBleTransport {
1555
2069
  return false;
1556
2070
  }
1557
2071
 
1558
- this.deviceProtocol.set(uuid, 'V2');
2072
+ this.probingProtocols.set(uuid, 'V2');
1559
2073
  this.protocolV2Assemblers.get(uuid)?.reset();
1560
2074
  const detected = await probeProtocolV2Helper({
1561
2075
  call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
@@ -1570,6 +2084,8 @@ export default class ReactNativeBleTransport {
1570
2084
  });
1571
2085
  if (!detected) {
1572
2086
  this.clearProbeProtocol(uuid, 'V2');
2087
+ } else {
2088
+ this.probingProtocols.delete(uuid);
1573
2089
  }
1574
2090
  return detected;
1575
2091
  }
@@ -1620,17 +2136,8 @@ export default class ReactNativeBleTransport {
1620
2136
  this.getProtocolV2FrameQueue(uuid).push(frame);
1621
2137
  }
1622
2138
 
1623
- private rejectAllProtocolV2Frames(error: Error) {
1624
- this.protocolV2FrameQueues.clear();
1625
- for (const framePromise of this.protocolV2FramePromises.values()) {
1626
- framePromise.reject(error);
1627
- }
1628
- this.protocolV2FramePromises.clear();
1629
- }
1630
-
1631
2139
  private resetProtocolV2Frames(uuid: string) {
1632
- this.protocolV2FrameQueues.delete(uuid);
1633
- this.protocolV2FramePromises.delete(uuid);
2140
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1634
2141
  }
1635
2142
 
1636
2143
  private rejectProtocolV2Frames(uuid: string, error: Error) {
@@ -1659,49 +2166,95 @@ export default class ReactNativeBleTransport {
1659
2166
  }
1660
2167
  }
1661
2168
 
2169
+ private async writeProtocolV2Packet(
2170
+ uuid: string,
2171
+ transport: BleTransport,
2172
+ base64: string,
2173
+ context: ProtocolV2CallContext,
2174
+ assertCurrentGeneration: () => void
2175
+ ) {
2176
+ const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
2177
+ platform: Platform.OS,
2178
+ highThroughput: context.highThroughput,
2179
+ requestedWithResponse: context.writeWithResponse,
2180
+ characteristic: transport.writeCharacteristic,
2181
+ });
2182
+ let attempt = 0;
2183
+ for (;;) {
2184
+ assertCurrentGeneration();
2185
+ if (context.signal.aborted) {
2186
+ throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
2187
+ }
2188
+ try {
2189
+ await this.writeBlePacket(
2190
+ uuid,
2191
+ base64,
2192
+ payload =>
2193
+ shouldUseWriteWithResponse
2194
+ ? transport.writeCharacteristic.writeWithResponse(payload)
2195
+ : transport.writeCharacteristic.writeWithoutResponse(payload),
2196
+ // Same rule as Protocol V1: a write from a superseded generation must not
2197
+ // tear down the link that the current generation is using.
2198
+ () => {
2199
+ try {
2200
+ assertCurrentGeneration();
2201
+ return !context.signal.aborted;
2202
+ } catch {
2203
+ return false;
2204
+ }
2205
+ }
2206
+ );
2207
+ assertCurrentGeneration();
2208
+ return;
2209
+ } catch (error) {
2210
+ if (
2211
+ getFirmwareUploadWriteRetryType(error) !== 'congested' ||
2212
+ attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
2213
+ ) {
2214
+ throw error;
2215
+ }
2216
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
2217
+ attempt += 1;
2218
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
2219
+ name: context.messageName,
2220
+ attempt,
2221
+ delayMs,
2222
+ });
2223
+ await delay(delayMs);
2224
+ }
2225
+ }
2226
+ }
2227
+
1662
2228
  private async writeProtocolV2Frame(
2229
+ uuid: string,
1663
2230
  transport: BleTransport,
1664
2231
  frame: Uint8Array,
1665
- options?: { highVolume?: boolean; writeWithResponse?: boolean }
2232
+ context: ProtocolV2CallContext,
2233
+ assertCurrentGeneration: () => void
1666
2234
  ) {
1667
2235
  const tuning = getProtocolV2BleTuning();
1668
2236
  const packetCapacity = resolveProtocolV2PacketCapacity({
1669
2237
  platform: Platform.OS,
1670
2238
  iosPacketLength: tuning.iosPacketLength,
1671
2239
  androidPacketLength: tuning.androidPacketLength,
1672
- mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
2240
+ mtu: transport.mtuSize,
2241
+ });
2242
+ await writeProtocolV2BleFrame({
2243
+ frame,
2244
+ packetCapacity,
2245
+ assertActive: assertCurrentGeneration,
2246
+ signal: context.signal,
2247
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
2248
+ wait: delay,
2249
+ writePacket: packet =>
2250
+ this.writeProtocolV2Packet(
2251
+ uuid,
2252
+ transport,
2253
+ Buffer.from(packet).toString('base64'),
2254
+ context,
2255
+ assertCurrentGeneration
2256
+ ),
1673
2257
  });
1674
- const writeWithResponse =
1675
- !!options?.writeWithResponse || (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
1676
- const writeMode = resolveBleWriteMode(
1677
- transport.writeCharacteristic,
1678
- writeWithResponse ? 'withResponse' : 'withoutResponse'
1679
- );
1680
- const shouldThrottle = !!options?.highVolume && writeMode === 'withoutResponse';
1681
- let packetsWritten = 0;
1682
-
1683
- for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1684
- const chunk = frame.slice(offset, offset + packetCapacity);
1685
- const base64 = Buffer.from(chunk).toString('base64');
1686
- if (writeMode === 'withResponse') {
1687
- await transport.writeCharacteristic.writeWithResponse(base64);
1688
- } else {
1689
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1690
- }
1691
- packetsWritten += 1;
1692
-
1693
- if (
1694
- shouldThrottle &&
1695
- packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1696
- offset + packetCapacity < frame.length
1697
- ) {
1698
- await delay(tuning.highVolumeWritePauseMs);
1699
- }
1700
- }
1701
-
1702
- if (shouldThrottle) {
1703
- await delay(tuning.highVolumeWriteFlushDelayMs);
1704
- }
1705
2258
  }
1706
2259
 
1707
2260
  private async callProtocolV2(
@@ -1714,26 +2267,45 @@ export default class ReactNativeBleTransport {
1714
2267
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1715
2268
  }
1716
2269
 
1717
- const callOptions = {
1718
- ...options,
1719
- timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1720
- };
1721
- const highVolumeWrite = LogBlockCommand.has(name);
2270
+ const callOptions = options;
2271
+ const highThroughputWrite = isProtocolV2HighThroughputCall(name);
1722
2272
 
1723
- if (highVolumeWrite) {
2273
+ if (highThroughputWrite) {
2274
+ await this.ensureProtocolV2HighThroughputMtu(uuid);
1724
2275
  const tuning = getProtocolV2BleTuning();
1725
- Log?.debug(
1726
- '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1727
- name,
1728
- {
1729
- packetCapacity:
1730
- Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1731
- burstSize: tuning.highVolumeWriteBurstSize,
1732
- pauseMs: tuning.highVolumeWritePauseMs,
1733
- flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1734
- writeWithResponse: tuning.highVolumeWriteWithResponse,
1735
- }
1736
- );
2276
+ const currentTransport = this.getCachedTransport(uuid);
2277
+ const writeWithResponse = shouldWriteProtocolV2WithResponse({
2278
+ platform: Platform.OS,
2279
+ highThroughput: true,
2280
+ requestedWithResponse: options?.writeWithResponse,
2281
+ characteristic: currentTransport.writeCharacteristic,
2282
+ });
2283
+ const packetCapacity = resolveProtocolV2PacketCapacity({
2284
+ platform: Platform.OS,
2285
+ iosPacketLength: tuning.iosPacketLength,
2286
+ androidPacketLength: tuning.androidPacketLength,
2287
+ mtu: currentTransport.mtuSize,
2288
+ });
2289
+ const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
2290
+ const logSignature = `${name}:${writeMode}:${String(
2291
+ currentTransport.mtuSize
2292
+ )}:${packetCapacity}`;
2293
+ const loggedSignatures =
2294
+ this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
2295
+ if (!loggedSignatures.has(logSignature)) {
2296
+ loggedSignatures.add(logSignature);
2297
+ this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
2298
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
2299
+ name,
2300
+ writeMode,
2301
+ reportedMtu: currentTransport.mtuSize,
2302
+ packetCapacity,
2303
+ });
2304
+ }
2305
+ }
2306
+
2307
+ if (highThroughputWrite) {
2308
+ await this.enableAndroidHighConnectionPriority(uuid);
1737
2309
  }
1738
2310
 
1739
2311
  try {
@@ -1747,6 +2319,90 @@ export default class ReactNativeBleTransport {
1747
2319
  } catch (e) {
1748
2320
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1749
2321
  throw e;
2322
+ } finally {
2323
+ if (highThroughputWrite) {
2324
+ this.scheduleAndroidBalancedConnectionPriority(uuid);
2325
+ }
2326
+ }
2327
+ }
2328
+
2329
+ private async ensureProtocolV2HighThroughputMtu(uuid: string) {
2330
+ const transport = this.getCachedTransport(uuid);
2331
+ if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
2332
+
2333
+ const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
2334
+ transport.device = refreshedDevice;
2335
+ transport.mtuSize =
2336
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
2337
+
2338
+ if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2339
+ throw ERRORS.TypedError(
2340
+ HardwareErrorCode.BleConnectedError,
2341
+ `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
2342
+ );
2343
+ }
2344
+ }
2345
+
2346
+ private clearAndroidPriorityResetTimer(uuid: string) {
2347
+ const timerId = this.androidPriorityResetTimers.get(uuid);
2348
+ if (timerId !== undefined) {
2349
+ clearTimeout(timerId);
2350
+ this.androidPriorityResetTimers.delete(uuid);
2351
+ }
2352
+ }
2353
+
2354
+ private async enableAndroidHighConnectionPriority(uuid: string) {
2355
+ if (Platform.OS !== 'android') return;
2356
+
2357
+ this.clearAndroidPriorityResetTimer(uuid);
2358
+ if (this.androidHighPriorityDevices.has(uuid)) return;
2359
+
2360
+ const transport = transportCache[uuid];
2361
+ if (!transport) return;
2362
+
2363
+ try {
2364
+ transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
2365
+ this.androidHighPriorityDevices.add(uuid);
2366
+ Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
2367
+ priority: 'high',
2368
+ });
2369
+ } catch (error) {
2370
+ Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
2371
+ error: error instanceof Error ? error.message : String(error),
2372
+ });
2373
+ }
2374
+ }
2375
+
2376
+ private scheduleAndroidBalancedConnectionPriority(uuid: string) {
2377
+ if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
2378
+
2379
+ this.clearAndroidPriorityResetTimer(uuid);
2380
+ const timerId = setTimeout(() => {
2381
+ this.androidPriorityResetTimers.delete(uuid);
2382
+ this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
2383
+ Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
2384
+ );
2385
+ }, ANDROID_HIGH_PRIORITY_IDLE_MS);
2386
+ this.androidPriorityResetTimers.set(uuid, timerId);
2387
+ }
2388
+
2389
+ private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
2390
+ this.clearAndroidPriorityResetTimer(uuid);
2391
+ if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
2392
+ return;
2393
+ }
2394
+
2395
+ try {
2396
+ transport.device = await transport.device.requestConnectionPriority(
2397
+ ConnectionPriority.Balanced
2398
+ );
2399
+ Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
2400
+ priority: 'balanced',
2401
+ });
2402
+ } catch (error) {
2403
+ Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
2404
+ error: error instanceof Error ? error.message : String(error),
2405
+ });
1750
2406
  }
1751
2407
  }
1752
2408
 
@@ -1767,12 +2423,16 @@ export default class ReactNativeBleTransport {
1767
2423
  this.protocolV2Assemblers.get(uuid)?.reset();
1768
2424
  this.resetProtocolV2Frames(uuid);
1769
2425
  },
1770
- writeFrame: async (frame: Uint8Array, context: { highVolume: boolean }) => {
2426
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
1771
2427
  assertCurrentGeneration();
1772
2428
  const currentTransport = this.getCachedTransport(uuid);
1773
- await this.writeProtocolV2Frame(currentTransport, frame, {
1774
- highVolume: context.highVolume,
1775
- });
2429
+ await this.writeProtocolV2Frame(
2430
+ uuid,
2431
+ currentTransport,
2432
+ frame,
2433
+ context,
2434
+ assertCurrentGeneration
2435
+ );
1776
2436
  },
1777
2437
  readFrame: async () => {
1778
2438
  assertCurrentGeneration();
@@ -1797,6 +2457,6 @@ export default class ReactNativeBleTransport {
1797
2457
  }
1798
2458
 
1799
2459
  getProtocolType(path: string): ProtocolType | undefined {
1800
- return this.deviceProtocol.get(path);
2460
+ return this.getActiveProtocol(path);
1801
2461
  }
1802
2462
  }