@onekeyfe/hd-transport-react-native 1.1.27-patch.1 → 1.2.0-alpha.0

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
@@ -9,33 +9,46 @@ import {
9
9
  } from 'react-native-ble-plx';
10
10
  import ByteBuffer from 'bytebuffer';
11
11
  import transport, {
12
- COMMON_HEADER_SIZE,
13
12
  LogBlockCommand,
14
13
  type OneKeyDeviceInfoBase,
14
+ PROTOCOL_V1_MESSAGE_HEADER_SIZE,
15
+ PROTOCOL_V2_CHANNEL_BLE_UART,
16
+ type ProtocolType,
17
+ ProtocolV2FrameAssembler,
18
+ ProtocolV2Session,
19
+ type TransportCallOptions,
20
+ probeProtocolV2 as probeProtocolV2Helper,
15
21
  } from '@onekeyfe/hd-transport';
16
22
  import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
17
- import { LoggerNames, getLogger } from '@onekeyfe/hd-core';
18
23
 
19
24
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
25
+ import {
26
+ hasWritableCapability,
27
+ resolveBleWriteMode,
28
+ resolveProtocolV2PacketCapacity,
29
+ } from './bleStrategy';
20
30
  import { subscribeBleOn } from './subscribeBleOn';
21
31
  import {
22
32
  ANDROID_PACKET_LENGTH,
23
33
  IOS_PACKET_LENGTH,
34
+ getBleUuidKey,
24
35
  getBluetoothServiceUuids,
25
36
  getInfosForServiceUuid,
37
+ isSameBleUuid,
26
38
  } from './constants';
27
39
  import { isHeaderChunk } from './utils/validateNotify';
28
40
  import BleTransport from './BleTransport';
29
41
  import timer from './utils/timer';
42
+ import { bleLogger, setBleLogger } from './logger';
30
43
 
31
44
  import type { Deferred } from '@onekeyfe/hd-shared';
32
45
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
33
46
  import type EventEmitter from 'events';
34
47
  import type { BleAcquireInput, TransportOptions } from './types';
35
48
 
36
- const { check, buildBuffers, receiveOne, parseConfigure } = transport;
49
+ const { check, ProtocolV1, parseConfigure } = transport;
37
50
 
38
- const Log = getLogger(LoggerNames.HdBleTransport);
51
+ const Log = bleLogger;
39
52
 
40
53
  const transportCache: Record<string, BleTransport> = {};
41
54
  const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
@@ -92,9 +105,103 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
92
105
 
93
106
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
94
107
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
108
+ const BLE_RESPONSE_TIMEOUT_MS = 30_000;
109
+ const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
110
+ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
111
+ const DEVICE_SCAN_TIMEOUT_MS = 8000;
112
+ const IOS_NOTIFY_READY_DELAY_MS = 150;
113
+ 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
+ export type ProtocolV2BleTuning = {
119
+ iosPacketLength?: number;
120
+ androidPacketLength?: number;
121
+ highVolumeWriteBurstSize?: number;
122
+ highVolumeWritePauseMs?: number;
123
+ highVolumeWriteFlushDelayMs?: number;
124
+ highVolumeWriteWithResponse?: boolean;
125
+ };
126
+
127
+ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
128
+
129
+ const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
130
+ iosPacketLength: IOS_PACKET_LENGTH,
131
+ 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
+ };
137
+
138
+ let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
139
+
140
+ const normalizePositiveInteger = (value: unknown, fallback: number) => {
141
+ const normalized = Number(value);
142
+ if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
143
+ return Math.floor(normalized);
144
+ };
145
+
146
+ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
147
+ protocolV2BleTuning = {
148
+ iosPacketLength: normalizePositiveInteger(
149
+ tuning.iosPacketLength,
150
+ protocolV2BleTuning.iosPacketLength
151
+ ),
152
+ androidPacketLength: normalizePositiveInteger(
153
+ tuning.androidPacketLength,
154
+ protocolV2BleTuning.androidPacketLength
155
+ ),
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
+ };
171
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
172
+ }
95
173
 
96
- let connectOptions: Record<string, unknown> = {
97
- requestMTU: 256,
174
+ export function resetProtocolV2BleTuning() {
175
+ protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
176
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
177
+ }
178
+
179
+ export function getProtocolV2BleTuning() {
180
+ return { ...protocolV2BleTuning };
181
+ }
182
+
183
+ function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
184
+ return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
185
+ }
186
+
187
+ function getDeviceDisplayName(device?: Device | null) {
188
+ return device?.name || device?.localName || null;
189
+ }
190
+
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
+ const ANDROID_REQUEST_MTU = 256;
202
+
203
+ const connectOptions: Record<string, unknown> = {
204
+ requestMTU: ANDROID_REQUEST_MTU,
98
205
  timeout: 3000,
99
206
  refreshGatt: 'OnConnected',
100
207
  };
@@ -103,12 +210,29 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
103
210
 
104
211
  const tryToGetConfiguration = (device: Device) => {
105
212
  if (!device || !device.serviceUUIDs) return null;
106
- const [serviceUUID] = device.serviceUUIDs;
213
+ const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
214
+ if (!serviceUUID) return null;
107
215
  const infos = getInfosForServiceUuid(serviceUUID, 'classic');
108
216
  if (!infos) return null;
109
217
  return infos;
110
218
  };
111
219
 
220
+ const requestAndroidMtu = async (device: Device) => {
221
+ if (Platform.OS !== 'android') return device;
222
+
223
+ try {
224
+ const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
225
+ Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
226
+ requested: ANDROID_REQUEST_MTU,
227
+ mtu: mtuDevice.mtu,
228
+ });
229
+ return mtuDevice;
230
+ } catch (error) {
231
+ Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
232
+ return device;
233
+ }
234
+ };
235
+
112
236
  type IOBleErrorRemap = Error | BleError | null | undefined;
113
237
 
114
238
  function remapError(error: IOBleErrorRemap) {
@@ -146,13 +270,15 @@ export default class ReactNativeBleTransport {
146
270
 
147
271
  _messages: ReturnType<typeof transport.parseConfigure> | undefined;
148
272
 
273
+ _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
274
+
149
275
  name = 'ReactNativeBleTransport';
150
276
 
151
277
  configured = false;
152
278
 
153
279
  stopped = false;
154
280
 
155
- scanTimeout = 3000;
281
+ scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
156
282
 
157
283
  runPromise: Deferred<any> | null = null;
158
284
 
@@ -160,11 +286,31 @@ export default class ReactNativeBleTransport {
160
286
 
161
287
  firmwareUploadWriteRecoveryIds = new Set<string>();
162
288
 
289
+ /** Per-device protocol type detected by active wire-level probe after connect. */
290
+ private deviceProtocol: Map<string, ProtocolType> = new Map();
291
+
292
+ private deviceProtocolHints: Map<string, ProtocolType> = new Map();
293
+
294
+ private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
295
+
296
+ private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
297
+
298
+ private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
299
+
300
+ private activeProtocolV2Call: { uuid: string; token: number } | null = null;
301
+
302
+ private nextProtocolV2CallToken = 1;
303
+
304
+ private monitorTokens: Map<string, number> = new Map();
305
+
306
+ private nextMonitorToken = 1;
307
+
163
308
  constructor(options: TransportOptions) {
164
- this.scanTimeout = options.scanTimeout ?? 3000;
309
+ this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
165
310
  }
166
311
 
167
- init(_logger: any, emitter: EventEmitter) {
312
+ init(logger: any, emitter: EventEmitter) {
313
+ setBleLogger(logger);
168
314
  this.emitter = emitter;
169
315
  }
170
316
 
@@ -174,6 +320,11 @@ export default class ReactNativeBleTransport {
174
320
  this._messages = messages;
175
321
  }
176
322
 
323
+ configureProtocolV2(signedData: any) {
324
+ this._messagesV2 = parseConfigure(signedData);
325
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
326
+ }
327
+
177
328
  listen() {
178
329
  // empty
179
330
  }
@@ -201,7 +352,29 @@ export default class ReactNativeBleTransport {
201
352
  }
202
353
  }
203
354
 
355
+ let fallbackServiceUuid: string | undefined;
356
+
204
357
  if (!infos) {
358
+ const services = await device.services();
359
+ Log?.debug(
360
+ '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
361
+ services?.map(service => service.uuid)
362
+ );
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
+ }
376
+
377
+ if (!infos && !fallbackServiceUuid) {
205
378
  try {
206
379
  Log?.debug('cancel connection when service not found');
207
380
  await device.cancelConnection();
@@ -211,7 +384,13 @@ export default class ReactNativeBleTransport {
211
384
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
212
385
  }
213
386
 
214
- const { serviceUuid, writeUuid, notifyUuid } = infos;
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';
390
+
391
+ if (!serviceUuid) {
392
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
393
+ }
215
394
 
216
395
  if (!characteristics) {
217
396
  characteristics = await device.characteristicsForService(serviceUuid);
@@ -224,9 +403,9 @@ export default class ReactNativeBleTransport {
224
403
  let writeCharacteristic;
225
404
  let notifyCharacteristic;
226
405
  for (const c of characteristics) {
227
- if (c.uuid === writeUuid) {
406
+ if (isSameBleUuid(c.uuid, writeUuid)) {
228
407
  writeCharacteristic = c;
229
- } else if (c.uuid === notifyUuid) {
408
+ } else if (isSameBleUuid(c.uuid, notifyUuid)) {
230
409
  notifyCharacteristic = c;
231
410
  }
232
411
  }
@@ -239,7 +418,7 @@ export default class ReactNativeBleTransport {
239
418
  throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found');
240
419
  }
241
420
 
242
- if (!writeCharacteristic.isWritableWithResponse) {
421
+ if (!hasWritableCapability(writeCharacteristic)) {
243
422
  throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
244
423
  }
245
424
 
@@ -262,6 +441,10 @@ export default class ReactNativeBleTransport {
262
441
  Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
263
442
  return;
264
443
  }
444
+ if (transportCache[uuid] !== transport) {
445
+ Log?.debug('device disconnect ignored for stale transport: ', device?.id);
446
+ return;
447
+ }
265
448
 
266
449
  try {
267
450
  Log?.debug('device disconnect: ', device?.id);
@@ -271,12 +454,14 @@ export default class ReactNativeBleTransport {
271
454
  connectId: device?.id,
272
455
  });
273
456
  if (this.runPromise) {
274
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError));
457
+ const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
458
+ this.runPromise.reject(error);
459
+ this.rejectAllProtocolV2Frames(error);
275
460
  }
276
461
  } catch (e) {
277
462
  Log?.debug('device disconnect error: ', e);
278
463
  } finally {
279
- this.release(uuid);
464
+ this.release(uuid, true);
280
465
  }
281
466
  });
282
467
  }
@@ -299,7 +484,6 @@ export default class ReactNativeBleTransport {
299
484
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
300
485
  e.errorCode === BleErrorCode.OperationCancelled
301
486
  ) {
302
- connectOptions = {};
303
487
  device = await device.connect();
304
488
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
305
489
  throw e;
@@ -314,7 +498,18 @@ export default class ReactNativeBleTransport {
314
498
  transport.device = device;
315
499
  transport.writeCharacteristic = writeCharacteristic;
316
500
  transport.notifyCharacteristic = notifyCharacteristic;
317
- transport.notifySubscription = this._monitorCharacteristic(notifyCharacteristic, uuid);
501
+ const monitorToken = this.nextMonitorToken;
502
+ this.nextMonitorToken += 1;
503
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
504
+ transport.monitorToken = monitorToken;
505
+ transport.notifyTransactionId = notifyTransactionId;
506
+ this.monitorTokens.set(uuid, monitorToken);
507
+ transport.notifySubscription = this._monitorCharacteristic(
508
+ notifyCharacteristic,
509
+ uuid,
510
+ monitorToken,
511
+ notifyTransactionId
512
+ );
318
513
  this.attachDisconnectSubscription(transport, device, uuid);
319
514
  } finally {
320
515
  this.firmwareUploadWriteRecoveryIds.delete(uuid);
@@ -360,6 +555,7 @@ export default class ReactNativeBleTransport {
360
555
  blePlxManager.startDeviceScan(
361
556
  null,
362
557
  {
558
+ allowDuplicates: true,
363
559
  scanMode: ScanMode.LowLatency,
364
560
  },
365
561
  (error, device) => {
@@ -387,14 +583,41 @@ export default class ReactNativeBleTransport {
387
583
  return;
388
584
  }
389
585
 
390
- if (isOnekeyDevice(device?.name ?? null, device?.id)) {
586
+ const displayName = getDeviceDisplayName(device);
587
+ 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', {
596
+ name: device?.name,
597
+ localName: device?.localName,
598
+ id: device?.id,
599
+ serviceUUIDs: device?.serviceUUIDs,
600
+ accepted: isOneKey,
601
+ });
602
+ }
603
+
604
+ if (isOneKey) {
391
605
  Log?.debug('search device start ======================');
392
- const { name, localName, id } = device ?? {};
606
+ const { name, localName, id, serviceUUIDs } = device ?? {};
393
607
  Log?.debug(
394
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${id ?? ''}`
608
+ `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
609
+ id ?? ''
610
+ }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
395
611
  );
396
612
  addDevice(device as unknown as Device);
397
613
  Log?.debug('search device end ======================\n');
614
+ } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
615
+ Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
616
+ name: device?.name,
617
+ localName: device?.localName,
618
+ id: device?.id,
619
+ serviceUUIDs: device?.serviceUUIDs,
620
+ });
398
621
  }
399
622
  }
400
623
  );
@@ -408,7 +631,16 @@ export default class ReactNativeBleTransport {
408
631
 
409
632
  const addDevice = (device: Device) => {
410
633
  if (deviceList.every(d => d.id !== device.id)) {
411
- deviceList.push({ ...device, commType: 'ble' } as IOneKeyDevice);
634
+ const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
635
+ const protocolHint = inferProtocolHintFromDeviceName(displayName);
636
+ if (protocolHint) {
637
+ this.deviceProtocolHints.set(device.id, protocolHint);
638
+ }
639
+ deviceList.push({
640
+ ...device,
641
+ name: displayName,
642
+ commType: 'ble',
643
+ } as IOneKeyDevice);
412
644
  }
413
645
  };
414
646
 
@@ -420,25 +652,41 @@ export default class ReactNativeBleTransport {
420
652
  }
421
653
 
422
654
  async acquire(input: BleAcquireInput) {
423
- const { uuid, forceCleanRunPromise } = input;
655
+ const { uuid, forceCleanRunPromise, expectedProtocol } = input;
424
656
 
425
657
  if (!uuid) {
426
658
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
427
659
  }
428
660
 
429
- let device: Device | null = null;
661
+ const cachedTransport = transportCache[uuid];
662
+ if (cachedTransport) {
663
+ const cachedProtocol = this.deviceProtocol.get(uuid);
664
+ const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false);
665
+ if (
666
+ isCachedDeviceConnected &&
667
+ cachedProtocol &&
668
+ (!expectedProtocol || cachedProtocol === expectedProtocol)
669
+ ) {
670
+ Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
671
+ return { uuid, protocolType: cachedProtocol };
672
+ }
430
673
 
431
- if (transportCache[uuid]) {
432
674
  /**
433
- * If the transport is not released due to an exception operation
434
- * it will be handled again here
675
+ * If the transport is not reusable due to a protocol mismatch or stale
676
+ * connection, clean it up before creating a new transport instance.
435
677
  */
436
- Log?.debug('transport not be released, will release: ', uuid);
437
- await this.release(uuid);
678
+ Log?.debug('transport not reusable, will release: ', uuid);
679
+ await this.release(uuid, true);
438
680
  }
439
681
 
682
+ let device: Device | null = null;
683
+
440
684
  if (forceCleanRunPromise && this.runPromise) {
441
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
685
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
686
+ this.runPromise.reject(error);
687
+ this.rejectAllProtocolV2Frames(error);
688
+ this.runPromise = null;
689
+ this.activeProtocolV2Call = null;
442
690
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
443
691
  }
444
692
 
@@ -450,11 +698,12 @@ export default class ReactNativeBleTransport {
450
698
  throw error;
451
699
  }
452
700
 
453
- // check device is bonded
454
701
  if (Platform.OS === 'android') {
455
702
  const bondState = await pairDevice(uuid);
456
703
  if (bondState.bonding) {
457
704
  await onDeviceBondState(uuid);
705
+ } else if (!bondState.bonded) {
706
+ throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
458
707
  }
459
708
  }
460
709
 
@@ -480,7 +729,6 @@ export default class ReactNativeBleTransport {
480
729
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
481
730
  e.errorCode === BleErrorCode.OperationCancelled
482
731
  ) {
483
- connectOptions = {};
484
732
  Log?.debug('first try to reconnect without params');
485
733
  device = await blePlxManager.connectToDevice(uuid);
486
734
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
@@ -500,17 +748,16 @@ export default class ReactNativeBleTransport {
500
748
  Log?.debug('not connected, try to connect to device: ', uuid);
501
749
 
502
750
  try {
503
- await device.connect(connectOptions);
751
+ device = await device.connect(connectOptions);
504
752
  } catch (e) {
505
753
  Log?.debug('not connected, try to connect to device has error: ', e);
506
754
  if (
507
755
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
508
756
  e.errorCode === BleErrorCode.OperationCancelled
509
757
  ) {
510
- connectOptions = {};
511
758
  Log?.debug('second try to reconnect without params');
512
759
  try {
513
- await device.connect();
760
+ device = await device.connect();
514
761
  } catch (e) {
515
762
  Log?.debug('last try to reconnect error: ', e);
516
763
  // last try to reconnect device if this issue exists
@@ -518,7 +765,7 @@ export default class ReactNativeBleTransport {
518
765
  if (e.errorCode === BleErrorCode.OperationCancelled) {
519
766
  Log?.debug('last try to reconnect');
520
767
  await device.cancelConnection();
521
- await device.connect();
768
+ device = await device.connect();
522
769
  }
523
770
  }
524
771
  } else {
@@ -527,18 +774,50 @@ export default class ReactNativeBleTransport {
527
774
  }
528
775
  }
529
776
 
777
+ device = await requestAndroidMtu(device);
530
778
  const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
531
779
 
780
+ const protocolHint = expectedProtocol
781
+ ? undefined
782
+ : this.deviceProtocolHints.get(uuid) ??
783
+ inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
784
+
532
785
  // release transport before new transport instance
533
- await this.release(uuid);
786
+ await this.release(uuid, true);
787
+ if (protocolHint) {
788
+ this.deviceProtocolHints.set(uuid, protocolHint);
789
+ }
534
790
 
535
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);
536
801
  transport.notifySubscription = this._monitorCharacteristic(
537
802
  transport.notifyCharacteristic,
538
- uuid
803
+ uuid,
804
+ monitorToken,
805
+ notifyTransactionId
539
806
  );
540
807
  transportCache[uuid] = transport;
541
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
+
542
821
  this.emitter?.emit('device-connect', {
543
822
  name: device.name,
544
823
  id: device.id,
@@ -547,13 +826,19 @@ export default class ReactNativeBleTransport {
547
826
 
548
827
  this.attachDisconnectSubscription(transport, device, uuid);
549
828
 
550
- return { uuid };
829
+ return { uuid, protocolType };
551
830
  }
552
831
 
553
- _monitorCharacteristic(characteristic: Characteristic, uuid: string): Subscription {
832
+ _monitorCharacteristic(
833
+ characteristic: Characteristic,
834
+ uuid: string,
835
+ monitorToken: number,
836
+ notifyTransactionId: string
837
+ ): Subscription {
554
838
  let bufferLength = 0;
555
839
  let buffer: any[] = [];
556
840
  const subscription = characteristic.monitor((error, c) => {
841
+ const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
557
842
  if (error) {
558
843
  Log?.debug(
559
844
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
@@ -564,6 +849,10 @@ export default class ReactNativeBleTransport {
564
849
  Log?.debug('notify error ignored during FirmwareUpload write recovery: ', uuid);
565
850
  return;
566
851
  }
852
+ if (!isCurrentMonitor) {
853
+ Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
854
+ return;
855
+ }
567
856
  if (this.runPromise) {
568
857
  let ERROR:
569
858
  | typeof HardwareErrorCode.BleDeviceBondError
@@ -583,27 +872,45 @@ export default class ReactNativeBleTransport {
583
872
  error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
584
873
  error.reason?.includes('notify change failed for device')
585
874
  ) {
586
- this.runPromise.reject(
587
- ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure)
875
+ const notifyError = ERRORS.TypedError(
876
+ HardwareErrorCode.BleCharacteristicNotifyChangeFailure
588
877
  );
878
+ this.runPromise.reject(notifyError);
879
+ this.rejectAllProtocolV2Frames(notifyError);
589
880
  Log?.debug(
590
881
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
591
882
  );
592
883
  return;
593
884
  }
594
- this.runPromise.reject(ERRORS.TypedError(ERROR));
885
+ const notifyError = ERRORS.TypedError(ERROR);
886
+ this.runPromise.reject(notifyError);
887
+ this.rejectAllProtocolV2Frames(notifyError);
595
888
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
596
889
  }
597
890
 
598
891
  return;
599
892
  }
600
893
 
894
+ if (!isCurrentMonitor) {
895
+ Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
896
+ return;
897
+ }
898
+
601
899
  if (!c) {
602
900
  throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
603
901
  }
604
902
 
605
903
  try {
606
904
  const data = Buffer.from(c.value as string, 'base64');
905
+ const protocol = this.deviceProtocol.get(uuid);
906
+ if (!protocol) {
907
+ Log?.debug('monitor data ignored before protocol detection: ', uuid);
908
+ return;
909
+ }
910
+ if (protocol === 'V2') {
911
+ this.handleProtocolV2Notification(uuid, new Uint8Array(data));
912
+ return;
913
+ }
607
914
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
608
915
  if (isHeaderChunk(data)) {
609
916
  bufferLength = data.readInt32BE(5);
@@ -612,7 +919,7 @@ export default class ReactNativeBleTransport {
612
919
  buffer = buffer.concat([...data]);
613
920
  }
614
921
 
615
- if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) {
922
+ if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
616
923
  const value = Buffer.from(buffer);
617
924
  // console.log(
618
925
  // '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
@@ -626,17 +933,41 @@ export default class ReactNativeBleTransport {
626
933
  }
627
934
  } catch (error) {
628
935
  Log?.debug('monitor data error: ', error);
629
- this.runPromise?.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
936
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
937
+ this.runPromise?.reject(notifyError);
938
+ this.rejectAllProtocolV2Frames(notifyError);
630
939
  }
631
- }, uuid);
940
+ }, notifyTransactionId);
632
941
 
633
942
  return subscription;
634
943
  }
635
944
 
636
- async release(uuid: string) {
945
+ async release(uuid: string, onclose = false) {
637
946
  const transport = transportCache[uuid];
947
+ if (this.runPromise) {
948
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
949
+ this.runPromise.reject(error);
950
+ this.runPromise = null;
951
+ this.rejectAllProtocolV2Frames(error);
952
+ this.activeProtocolV2Call = null;
953
+ } else {
954
+ this.resetProtocolV2Frames(uuid);
955
+ }
956
+
957
+ if (Platform.OS === 'android' && !onclose && transport) {
958
+ this.protocolV2Assemblers.get(uuid)?.reset();
959
+ this.resetProtocolV2Frames(uuid);
960
+ if (this.activeProtocolV2Call?.uuid === uuid) {
961
+ this.activeProtocolV2Call = null;
962
+ }
963
+ return Promise.resolve(true);
964
+ }
638
965
 
639
966
  if (transport) {
967
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
968
+ this.monitorTokens.delete(uuid);
969
+ }
970
+
640
971
  // Clean up disconnect subscription first to prevent callbacks on released transport
641
972
  Log?.debug('release: removing disconnect subscription for device: ', uuid);
642
973
  transport.disconnectSubscription?.remove();
@@ -650,12 +981,27 @@ export default class ReactNativeBleTransport {
650
981
  transport.notifySubscription?.remove();
651
982
  transport.notifySubscription = undefined;
652
983
 
984
+ if (transport.notifyTransactionId) {
985
+ try {
986
+ await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
987
+ } catch (e) {
988
+ Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
989
+ }
990
+ }
991
+
653
992
  delete transportCache[uuid];
993
+ }
654
994
 
655
- // Temporary close the Android disconnect after each request
656
- if (Platform.OS === 'android') {
657
- // await this.blePlxManager?.cancelDeviceConnection(uuid);
658
- }
995
+ this.deviceProtocol.delete(uuid);
996
+ this.deviceProtocolHints.delete(uuid);
997
+ this.protocolV2Assemblers.get(uuid)?.reset();
998
+ this.protocolV2Assemblers.delete(uuid);
999
+ this.resetProtocolV2Frames(uuid);
1000
+
1001
+ try {
1002
+ await this.blePlxManager?.cancelTransaction(uuid);
1003
+ } catch (e) {
1004
+ Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
659
1005
  }
660
1006
 
661
1007
  return Promise.resolve(true);
@@ -665,7 +1011,12 @@ export default class ReactNativeBleTransport {
665
1011
  await this.call(session, name, data);
666
1012
  }
667
1013
 
668
- async call(uuid: string, name: string, data: Record<string, unknown>) {
1014
+ async call(
1015
+ uuid: string,
1016
+ name: string,
1017
+ data: Record<string, unknown>,
1018
+ options?: TransportCallOptions
1019
+ ) {
669
1020
  if (this.stopped) {
670
1021
  // eslint-disable-next-line prefer-promise-reject-errors
671
1022
  return Promise.reject(ERRORS.TypedError('Transport stopped.'));
@@ -681,13 +1032,13 @@ export default class ReactNativeBleTransport {
681
1032
  throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
682
1033
  }
683
1034
 
684
- const transport = transportCache[uuid];
685
- if (!transport) {
686
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1035
+ const protocol = this.getProtocolType(uuid);
1036
+ if (!protocol) {
1037
+ throw ERRORS.TypedError(
1038
+ HardwareErrorCode.RuntimeError,
1039
+ `Device protocol has not been detected for ${uuid}`
1040
+ );
687
1041
  }
688
-
689
- this.runPromise = createDeferred();
690
- const messages = this._messages;
691
1042
  // Upload resources on low-end phones may OOM
692
1043
  if (name === 'ResourceUpdate' || name === 'ResourceAck') {
693
1044
  Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
@@ -695,12 +1046,44 @@ export default class ReactNativeBleTransport {
695
1046
  hash: data?.hash,
696
1047
  });
697
1048
  } else if (LogBlockCommand.has(name)) {
698
- Log?.debug('transport-react-native', 'call-', ' name: ', name);
1049
+ Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
699
1050
  } else {
700
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data);
1051
+ Log?.debug(
1052
+ 'transport-react-native',
1053
+ 'call-',
1054
+ ' name: ',
1055
+ name,
1056
+ ' data: ',
1057
+ data,
1058
+ ' protocol: ',
1059
+ protocol
1060
+ );
1061
+ }
1062
+
1063
+ if (protocol === 'V2') {
1064
+ return this.callProtocolV2(uuid, name, data, options);
701
1065
  }
702
1066
 
703
- const buffers = buildBuffers(messages, name, data);
1067
+ return this.callProtocolV1(uuid, name, data, options);
1068
+ }
1069
+
1070
+ private async callProtocolV1(
1071
+ uuid: string,
1072
+ name: string,
1073
+ data: Record<string, unknown>,
1074
+ options?: TransportCallOptions
1075
+ ) {
1076
+ if (!this._messages) {
1077
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1078
+ }
1079
+
1080
+ const transport = this.getCachedTransport(uuid);
1081
+ const runPromise = createDeferred<string>();
1082
+ runPromise.promise.catch(() => undefined);
1083
+ this.runPromise = runPromise;
1084
+ const messages = this._messages;
1085
+ const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1086
+ let timeout: ReturnType<typeof setTimeout> | undefined;
704
1087
 
705
1088
  async function writeChunkedData(
706
1089
  buffers: ByteBuffer[],
@@ -853,20 +1236,41 @@ export default class ReactNativeBleTransport {
853
1236
  }
854
1237
 
855
1238
  try {
856
- const response = await this.runPromise.promise;
1239
+ const response = await Promise.race([
1240
+ runPromise.promise,
1241
+ new Promise<never>((_, reject) => {
1242
+ if (options?.timeoutMs) {
1243
+ timeout = setTimeout(() => {
1244
+ const error = ERRORS.TypedError(
1245
+ HardwareErrorCode.BleTimeoutError,
1246
+ `BLE response timeout after ${options.timeoutMs}ms for ${name}`
1247
+ );
1248
+ runPromise.reject(error);
1249
+ reject(error);
1250
+ }, options.timeoutMs);
1251
+ }
1252
+ }),
1253
+ ]);
857
1254
 
858
1255
  if (typeof response !== 'string') {
859
1256
  throw new Error('Returning data is not string.');
860
1257
  }
861
1258
 
862
1259
  Log?.debug('receive data: ', response);
863
- const jsonData = receiveOne(messages, response);
1260
+ const jsonData = ProtocolV1.decodeMessage(messages, response);
864
1261
  return check.call(jsonData);
865
1262
  } catch (e) {
866
- Log?.error('call error: ', e);
1263
+ if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1264
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1265
+ } else {
1266
+ Log?.error('call error: ', e);
1267
+ }
867
1268
  throw e;
868
1269
  } finally {
869
- this.runPromise = null;
1270
+ if (timeout) clearTimeout(timeout);
1271
+ if (this.runPromise === runPromise) {
1272
+ this.runPromise = null;
1273
+ }
870
1274
  }
871
1275
  }
872
1276
 
@@ -933,6 +1337,13 @@ export default class ReactNativeBleTransport {
933
1337
  if (transportCache[session]) {
934
1338
  delete transportCache[session];
935
1339
  }
1340
+ this.deviceProtocol.delete(session);
1341
+ this.deviceProtocolHints.delete(session);
1342
+ this.protocolV2Assemblers.delete(session);
1343
+ this.resetProtocolV2Frames(session);
1344
+ if (this.activeProtocolV2Call?.uuid === session) {
1345
+ this.activeProtocolV2Call = null;
1346
+ }
936
1347
 
937
1348
  // emit the disconnect event
938
1349
  try {
@@ -955,4 +1366,416 @@ export default class ReactNativeBleTransport {
955
1366
  }
956
1367
  this.runPromise = null;
957
1368
  }
1369
+
1370
+ private getCachedTransport(uuid: string) {
1371
+ const transport = transportCache[uuid];
1372
+ if (!transport) {
1373
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1374
+ }
1375
+ return transport;
1376
+ }
1377
+
1378
+ private createProtocolMismatchError(expected: ProtocolType) {
1379
+ return ERRORS.TypedError(
1380
+ HardwareErrorCode.RuntimeError,
1381
+ `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
1382
+ );
1383
+ }
1384
+
1385
+ private createProtocolDetectionError() {
1386
+ return ERRORS.TypedError(
1387
+ HardwareErrorCode.BleTimeoutError,
1388
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1389
+ );
1390
+ }
1391
+
1392
+ private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1393
+ if (this.deviceProtocol.get(uuid) === protocol) {
1394
+ this.deviceProtocol.delete(uuid);
1395
+ }
1396
+ }
1397
+
1398
+ private async detectProtocol(
1399
+ uuid: string,
1400
+ expectedProtocol?: ProtocolType,
1401
+ protocolHint?: ProtocolType
1402
+ ): Promise<ProtocolType> {
1403
+ if (expectedProtocol === 'V1') {
1404
+ if (await this.probeProtocolV1(uuid)) {
1405
+ this.deviceProtocol.set(uuid, 'V1');
1406
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1407
+ return 'V1';
1408
+ }
1409
+ throw this.createProtocolMismatchError(expectedProtocol);
1410
+ }
1411
+
1412
+ 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';
1418
+ }
1419
+
1420
+ // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
1421
+ // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
1422
+ // 不能作为最终结论。
1423
+ const probeOrder: ProtocolType[] =
1424
+ protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1425
+
1426
+ for (let i = 0; i < probeOrder.length; i += 1) {
1427
+ const protocol = probeOrder[i];
1428
+ if (i > 0) {
1429
+ // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
1430
+ await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1431
+ }
1432
+ const detected =
1433
+ protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1434
+ if (detected) {
1435
+ this.deviceProtocol.set(uuid, protocol);
1436
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1437
+ return protocol;
1438
+ }
1439
+ }
1440
+
1441
+ this.deviceProtocol.delete(uuid);
1442
+ throw this.createProtocolDetectionError();
1443
+ }
1444
+
1445
+ private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1446
+ const transport = transportCache[uuid];
1447
+ this.protocolV2Assemblers.get(uuid)?.reset();
1448
+ this.resetProtocolV2Frames(uuid);
1449
+ if (this.activeProtocolV2Call?.uuid === uuid) {
1450
+ this.activeProtocolV2Call = null;
1451
+ }
1452
+ if (this.runPromise) {
1453
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1454
+ this.runPromise.reject(error);
1455
+ this.runPromise = null;
1456
+ }
1457
+
1458
+ if (!transport) return;
1459
+
1460
+ const previousNotifyTransactionId = transport.notifyTransactionId;
1461
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1462
+ this.monitorTokens.delete(uuid);
1463
+ }
1464
+ transport.notifySubscription?.remove();
1465
+ transport.notifySubscription = undefined;
1466
+ if (previousNotifyTransactionId) {
1467
+ try {
1468
+ await this.blePlxManager?.cancelTransaction(previousNotifyTransactionId);
1469
+ } catch (error) {
1470
+ Log?.debug(
1471
+ `[ReactNativeBleTransport] cancel notify after Protocol ${protocol} probe failed:`,
1472
+ error?.message || error
1473
+ );
1474
+ }
1475
+ }
1476
+
1477
+ const monitorToken = this.nextMonitorToken;
1478
+ this.nextMonitorToken += 1;
1479
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
1480
+ transport.monitorToken = monitorToken;
1481
+ transport.notifyTransactionId = notifyTransactionId;
1482
+ this.monitorTokens.set(uuid, monitorToken);
1483
+ transport.notifySubscription = this._monitorCharacteristic(
1484
+ transport.notifyCharacteristic,
1485
+ uuid,
1486
+ monitorToken,
1487
+ notifyTransactionId
1488
+ );
1489
+ if (Platform.OS === 'ios') {
1490
+ await new Promise<void>(resolve => {
1491
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
1492
+ });
1493
+ }
1494
+ }
1495
+
1496
+ private async probeProtocolV1(uuid: string) {
1497
+ if (!this._messages) {
1498
+ return false;
1499
+ }
1500
+
1501
+ try {
1502
+ this.deviceProtocol.set(uuid, 'V1');
1503
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1504
+ return true;
1505
+ } catch (error) {
1506
+ this.clearProbeProtocol(uuid, 'V1');
1507
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1508
+ return false;
1509
+ }
1510
+ }
1511
+
1512
+ private async probeProtocolV2(uuid: string) {
1513
+ if (!this._messages || !this._messagesV2) {
1514
+ return false;
1515
+ }
1516
+
1517
+ this.deviceProtocol.set(uuid, 'V2');
1518
+ this.protocolV2Assemblers.get(uuid)?.reset();
1519
+ const detected = await probeProtocolV2Helper({
1520
+ call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
1521
+ this.callProtocolV2(uuid, name, data, options),
1522
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
1523
+ logger: Log,
1524
+ logPrefix: 'ProtocolV2 RN-BLE',
1525
+ onProbeFailed: () => {
1526
+ this.protocolV2Assemblers.get(uuid)?.reset();
1527
+ this.resetProtocolV2Frames(uuid);
1528
+ },
1529
+ });
1530
+ if (!detected) {
1531
+ this.clearProbeProtocol(uuid, 'V2');
1532
+ }
1533
+ return detected;
1534
+ }
1535
+
1536
+ private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1537
+ try {
1538
+ if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1539
+ this.protocolV2Assemblers.get(uuid)?.reset();
1540
+ this.resetProtocolV2Frames(uuid);
1541
+ return;
1542
+ }
1543
+
1544
+ if (data.length === 0) return;
1545
+
1546
+ const assembler = this.protocolV2Assemblers.get(uuid);
1547
+ if (!assembler) return;
1548
+
1549
+ for (const frameData of assembler.drain(data)) {
1550
+ this.resolveProtocolV2Frame(uuid, frameData);
1551
+ }
1552
+ } catch (error) {
1553
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1554
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1555
+ this.runPromise?.reject(notifyError);
1556
+ this.rejectAllProtocolV2Frames(notifyError);
1557
+ }
1558
+ }
1559
+
1560
+ private getProtocolV2FrameQueue(uuid: string) {
1561
+ let queue = this.protocolV2FrameQueues.get(uuid);
1562
+ if (!queue) {
1563
+ queue = [];
1564
+ this.protocolV2FrameQueues.set(uuid, queue);
1565
+ }
1566
+ return queue;
1567
+ }
1568
+
1569
+ private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
1570
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1571
+ if (framePromise) {
1572
+ framePromise.resolve(frame);
1573
+ this.protocolV2FramePromises.delete(uuid);
1574
+ return;
1575
+ }
1576
+ this.getProtocolV2FrameQueue(uuid).push(frame);
1577
+ }
1578
+
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
+ private resetProtocolV2Frames(uuid: string) {
1588
+ this.protocolV2FrameQueues.delete(uuid);
1589
+ this.protocolV2FramePromises.delete(uuid);
1590
+ }
1591
+
1592
+ private isActiveProtocolV2Call(uuid: string, token: number) {
1593
+ return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
1594
+ }
1595
+
1596
+ private async readProtocolV2Frame(uuid: string) {
1597
+ const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
1598
+ if (queuedFrame) {
1599
+ return queuedFrame;
1600
+ }
1601
+
1602
+ const framePromise = createDeferred<Uint8Array>();
1603
+ this.protocolV2FramePromises.set(uuid, framePromise);
1604
+ try {
1605
+ return await framePromise.promise;
1606
+ } finally {
1607
+ if (this.protocolV2FramePromises.get(uuid) === framePromise) {
1608
+ this.protocolV2FramePromises.delete(uuid);
1609
+ }
1610
+ }
1611
+ }
1612
+
1613
+ private async writeProtocolV2Frame(
1614
+ transport: BleTransport,
1615
+ frame: Uint8Array,
1616
+ options?: { highVolume?: boolean; writeWithResponse?: boolean }
1617
+ ) {
1618
+ const tuning = getProtocolV2BleTuning();
1619
+ const packetCapacity = resolveProtocolV2PacketCapacity({
1620
+ platform: Platform.OS,
1621
+ iosPacketLength: tuning.iosPacketLength,
1622
+ androidPacketLength: tuning.androidPacketLength,
1623
+ mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1624
+ });
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
+ try {
1635
+ for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1636
+ const chunk = frame.slice(offset, offset + packetCapacity);
1637
+ const base64 = Buffer.from(chunk).toString('base64');
1638
+ if (writeMode === 'withResponse') {
1639
+ await transport.writeCharacteristic.writeWithResponse(base64);
1640
+ } else {
1641
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1642
+ }
1643
+ packetsWritten += 1;
1644
+
1645
+ if (
1646
+ shouldThrottle &&
1647
+ packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1648
+ offset + packetCapacity < frame.length
1649
+ ) {
1650
+ await delay(tuning.highVolumeWritePauseMs);
1651
+ }
1652
+ }
1653
+
1654
+ if (shouldThrottle) {
1655
+ await delay(tuning.highVolumeWriteFlushDelayMs);
1656
+ }
1657
+ } catch (error) {
1658
+ if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
1659
+ Log?.debug(
1660
+ '[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
1661
+ error
1662
+ );
1663
+ await this.writeProtocolV2Frame(transport, frame, {
1664
+ highVolume: true,
1665
+ writeWithResponse: true,
1666
+ });
1667
+ return;
1668
+ }
1669
+ throw error;
1670
+ }
1671
+ }
1672
+
1673
+ private async callProtocolV2(
1674
+ uuid: string,
1675
+ name: string,
1676
+ data: Record<string, unknown>,
1677
+ options?: TransportCallOptions
1678
+ ) {
1679
+ if (!this._messages || !this._messagesV2) {
1680
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1681
+ }
1682
+
1683
+ const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'GetProtoVersion';
1684
+ if (this.runPromise) {
1685
+ if (!forceRun) {
1686
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1687
+ }
1688
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1689
+ this.runPromise.reject(error);
1690
+ this.rejectAllProtocolV2Frames(error);
1691
+ this.runPromise = null;
1692
+ this.activeProtocolV2Call = null;
1693
+ }
1694
+
1695
+ const transport = this.getCachedTransport(uuid);
1696
+ const runPromise = createDeferred<Uint8Array>();
1697
+ runPromise.promise.catch(() => undefined);
1698
+ this.runPromise = runPromise;
1699
+ const callToken = this.nextProtocolV2CallToken++;
1700
+ this.activeProtocolV2Call = { uuid, token: callToken };
1701
+ this.protocolV2Assemblers.get(uuid)?.reset();
1702
+ this.resetProtocolV2Frames(uuid);
1703
+ let completed = false;
1704
+ const callOptions = {
1705
+ ...options,
1706
+ timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1707
+ };
1708
+ const highVolumeWrite = LogBlockCommand.has(name);
1709
+
1710
+ if (highVolumeWrite) {
1711
+ const tuning = getProtocolV2BleTuning();
1712
+ Log?.debug(
1713
+ '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1714
+ name,
1715
+ {
1716
+ packetCapacity:
1717
+ Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1718
+ burstSize: tuning.highVolumeWriteBurstSize,
1719
+ pauseMs: tuning.highVolumeWritePauseMs,
1720
+ flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1721
+ writeWithResponse: tuning.highVolumeWriteWithResponse,
1722
+ }
1723
+ );
1724
+ }
1725
+
1726
+ try {
1727
+ const session = new ProtocolV2Session({
1728
+ schemas: {
1729
+ protocolV1: this._messages,
1730
+ protocolV2: this._messagesV2,
1731
+ },
1732
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1733
+ writeFrame: async (frame: Uint8Array) => {
1734
+ await this.writeProtocolV2Frame(transport, frame, {
1735
+ highVolume: highVolumeWrite,
1736
+ });
1737
+ },
1738
+ readFrame: async () => {
1739
+ const rxFrame = await this.readProtocolV2Frame(uuid);
1740
+ if (!(rxFrame instanceof Uint8Array)) {
1741
+ throw new Error('Protocol V2 response is not Uint8Array');
1742
+ }
1743
+ return rxFrame;
1744
+ },
1745
+ logger: Log,
1746
+ logPrefix: 'ProtocolV2 RN-BLE',
1747
+ createTimeoutError: (_messageName: string, timeout: number) =>
1748
+ ERRORS.TypedError(
1749
+ HardwareErrorCode.BleTimeoutError,
1750
+ `BLE response timeout after ${timeout}ms for ${name}`
1751
+ ),
1752
+ });
1753
+
1754
+ const result = await session.call(name, data, callOptions);
1755
+ completed = true;
1756
+ return result;
1757
+ } catch (e) {
1758
+ if (this.isActiveProtocolV2Call(uuid, callToken)) {
1759
+ this.protocolV2Assemblers.get(uuid)?.reset();
1760
+ this.resetProtocolV2Frames(uuid);
1761
+ }
1762
+ Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1763
+ throw e;
1764
+ } finally {
1765
+ if (this.isActiveProtocolV2Call(uuid, callToken)) {
1766
+ if (!completed) {
1767
+ this.protocolV2Assemblers.get(uuid)?.reset();
1768
+ }
1769
+ this.resetProtocolV2Frames(uuid);
1770
+ this.activeProtocolV2Call = null;
1771
+ }
1772
+ if (this.runPromise === runPromise) {
1773
+ this.runPromise = null;
1774
+ }
1775
+ }
1776
+ }
1777
+
1778
+ getProtocolType(path: string): ProtocolType | undefined {
1779
+ return this.deviceProtocol.get(path);
1780
+ }
958
1781
  }