@onekeyfe/hd-transport-react-native 1.2.0-alpha.7 → 1.2.0-alpha.70

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
@@ -12,26 +12,31 @@ import transport, {
12
12
  LogBlockCommand,
13
13
  type OneKeyDeviceInfoBase,
14
14
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
15
+ PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
15
16
  PROTOCOL_V2_CHANNEL_BLE_UART,
16
17
  type ProtocolType,
18
+ type ProtocolV2CallContext,
17
19
  ProtocolV2FrameAssembler,
18
- ProtocolV2Session,
20
+ ProtocolV2LinkManager,
21
+ TRANSPORT_EVENT,
19
22
  type TransportCallOptions,
20
23
  probeProtocolV2 as probeProtocolV2Helper,
24
+ writeProtocolV2BleFrame,
21
25
  } from '@onekeyfe/hd-transport';
22
- import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
26
+ import {
27
+ ERRORS,
28
+ HardwareErrorCode,
29
+ createDeferred,
30
+ isOnekeyBluetoothDevice,
31
+ isPro2FindMyAdvertisementName,
32
+ } from '@onekeyfe/hd-shared';
23
33
 
24
34
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
25
- import {
26
- hasWritableCapability,
27
- resolveBleWriteMode,
28
- resolveProtocolV2PacketCapacity,
29
- } from './bleStrategy';
35
+ import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
30
36
  import { subscribeBleOn } from './subscribeBleOn';
31
37
  import {
32
38
  ANDROID_PACKET_LENGTH,
33
39
  IOS_PACKET_LENGTH,
34
- getBleUuidKey,
35
40
  getBluetoothServiceUuids,
36
41
  getInfosForServiceUuid,
37
42
  isSameBleUuid,
@@ -40,6 +45,7 @@ import { isHeaderChunk } from './utils/validateNotify';
40
45
  import BleTransport from './BleTransport';
41
46
  import timer from './utils/timer';
42
47
  import { bleLogger, setBleLogger } from './logger';
48
+ import { createTransportCallLog } from './transportLog';
43
49
 
44
50
  import type { Deferred } from '@onekeyfe/hd-shared';
45
51
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
@@ -55,24 +61,53 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
55
61
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
56
62
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
57
63
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
58
- const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
64
+ const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
59
65
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
60
66
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
61
67
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
62
68
  const ANDROID_GATT_CONGESTED_STATUS = 143;
63
69
 
64
- type FirmwareUploadWriteRetryType = 'congested' | 'reconnectable';
70
+ type FirmwareUploadWriteRetryType = 'congested';
65
71
  type ResolvedBleCharacteristics = {
66
72
  writeCharacteristic: Characteristic;
67
73
  notifyCharacteristic: Characteristic;
68
74
  };
69
75
 
76
+ const isAsciiWhitespace = (code: number) =>
77
+ code === 0x09 ||
78
+ code === 0x0a ||
79
+ code === 0x0b ||
80
+ code === 0x0c ||
81
+ code === 0x0d ||
82
+ code === 0x20;
83
+
84
+ const hasGattCongestedStatus = (text: string) => {
85
+ let searchFrom = 0;
86
+ while (searchFrom < text.length) {
87
+ const statusIndex = text.indexOf('status', searchFrom);
88
+ if (statusIndex < 0) return false;
89
+
90
+ let cursor = statusIndex + 'status'.length;
91
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
92
+ if (text[cursor] === ':' || text[cursor] === '=') {
93
+ cursor += 1;
94
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
95
+ }
96
+ if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
97
+
98
+ searchFrom = statusIndex + 'status'.length;
99
+ }
100
+ return false;
101
+ };
102
+
70
103
  const delay = (ms: number) =>
71
104
  new Promise<void>(resolve => {
72
105
  setTimeout(resolve, ms);
73
106
  });
74
107
 
75
- const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRetryType | null => {
108
+ export const getFirmwareUploadWriteRetryType = (
109
+ error: unknown
110
+ ): FirmwareUploadWriteRetryType | null => {
76
111
  if (!error || typeof error !== 'object') return null;
77
112
  const bleWriteError = error as {
78
113
  androidErrorCode?: unknown;
@@ -83,13 +118,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
83
118
  name?: unknown;
84
119
  };
85
120
 
86
- if (
87
- bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
88
- bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
89
- ) {
90
- return 'reconnectable';
91
- }
92
-
93
121
  if (
94
122
  bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
95
123
  bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
@@ -100,28 +128,35 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
100
128
  const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
101
129
  .filter(value => typeof value === 'string')
102
130
  .join(' ');
103
- return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
131
+ return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
104
132
  };
105
133
 
106
134
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
107
135
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
108
- const BLE_RESPONSE_TIMEOUT_MS = 30_000;
109
136
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
110
137
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
111
- const DEVICE_SCAN_TIMEOUT_MS = 8000;
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
+ const DEVICE_SCAN_TIMEOUT_MS = 3000;
112
155
  const IOS_NOTIFY_READY_DELAY_MS = 150;
113
156
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
114
- const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
115
- const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
116
- const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
117
-
118
157
  export type ProtocolV2BleTuning = {
119
158
  iosPacketLength?: number;
120
159
  androidPacketLength?: number;
121
- highVolumeWriteBurstSize?: number;
122
- highVolumeWritePauseMs?: number;
123
- highVolumeWriteFlushDelayMs?: number;
124
- highVolumeWriteWithResponse?: boolean;
125
160
  };
126
161
 
127
162
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
@@ -129,10 +164,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
129
164
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
130
165
  iosPacketLength: IOS_PACKET_LENGTH,
131
166
  androidPacketLength: ANDROID_PACKET_LENGTH,
132
- highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
133
- highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
134
- highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
135
- highVolumeWriteWithResponse: false,
136
167
  };
137
168
 
138
169
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -153,27 +184,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
153
184
  tuning.androidPacketLength,
154
185
  protocolV2BleTuning.androidPacketLength
155
186
  ),
156
- highVolumeWriteBurstSize: normalizePositiveInteger(
157
- tuning.highVolumeWriteBurstSize,
158
- protocolV2BleTuning.highVolumeWriteBurstSize
159
- ),
160
- highVolumeWritePauseMs: normalizePositiveInteger(
161
- tuning.highVolumeWritePauseMs,
162
- protocolV2BleTuning.highVolumeWritePauseMs
163
- ),
164
- highVolumeWriteFlushDelayMs: normalizePositiveInteger(
165
- tuning.highVolumeWriteFlushDelayMs,
166
- protocolV2BleTuning.highVolumeWriteFlushDelayMs
167
- ),
168
- highVolumeWriteWithResponse:
169
- tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
170
187
  };
171
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
188
+ Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
172
189
  }
173
190
 
174
191
  export function resetProtocolV2BleTuning() {
175
192
  protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
176
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
193
+ Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
177
194
  }
178
195
 
179
196
  export function getProtocolV2BleTuning() {
@@ -188,24 +205,54 @@ function getDeviceDisplayName(device?: Device | null) {
188
205
  return device?.name || device?.localName || null;
189
206
  }
190
207
 
191
- function isGenericBleService(uuid?: string | null) {
192
- return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
193
- }
194
-
195
- function hasKnownOneKeyService(device?: Device | null) {
196
- return (device?.serviceUUIDs ?? []).some(serviceUuid =>
197
- getInfosForServiceUuid(serviceUuid, 'classic')
198
- );
199
- }
200
-
201
208
  const ANDROID_REQUEST_MTU = 256;
202
209
 
210
+ const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
211
+
203
212
  const connectOptions: Record<string, unknown> = {
204
213
  requestMTU: ANDROID_REQUEST_MTU,
205
- timeout: 3000,
214
+ timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
206
215
  refreshGatt: 'OnConnected',
207
216
  };
208
217
 
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
+
209
256
  export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
210
257
 
211
258
  const tryToGetConfiguration = (device: Device) => {
@@ -222,9 +269,10 @@ const requestAndroidMtu = async (device: Device) => {
222
269
 
223
270
  try {
224
271
  const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
225
- Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
272
+ Log?.debug('[ReactNativeBleTransport] MTU configured', {
273
+ deviceId: device.id,
226
274
  requested: ANDROID_REQUEST_MTU,
227
- mtu: mtuDevice.mtu,
275
+ actual: mtuDevice.mtu,
228
276
  });
229
277
  return mtuDevice;
230
278
  } catch (error) {
@@ -272,6 +320,8 @@ export default class ReactNativeBleTransport {
272
320
 
273
321
  _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
274
322
 
323
+ private protocolV2SchemaConfiguration: string | undefined;
324
+
275
325
  name = 'ReactNativeBleTransport';
276
326
 
277
327
  configured = false;
@@ -282,6 +332,8 @@ export default class ReactNativeBleTransport {
282
332
 
283
333
  runPromise: Deferred<any> | null = null;
284
334
 
335
+ private runPromiseDeviceId: string | null = null;
336
+
285
337
  emitter?: EventEmitter;
286
338
 
287
339
  firmwareUploadWriteRecoveryIds = new Set<string>();
@@ -289,20 +341,59 @@ export default class ReactNativeBleTransport {
289
341
  /** Per-device protocol type detected by active wire-level probe after connect. */
290
342
  private deviceProtocol: Map<string, ProtocolType> = new Map();
291
343
 
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
+
292
358
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
293
359
 
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
+
294
366
  private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
295
367
 
296
368
  private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
297
369
 
298
370
  private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
299
371
 
300
- private activeProtocolV2Call: { uuid: string; token: number } | null = null;
301
-
302
- private nextProtocolV2CallToken = 1;
372
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
373
+ getSchemas: () => {
374
+ if (!this._messages || !this._messagesV2) {
375
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
376
+ }
377
+ return {
378
+ protocolV1: this._messages,
379
+ protocolV2: this._messagesV2,
380
+ };
381
+ },
382
+ classifyError: () => 'link-fatal',
383
+ onLinkInvalidated: async (uuid, reason) => {
384
+ this.protocolV2Assemblers.get(uuid)?.reset();
385
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
386
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
387
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
388
+ await this.releaseNative(uuid, true);
389
+ }
390
+ },
391
+ });
303
392
 
304
393
  private monitorTokens: Map<string, number> = new Map();
305
394
 
395
+ private disconnectEventTokens: Map<string, number> = new Map();
396
+
306
397
  private nextMonitorToken = 1;
307
398
 
308
399
  constructor(options: TransportOptions) {
@@ -321,8 +412,19 @@ export default class ReactNativeBleTransport {
321
412
  }
322
413
 
323
414
  configureProtocolV2(signedData: any) {
415
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
416
+ if (this.protocolV2SchemaConfiguration === configuration) {
417
+ return;
418
+ }
419
+
420
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
324
421
  this._messagesV2 = parseConfigure(signedData);
325
- Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
422
+ this.protocolV2SchemaConfiguration = configuration;
423
+ if (isReconfiguration) {
424
+ this.protocolV2Links
425
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
426
+ .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
427
+ }
326
428
  }
327
429
 
328
430
  listen() {
@@ -352,29 +454,15 @@ export default class ReactNativeBleTransport {
352
454
  }
353
455
  }
354
456
 
355
- let fallbackServiceUuid: string | undefined;
356
-
357
457
  if (!infos) {
358
458
  const services = await device.services();
359
459
  Log?.debug(
360
460
  '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
361
461
  services?.map(service => service.uuid)
362
462
  );
363
-
364
- const knownService = services.find(service =>
365
- getInfosForServiceUuid(service.uuid, 'classic')
366
- );
367
- const fallbackService =
368
- knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
369
-
370
- if (fallbackService) {
371
- fallbackServiceUuid = fallbackService.uuid;
372
- characteristics = await device.characteristicsForService(fallbackService.uuid);
373
- Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
374
- }
375
463
  }
376
464
 
377
- if (!infos && !fallbackServiceUuid) {
465
+ if (!infos) {
378
466
  try {
379
467
  Log?.debug('cancel connection when service not found');
380
468
  await device.cancelConnection();
@@ -384,9 +472,7 @@ export default class ReactNativeBleTransport {
384
472
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
385
473
  }
386
474
 
387
- const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
388
- const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
389
- const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
475
+ const { serviceUuid, writeUuid, notifyUuid } = infos;
390
476
 
391
477
  if (!serviceUuid) {
392
478
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
@@ -436,6 +522,7 @@ export default class ReactNativeBleTransport {
436
522
 
437
523
  attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
438
524
  transport.disconnectSubscription?.remove();
525
+ const { monitorToken } = transport;
439
526
  transport.disconnectSubscription = device.onDisconnected(() => {
440
527
  if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
441
528
  Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
@@ -445,18 +532,17 @@ export default class ReactNativeBleTransport {
445
532
  Log?.debug('device disconnect ignored for stale transport: ', device?.id);
446
533
  return;
447
534
  }
535
+ if (this.monitorTokens.get(uuid) !== monitorToken) {
536
+ Log?.debug('device disconnect ignored for stale generation: ', device?.id);
537
+ return;
538
+ }
448
539
 
449
540
  try {
450
541
  Log?.debug('device disconnect: ', device?.id);
451
- this.emitter?.emit('device-disconnect', {
452
- name: device?.name,
453
- id: device?.id,
454
- connectId: device?.id,
455
- });
456
- if (this.runPromise) {
542
+ this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
543
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
457
544
  const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
458
545
  this.runPromise.reject(error);
459
- this.rejectAllProtocolV2Frames(error);
460
546
  }
461
547
  } catch (e) {
462
548
  Log?.debug('device disconnect error: ', e);
@@ -466,6 +552,22 @@ export default class ReactNativeBleTransport {
466
552
  });
467
553
  }
468
554
 
555
+ private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
556
+ if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
557
+ return;
558
+ }
559
+ if (this.monitorTokens.get(uuid) !== token) {
560
+ Log?.debug('device disconnect event ignored for stale generation: ', uuid);
561
+ return;
562
+ }
563
+ this.disconnectEventTokens.set(uuid, token);
564
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
565
+ name,
566
+ id: uuid,
567
+ connectId: uuid,
568
+ });
569
+ }
570
+
469
571
  async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
470
572
  this.firmwareUploadWriteRecoveryIds.add(uuid);
471
573
  try {
@@ -478,22 +580,21 @@ export default class ReactNativeBleTransport {
478
580
  const isConnected = await device.isConnected().catch(() => false);
479
581
  if (!isConnected) {
480
582
  try {
481
- device = await device.connect(connectOptions);
583
+ device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
482
584
  } catch (e) {
483
585
  if (
484
586
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
485
587
  e.errorCode === BleErrorCode.OperationCancelled
486
588
  ) {
487
- device = await device.connect();
589
+ device = await this.connectWithTimeout(uuid, () => device.connect());
488
590
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
489
591
  throw e;
490
592
  }
491
593
  }
492
594
  }
493
595
 
494
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
495
- device
496
- );
596
+ const { writeCharacteristic, notifyCharacteristic } =
597
+ await this.resolveCharacteristicsWithTimeout(uuid, device);
497
598
 
498
599
  transport.device = device;
499
600
  transport.writeCharacteristic = writeCharacteristic;
@@ -553,14 +654,13 @@ export default class ReactNativeBleTransport {
553
654
  }
554
655
 
555
656
  blePlxManager.startDeviceScan(
556
- null,
657
+ getBluetoothServiceUuids(),
557
658
  {
558
659
  allowDuplicates: true,
559
660
  scanMode: ScanMode.LowLatency,
560
661
  },
561
662
  (error, device) => {
562
663
  if (error) {
563
- Log?.debug('ble scan manager: ', blePlxManager);
564
664
  Log?.debug('ble scan error: ', error);
565
665
  if (
566
666
  [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
@@ -584,33 +684,23 @@ export default class ReactNativeBleTransport {
584
684
  }
585
685
 
586
686
  const displayName = getDeviceDisplayName(device);
687
+ // iOS may report a service-only advertisement before the named scan response.
688
+ // Do not cache that incomplete advertisement as an unknown device.
689
+ const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
690
+ const isFindMyPeripheral =
691
+ isPro2FindMyAdvertisementName(device?.name) ||
692
+ isPro2FindMyAdvertisementName(device?.localName);
587
693
  const isOneKey =
588
- isOnekeyDevice(device?.name ?? null, device?.id) ||
589
- isOnekeyDevice(device?.localName ?? null, device?.id) ||
590
- hasKnownOneKeyService(device);
591
- const shouldTraceCandidate =
592
- !!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
593
-
594
- if (shouldTraceCandidate) {
595
- Log?.debug('[ReactNativeBleTransport] scan candidate', {
694
+ !isUnnamedIOSPeripheral &&
695
+ !isFindMyPeripheral &&
696
+ isOnekeyBluetoothDevice({
697
+ id: device?.id,
596
698
  name: device?.name,
597
699
  localName: device?.localName,
598
- id: device?.id,
599
- serviceUUIDs: device?.serviceUUIDs,
600
- accepted: isOneKey,
700
+ serviceUuids: device?.serviceUUIDs,
601
701
  });
602
- }
603
-
604
702
  if (isOneKey) {
605
- Log?.debug('search device start ======================');
606
- const { name, localName, id, serviceUUIDs } = device ?? {};
607
- Log?.debug(
608
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
609
- id ?? ''
610
- }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
611
- );
612
703
  addDevice(device as unknown as Device);
613
- Log?.debug('search device end ======================\n');
614
704
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
615
705
  Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
616
706
  name: device?.name,
@@ -622,12 +712,32 @@ export default class ReactNativeBleTransport {
622
712
  }
623
713
  );
624
714
 
625
- getConnectedDeviceIds(getBluetoothServiceUuids()).then(devices => {
626
- for (const device of devices) {
627
- Log?.debug('search connected peripheral: ', device.id);
628
- addDevice(device as unknown as Device);
715
+ getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
716
+ devices => {
717
+ for (const device of devices) {
718
+ const localName =
719
+ 'localName' in device && typeof device.localName === 'string'
720
+ ? device.localName
721
+ : null;
722
+ const isFindMyPeripheral =
723
+ isPro2FindMyAdvertisementName(device.name) ||
724
+ isPro2FindMyAdvertisementName(localName);
725
+
726
+ if (
727
+ !isFindMyPeripheral &&
728
+ isOnekeyBluetoothDevice({
729
+ id: device.id,
730
+ name: device.name,
731
+ localName,
732
+ serviceUuids: device.serviceUUIDs,
733
+ })
734
+ ) {
735
+ Log?.debug('search connected peripheral: ', device.id);
736
+ addDevice(device as unknown as Device);
737
+ }
738
+ }
629
739
  }
630
- });
740
+ );
631
741
 
632
742
  const addDevice = (device: Device) => {
633
743
  if (deviceList.every(d => d.id !== device.id)) {
@@ -641,6 +751,12 @@ export default class ReactNativeBleTransport {
641
751
  name: displayName,
642
752
  commType: 'ble',
643
753
  } as IOneKeyDevice);
754
+ Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
755
+ deviceId: device.id,
756
+ name: displayName,
757
+ serviceUUIDs: device.serviceUUIDs,
758
+ protocolHint,
759
+ });
644
760
  }
645
761
  };
646
762
 
@@ -651,6 +767,46 @@ export default class ReactNativeBleTransport {
651
767
  });
652
768
  }
653
769
 
770
+ private async installTransportForAcquire(
771
+ uuid: string,
772
+ device: Device,
773
+ characteristics?: ResolvedBleCharacteristics
774
+ ) {
775
+ const { writeCharacteristic, notifyCharacteristic } =
776
+ characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
777
+ const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
778
+ if (Platform.OS === 'android') {
779
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
780
+ }
781
+ const monitorToken = this.nextMonitorToken;
782
+ this.nextMonitorToken += 1;
783
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
784
+ transport.monitorToken = monitorToken;
785
+ transport.notifyTransactionId = notifyTransactionId;
786
+ this.monitorTokens.set(uuid, monitorToken);
787
+ transport.notifySubscription = this._monitorCharacteristic(
788
+ transport.notifyCharacteristic,
789
+ uuid,
790
+ monitorToken,
791
+ notifyTransactionId
792
+ );
793
+ transportCache[uuid] = transport;
794
+ this.protocolV2Assemblers.set(
795
+ uuid,
796
+ new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
797
+ );
798
+
799
+ if (Platform.OS === 'ios') {
800
+ await new Promise<void>(resolve => {
801
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
802
+ });
803
+ } else if (Platform.OS === 'android') {
804
+ await delay(ANDROID_NOTIFY_READY_DELAY_MS);
805
+ }
806
+
807
+ return transport;
808
+ }
809
+
654
810
  async acquire(input: BleAcquireInput) {
655
811
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
656
812
 
@@ -684,9 +840,8 @@ export default class ReactNativeBleTransport {
684
840
  if (forceCleanRunPromise && this.runPromise) {
685
841
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
686
842
  this.runPromise.reject(error);
687
- this.rejectAllProtocolV2Frames(error);
688
843
  this.runPromise = null;
689
- this.activeProtocolV2Call = null;
844
+ this.runPromiseDeviceId = null;
690
845
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
691
846
  }
692
847
 
@@ -722,15 +877,22 @@ export default class ReactNativeBleTransport {
722
877
  if (!device) {
723
878
  Log?.debug('try to connect to device: ', uuid);
724
879
  try {
725
- device = await blePlxManager.connectToDevice(uuid, connectOptions);
880
+ device = await this.connectWithTimeout(uuid, () =>
881
+ blePlxManager.connectToDevice(uuid, connectOptions)
882
+ );
726
883
  } catch (e) {
727
884
  Log?.debug('try to connect to device has error: ', e);
885
+ if (isConnectTimeoutError(e)) {
886
+ throw e;
887
+ }
728
888
  if (
729
889
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
730
890
  e.errorCode === BleErrorCode.OperationCancelled
731
891
  ) {
732
892
  Log?.debug('first try to reconnect without params');
733
- device = await blePlxManager.connectToDevice(uuid);
893
+ device = await this.connectWithTimeout(uuid, () =>
894
+ blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
895
+ );
734
896
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
735
897
  Log?.debug('device already connected');
736
898
  throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
@@ -746,26 +908,36 @@ export default class ReactNativeBleTransport {
746
908
 
747
909
  if (!(await device.isConnected())) {
748
910
  Log?.debug('not connected, try to connect to device: ', uuid);
911
+ const disconnectedDevice = device;
749
912
 
750
913
  try {
751
- device = await device.connect(connectOptions);
914
+ device = await this.connectWithTimeout(uuid, () =>
915
+ disconnectedDevice.connect(connectOptions)
916
+ );
752
917
  } catch (e) {
753
918
  Log?.debug('not connected, try to connect to device has error: ', e);
919
+ if (isConnectTimeoutError(e)) {
920
+ throw e;
921
+ }
754
922
  if (
755
923
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
756
924
  e.errorCode === BleErrorCode.OperationCancelled
757
925
  ) {
758
926
  Log?.debug('second try to reconnect without params');
759
927
  try {
760
- device = await device.connect();
928
+ device = await this.connectWithTimeout(uuid, () =>
929
+ disconnectedDevice.connect(fallbackConnectOptions)
930
+ );
761
931
  } catch (e) {
762
932
  Log?.debug('last try to reconnect error: ', e);
763
933
  // last try to reconnect device if this issue exists
764
934
  // https://github.com/dotintent/react-native-ble-plx/issues/426
765
935
  if (e.errorCode === BleErrorCode.OperationCancelled) {
766
936
  Log?.debug('last try to reconnect');
767
- await device.cancelConnection();
768
- device = await device.connect();
937
+ await disconnectedDevice.cancelConnection();
938
+ device = await this.connectWithTimeout(uuid, () =>
939
+ disconnectedDevice.connect(fallbackConnectOptions)
940
+ );
769
941
  }
770
942
  }
771
943
  } else {
@@ -775,12 +947,15 @@ export default class ReactNativeBleTransport {
775
947
  }
776
948
 
777
949
  device = await requestAndroidMtu(device);
778
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
950
+ const acquiredDevice = device;
951
+ const { writeCharacteristic, notifyCharacteristic } =
952
+ await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
779
953
 
780
954
  const protocolHint = expectedProtocol
781
955
  ? undefined
782
- : this.deviceProtocolHints.get(uuid) ??
783
- inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
956
+ : input.protocolHint ??
957
+ this.deviceProtocolHints.get(uuid) ??
958
+ inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
784
959
 
785
960
  // release transport before new transport instance
786
961
  await this.release(uuid, true);
@@ -788,45 +963,30 @@ export default class ReactNativeBleTransport {
788
963
  this.deviceProtocolHints.set(uuid, protocolHint);
789
964
  }
790
965
 
791
- const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
792
- if (Platform.OS === 'android') {
793
- transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
794
- }
795
- const monitorToken = this.nextMonitorToken;
796
- this.nextMonitorToken += 1;
797
- const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
798
- transport.monitorToken = monitorToken;
799
- transport.notifyTransactionId = notifyTransactionId;
800
- this.monitorTokens.set(uuid, monitorToken);
801
- transport.notifySubscription = this._monitorCharacteristic(
802
- transport.notifyCharacteristic,
803
- uuid,
804
- monitorToken,
805
- notifyTransactionId
806
- );
807
- transportCache[uuid] = transport;
808
-
809
- this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
810
-
811
- if (Platform.OS === 'ios') {
812
- await new Promise<void>(resolve => {
813
- setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
814
- });
815
- } else if (Platform.OS === 'android') {
816
- await delay(ANDROID_NOTIFY_READY_DELAY_MS);
817
- }
818
-
819
- const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
820
-
821
- this.emitter?.emit('device-connect', {
822
- name: device.name,
823
- id: device.id,
824
- connectId: device.id,
966
+ await this.installTransportForAcquire(uuid, acquiredDevice, {
967
+ writeCharacteristic,
968
+ notifyCharacteristic,
825
969
  });
826
970
 
827
- this.attachDisconnectSubscription(transport, device, uuid);
828
-
829
- return { uuid, protocolType };
971
+ try {
972
+ const protocolType = await this.detectProtocol(
973
+ uuid,
974
+ expectedProtocol,
975
+ protocolHint,
976
+ async () => {
977
+ await this.installTransportForAcquire(uuid, acquiredDevice);
978
+ }
979
+ );
980
+ const currentTransport = transportCache[uuid];
981
+ if (!currentTransport) {
982
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
983
+ }
984
+ this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
985
+ return { uuid, protocolType };
986
+ } catch (error) {
987
+ await this.release(uuid, true);
988
+ throw error;
989
+ }
830
990
  }
831
991
 
832
992
  _monitorCharacteristic(
@@ -853,7 +1013,30 @@ export default class ReactNativeBleTransport {
853
1013
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
854
1014
  return;
855
1015
  }
856
- if (this.runPromise) {
1016
+ if (this.getActiveProtocol(uuid) === 'V2') {
1017
+ let errorCode:
1018
+ | typeof HardwareErrorCode.BleDeviceBondError
1019
+ | typeof HardwareErrorCode.BleCharacteristicNotifyError
1020
+ | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
1021
+ | typeof HardwareErrorCode.BleTimeoutError =
1022
+ HardwareErrorCode.BleCharacteristicNotifyError;
1023
+ if (error.reason?.includes('The connection has timed out unexpectedly')) {
1024
+ errorCode = HardwareErrorCode.BleTimeoutError;
1025
+ } else if (error.reason?.includes('Encryption is insufficient')) {
1026
+ errorCode = HardwareErrorCode.BleDeviceBondError;
1027
+ } else if (
1028
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
1029
+ error.reason?.includes('Cannot find client characteristic config descriptor') ||
1030
+ error.reason?.includes('The handle is invalid') ||
1031
+ error.reason?.includes('Writing is not permitted') ||
1032
+ error.reason?.includes('notify change failed for device')
1033
+ ) {
1034
+ errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
1035
+ }
1036
+ this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
1037
+ return;
1038
+ }
1039
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
857
1040
  let ERROR:
858
1041
  | typeof HardwareErrorCode.BleDeviceBondError
859
1042
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -876,7 +1059,6 @@ export default class ReactNativeBleTransport {
876
1059
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure
877
1060
  );
878
1061
  this.runPromise.reject(notifyError);
879
- this.rejectAllProtocolV2Frames(notifyError);
880
1062
  Log?.debug(
881
1063
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
882
1064
  );
@@ -884,7 +1066,6 @@ export default class ReactNativeBleTransport {
884
1066
  }
885
1067
  const notifyError = ERRORS.TypedError(ERROR);
886
1068
  this.runPromise.reject(notifyError);
887
- this.rejectAllProtocolV2Frames(notifyError);
888
1069
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
889
1070
  }
890
1071
 
@@ -902,13 +1083,13 @@ export default class ReactNativeBleTransport {
902
1083
 
903
1084
  try {
904
1085
  const data = Buffer.from(c.value as string, 'base64');
905
- const protocol = this.deviceProtocol.get(uuid);
1086
+ const protocol = this.getActiveProtocol(uuid);
906
1087
  if (!protocol) {
907
1088
  Log?.debug('monitor data ignored before protocol detection: ', uuid);
908
1089
  return;
909
1090
  }
910
1091
  if (protocol === 'V2') {
911
- this.handleProtocolV2Notification(uuid, new Uint8Array(data));
1092
+ this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
912
1093
  return;
913
1094
  }
914
1095
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
@@ -929,13 +1110,18 @@ export default class ReactNativeBleTransport {
929
1110
  // );
930
1111
  bufferLength = 0;
931
1112
  buffer = [];
932
- this.runPromise?.resolve(value.toString('hex'));
1113
+ if (this.runPromiseDeviceId === uuid) {
1114
+ this.runPromise?.resolve(value.toString('hex'));
1115
+ }
933
1116
  }
934
1117
  } catch (error) {
935
1118
  Log?.debug('monitor data error: ', error);
936
1119
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
937
- this.runPromise?.reject(notifyError);
938
- this.rejectAllProtocolV2Frames(notifyError);
1120
+ if (this.getActiveProtocol(uuid) === 'V2') {
1121
+ this.rejectProtocolV2Frames(uuid, notifyError);
1122
+ } else if (this.runPromiseDeviceId === uuid) {
1123
+ this.runPromise?.reject(notifyError);
1124
+ }
939
1125
  }
940
1126
  }, notifyTransactionId);
941
1127
 
@@ -943,13 +1129,18 @@ export default class ReactNativeBleTransport {
943
1129
  }
944
1130
 
945
1131
  async release(uuid: string, onclose = false) {
1132
+ await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
1133
+ return this.releaseNative(uuid, onclose);
1134
+ }
1135
+
1136
+ private async releaseNative(uuid: string, onclose = false) {
946
1137
  const transport = transportCache[uuid];
947
- if (this.runPromise) {
1138
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
948
1139
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
949
1140
  this.runPromise.reject(error);
950
1141
  this.runPromise = null;
951
- this.rejectAllProtocolV2Frames(error);
952
- this.activeProtocolV2Call = null;
1142
+ this.runPromiseDeviceId = null;
1143
+ this.rejectProtocolV2Frames(uuid, error);
953
1144
  } else {
954
1145
  this.resetProtocolV2Frames(uuid);
955
1146
  }
@@ -957,9 +1148,6 @@ export default class ReactNativeBleTransport {
957
1148
  if (Platform.OS === 'android' && !onclose && transport) {
958
1149
  this.protocolV2Assemblers.get(uuid)?.reset();
959
1150
  this.resetProtocolV2Frames(uuid);
960
- if (this.activeProtocolV2Call?.uuid === uuid) {
961
- this.activeProtocolV2Call = null;
962
- }
963
1151
  return Promise.resolve(true);
964
1152
  }
965
1153
 
@@ -993,7 +1181,8 @@ export default class ReactNativeBleTransport {
993
1181
  }
994
1182
 
995
1183
  this.deviceProtocol.delete(uuid);
996
- this.deviceProtocolHints.delete(uuid);
1184
+ this.probingProtocols.delete(uuid);
1185
+ // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
997
1186
  this.protocolV2Assemblers.get(uuid)?.reset();
998
1187
  this.protocolV2Assemblers.delete(uuid);
999
1188
  this.resetProtocolV2Frames(uuid);
@@ -1025,13 +1214,6 @@ export default class ReactNativeBleTransport {
1025
1214
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1026
1215
  }
1027
1216
 
1028
- const forceRun = name === 'Initialize' || name === 'Cancel';
1029
-
1030
- Log?.debug('transport-react-native call this.runPromise', this.runPromise);
1031
- if (this.runPromise && !forceRun) {
1032
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1033
- }
1034
-
1035
1217
  const protocol = this.getProtocolType(uuid);
1036
1218
  if (!protocol) {
1037
1219
  throw ERRORS.TypedError(
@@ -1039,31 +1221,17 @@ export default class ReactNativeBleTransport {
1039
1221
  `Device protocol has not been detected for ${uuid}`
1040
1222
  );
1041
1223
  }
1042
- // Upload resources on low-end phones may OOM
1043
- if (name === 'ResourceUpdate' || name === 'ResourceAck') {
1044
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
1045
- file_name: data?.file_name,
1046
- hash: data?.hash,
1047
- });
1048
- } else if (LogBlockCommand.has(name)) {
1049
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
1050
- } else {
1051
- Log?.debug(
1052
- 'transport-react-native',
1053
- 'call-',
1054
- ' name: ',
1055
- name,
1056
- ' data: ',
1057
- data,
1058
- ' protocol: ',
1059
- protocol
1060
- );
1061
- }
1224
+ Log?.debug('transport call', createTransportCallLog(name, protocol, data));
1062
1225
 
1063
1226
  if (protocol === 'V2') {
1064
1227
  return this.callProtocolV2(uuid, name, data, options);
1065
1228
  }
1066
1229
 
1230
+ const forceRun = name === 'Initialize' || name === 'Cancel';
1231
+ if (this.runPromise && !forceRun) {
1232
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1233
+ }
1234
+
1067
1235
  return this.callProtocolV1(uuid, name, data, options);
1068
1236
  }
1069
1237
 
@@ -1080,7 +1248,25 @@ export default class ReactNativeBleTransport {
1080
1248
  const transport = this.getCachedTransport(uuid);
1081
1249
  const runPromise = createDeferred<string>();
1082
1250
  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
+ }
1083
1259
  this.runPromise = runPromise;
1260
+ 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;
1084
1270
  const messages = this._messages;
1085
1271
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1086
1272
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1106,6 +1292,9 @@ export default class ReactNativeBleTransport {
1106
1292
  chunk = ByteBuffer.allocate(packetCapacity);
1107
1293
  } catch (e) {
1108
1294
  onError(e);
1295
+ if (isWedgedWriteError(e)) {
1296
+ throw e;
1297
+ }
1109
1298
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1110
1299
  }
1111
1300
  }
@@ -1137,6 +1326,9 @@ export default class ReactNativeBleTransport {
1137
1326
  }
1138
1327
  } catch (e) {
1139
1328
  onError(e);
1329
+ if (isWedgedWriteError(e)) {
1330
+ throw e;
1331
+ }
1140
1332
  throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1141
1333
  }
1142
1334
  }
@@ -1150,21 +1342,26 @@ export default class ReactNativeBleTransport {
1150
1342
  if (name === 'EmmcFileWrite') {
1151
1343
  await writeChunkedData(
1152
1344
  buffers,
1153
- data => transport.writeWithRetry(data),
1345
+ data =>
1346
+ this.writeBlePacket(
1347
+ uuid,
1348
+ data,
1349
+ payload => transport.writeWithRetry(payload),
1350
+ isCurrentOwner
1351
+ ),
1154
1352
  e => {
1155
- this.runPromise = null;
1353
+ releaseOwnershipIfCurrent();
1156
1354
  Log?.error('writeCharacteristic write error: ', e);
1157
1355
  }
1158
1356
  );
1159
1357
  } else if (name === 'FirmwareUpload') {
1160
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
1358
+ Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
1161
1359
  packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
1162
1360
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1163
1361
  pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1164
1362
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1165
1363
  maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
1166
1364
  });
1167
-
1168
1365
  await writeFirmwareUploadChunkedData(
1169
1366
  buffers,
1170
1367
  async data => {
@@ -1174,43 +1371,31 @@ export default class ReactNativeBleTransport {
1174
1371
  // eslint-disable-next-line no-constant-condition
1175
1372
  while (true) {
1176
1373
  try {
1177
- await transport.writeCharacteristic.writeWithoutResponse(data);
1374
+ await this.writeBlePacket(
1375
+ uuid,
1376
+ data,
1377
+ payload => transport.writeWithRetry(payload),
1378
+ isCurrentOwner
1379
+ );
1178
1380
  return;
1179
1381
  } catch (error) {
1180
1382
  const retryType = getFirmwareUploadWriteRetryType(error);
1181
1383
  if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1182
1384
  throw error;
1183
1385
  }
1184
- const shouldReconnect = retryType === 'reconnectable';
1185
- const delayMs = shouldReconnect
1186
- ? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
1187
- : resolveFirmwareUploadRetryDelay(attempt);
1386
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
1188
1387
  Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
1189
1388
  attempt: attempt + 1,
1190
1389
  delayMs,
1191
- reconnect: shouldReconnect,
1192
1390
  error,
1193
1391
  });
1194
- if (shouldReconnect) {
1195
- this.firmwareUploadWriteRecoveryIds.add(uuid);
1196
- }
1197
1392
  await delay(delayMs);
1198
1393
  attempt += 1;
1199
- if (shouldReconnect) {
1200
- try {
1201
- await this.reconnectFirmwareUploadTransport(uuid, transport);
1202
- } catch (e) {
1203
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
1204
- if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1205
- throw e;
1206
- }
1207
- }
1208
- }
1209
1394
  }
1210
1395
  }
1211
1396
  },
1212
1397
  e => {
1213
- this.runPromise = null;
1398
+ releaseOwnershipIfCurrent();
1214
1399
  Log?.error('writeCharacteristic write error: ', e);
1215
1400
  }
1216
1401
  );
@@ -1218,12 +1403,24 @@ export default class ReactNativeBleTransport {
1218
1403
  for (const o of buffers) {
1219
1404
  const outData = o.toString('base64');
1220
1405
  // Upload resources on low-end phones may OOM
1221
- // this.Log.debug('send hex strting: ', o.toString('hex'));
1222
1406
  try {
1223
- await transport.writeCharacteristic.writeWithoutResponse(outData);
1407
+ const shouldUseWriteWithResponse =
1408
+ 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
+ );
1224
1418
  } catch (e) {
1225
1419
  Log?.debug('writeCharacteristic write error: ', e);
1226
- this.runPromise = null;
1420
+ releaseOwnershipIfCurrent();
1421
+ if (isWedgedWriteError(e)) {
1422
+ throw e;
1423
+ }
1227
1424
  if (e.errorCode === BleErrorCode.DeviceDisconnected) {
1228
1425
  throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
1229
1426
  } else if (e.errorCode === BleErrorCode.OperationStartFailed) {
@@ -1256,20 +1453,33 @@ export default class ReactNativeBleTransport {
1256
1453
  throw new Error('Returning data is not string.');
1257
1454
  }
1258
1455
 
1259
- Log?.debug('receive data: ', response);
1260
1456
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1261
1457
  return check.call(jsonData);
1262
1458
  } catch (e) {
1263
- if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1264
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1459
+ if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1460
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
1265
1461
  } else {
1266
1462
  Log?.error('call error: ', e);
1267
1463
  }
1464
+ const isProbeTimeout =
1465
+ 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
+ if (
1471
+ !isProbeTimeout &&
1472
+ !isStaleCall &&
1473
+ (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1474
+ ) {
1475
+ await this.disconnect(uuid);
1476
+ }
1268
1477
  throw e;
1269
1478
  } finally {
1270
1479
  if (timeout) clearTimeout(timeout);
1271
1480
  if (this.runPromise === runPromise) {
1272
1481
  this.runPromise = null;
1482
+ this.runPromiseDeviceId = null;
1273
1483
  }
1274
1484
  }
1275
1485
  }
@@ -1279,8 +1489,9 @@ export default class ReactNativeBleTransport {
1279
1489
  }
1280
1490
 
1281
1491
  async disconnect(session: string) {
1282
- Log?.debug('transport-react-native transport resetSession: ', session);
1492
+ await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
1283
1493
  const transport = transportCache[session];
1494
+ const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
1284
1495
 
1285
1496
  // Clean up disconnect subscription first to prevent onDisconnected callback
1286
1497
  // from being triggered when we cancel the connection below
@@ -1338,23 +1549,22 @@ export default class ReactNativeBleTransport {
1338
1549
  delete transportCache[session];
1339
1550
  }
1340
1551
  this.deviceProtocol.delete(session);
1552
+ this.probingProtocols.delete(session);
1341
1553
  this.deviceProtocolHints.delete(session);
1554
+ this.sessionProtocols.delete(session);
1555
+ this.protocolReprobeFailures.delete(session);
1342
1556
  this.protocolV2Assemblers.delete(session);
1343
1557
  this.resetProtocolV2Frames(session);
1344
- if (this.activeProtocolV2Call?.uuid === session) {
1345
- this.activeProtocolV2Call = null;
1346
- }
1347
1558
 
1348
1559
  // emit the disconnect event
1349
1560
  try {
1350
- this.emitter?.emit('device-disconnect', {
1351
- name: transport?.device?.name,
1352
- id: session,
1353
- connectId: session,
1354
- });
1561
+ this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
1355
1562
  } catch (e) {
1356
1563
  Log?.error('resetSession: emit disconnect event error: ', e);
1357
1564
  }
1565
+ if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
1566
+ this.monitorTokens.delete(session);
1567
+ }
1358
1568
  // eslint-disable-next-line no-promise-executor-return
1359
1569
  await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
1360
1570
  }
@@ -1365,6 +1575,114 @@ export default class ReactNativeBleTransport {
1365
1575
  // this.runPromise.reject(new Error('Transport_CallCanceled'));
1366
1576
  }
1367
1577
  this.runPromise = null;
1578
+ this.runPromiseDeviceId = null;
1579
+ }
1580
+
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
+ }
1368
1686
  }
1369
1687
 
1370
1688
  private getCachedTransport(uuid: string) {
@@ -1375,6 +1693,107 @@ export default class ReactNativeBleTransport {
1375
1693
  return transport;
1376
1694
  }
1377
1695
 
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
+
1378
1797
  private createProtocolMismatchError(expected: ProtocolType) {
1379
1798
  return ERRORS.TypedError(
1380
1799
  HardwareErrorCode.RuntimeError,
@@ -1385,70 +1804,131 @@ export default class ReactNativeBleTransport {
1385
1804
  private createProtocolDetectionError() {
1386
1805
  return ERRORS.TypedError(
1387
1806
  HardwareErrorCode.BleTimeoutError,
1388
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1807
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
1389
1808
  );
1390
1809
  }
1391
1810
 
1392
1811
  private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1812
+ if (this.probingProtocols.get(uuid) === protocol) {
1813
+ this.probingProtocols.delete(uuid);
1814
+ }
1393
1815
  if (this.deviceProtocol.get(uuid) === protocol) {
1394
1816
  this.deviceProtocol.delete(uuid);
1395
1817
  }
1396
1818
  }
1397
1819
 
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
+
1398
1825
  private async detectProtocol(
1399
1826
  uuid: string,
1400
1827
  expectedProtocol?: ProtocolType,
1401
- protocolHint?: ProtocolType
1828
+ protocolHint?: ProtocolType,
1829
+ rebuildTransport?: () => Promise<void>
1402
1830
  ): Promise<ProtocolType> {
1831
+ if (Platform.OS === 'ios' && expectedProtocol) {
1832
+ this.deviceProtocol.set(uuid, expectedProtocol);
1833
+ Log?.debug('[ReactNativeBleTransport] protocol selected', {
1834
+ deviceId: uuid,
1835
+ protocol: expectedProtocol,
1836
+ source: 'expected',
1837
+ });
1838
+ return expectedProtocol;
1839
+ }
1840
+
1403
1841
  if (expectedProtocol === 'V1') {
1404
1842
  if (await this.probeProtocolV1(uuid)) {
1405
1843
  this.deviceProtocol.set(uuid, 'V1');
1406
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1844
+ this.sessionProtocols.set(uuid, 'V1');
1845
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1846
+ deviceId: uuid,
1847
+ protocol: 'V1',
1848
+ source: 'expected',
1849
+ });
1407
1850
  return 'V1';
1408
1851
  }
1409
1852
  throw this.createProtocolMismatchError(expectedProtocol);
1410
1853
  }
1411
1854
 
1412
1855
  if (expectedProtocol === 'V2') {
1413
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
1414
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
1415
- this.deviceProtocol.set(uuid, 'V2');
1416
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1417
- return 'V2';
1856
+ if (await this.probeProtocolV2(uuid)) {
1857
+ this.deviceProtocol.set(uuid, 'V2');
1858
+ this.sessionProtocols.set(uuid, 'V2');
1859
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1860
+ deviceId: uuid,
1861
+ protocol: 'V2',
1862
+ source: 'expected',
1863
+ });
1864
+ return 'V2';
1865
+ }
1866
+ throw this.createProtocolMismatchError(expectedProtocol);
1418
1867
  }
1419
1868
 
1420
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
1421
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1
1422
- // 不能作为最终结论。
1423
- const probeOrder: ProtocolType[] =
1869
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
1870
+ // 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[] =
1424
1874
  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;
1425
1883
 
1426
1884
  for (let i = 0; i < probeOrder.length; i += 1) {
1427
1885
  const protocol = probeOrder[i];
1428
1886
  if (i > 0) {
1429
- // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
1887
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
1430
1888
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1889
+ if (!transportCache[uuid]) {
1890
+ if (!rebuildTransport) {
1891
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1892
+ }
1893
+ await rebuildTransport();
1894
+ }
1431
1895
  }
1432
1896
  const detected =
1433
1897
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1434
1898
  if (detected) {
1435
1899
  this.deviceProtocol.set(uuid, protocol);
1436
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1900
+ this.sessionProtocols.set(uuid, protocol);
1901
+ this.protocolReprobeFailures.delete(uuid);
1902
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1903
+ deviceId: uuid,
1904
+ protocol,
1905
+ source: 'probe',
1906
+ });
1437
1907
  return protocol;
1438
1908
  }
1439
1909
  }
1440
1910
 
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
+
1441
1919
  this.deviceProtocol.delete(uuid);
1920
+ this.probingProtocols.delete(uuid);
1442
1921
  throw this.createProtocolDetectionError();
1443
1922
  }
1444
1923
 
1445
1924
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1446
1925
  const transport = transportCache[uuid];
1926
+ await this.protocolV2Links.invalidateLink(
1927
+ uuid,
1928
+ `Reset notify state after Protocol ${protocol} probe`
1929
+ );
1447
1930
  this.protocolV2Assemblers.get(uuid)?.reset();
1448
1931
  this.resetProtocolV2Frames(uuid);
1449
- if (this.activeProtocolV2Call?.uuid === uuid) {
1450
- this.activeProtocolV2Call = null;
1451
- }
1452
1932
  if (this.runPromise) {
1453
1933
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1454
1934
  this.runPromise.reject(error);
@@ -1499,12 +1979,20 @@ export default class ReactNativeBleTransport {
1499
1979
  }
1500
1980
 
1501
1981
  try {
1502
- this.deviceProtocol.set(uuid, 'V1');
1503
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1982
+ this.probingProtocols.set(uuid, 'V1');
1983
+ // GetFeatures identifies Protocol V1 without resetting an existing wallet
1984
+ // session before Core has a chance to restore a hidden wallet.
1985
+ await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1986
+ this.probingProtocols.delete(uuid);
1504
1987
  return true;
1505
1988
  } catch (error) {
1506
1989
  this.clearProbeProtocol(uuid, 'V1');
1507
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1990
+ 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
+ }
1508
1996
  return false;
1509
1997
  }
1510
1998
  }
@@ -1514,7 +2002,7 @@ export default class ReactNativeBleTransport {
1514
2002
  return false;
1515
2003
  }
1516
2004
 
1517
- this.deviceProtocol.set(uuid, 'V2');
2005
+ this.probingProtocols.set(uuid, 'V2');
1518
2006
  this.protocolV2Assemblers.get(uuid)?.reset();
1519
2007
  const detected = await probeProtocolV2Helper({
1520
2008
  call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
@@ -1529,17 +2017,15 @@ export default class ReactNativeBleTransport {
1529
2017
  });
1530
2018
  if (!detected) {
1531
2019
  this.clearProbeProtocol(uuid, 'V2');
2020
+ } else {
2021
+ this.probingProtocols.delete(uuid);
1532
2022
  }
1533
2023
  return detected;
1534
2024
  }
1535
2025
 
1536
- private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
2026
+ private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
1537
2027
  try {
1538
- if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1539
- this.protocolV2Assemblers.get(uuid)?.reset();
1540
- this.resetProtocolV2Frames(uuid);
1541
- return;
1542
- }
2028
+ if (this.monitorTokens.get(uuid) !== monitorToken) return;
1543
2029
 
1544
2030
  if (data.length === 0) return;
1545
2031
 
@@ -1552,8 +2038,15 @@ export default class ReactNativeBleTransport {
1552
2038
  } catch (error) {
1553
2039
  Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1554
2040
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1555
- this.runPromise?.reject(notifyError);
1556
- this.rejectAllProtocolV2Frames(notifyError);
2041
+ this.rejectProtocolV2Frames(uuid, notifyError);
2042
+ this.protocolV2Links
2043
+ .invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
2044
+ .catch(invalidateError =>
2045
+ Log?.debug(
2046
+ '[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
2047
+ invalidateError
2048
+ )
2049
+ );
1557
2050
  }
1558
2051
  }
1559
2052
 
@@ -1576,21 +2069,17 @@ export default class ReactNativeBleTransport {
1576
2069
  this.getProtocolV2FrameQueue(uuid).push(frame);
1577
2070
  }
1578
2071
 
1579
- private rejectAllProtocolV2Frames(error: Error) {
1580
- this.protocolV2FrameQueues.clear();
1581
- for (const framePromise of this.protocolV2FramePromises.values()) {
1582
- framePromise.reject(error);
1583
- }
1584
- this.protocolV2FramePromises.clear();
1585
- }
1586
-
1587
2072
  private resetProtocolV2Frames(uuid: string) {
1588
- this.protocolV2FrameQueues.delete(uuid);
1589
- this.protocolV2FramePromises.delete(uuid);
2073
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1590
2074
  }
1591
2075
 
1592
- private isActiveProtocolV2Call(uuid: string, token: number) {
1593
- return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
2076
+ private rejectProtocolV2Frames(uuid: string, error: Error) {
2077
+ this.protocolV2FrameQueues.delete(uuid);
2078
+ const framePromise = this.protocolV2FramePromises.get(uuid);
2079
+ if (framePromise) {
2080
+ this.protocolV2FramePromises.delete(uuid);
2081
+ framePromise.reject(error);
2082
+ }
1594
2083
  }
1595
2084
 
1596
2085
  private async readProtocolV2Frame(uuid: string) {
@@ -1610,10 +2099,68 @@ export default class ReactNativeBleTransport {
1610
2099
  }
1611
2100
  }
1612
2101
 
2102
+ private async writeProtocolV2Packet(
2103
+ uuid: string,
2104
+ transport: BleTransport,
2105
+ base64: string,
2106
+ context: ProtocolV2CallContext,
2107
+ assertCurrentGeneration: () => void
2108
+ ) {
2109
+ const shouldUseWriteWithResponse =
2110
+ transport.writeCharacteristic.isWritableWithResponse &&
2111
+ (context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
2112
+ let attempt = 0;
2113
+ for (;;) {
2114
+ assertCurrentGeneration();
2115
+ if (context.signal.aborted) {
2116
+ throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
2117
+ }
2118
+ 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
+ );
2137
+ assertCurrentGeneration();
2138
+ return;
2139
+ } catch (error) {
2140
+ if (
2141
+ getFirmwareUploadWriteRetryType(error) !== 'congested' ||
2142
+ attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
2143
+ ) {
2144
+ throw error;
2145
+ }
2146
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
2147
+ attempt += 1;
2148
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
2149
+ name: context.messageName,
2150
+ attempt,
2151
+ delayMs,
2152
+ });
2153
+ await delay(delayMs);
2154
+ }
2155
+ }
2156
+ }
2157
+
1613
2158
  private async writeProtocolV2Frame(
2159
+ uuid: string,
1614
2160
  transport: BleTransport,
1615
2161
  frame: Uint8Array,
1616
- options?: { highVolume?: boolean; writeWithResponse?: boolean }
2162
+ context: ProtocolV2CallContext,
2163
+ assertCurrentGeneration: () => void
1617
2164
  ) {
1618
2165
  const tuning = getProtocolV2BleTuning();
1619
2166
  const packetCapacity = resolveProtocolV2PacketCapacity({
@@ -1622,37 +2169,32 @@ export default class ReactNativeBleTransport {
1622
2169
  androidPacketLength: tuning.androidPacketLength,
1623
2170
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1624
2171
  });
1625
- const writeWithResponse =
1626
- !!options?.writeWithResponse || (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
1627
- const writeMode = resolveBleWriteMode(
1628
- transport.writeCharacteristic,
1629
- writeWithResponse ? 'withResponse' : 'withoutResponse'
1630
- );
1631
- const shouldThrottle = !!options?.highVolume && writeMode === 'withoutResponse';
1632
- let packetsWritten = 0;
1633
-
1634
- for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1635
- const chunk = frame.slice(offset, offset + packetCapacity);
1636
- const base64 = Buffer.from(chunk).toString('base64');
1637
- if (writeMode === 'withResponse') {
1638
- await transport.writeCharacteristic.writeWithResponse(base64);
1639
- } else {
1640
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1641
- }
1642
- packetsWritten += 1;
1643
-
1644
- if (
1645
- shouldThrottle &&
1646
- packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1647
- offset + packetCapacity < frame.length
1648
- ) {
1649
- await delay(tuning.highVolumeWritePauseMs);
1650
- }
1651
- }
1652
-
1653
- if (shouldThrottle) {
1654
- await delay(tuning.highVolumeWriteFlushDelayMs);
1655
- }
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
+ await writeProtocolV2BleFrame({
2179
+ frame,
2180
+ packetCapacity,
2181
+ assertActive: assertCurrentGeneration,
2182
+ signal: context.signal,
2183
+ 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
+ wait: delay,
2189
+ writePacket: packet =>
2190
+ this.writeProtocolV2Packet(
2191
+ uuid,
2192
+ transport,
2193
+ Buffer.from(packet).toString('base64'),
2194
+ context,
2195
+ assertCurrentGeneration
2196
+ ),
2197
+ });
1656
2198
  }
1657
2199
 
1658
2200
  private async callProtocolV2(
@@ -1665,102 +2207,83 @@ export default class ReactNativeBleTransport {
1665
2207
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1666
2208
  }
1667
2209
 
1668
- const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'Ping';
1669
- if (this.runPromise) {
1670
- if (!forceRun) {
1671
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1672
- }
1673
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1674
- this.runPromise.reject(error);
1675
- this.rejectAllProtocolV2Frames(error);
1676
- this.runPromise = null;
1677
- this.activeProtocolV2Call = null;
1678
- }
1679
-
1680
- const transport = this.getCachedTransport(uuid);
1681
- const runPromise = createDeferred<Uint8Array>();
1682
- runPromise.promise.catch(() => undefined);
1683
- this.runPromise = runPromise;
1684
- const callToken = this.nextProtocolV2CallToken++;
1685
- this.activeProtocolV2Call = { uuid, token: callToken };
1686
- this.protocolV2Assemblers.get(uuid)?.reset();
1687
- this.resetProtocolV2Frames(uuid);
1688
- let completed = false;
1689
- const callOptions = {
1690
- ...options,
1691
- timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1692
- };
2210
+ const callOptions = options;
1693
2211
  const highVolumeWrite = LogBlockCommand.has(name);
1694
2212
 
1695
2213
  if (highVolumeWrite) {
1696
2214
  const tuning = getProtocolV2BleTuning();
1697
- Log?.debug(
1698
- '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
2215
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1699
2216
  name,
1700
- {
1701
- packetCapacity:
1702
- Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1703
- burstSize: tuning.highVolumeWriteBurstSize,
1704
- pauseMs: tuning.highVolumeWritePauseMs,
1705
- flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1706
- writeWithResponse: tuning.highVolumeWriteWithResponse,
1707
- }
1708
- );
2217
+ writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
2218
+ packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
2219
+ });
1709
2220
  }
1710
2221
 
1711
2222
  try {
1712
- const session = new ProtocolV2Session({
1713
- schemas: {
1714
- protocolV1: this._messages,
1715
- protocolV2: this._messagesV2,
1716
- },
1717
- router: PROTOCOL_V2_CHANNEL_BLE_UART,
1718
- writeFrame: async (frame: Uint8Array) => {
1719
- await this.writeProtocolV2Frame(transport, frame, {
1720
- highVolume: highVolumeWrite,
1721
- });
1722
- },
1723
- readFrame: async () => {
1724
- const rxFrame = await this.readProtocolV2Frame(uuid);
1725
- if (!(rxFrame instanceof Uint8Array)) {
1726
- throw new Error('Protocol V2 response is not Uint8Array');
1727
- }
1728
- return rxFrame;
1729
- },
1730
- logger: Log,
1731
- logPrefix: 'ProtocolV2 RN-BLE',
1732
- createTimeoutError: (_messageName: string, timeout: number) =>
1733
- ERRORS.TypedError(
1734
- HardwareErrorCode.BleTimeoutError,
1735
- `BLE response timeout after ${timeout}ms for ${name}`
1736
- ),
1737
- });
1738
-
1739
- const result = await session.call(name, data, callOptions);
1740
- completed = true;
1741
- return result;
2223
+ return await this.protocolV2Links.call(
2224
+ uuid,
2225
+ () => this.createProtocolV2Adapter(uuid),
2226
+ name,
2227
+ data,
2228
+ callOptions
2229
+ );
1742
2230
  } catch (e) {
1743
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1744
- this.protocolV2Assemblers.get(uuid)?.reset();
1745
- this.resetProtocolV2Frames(uuid);
1746
- }
1747
2231
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1748
2232
  throw e;
1749
- } finally {
1750
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1751
- if (!completed) {
1752
- this.protocolV2Assemblers.get(uuid)?.reset();
1753
- }
1754
- this.resetProtocolV2Frames(uuid);
1755
- this.activeProtocolV2Call = null;
1756
- }
1757
- if (this.runPromise === runPromise) {
1758
- this.runPromise = null;
1759
- }
1760
2233
  }
1761
2234
  }
1762
2235
 
2236
+ private createProtocolV2Adapter(uuid: string) {
2237
+ const generation = this.monitorTokens.get(uuid) ?? 0;
2238
+ const assertCurrentGeneration = () => {
2239
+ if (this.monitorTokens.get(uuid) !== generation) {
2240
+ throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
2241
+ }
2242
+ };
2243
+
2244
+ return {
2245
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
2246
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
2247
+ generation,
2248
+ prepareCall: () => {
2249
+ assertCurrentGeneration();
2250
+ this.protocolV2Assemblers.get(uuid)?.reset();
2251
+ this.resetProtocolV2Frames(uuid);
2252
+ },
2253
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
2254
+ assertCurrentGeneration();
2255
+ const currentTransport = this.getCachedTransport(uuid);
2256
+ await this.writeProtocolV2Frame(
2257
+ uuid,
2258
+ currentTransport,
2259
+ frame,
2260
+ context,
2261
+ assertCurrentGeneration
2262
+ );
2263
+ },
2264
+ readFrame: async () => {
2265
+ assertCurrentGeneration();
2266
+ const rxFrame = await this.readProtocolV2Frame(uuid);
2267
+ if (!(rxFrame instanceof Uint8Array)) {
2268
+ throw new Error('Protocol V2 response is not Uint8Array');
2269
+ }
2270
+ return rxFrame;
2271
+ },
2272
+ reset: (reason: string) => {
2273
+ this.protocolV2Assemblers.get(uuid)?.reset();
2274
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
2275
+ },
2276
+ logger: Log,
2277
+ logPrefix: 'ProtocolV2 RN-BLE',
2278
+ createTimeoutError: (messageName: string, timeout: number) =>
2279
+ ERRORS.TypedError(
2280
+ HardwareErrorCode.BleTimeoutError,
2281
+ `BLE response timeout after ${timeout}ms for ${messageName}`
2282
+ ),
2283
+ };
2284
+ }
2285
+
1763
2286
  getProtocolType(path: string): ProtocolType | undefined {
1764
- return this.deviceProtocol.get(path);
2287
+ return this.getActiveProtocol(path);
1765
2288
  }
1766
2289
  }