@onekeyfe/hd-transport-react-native 1.1.27-alpha.4 → 1.1.27-alpha.41

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,38 +9,150 @@ 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> = {};
54
+ const BLE_RESPONSE_TIMEOUT_MS = 30_000;
55
+ const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
56
+ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
57
+ const DEVICE_SCAN_TIMEOUT_MS = 8000;
58
+ const IOS_NOTIFY_READY_DELAY_MS = 150;
59
+ const ANDROID_NOTIFY_READY_DELAY_MS = 300;
60
+ const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
61
+ const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
62
+ const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
63
+
64
+ const delay = (ms: number) =>
65
+ new Promise<void>(resolve => {
66
+ setTimeout(resolve, ms);
67
+ });
68
+
69
+ export type ProtocolV2BleTuning = {
70
+ iosPacketLength?: number;
71
+ androidPacketLength?: number;
72
+ highVolumeWriteBurstSize?: number;
73
+ highVolumeWritePauseMs?: number;
74
+ highVolumeWriteFlushDelayMs?: number;
75
+ highVolumeWriteWithResponse?: boolean;
76
+ };
77
+
78
+ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
79
+
80
+ const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
81
+ iosPacketLength: IOS_PACKET_LENGTH,
82
+ androidPacketLength: ANDROID_PACKET_LENGTH,
83
+ highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
84
+ highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
85
+ highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
86
+ highVolumeWriteWithResponse: false,
87
+ };
88
+
89
+ let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
90
+
91
+ const normalizePositiveInteger = (value: unknown, fallback: number) => {
92
+ const normalized = Number(value);
93
+ if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
94
+ return Math.floor(normalized);
95
+ };
96
+
97
+ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
98
+ protocolV2BleTuning = {
99
+ iosPacketLength: normalizePositiveInteger(
100
+ tuning.iosPacketLength,
101
+ protocolV2BleTuning.iosPacketLength
102
+ ),
103
+ androidPacketLength: normalizePositiveInteger(
104
+ tuning.androidPacketLength,
105
+ protocolV2BleTuning.androidPacketLength
106
+ ),
107
+ highVolumeWriteBurstSize: normalizePositiveInteger(
108
+ tuning.highVolumeWriteBurstSize,
109
+ protocolV2BleTuning.highVolumeWriteBurstSize
110
+ ),
111
+ highVolumeWritePauseMs: normalizePositiveInteger(
112
+ tuning.highVolumeWritePauseMs,
113
+ protocolV2BleTuning.highVolumeWritePauseMs
114
+ ),
115
+ highVolumeWriteFlushDelayMs: normalizePositiveInteger(
116
+ tuning.highVolumeWriteFlushDelayMs,
117
+ protocolV2BleTuning.highVolumeWriteFlushDelayMs
118
+ ),
119
+ highVolumeWriteWithResponse:
120
+ tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
121
+ };
122
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
123
+ }
124
+
125
+ export function resetProtocolV2BleTuning() {
126
+ protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
127
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
128
+ }
129
+
130
+ export function getProtocolV2BleTuning() {
131
+ return { ...protocolV2BleTuning };
132
+ }
133
+
134
+ function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
135
+ return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
136
+ }
137
+
138
+ function getDeviceDisplayName(device?: Device | null) {
139
+ return device?.name || device?.localName || null;
140
+ }
41
141
 
42
- let connectOptions: Record<string, unknown> = {
43
- requestMTU: 256,
142
+ function isGenericBleService(uuid?: string | null) {
143
+ return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
144
+ }
145
+
146
+ function hasKnownOneKeyService(device?: Device | null) {
147
+ return (device?.serviceUUIDs ?? []).some(serviceUuid =>
148
+ getInfosForServiceUuid(serviceUuid, 'classic')
149
+ );
150
+ }
151
+
152
+ const ANDROID_REQUEST_MTU = 256;
153
+
154
+ const connectOptions: Record<string, unknown> = {
155
+ requestMTU: ANDROID_REQUEST_MTU,
44
156
  timeout: 3000,
45
157
  refreshGatt: 'OnConnected',
46
158
  };
@@ -49,12 +161,29 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
49
161
 
50
162
  const tryToGetConfiguration = (device: Device) => {
51
163
  if (!device || !device.serviceUUIDs) return null;
52
- const [serviceUUID] = device.serviceUUIDs;
164
+ const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
165
+ if (!serviceUUID) return null;
53
166
  const infos = getInfosForServiceUuid(serviceUUID, 'classic');
54
167
  if (!infos) return null;
55
168
  return infos;
56
169
  };
57
170
 
171
+ const requestAndroidMtu = async (device: Device) => {
172
+ if (Platform.OS !== 'android') return device;
173
+
174
+ try {
175
+ const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
176
+ Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
177
+ requested: ANDROID_REQUEST_MTU,
178
+ mtu: mtuDevice.mtu,
179
+ });
180
+ return mtuDevice;
181
+ } catch (error) {
182
+ Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
183
+ return device;
184
+ }
185
+ };
186
+
58
187
  type IOBleErrorRemap = Error | BleError | null | undefined;
59
188
 
60
189
  function remapError(error: IOBleErrorRemap) {
@@ -92,23 +221,45 @@ export default class ReactNativeBleTransport {
92
221
 
93
222
  _messages: ReturnType<typeof transport.parseConfigure> | undefined;
94
223
 
224
+ _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
225
+
95
226
  name = 'ReactNativeBleTransport';
96
227
 
97
228
  configured = false;
98
229
 
99
230
  stopped = false;
100
231
 
101
- scanTimeout = 3000;
232
+ scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
102
233
 
103
234
  runPromise: Deferred<any> | null = null;
104
235
 
105
236
  emitter?: EventEmitter;
106
237
 
238
+ /** Per-device protocol type detected by active wire-level probe after connect. */
239
+ private deviceProtocol: Map<string, ProtocolType> = new Map();
240
+
241
+ private deviceProtocolHints: Map<string, ProtocolType> = new Map();
242
+
243
+ private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
244
+
245
+ private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
246
+
247
+ private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
248
+
249
+ private activeProtocolV2Call: { uuid: string; token: number } | null = null;
250
+
251
+ private nextProtocolV2CallToken = 1;
252
+
253
+ private monitorTokens: Map<string, number> = new Map();
254
+
255
+ private nextMonitorToken = 1;
256
+
107
257
  constructor(options: TransportOptions) {
108
- this.scanTimeout = options.scanTimeout ?? 3000;
258
+ this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
109
259
  }
110
260
 
111
- init(_logger: any, emitter: EventEmitter) {
261
+ init(logger: any, emitter: EventEmitter) {
262
+ setBleLogger(logger);
112
263
  this.emitter = emitter;
113
264
  }
114
265
 
@@ -118,6 +269,11 @@ export default class ReactNativeBleTransport {
118
269
  this._messages = messages;
119
270
  }
120
271
 
272
+ configureProtocolV2(signedData: any) {
273
+ this._messagesV2 = parseConfigure(signedData);
274
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
275
+ }
276
+
121
277
  listen() {
122
278
  // empty
123
279
  }
@@ -167,6 +323,7 @@ export default class ReactNativeBleTransport {
167
323
  blePlxManager.startDeviceScan(
168
324
  null,
169
325
  {
326
+ allowDuplicates: true,
170
327
  scanMode: ScanMode.LowLatency,
171
328
  },
172
329
  (error, device) => {
@@ -194,14 +351,41 @@ export default class ReactNativeBleTransport {
194
351
  return;
195
352
  }
196
353
 
197
- if (isOnekeyDevice(device?.name ?? null, device?.id)) {
354
+ const displayName = getDeviceDisplayName(device);
355
+ const isOneKey =
356
+ isOnekeyDevice(device?.name ?? null, device?.id) ||
357
+ isOnekeyDevice(device?.localName ?? null, device?.id) ||
358
+ hasKnownOneKeyService(device);
359
+ const shouldTraceCandidate =
360
+ !!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
361
+
362
+ if (shouldTraceCandidate) {
363
+ Log?.debug('[ReactNativeBleTransport] scan candidate', {
364
+ name: device?.name,
365
+ localName: device?.localName,
366
+ id: device?.id,
367
+ serviceUUIDs: device?.serviceUUIDs,
368
+ accepted: isOneKey,
369
+ });
370
+ }
371
+
372
+ if (isOneKey) {
198
373
  Log?.debug('search device start ======================');
199
- const { name, localName, id } = device ?? {};
374
+ const { name, localName, id, serviceUUIDs } = device ?? {};
200
375
  Log?.debug(
201
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${id ?? ''}`
376
+ `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
377
+ id ?? ''
378
+ }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
202
379
  );
203
380
  addDevice(device as unknown as Device);
204
381
  Log?.debug('search device end ======================\n');
382
+ } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
383
+ Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
384
+ name: device?.name,
385
+ localName: device?.localName,
386
+ id: device?.id,
387
+ serviceUUIDs: device?.serviceUUIDs,
388
+ });
205
389
  }
206
390
  }
207
391
  );
@@ -215,7 +399,16 @@ export default class ReactNativeBleTransport {
215
399
 
216
400
  const addDevice = (device: Device) => {
217
401
  if (deviceList.every(d => d.id !== device.id)) {
218
- deviceList.push({ ...device, commType: 'ble' } as IOneKeyDevice);
402
+ const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
403
+ const protocolHint = inferProtocolHintFromDeviceName(displayName);
404
+ if (protocolHint) {
405
+ this.deviceProtocolHints.set(device.id, protocolHint);
406
+ }
407
+ deviceList.push({
408
+ ...device,
409
+ name: displayName,
410
+ commType: 'ble',
411
+ } as IOneKeyDevice);
219
412
  }
220
413
  };
221
414
 
@@ -227,25 +420,45 @@ export default class ReactNativeBleTransport {
227
420
  }
228
421
 
229
422
  async acquire(input: BleAcquireInput) {
230
- const { uuid, forceCleanRunPromise } = input;
423
+ const { uuid, forceCleanRunPromise, expectedProtocol } = input;
231
424
 
232
425
  if (!uuid) {
233
426
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
234
427
  }
235
428
 
236
- let device: Device | null = null;
429
+ const cachedTransport = transportCache[uuid];
430
+ if (cachedTransport) {
431
+ const cachedProtocol = this.deviceProtocol.get(uuid);
432
+ const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false);
433
+ if (
434
+ isCachedDeviceConnected &&
435
+ cachedProtocol &&
436
+ (!expectedProtocol || cachedProtocol === expectedProtocol)
437
+ ) {
438
+ Log?.debug(
439
+ '[ReactNativeBleTransport] reuse cached BLE transport:',
440
+ uuid,
441
+ cachedProtocol
442
+ );
443
+ return { uuid, protocolType: cachedProtocol };
444
+ }
237
445
 
238
- if (transportCache[uuid]) {
239
446
  /**
240
- * If the transport is not released due to an exception operation
241
- * it will be handled again here
447
+ * If the transport is not reusable due to a protocol mismatch or stale
448
+ * connection, clean it up before creating a new transport instance.
242
449
  */
243
- Log?.debug('transport not be released, will release: ', uuid);
244
- await this.release(uuid);
450
+ Log?.debug('transport not reusable, will release: ', uuid);
451
+ await this.release(uuid, true);
245
452
  }
246
453
 
454
+ let device: Device | null = null;
455
+
247
456
  if (forceCleanRunPromise && this.runPromise) {
248
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
457
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
458
+ this.runPromise.reject(error);
459
+ this.rejectAllProtocolV2Frames(error);
460
+ this.runPromise = null;
461
+ this.activeProtocolV2Call = null;
249
462
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
250
463
  }
251
464
 
@@ -257,11 +470,12 @@ export default class ReactNativeBleTransport {
257
470
  throw error;
258
471
  }
259
472
 
260
- // check device is bonded
261
473
  if (Platform.OS === 'android') {
262
474
  const bondState = await pairDevice(uuid);
263
475
  if (bondState.bonding) {
264
476
  await onDeviceBondState(uuid);
477
+ } else if (!bondState.bonded) {
478
+ throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
265
479
  }
266
480
  }
267
481
 
@@ -287,7 +501,6 @@ export default class ReactNativeBleTransport {
287
501
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
288
502
  e.errorCode === BleErrorCode.OperationCancelled
289
503
  ) {
290
- connectOptions = {};
291
504
  Log?.debug('first try to reconnect without params');
292
505
  device = await blePlxManager.connectToDevice(uuid);
293
506
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
@@ -307,17 +520,16 @@ export default class ReactNativeBleTransport {
307
520
  Log?.debug('not connected, try to connect to device: ', uuid);
308
521
 
309
522
  try {
310
- await device.connect(connectOptions);
523
+ device = await device.connect(connectOptions);
311
524
  } catch (e) {
312
525
  Log?.debug('not connected, try to connect to device has error: ', e);
313
526
  if (
314
527
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
315
528
  e.errorCode === BleErrorCode.OperationCancelled
316
529
  ) {
317
- connectOptions = {};
318
530
  Log?.debug('second try to reconnect without params');
319
531
  try {
320
- await device.connect();
532
+ device = await device.connect();
321
533
  } catch (e) {
322
534
  Log?.debug('last try to reconnect error: ', e);
323
535
  // last try to reconnect device if this issue exists
@@ -325,7 +537,7 @@ export default class ReactNativeBleTransport {
325
537
  if (e.errorCode === BleErrorCode.OperationCancelled) {
326
538
  Log?.debug('last try to reconnect');
327
539
  await device.cancelConnection();
328
- await device.connect();
540
+ device = await device.connect();
329
541
  }
330
542
  }
331
543
  } else {
@@ -334,9 +546,12 @@ export default class ReactNativeBleTransport {
334
546
  }
335
547
  }
336
548
 
549
+ device = await requestAndroidMtu(device);
550
+
337
551
  await device.discoverAllServicesAndCharacteristics();
338
552
  let infos = tryToGetConfiguration(device);
339
553
  let characteristics;
554
+ let fallbackServiceUuid: string | undefined;
340
555
 
341
556
  if (!infos) {
342
557
  for (const serviceUuid of getBluetoothServiceUuids()) {
@@ -351,16 +566,44 @@ export default class ReactNativeBleTransport {
351
566
  }
352
567
 
353
568
  if (!infos) {
354
- try {
355
- Log?.debug('cancel connection when service not found');
356
- await device.cancelConnection();
357
- } catch (e) {
358
- Log?.debug('cancel connection error when service not found: ', e.message || e.reason);
569
+ const services = await device.services();
570
+ Log?.debug(
571
+ '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
572
+ services?.map(service => service.uuid)
573
+ );
574
+
575
+ const knownService = services.find(service =>
576
+ getInfosForServiceUuid(service.uuid, 'classic')
577
+ );
578
+ const fallbackService =
579
+ knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
580
+
581
+ if (fallbackService) {
582
+ fallbackServiceUuid = fallbackService.uuid;
583
+ characteristics = await device.characteristicsForService(fallbackService.uuid);
584
+ Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
585
+ }
586
+ }
587
+
588
+ if (!infos) {
589
+ if (!fallbackServiceUuid) {
590
+ try {
591
+ Log?.debug('cancel connection when service not found');
592
+ await device.cancelConnection();
593
+ } catch (e) {
594
+ Log?.debug('cancel connection error when service not found: ', e.message || e.reason);
595
+ }
596
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
359
597
  }
360
- throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
361
598
  }
362
599
 
363
- const { serviceUuid, writeUuid, notifyUuid } = infos;
600
+ const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
601
+ const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
602
+ const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
603
+
604
+ if (!serviceUuid) {
605
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
606
+ }
364
607
 
365
608
  if (!characteristics) {
366
609
  characteristics = await device.characteristicsForService(serviceUuid);
@@ -373,9 +616,9 @@ export default class ReactNativeBleTransport {
373
616
  let writeCharacteristic;
374
617
  let notifyCharacteristic;
375
618
  for (const c of characteristics) {
376
- if (c.uuid === writeUuid) {
619
+ if (isSameBleUuid(c.uuid, writeUuid)) {
377
620
  writeCharacteristic = c;
378
- } else if (c.uuid === notifyUuid) {
621
+ } else if (isSameBleUuid(c.uuid, notifyUuid)) {
379
622
  notifyCharacteristic = c;
380
623
  }
381
624
  }
@@ -388,7 +631,7 @@ export default class ReactNativeBleTransport {
388
631
  throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found');
389
632
  }
390
633
 
391
- if (!writeCharacteristic.isWritableWithResponse) {
634
+ if (!hasWritableCapability(writeCharacteristic)) {
392
635
  throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
393
636
  }
394
637
 
@@ -398,16 +641,47 @@ export default class ReactNativeBleTransport {
398
641
  );
399
642
  }
400
643
 
644
+ const protocolHint = expectedProtocol
645
+ ? undefined
646
+ : this.deviceProtocolHints.get(uuid) ??
647
+ inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
648
+
401
649
  // release transport before new transport instance
402
- await this.release(uuid);
650
+ await this.release(uuid, true);
651
+ if (protocolHint) {
652
+ this.deviceProtocolHints.set(uuid, protocolHint);
653
+ }
403
654
 
404
655
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
656
+ if (Platform.OS === 'android') {
657
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
658
+ }
659
+ const monitorToken = this.nextMonitorToken;
660
+ this.nextMonitorToken += 1;
661
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
662
+ transport.monitorToken = monitorToken;
663
+ transport.notifyTransactionId = notifyTransactionId;
664
+ this.monitorTokens.set(uuid, monitorToken);
405
665
  transport.notifySubscription = this._monitorCharacteristic(
406
666
  transport.notifyCharacteristic,
407
- uuid
667
+ uuid,
668
+ monitorToken,
669
+ notifyTransactionId
408
670
  );
409
671
  transportCache[uuid] = transport;
410
672
 
673
+ this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
674
+
675
+ if (Platform.OS === 'ios') {
676
+ await new Promise<void>(resolve => {
677
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
678
+ });
679
+ } else if (Platform.OS === 'android') {
680
+ await delay(ANDROID_NOTIFY_READY_DELAY_MS);
681
+ }
682
+
683
+ const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
684
+
411
685
  this.emitter?.emit('device-connect', {
412
686
  name: device.name,
413
687
  id: device.id,
@@ -415,6 +689,11 @@ export default class ReactNativeBleTransport {
415
689
  });
416
690
 
417
691
  transport.disconnectSubscription = device.onDisconnected(() => {
692
+ if (transportCache[uuid] !== transport) {
693
+ Log?.debug('device disconnect ignored for stale transport: ', device?.id);
694
+ return;
695
+ }
696
+
418
697
  try {
419
698
  Log?.debug('device disconnect: ', device?.id);
420
699
  this.emitter?.emit('device-disconnect', {
@@ -423,28 +702,40 @@ export default class ReactNativeBleTransport {
423
702
  connectId: device?.id,
424
703
  });
425
704
  if (this.runPromise) {
426
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError));
705
+ const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
706
+ this.runPromise.reject(error);
707
+ this.rejectAllProtocolV2Frames(error);
427
708
  }
428
709
  } catch (e) {
429
710
  Log?.debug('device disconnect error: ', e);
430
711
  } finally {
431
- this.release(uuid);
712
+ this.release(uuid, true);
432
713
  }
433
714
  });
434
715
 
435
- return { uuid };
716
+ return { uuid, protocolType };
436
717
  }
437
718
 
438
- _monitorCharacteristic(characteristic: Characteristic, uuid: string): Subscription {
719
+ _monitorCharacteristic(
720
+ characteristic: Characteristic,
721
+ uuid: string,
722
+ monitorToken: number,
723
+ notifyTransactionId: string
724
+ ): Subscription {
439
725
  let bufferLength = 0;
440
726
  let buffer: any[] = [];
441
727
  const subscription = characteristic.monitor((error, c) => {
728
+ const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
442
729
  if (error) {
443
730
  Log?.debug(
444
731
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
445
732
  error as unknown as string
446
733
  }`
447
734
  );
735
+ if (!isCurrentMonitor) {
736
+ Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
737
+ return;
738
+ }
448
739
  if (this.runPromise) {
449
740
  let ERROR:
450
741
  | typeof HardwareErrorCode.BleDeviceBondError
@@ -464,27 +755,45 @@ export default class ReactNativeBleTransport {
464
755
  error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
465
756
  error.reason?.includes('notify change failed for device')
466
757
  ) {
467
- this.runPromise.reject(
468
- ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure)
758
+ const notifyError = ERRORS.TypedError(
759
+ HardwareErrorCode.BleCharacteristicNotifyChangeFailure
469
760
  );
761
+ this.runPromise.reject(notifyError);
762
+ this.rejectAllProtocolV2Frames(notifyError);
470
763
  Log?.debug(
471
764
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
472
765
  );
473
766
  return;
474
767
  }
475
- this.runPromise.reject(ERRORS.TypedError(ERROR));
768
+ const notifyError = ERRORS.TypedError(ERROR);
769
+ this.runPromise.reject(notifyError);
770
+ this.rejectAllProtocolV2Frames(notifyError);
476
771
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
477
772
  }
478
773
 
479
774
  return;
480
775
  }
481
776
 
777
+ if (!isCurrentMonitor) {
778
+ Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
779
+ return;
780
+ }
781
+
482
782
  if (!c) {
483
783
  throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
484
784
  }
485
785
 
486
786
  try {
487
787
  const data = Buffer.from(c.value as string, 'base64');
788
+ const protocol = this.deviceProtocol.get(uuid);
789
+ if (!protocol) {
790
+ Log?.debug('monitor data ignored before protocol detection: ', uuid);
791
+ return;
792
+ }
793
+ if (protocol === 'V2') {
794
+ this.handleProtocolV2Notification(uuid, new Uint8Array(data));
795
+ return;
796
+ }
488
797
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
489
798
  if (isHeaderChunk(data)) {
490
799
  bufferLength = data.readInt32BE(5);
@@ -493,7 +802,7 @@ export default class ReactNativeBleTransport {
493
802
  buffer = buffer.concat([...data]);
494
803
  }
495
804
 
496
- if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) {
805
+ if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
497
806
  const value = Buffer.from(buffer);
498
807
  // console.log(
499
808
  // '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
@@ -507,17 +816,41 @@ export default class ReactNativeBleTransport {
507
816
  }
508
817
  } catch (error) {
509
818
  Log?.debug('monitor data error: ', error);
510
- this.runPromise?.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
819
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
820
+ this.runPromise?.reject(notifyError);
821
+ this.rejectAllProtocolV2Frames(notifyError);
511
822
  }
512
- }, uuid);
823
+ }, notifyTransactionId);
513
824
 
514
825
  return subscription;
515
826
  }
516
827
 
517
- async release(uuid: string) {
828
+ async release(uuid: string, onclose = false) {
518
829
  const transport = transportCache[uuid];
830
+ if (this.runPromise) {
831
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
832
+ this.runPromise.reject(error);
833
+ this.runPromise = null;
834
+ this.rejectAllProtocolV2Frames(error);
835
+ this.activeProtocolV2Call = null;
836
+ } else {
837
+ this.resetProtocolV2Frames(uuid);
838
+ }
839
+
840
+ if (Platform.OS === 'android' && !onclose && transport) {
841
+ this.protocolV2Assemblers.get(uuid)?.reset();
842
+ this.resetProtocolV2Frames(uuid);
843
+ if (this.activeProtocolV2Call?.uuid === uuid) {
844
+ this.activeProtocolV2Call = null;
845
+ }
846
+ return Promise.resolve(true);
847
+ }
519
848
 
520
849
  if (transport) {
850
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
851
+ this.monitorTokens.delete(uuid);
852
+ }
853
+
521
854
  // Clean up disconnect subscription first to prevent callbacks on released transport
522
855
  Log?.debug('release: removing disconnect subscription for device: ', uuid);
523
856
  transport.disconnectSubscription?.remove();
@@ -531,12 +864,27 @@ export default class ReactNativeBleTransport {
531
864
  transport.notifySubscription?.remove();
532
865
  transport.notifySubscription = undefined;
533
866
 
867
+ if (transport.notifyTransactionId) {
868
+ try {
869
+ await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
870
+ } catch (e) {
871
+ Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
872
+ }
873
+ }
874
+
534
875
  delete transportCache[uuid];
876
+ }
535
877
 
536
- // Temporary close the Android disconnect after each request
537
- if (Platform.OS === 'android') {
538
- // await this.blePlxManager?.cancelDeviceConnection(uuid);
539
- }
878
+ this.deviceProtocol.delete(uuid);
879
+ this.deviceProtocolHints.delete(uuid);
880
+ this.protocolV2Assemblers.get(uuid)?.reset();
881
+ this.protocolV2Assemblers.delete(uuid);
882
+ this.resetProtocolV2Frames(uuid);
883
+
884
+ try {
885
+ await this.blePlxManager?.cancelTransaction(uuid);
886
+ } catch (e) {
887
+ Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
540
888
  }
541
889
 
542
890
  return Promise.resolve(true);
@@ -546,7 +894,12 @@ export default class ReactNativeBleTransport {
546
894
  await this.call(session, name, data);
547
895
  }
548
896
 
549
- async call(uuid: string, name: string, data: Record<string, unknown>) {
897
+ async call(
898
+ uuid: string,
899
+ name: string,
900
+ data: Record<string, unknown>,
901
+ options?: TransportCallOptions
902
+ ) {
550
903
  if (this.stopped) {
551
904
  // eslint-disable-next-line prefer-promise-reject-errors
552
905
  return Promise.reject(ERRORS.TypedError('Transport stopped.'));
@@ -562,13 +915,13 @@ export default class ReactNativeBleTransport {
562
915
  throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
563
916
  }
564
917
 
565
- const transport = transportCache[uuid];
566
- if (!transport) {
567
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
918
+ const protocol = this.getProtocolType(uuid);
919
+ if (!protocol) {
920
+ throw ERRORS.TypedError(
921
+ HardwareErrorCode.RuntimeError,
922
+ `Device protocol has not been detected for ${uuid}`
923
+ );
568
924
  }
569
-
570
- this.runPromise = createDeferred();
571
- const messages = this._messages;
572
925
  // Upload resources on low-end phones may OOM
573
926
  if (name === 'ResourceUpdate' || name === 'ResourceAck') {
574
927
  Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
@@ -576,12 +929,44 @@ export default class ReactNativeBleTransport {
576
929
  hash: data?.hash,
577
930
  });
578
931
  } else if (LogBlockCommand.has(name)) {
579
- Log?.debug('transport-react-native', 'call-', ' name: ', name);
932
+ Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
580
933
  } else {
581
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data);
934
+ Log?.debug(
935
+ 'transport-react-native',
936
+ 'call-',
937
+ ' name: ',
938
+ name,
939
+ ' data: ',
940
+ data,
941
+ ' protocol: ',
942
+ protocol
943
+ );
944
+ }
945
+
946
+ if (protocol === 'V2') {
947
+ return this.callProtocolV2(uuid, name, data, options);
948
+ }
949
+
950
+ return this.callProtocolV1(uuid, name, data, options);
951
+ }
952
+
953
+ private async callProtocolV1(
954
+ uuid: string,
955
+ name: string,
956
+ data: Record<string, unknown>,
957
+ options?: TransportCallOptions
958
+ ) {
959
+ if (!this._messages) {
960
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
582
961
  }
583
962
 
584
- const buffers = buildBuffers(messages, name, data);
963
+ const transport = this.getCachedTransport(uuid);
964
+ const runPromise = createDeferred<string>();
965
+ runPromise.promise.catch(() => undefined);
966
+ this.runPromise = runPromise;
967
+ const messages = this._messages;
968
+ const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
969
+ let timeout: ReturnType<typeof setTimeout> | undefined;
585
970
 
586
971
  async function writeChunkedData(
587
972
  buffers: ByteBuffer[],
@@ -652,20 +1037,41 @@ export default class ReactNativeBleTransport {
652
1037
  }
653
1038
 
654
1039
  try {
655
- const response = await this.runPromise.promise;
1040
+ const response = await Promise.race([
1041
+ runPromise.promise,
1042
+ new Promise<never>((_, reject) => {
1043
+ if (options?.timeoutMs) {
1044
+ timeout = setTimeout(() => {
1045
+ const error = ERRORS.TypedError(
1046
+ HardwareErrorCode.BleTimeoutError,
1047
+ `BLE response timeout after ${options.timeoutMs}ms for ${name}`
1048
+ );
1049
+ runPromise.reject(error);
1050
+ reject(error);
1051
+ }, options.timeoutMs);
1052
+ }
1053
+ }),
1054
+ ]);
656
1055
 
657
1056
  if (typeof response !== 'string') {
658
1057
  throw new Error('Returning data is not string.');
659
1058
  }
660
1059
 
661
1060
  Log?.debug('receive data: ', response);
662
- const jsonData = receiveOne(messages, response);
1061
+ const jsonData = ProtocolV1.decodeMessage(messages, response);
663
1062
  return check.call(jsonData);
664
1063
  } catch (e) {
665
- Log?.error('call error: ', e);
1064
+ if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1065
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1066
+ } else {
1067
+ Log?.error('call error: ', e);
1068
+ }
666
1069
  throw e;
667
1070
  } finally {
668
- this.runPromise = null;
1071
+ if (timeout) clearTimeout(timeout);
1072
+ if (this.runPromise === runPromise) {
1073
+ this.runPromise = null;
1074
+ }
669
1075
  }
670
1076
  }
671
1077
 
@@ -732,6 +1138,13 @@ export default class ReactNativeBleTransport {
732
1138
  if (transportCache[session]) {
733
1139
  delete transportCache[session];
734
1140
  }
1141
+ this.deviceProtocol.delete(session);
1142
+ this.deviceProtocolHints.delete(session);
1143
+ this.protocolV2Assemblers.delete(session);
1144
+ this.resetProtocolV2Frames(session);
1145
+ if (this.activeProtocolV2Call?.uuid === session) {
1146
+ this.activeProtocolV2Call = null;
1147
+ }
735
1148
 
736
1149
  // emit the disconnect event
737
1150
  try {
@@ -754,4 +1167,421 @@ export default class ReactNativeBleTransport {
754
1167
  }
755
1168
  this.runPromise = null;
756
1169
  }
1170
+
1171
+ private getCachedTransport(uuid: string) {
1172
+ const transport = transportCache[uuid];
1173
+ if (!transport) {
1174
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1175
+ }
1176
+ return transport;
1177
+ }
1178
+
1179
+ private createProtocolMismatchError(expected: ProtocolType) {
1180
+ return ERRORS.TypedError(
1181
+ HardwareErrorCode.RuntimeError,
1182
+ `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
1183
+ );
1184
+ }
1185
+
1186
+ private createProtocolDetectionError() {
1187
+ return ERRORS.TypedError(
1188
+ HardwareErrorCode.BleTimeoutError,
1189
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1190
+ );
1191
+ }
1192
+
1193
+ private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1194
+ if (this.deviceProtocol.get(uuid) === protocol) {
1195
+ this.deviceProtocol.delete(uuid);
1196
+ }
1197
+ }
1198
+
1199
+ private async detectProtocol(
1200
+ uuid: string,
1201
+ expectedProtocol?: ProtocolType,
1202
+ protocolHint?: ProtocolType
1203
+ ): Promise<ProtocolType> {
1204
+ if (expectedProtocol === 'V1') {
1205
+ if (await this.probeProtocolV1(uuid)) {
1206
+ this.deviceProtocol.set(uuid, 'V1');
1207
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1208
+ return 'V1';
1209
+ }
1210
+ throw this.createProtocolMismatchError(expectedProtocol);
1211
+ }
1212
+
1213
+ if (expectedProtocol === 'V2') {
1214
+ this.deviceProtocol.set(uuid, 'V2');
1215
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1216
+ return 'V2';
1217
+ }
1218
+
1219
+ if (protocolHint === 'V2') {
1220
+ this.deviceProtocol.set(uuid, 'V2');
1221
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (hint)`);
1222
+ return 'V2';
1223
+ }
1224
+
1225
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1226
+ this.deviceProtocol.set(uuid, 'V2');
1227
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
1228
+ return 'V2';
1229
+ }
1230
+
1231
+ const protocolV1Detected = await this.probeProtocolV1(uuid);
1232
+ if (protocolV1Detected) {
1233
+ this.deviceProtocol.set(uuid, 'V1');
1234
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1`);
1235
+ return 'V1';
1236
+ }
1237
+
1238
+ await this.resetProbeStateAfterProtocolProbe(uuid, 'V1');
1239
+ if (await this.probeProtocolV2(uuid)) {
1240
+ this.deviceProtocol.set(uuid, 'V2');
1241
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2`);
1242
+ return 'V2';
1243
+ }
1244
+
1245
+ this.deviceProtocol.delete(uuid);
1246
+ throw this.createProtocolDetectionError();
1247
+ }
1248
+
1249
+ private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1250
+ const transport = transportCache[uuid];
1251
+ this.protocolV2Assemblers.get(uuid)?.reset();
1252
+ this.resetProtocolV2Frames(uuid);
1253
+ if (this.activeProtocolV2Call?.uuid === uuid) {
1254
+ this.activeProtocolV2Call = null;
1255
+ }
1256
+ if (this.runPromise) {
1257
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1258
+ this.runPromise.reject(error);
1259
+ this.runPromise = null;
1260
+ }
1261
+
1262
+ if (!transport) return;
1263
+
1264
+ const previousNotifyTransactionId = transport.notifyTransactionId;
1265
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1266
+ this.monitorTokens.delete(uuid);
1267
+ }
1268
+ transport.notifySubscription?.remove();
1269
+ transport.notifySubscription = undefined;
1270
+ if (previousNotifyTransactionId) {
1271
+ try {
1272
+ await this.blePlxManager?.cancelTransaction(previousNotifyTransactionId);
1273
+ } catch (error) {
1274
+ Log?.debug(
1275
+ `[ReactNativeBleTransport] cancel notify after Protocol ${protocol} probe failed:`,
1276
+ error?.message || error
1277
+ );
1278
+ }
1279
+ }
1280
+
1281
+ const monitorToken = this.nextMonitorToken;
1282
+ this.nextMonitorToken += 1;
1283
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
1284
+ transport.monitorToken = monitorToken;
1285
+ transport.notifyTransactionId = notifyTransactionId;
1286
+ this.monitorTokens.set(uuid, monitorToken);
1287
+ transport.notifySubscription = this._monitorCharacteristic(
1288
+ transport.notifyCharacteristic,
1289
+ uuid,
1290
+ monitorToken,
1291
+ notifyTransactionId
1292
+ );
1293
+ if (Platform.OS === 'ios') {
1294
+ await new Promise<void>(resolve => {
1295
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
1296
+ });
1297
+ }
1298
+ }
1299
+
1300
+ private async probeProtocolV1(uuid: string) {
1301
+ if (!this._messages) {
1302
+ return false;
1303
+ }
1304
+
1305
+ try {
1306
+ this.deviceProtocol.set(uuid, 'V1');
1307
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1308
+ return true;
1309
+ } catch (error) {
1310
+ this.clearProbeProtocol(uuid, 'V1');
1311
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1312
+ return false;
1313
+ }
1314
+ }
1315
+
1316
+ private async probeProtocolV2(uuid: string) {
1317
+ if (!this._messages || !this._messagesV2) {
1318
+ return false;
1319
+ }
1320
+
1321
+ this.deviceProtocol.set(uuid, 'V2');
1322
+ this.protocolV2Assemblers.get(uuid)?.reset();
1323
+ const detected = await probeProtocolV2Helper({
1324
+ call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
1325
+ this.callProtocolV2(uuid, name, data, options),
1326
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
1327
+ logger: Log,
1328
+ logPrefix: 'ProtocolV2 RN-BLE',
1329
+ onProbeFailed: () => {
1330
+ this.protocolV2Assemblers.get(uuid)?.reset();
1331
+ this.resetProtocolV2Frames(uuid);
1332
+ },
1333
+ });
1334
+ if (!detected) {
1335
+ this.clearProbeProtocol(uuid, 'V2');
1336
+ }
1337
+ return detected;
1338
+ }
1339
+
1340
+ private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1341
+ try {
1342
+ if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1343
+ this.protocolV2Assemblers.get(uuid)?.reset();
1344
+ this.resetProtocolV2Frames(uuid);
1345
+ return;
1346
+ }
1347
+
1348
+ if (data.length === 0) return;
1349
+
1350
+ const assembler = this.protocolV2Assemblers.get(uuid);
1351
+ if (!assembler) return;
1352
+
1353
+ let frameData = assembler.push(data);
1354
+ while (frameData) {
1355
+ this.resolveProtocolV2Frame(uuid, frameData);
1356
+ frameData = assembler.push(new Uint8Array(0));
1357
+ }
1358
+ } catch (error) {
1359
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1360
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1361
+ this.runPromise?.reject(notifyError);
1362
+ this.rejectAllProtocolV2Frames(notifyError);
1363
+ }
1364
+ }
1365
+
1366
+ private getProtocolV2FrameQueue(uuid: string) {
1367
+ let queue = this.protocolV2FrameQueues.get(uuid);
1368
+ if (!queue) {
1369
+ queue = [];
1370
+ this.protocolV2FrameQueues.set(uuid, queue);
1371
+ }
1372
+ return queue;
1373
+ }
1374
+
1375
+ private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
1376
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1377
+ if (framePromise) {
1378
+ framePromise.resolve(frame);
1379
+ this.protocolV2FramePromises.delete(uuid);
1380
+ return;
1381
+ }
1382
+ this.getProtocolV2FrameQueue(uuid).push(frame);
1383
+ }
1384
+
1385
+ private rejectAllProtocolV2Frames(error: Error) {
1386
+ this.protocolV2FrameQueues.clear();
1387
+ for (const framePromise of this.protocolV2FramePromises.values()) {
1388
+ framePromise.reject(error);
1389
+ }
1390
+ this.protocolV2FramePromises.clear();
1391
+ }
1392
+
1393
+ private resetProtocolV2Frames(uuid: string) {
1394
+ this.protocolV2FrameQueues.delete(uuid);
1395
+ this.protocolV2FramePromises.delete(uuid);
1396
+ }
1397
+
1398
+ private isActiveProtocolV2Call(uuid: string, token: number) {
1399
+ return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
1400
+ }
1401
+
1402
+ private async readProtocolV2Frame(uuid: string) {
1403
+ const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
1404
+ if (queuedFrame) {
1405
+ return queuedFrame;
1406
+ }
1407
+
1408
+ const framePromise = createDeferred<Uint8Array>();
1409
+ this.protocolV2FramePromises.set(uuid, framePromise);
1410
+ try {
1411
+ return await framePromise.promise;
1412
+ } finally {
1413
+ if (this.protocolV2FramePromises.get(uuid) === framePromise) {
1414
+ this.protocolV2FramePromises.delete(uuid);
1415
+ }
1416
+ }
1417
+ }
1418
+
1419
+ private async writeProtocolV2Frame(
1420
+ transport: BleTransport,
1421
+ frame: Uint8Array,
1422
+ options?: { highVolume?: boolean; writeWithResponse?: boolean }
1423
+ ) {
1424
+ const tuning = getProtocolV2BleTuning();
1425
+ const packetCapacity = resolveProtocolV2PacketCapacity({
1426
+ platform: Platform.OS,
1427
+ iosPacketLength: tuning.iosPacketLength,
1428
+ androidPacketLength: tuning.androidPacketLength,
1429
+ mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1430
+ });
1431
+ const writeWithResponse =
1432
+ !!options?.writeWithResponse || (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
1433
+ const writeMode = resolveBleWriteMode(
1434
+ transport.writeCharacteristic,
1435
+ writeWithResponse ? 'withResponse' : 'withoutResponse'
1436
+ );
1437
+ const shouldThrottle = !!options?.highVolume && writeMode === 'withoutResponse';
1438
+ let packetsWritten = 0;
1439
+
1440
+ try {
1441
+ for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1442
+ const chunk = frame.slice(offset, offset + packetCapacity);
1443
+ const base64 = Buffer.from(chunk).toString('base64');
1444
+ if (writeMode === 'withResponse') {
1445
+ await transport.writeCharacteristic.writeWithResponse(base64);
1446
+ } else {
1447
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1448
+ }
1449
+ packetsWritten += 1;
1450
+
1451
+ if (
1452
+ shouldThrottle &&
1453
+ packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1454
+ offset + packetCapacity < frame.length
1455
+ ) {
1456
+ await delay(tuning.highVolumeWritePauseMs);
1457
+ }
1458
+ }
1459
+
1460
+ if (shouldThrottle) {
1461
+ await delay(tuning.highVolumeWriteFlushDelayMs);
1462
+ }
1463
+ } catch (error) {
1464
+ if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
1465
+ Log?.debug(
1466
+ '[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
1467
+ error
1468
+ );
1469
+ await this.writeProtocolV2Frame(transport, frame, {
1470
+ highVolume: true,
1471
+ writeWithResponse: true,
1472
+ });
1473
+ return;
1474
+ }
1475
+ throw error;
1476
+ }
1477
+ }
1478
+
1479
+ private async callProtocolV2(
1480
+ uuid: string,
1481
+ name: string,
1482
+ data: Record<string, unknown>,
1483
+ options?: TransportCallOptions
1484
+ ) {
1485
+ if (!this._messages || !this._messagesV2) {
1486
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1487
+ }
1488
+
1489
+ const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'GetProtoVersion';
1490
+ if (this.runPromise) {
1491
+ if (!forceRun) {
1492
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1493
+ }
1494
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1495
+ this.runPromise.reject(error);
1496
+ this.rejectAllProtocolV2Frames(error);
1497
+ this.runPromise = null;
1498
+ this.activeProtocolV2Call = null;
1499
+ }
1500
+
1501
+ const transport = this.getCachedTransport(uuid);
1502
+ const runPromise = createDeferred<Uint8Array>();
1503
+ runPromise.promise.catch(() => undefined);
1504
+ this.runPromise = runPromise;
1505
+ const callToken = this.nextProtocolV2CallToken++;
1506
+ this.activeProtocolV2Call = { uuid, token: callToken };
1507
+ this.protocolV2Assemblers.get(uuid)?.reset();
1508
+ this.resetProtocolV2Frames(uuid);
1509
+ let completed = false;
1510
+ const callOptions = {
1511
+ ...options,
1512
+ timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1513
+ };
1514
+ const highVolumeWrite = LogBlockCommand.has(name);
1515
+
1516
+ if (highVolumeWrite) {
1517
+ const tuning = getProtocolV2BleTuning();
1518
+ Log?.debug(
1519
+ '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1520
+ name,
1521
+ {
1522
+ packetCapacity:
1523
+ Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1524
+ burstSize: tuning.highVolumeWriteBurstSize,
1525
+ pauseMs: tuning.highVolumeWritePauseMs,
1526
+ flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1527
+ writeWithResponse: tuning.highVolumeWriteWithResponse,
1528
+ }
1529
+ );
1530
+ }
1531
+
1532
+ try {
1533
+ const session = new ProtocolV2Session({
1534
+ schemas: {
1535
+ protocolV1: this._messages,
1536
+ protocolV2: this._messagesV2,
1537
+ },
1538
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1539
+ writeFrame: async (frame: Uint8Array) => {
1540
+ await this.writeProtocolV2Frame(transport, frame, {
1541
+ highVolume: highVolumeWrite,
1542
+ });
1543
+ },
1544
+ readFrame: async () => {
1545
+ const rxFrame = await this.readProtocolV2Frame(uuid);
1546
+ if (!(rxFrame instanceof Uint8Array)) {
1547
+ throw new Error('Protocol V2 response is not Uint8Array');
1548
+ }
1549
+ return rxFrame;
1550
+ },
1551
+ logger: Log,
1552
+ logPrefix: 'ProtocolV2 RN-BLE',
1553
+ createTimeoutError: (_messageName: string, timeout: number) =>
1554
+ ERRORS.TypedError(
1555
+ HardwareErrorCode.BleTimeoutError,
1556
+ `BLE response timeout after ${timeout}ms for ${name}`
1557
+ ),
1558
+ });
1559
+
1560
+ const result = await session.call(name, data, callOptions);
1561
+ completed = true;
1562
+ return result;
1563
+ } catch (e) {
1564
+ if (this.isActiveProtocolV2Call(uuid, callToken)) {
1565
+ this.protocolV2Assemblers.get(uuid)?.reset();
1566
+ this.resetProtocolV2Frames(uuid);
1567
+ }
1568
+ Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1569
+ throw e;
1570
+ } finally {
1571
+ if (this.isActiveProtocolV2Call(uuid, callToken)) {
1572
+ if (!completed) {
1573
+ this.protocolV2Assemblers.get(uuid)?.reset();
1574
+ }
1575
+ this.resetProtocolV2Frames(uuid);
1576
+ this.activeProtocolV2Call = null;
1577
+ }
1578
+ if (this.runPromise === runPromise) {
1579
+ this.runPromise = null;
1580
+ }
1581
+ }
1582
+ }
1583
+
1584
+ getProtocolType(path: string): ProtocolType | undefined {
1585
+ return this.deviceProtocol.get(path);
1586
+ }
757
1587
  }