@onekeyfe/hd-transport-react-native 1.1.26-alpha.12 → 1.1.26-alpha.30

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,35 +9,136 @@ 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';
20
25
  import { subscribeBleOn } from './subscribeBleOn';
21
26
  import {
22
27
  ANDROID_PACKET_LENGTH,
23
28
  IOS_PACKET_LENGTH,
29
+ getBleUuidKey,
24
30
  getBluetoothServiceUuids,
25
31
  getInfosForServiceUuid,
32
+ isSameBleUuid,
26
33
  } from './constants';
27
34
  import { isHeaderChunk } from './utils/validateNotify';
28
35
  import BleTransport from './BleTransport';
29
36
  import timer from './utils/timer';
37
+ import { bleLogger, setBleLogger } from './logger';
30
38
 
31
39
  import type { Deferred } from '@onekeyfe/hd-shared';
32
40
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
33
41
  import type EventEmitter from 'events';
34
42
  import type { BleAcquireInput, TransportOptions } from './types';
35
43
 
36
- const { check, buildBuffers, receiveOne, parseConfigure } = transport;
44
+ const { check, ProtocolV1, parseConfigure } = transport;
37
45
 
38
- const Log = getLogger(LoggerNames.HdBleTransport);
46
+ const Log = bleLogger;
39
47
 
40
48
  const transportCache: Record<string, BleTransport> = {};
49
+ const BLE_RESPONSE_TIMEOUT_MS = 30_000;
50
+ const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
51
+ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
52
+ const DEVICE_SCAN_TIMEOUT_MS = 8000;
53
+ const IOS_NOTIFY_READY_DELAY_MS = 150;
54
+ const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
55
+ const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
56
+ const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
57
+
58
+ const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
59
+
60
+ export type ProtocolV2BleTuning = {
61
+ iosPacketLength?: number;
62
+ androidPacketLength?: number;
63
+ highVolumeWriteBurstSize?: number;
64
+ highVolumeWritePauseMs?: number;
65
+ highVolumeWriteFlushDelayMs?: number;
66
+ highVolumeWriteWithResponse?: boolean;
67
+ };
68
+
69
+ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
70
+
71
+ const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
72
+ iosPacketLength: IOS_PACKET_LENGTH,
73
+ androidPacketLength: ANDROID_PACKET_LENGTH,
74
+ highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
75
+ highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
76
+ highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
77
+ highVolumeWriteWithResponse: false,
78
+ };
79
+
80
+ let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
81
+
82
+ const normalizePositiveInteger = (value: unknown, fallback: number) => {
83
+ const normalized = Number(value);
84
+ if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
85
+ return Math.floor(normalized);
86
+ };
87
+
88
+ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
89
+ protocolV2BleTuning = {
90
+ iosPacketLength: normalizePositiveInteger(
91
+ tuning.iosPacketLength,
92
+ protocolV2BleTuning.iosPacketLength
93
+ ),
94
+ androidPacketLength: normalizePositiveInteger(
95
+ tuning.androidPacketLength,
96
+ protocolV2BleTuning.androidPacketLength
97
+ ),
98
+ highVolumeWriteBurstSize: normalizePositiveInteger(
99
+ tuning.highVolumeWriteBurstSize,
100
+ protocolV2BleTuning.highVolumeWriteBurstSize
101
+ ),
102
+ highVolumeWritePauseMs: normalizePositiveInteger(
103
+ tuning.highVolumeWritePauseMs,
104
+ protocolV2BleTuning.highVolumeWritePauseMs
105
+ ),
106
+ highVolumeWriteFlushDelayMs: normalizePositiveInteger(
107
+ tuning.highVolumeWriteFlushDelayMs,
108
+ protocolV2BleTuning.highVolumeWriteFlushDelayMs
109
+ ),
110
+ highVolumeWriteWithResponse:
111
+ tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
112
+ };
113
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
114
+ }
115
+
116
+ export function resetProtocolV2BleTuning() {
117
+ protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
118
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
119
+ }
120
+
121
+ export function getProtocolV2BleTuning() {
122
+ return { ...protocolV2BleTuning };
123
+ }
124
+
125
+ function inferProtocolTypeFromDeviceName(name?: string | null): ProtocolType | undefined {
126
+ return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
127
+ }
128
+
129
+ function getDeviceDisplayName(device?: Device | null) {
130
+ return device?.name || device?.localName || null;
131
+ }
132
+
133
+ function isGenericBleService(uuid?: string | null) {
134
+ return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
135
+ }
136
+
137
+ function hasKnownOneKeyService(device?: Device | null) {
138
+ return (device?.serviceUUIDs ?? []).some(serviceUuid =>
139
+ getInfosForServiceUuid(serviceUuid, 'classic')
140
+ );
141
+ }
41
142
 
42
143
  let connectOptions: Record<string, unknown> = {
43
144
  requestMTU: 256,
@@ -49,7 +150,8 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
49
150
 
50
151
  const tryToGetConfiguration = (device: Device) => {
51
152
  if (!device || !device.serviceUUIDs) return null;
52
- const [serviceUUID] = device.serviceUUIDs;
153
+ const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
154
+ if (!serviceUUID) return null;
53
155
  const infos = getInfosForServiceUuid(serviceUUID, 'classic');
54
156
  if (!infos) return null;
55
157
  return infos;
@@ -92,23 +194,39 @@ export default class ReactNativeBleTransport {
92
194
 
93
195
  _messages: ReturnType<typeof transport.parseConfigure> | undefined;
94
196
 
197
+ _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
198
+
95
199
  name = 'ReactNativeBleTransport';
96
200
 
97
201
  configured = false;
98
202
 
99
203
  stopped = false;
100
204
 
101
- scanTimeout = 3000;
205
+ scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
102
206
 
103
207
  runPromise: Deferred<any> | null = null;
104
208
 
105
209
  emitter?: EventEmitter;
106
210
 
211
+ /** Per-device protocol type detected by active wire-level probe after connect. */
212
+ private deviceProtocol: Map<string, ProtocolType> = new Map();
213
+
214
+ private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
215
+
216
+ private protocolV2FrameQueue: Uint8Array[] = [];
217
+
218
+ private protocolV2FramePromise: Deferred<Uint8Array> | null = null;
219
+
220
+ private monitorTokens: Map<string, number> = new Map();
221
+
222
+ private nextMonitorToken = 1;
223
+
107
224
  constructor(options: TransportOptions) {
108
- this.scanTimeout = options.scanTimeout ?? 3000;
225
+ this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
109
226
  }
110
227
 
111
- init(_logger: any, emitter: EventEmitter) {
228
+ init(logger: any, emitter: EventEmitter) {
229
+ setBleLogger(logger);
112
230
  this.emitter = emitter;
113
231
  }
114
232
 
@@ -118,6 +236,11 @@ export default class ReactNativeBleTransport {
118
236
  this._messages = messages;
119
237
  }
120
238
 
239
+ configureProtocolV2(signedData: any) {
240
+ this._messagesV2 = parseConfigure(signedData);
241
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
242
+ }
243
+
121
244
  listen() {
122
245
  // empty
123
246
  }
@@ -167,6 +290,7 @@ export default class ReactNativeBleTransport {
167
290
  blePlxManager.startDeviceScan(
168
291
  null,
169
292
  {
293
+ allowDuplicates: true,
170
294
  scanMode: ScanMode.LowLatency,
171
295
  },
172
296
  (error, device) => {
@@ -194,14 +318,41 @@ export default class ReactNativeBleTransport {
194
318
  return;
195
319
  }
196
320
 
197
- if (isOnekeyDevice(device?.name ?? null, device?.id)) {
321
+ const displayName = getDeviceDisplayName(device);
322
+ const isOneKey =
323
+ isOnekeyDevice(device?.name ?? null, device?.id) ||
324
+ isOnekeyDevice(device?.localName ?? null, device?.id) ||
325
+ hasKnownOneKeyService(device);
326
+ const shouldTraceCandidate =
327
+ !!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
328
+
329
+ if (shouldTraceCandidate) {
330
+ Log?.debug('[ReactNativeBleTransport] scan candidate', {
331
+ name: device?.name,
332
+ localName: device?.localName,
333
+ id: device?.id,
334
+ serviceUUIDs: device?.serviceUUIDs,
335
+ accepted: isOneKey,
336
+ });
337
+ }
338
+
339
+ if (isOneKey) {
198
340
  Log?.debug('search device start ======================');
199
- const { name, localName, id } = device ?? {};
341
+ const { name, localName, id, serviceUUIDs } = device ?? {};
200
342
  Log?.debug(
201
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${id ?? ''}`
343
+ `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
344
+ id ?? ''
345
+ }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
202
346
  );
203
347
  addDevice(device as unknown as Device);
204
348
  Log?.debug('search device end ======================\n');
349
+ } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
350
+ Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
351
+ name: device?.name,
352
+ localName: device?.localName,
353
+ id: device?.id,
354
+ serviceUUIDs: device?.serviceUUIDs,
355
+ });
205
356
  }
206
357
  }
207
358
  );
@@ -215,7 +366,14 @@ export default class ReactNativeBleTransport {
215
366
 
216
367
  const addDevice = (device: Device) => {
217
368
  if (deviceList.every(d => d.id !== device.id)) {
218
- deviceList.push({ ...device, commType: 'ble' } as IOneKeyDevice);
369
+ const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
370
+ const protocolType = inferProtocolTypeFromDeviceName(displayName);
371
+ deviceList.push({
372
+ ...device,
373
+ name: displayName,
374
+ commType: 'ble',
375
+ ...(protocolType ? { protocolType } : {}),
376
+ } as IOneKeyDevice);
219
377
  }
220
378
  };
221
379
 
@@ -227,7 +385,7 @@ export default class ReactNativeBleTransport {
227
385
  }
228
386
 
229
387
  async acquire(input: BleAcquireInput) {
230
- const { uuid, forceCleanRunPromise } = input;
388
+ const { uuid, forceCleanRunPromise, expectedProtocol } = input;
231
389
 
232
390
  if (!uuid) {
233
391
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
@@ -245,7 +403,10 @@ export default class ReactNativeBleTransport {
245
403
  }
246
404
 
247
405
  if (forceCleanRunPromise && this.runPromise) {
248
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
406
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
407
+ this.runPromise.reject(error);
408
+ this.rejectProtocolV2Frame(error);
409
+ this.runPromise = null;
249
410
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
250
411
  }
251
412
 
@@ -307,7 +468,7 @@ export default class ReactNativeBleTransport {
307
468
  Log?.debug('not connected, try to connect to device: ', uuid);
308
469
 
309
470
  try {
310
- await device.connect(connectOptions);
471
+ device = await device.connect(connectOptions);
311
472
  } catch (e) {
312
473
  Log?.debug('not connected, try to connect to device has error: ', e);
313
474
  if (
@@ -317,7 +478,7 @@ export default class ReactNativeBleTransport {
317
478
  connectOptions = {};
318
479
  Log?.debug('second try to reconnect without params');
319
480
  try {
320
- await device.connect();
481
+ device = await device.connect();
321
482
  } catch (e) {
322
483
  Log?.debug('last try to reconnect error: ', e);
323
484
  // last try to reconnect device if this issue exists
@@ -325,7 +486,7 @@ export default class ReactNativeBleTransport {
325
486
  if (e.errorCode === BleErrorCode.OperationCancelled) {
326
487
  Log?.debug('last try to reconnect');
327
488
  await device.cancelConnection();
328
- await device.connect();
489
+ device = await device.connect();
329
490
  }
330
491
  }
331
492
  } else {
@@ -337,6 +498,7 @@ export default class ReactNativeBleTransport {
337
498
  await device.discoverAllServicesAndCharacteristics();
338
499
  let infos = tryToGetConfiguration(device);
339
500
  let characteristics;
501
+ let fallbackServiceUuid: string | undefined;
340
502
 
341
503
  if (!infos) {
342
504
  for (const serviceUuid of getBluetoothServiceUuids()) {
@@ -351,16 +513,44 @@ export default class ReactNativeBleTransport {
351
513
  }
352
514
 
353
515
  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);
516
+ const services = await device.services();
517
+ Log?.debug(
518
+ '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
519
+ services?.map(service => service.uuid)
520
+ );
521
+
522
+ const knownService = services.find(service =>
523
+ getInfosForServiceUuid(service.uuid, 'classic')
524
+ );
525
+ const fallbackService =
526
+ knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
527
+
528
+ if (fallbackService) {
529
+ fallbackServiceUuid = fallbackService.uuid;
530
+ characteristics = await device.characteristicsForService(fallbackService.uuid);
531
+ Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
359
532
  }
360
- throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
361
533
  }
362
534
 
363
- const { serviceUuid, writeUuid, notifyUuid } = infos;
535
+ if (!infos) {
536
+ if (!fallbackServiceUuid) {
537
+ try {
538
+ Log?.debug('cancel connection when service not found');
539
+ await device.cancelConnection();
540
+ } catch (e) {
541
+ Log?.debug('cancel connection error when service not found: ', e.message || e.reason);
542
+ }
543
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
544
+ }
545
+ }
546
+
547
+ const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
548
+ const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
549
+ const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
550
+
551
+ if (!serviceUuid) {
552
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
553
+ }
364
554
 
365
555
  if (!characteristics) {
366
556
  characteristics = await device.characteristicsForService(serviceUuid);
@@ -373,9 +563,9 @@ export default class ReactNativeBleTransport {
373
563
  let writeCharacteristic;
374
564
  let notifyCharacteristic;
375
565
  for (const c of characteristics) {
376
- if (c.uuid === writeUuid) {
566
+ if (isSameBleUuid(c.uuid, writeUuid)) {
377
567
  writeCharacteristic = c;
378
- } else if (c.uuid === notifyUuid) {
568
+ } else if (isSameBleUuid(c.uuid, notifyUuid)) {
379
569
  notifyCharacteristic = c;
380
570
  }
381
571
  }
@@ -402,12 +592,28 @@ export default class ReactNativeBleTransport {
402
592
  await this.release(uuid);
403
593
 
404
594
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
595
+ const monitorToken = this.nextMonitorToken;
596
+ this.nextMonitorToken += 1;
597
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
598
+ transport.monitorToken = monitorToken;
599
+ transport.notifyTransactionId = notifyTransactionId;
600
+ this.monitorTokens.set(uuid, monitorToken);
405
601
  transport.notifySubscription = this._monitorCharacteristic(
406
602
  transport.notifyCharacteristic,
407
- uuid
603
+ uuid,
604
+ monitorToken,
605
+ notifyTransactionId
408
606
  );
409
607
  transportCache[uuid] = transport;
410
608
 
609
+ this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
610
+
611
+ if (Platform.OS === 'ios') {
612
+ await new Promise<void>(resolve => setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS));
613
+ }
614
+
615
+ const protocolType = await this.detectProtocol(uuid, expectedProtocol);
616
+
411
617
  this.emitter?.emit('device-connect', {
412
618
  name: device.name,
413
619
  id: device.id,
@@ -415,6 +621,11 @@ export default class ReactNativeBleTransport {
415
621
  });
416
622
 
417
623
  transport.disconnectSubscription = device.onDisconnected(() => {
624
+ if (transportCache[uuid] !== transport) {
625
+ Log?.debug('device disconnect ignored for stale transport: ', device?.id);
626
+ return;
627
+ }
628
+
418
629
  try {
419
630
  Log?.debug('device disconnect: ', device?.id);
420
631
  this.emitter?.emit('device-disconnect', {
@@ -423,7 +634,9 @@ export default class ReactNativeBleTransport {
423
634
  connectId: device?.id,
424
635
  });
425
636
  if (this.runPromise) {
426
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError));
637
+ const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
638
+ this.runPromise.reject(error);
639
+ this.rejectProtocolV2Frame(error);
427
640
  }
428
641
  } catch (e) {
429
642
  Log?.debug('device disconnect error: ', e);
@@ -432,19 +645,29 @@ export default class ReactNativeBleTransport {
432
645
  }
433
646
  });
434
647
 
435
- return { uuid };
648
+ return { uuid, protocolType };
436
649
  }
437
650
 
438
- _monitorCharacteristic(characteristic: Characteristic, uuid: string): Subscription {
651
+ _monitorCharacteristic(
652
+ characteristic: Characteristic,
653
+ uuid: string,
654
+ monitorToken: number,
655
+ notifyTransactionId: string
656
+ ): Subscription {
439
657
  let bufferLength = 0;
440
658
  let buffer: any[] = [];
441
659
  const subscription = characteristic.monitor((error, c) => {
660
+ const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
442
661
  if (error) {
443
662
  Log?.debug(
444
663
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
445
664
  error as unknown as string
446
665
  }`
447
666
  );
667
+ if (!isCurrentMonitor) {
668
+ Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
669
+ return;
670
+ }
448
671
  if (this.runPromise) {
449
672
  let ERROR:
450
673
  | typeof HardwareErrorCode.BleDeviceBondError
@@ -464,27 +687,40 @@ export default class ReactNativeBleTransport {
464
687
  error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
465
688
  error.reason?.includes('notify change failed for device')
466
689
  ) {
467
- this.runPromise.reject(
468
- ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure)
690
+ const notifyError = ERRORS.TypedError(
691
+ HardwareErrorCode.BleCharacteristicNotifyChangeFailure
469
692
  );
693
+ this.runPromise.reject(notifyError);
694
+ this.rejectProtocolV2Frame(notifyError);
470
695
  Log?.debug(
471
696
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
472
697
  );
473
698
  return;
474
699
  }
475
- this.runPromise.reject(ERRORS.TypedError(ERROR));
700
+ const notifyError = ERRORS.TypedError(ERROR);
701
+ this.runPromise.reject(notifyError);
702
+ this.rejectProtocolV2Frame(notifyError);
476
703
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
477
704
  }
478
705
 
479
706
  return;
480
707
  }
481
708
 
709
+ if (!isCurrentMonitor) {
710
+ Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
711
+ return;
712
+ }
713
+
482
714
  if (!c) {
483
715
  throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
484
716
  }
485
717
 
486
718
  try {
487
719
  const data = Buffer.from(c.value as string, 'base64');
720
+ if (this.deviceProtocol.get(uuid) === 'V2') {
721
+ this.handleProtocolV2Notification(uuid, new Uint8Array(data));
722
+ return;
723
+ }
488
724
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
489
725
  if (isHeaderChunk(data)) {
490
726
  bufferLength = data.readInt32BE(5);
@@ -493,7 +729,7 @@ export default class ReactNativeBleTransport {
493
729
  buffer = buffer.concat([...data]);
494
730
  }
495
731
 
496
- if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) {
732
+ if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
497
733
  const value = Buffer.from(buffer);
498
734
  // console.log(
499
735
  // '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
@@ -507,17 +743,31 @@ export default class ReactNativeBleTransport {
507
743
  }
508
744
  } catch (error) {
509
745
  Log?.debug('monitor data error: ', error);
510
- this.runPromise?.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
746
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
747
+ this.runPromise?.reject(notifyError);
748
+ this.rejectProtocolV2Frame(notifyError);
511
749
  }
512
- }, uuid);
750
+ }, notifyTransactionId);
513
751
 
514
752
  return subscription;
515
753
  }
516
754
 
517
755
  async release(uuid: string) {
518
756
  const transport = transportCache[uuid];
757
+ if (this.runPromise) {
758
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
759
+ this.runPromise.reject(error);
760
+ this.runPromise = null;
761
+ this.rejectProtocolV2Frame(error);
762
+ } else {
763
+ this.resetProtocolV2Frames();
764
+ }
519
765
 
520
766
  if (transport) {
767
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
768
+ this.monitorTokens.delete(uuid);
769
+ }
770
+
521
771
  // Clean up disconnect subscription first to prevent callbacks on released transport
522
772
  Log?.debug('release: removing disconnect subscription for device: ', uuid);
523
773
  transport.disconnectSubscription?.remove();
@@ -531,12 +781,25 @@ export default class ReactNativeBleTransport {
531
781
  transport.notifySubscription?.remove();
532
782
  transport.notifySubscription = undefined;
533
783
 
784
+ if (transport.notifyTransactionId) {
785
+ try {
786
+ await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
787
+ } catch (e) {
788
+ Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
789
+ }
790
+ }
791
+
534
792
  delete transportCache[uuid];
793
+ this.deviceProtocol.delete(uuid);
794
+ }
535
795
 
536
- // Temporary close the Android disconnect after each request
537
- if (Platform.OS === 'android') {
538
- // await this.blePlxManager?.cancelDeviceConnection(uuid);
539
- }
796
+ this.protocolV2Assemblers.get(uuid)?.reset();
797
+ this.protocolV2Assemblers.delete(uuid);
798
+
799
+ try {
800
+ await this.blePlxManager?.cancelTransaction(uuid);
801
+ } catch (e) {
802
+ Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
540
803
  }
541
804
 
542
805
  return Promise.resolve(true);
@@ -546,7 +809,12 @@ export default class ReactNativeBleTransport {
546
809
  await this.call(session, name, data);
547
810
  }
548
811
 
549
- async call(uuid: string, name: string, data: Record<string, unknown>) {
812
+ async call(
813
+ uuid: string,
814
+ name: string,
815
+ data: Record<string, unknown>,
816
+ options?: TransportCallOptions
817
+ ) {
550
818
  if (this.stopped) {
551
819
  // eslint-disable-next-line prefer-promise-reject-errors
552
820
  return Promise.reject(ERRORS.TypedError('Transport stopped.'));
@@ -562,13 +830,7 @@ export default class ReactNativeBleTransport {
562
830
  throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
563
831
  }
564
832
 
565
- const transport = transportCache[uuid];
566
- if (!transport) {
567
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
568
- }
569
-
570
- this.runPromise = createDeferred();
571
- const messages = this._messages;
833
+ const protocol = this.getProtocolType(uuid);
572
834
  // Upload resources on low-end phones may OOM
573
835
  if (name === 'ResourceUpdate' || name === 'ResourceAck') {
574
836
  Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
@@ -576,12 +838,44 @@ export default class ReactNativeBleTransport {
576
838
  hash: data?.hash,
577
839
  });
578
840
  } else if (LogBlockCommand.has(name)) {
579
- Log?.debug('transport-react-native', 'call-', ' name: ', name);
841
+ Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
580
842
  } else {
581
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data);
843
+ Log?.debug(
844
+ 'transport-react-native',
845
+ 'call-',
846
+ ' name: ',
847
+ name,
848
+ ' data: ',
849
+ data,
850
+ ' protocol: ',
851
+ protocol
852
+ );
582
853
  }
583
854
 
584
- const buffers = buildBuffers(messages, name, data);
855
+ if (protocol === 'V2') {
856
+ return this.callProtocolV2(uuid, name, data, options);
857
+ }
858
+
859
+ return this.callProtocolV1(uuid, name, data, options);
860
+ }
861
+
862
+ private async callProtocolV1(
863
+ uuid: string,
864
+ name: string,
865
+ data: Record<string, unknown>,
866
+ options?: TransportCallOptions
867
+ ) {
868
+ if (!this._messages) {
869
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
870
+ }
871
+
872
+ const transport = this.getCachedTransport(uuid);
873
+ const runPromise = createDeferred<string>();
874
+ runPromise.promise.catch(() => undefined);
875
+ this.runPromise = runPromise;
876
+ const messages = this._messages;
877
+ const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
878
+ let timeout: ReturnType<typeof setTimeout> | undefined;
585
879
 
586
880
  async function writeChunkedData(
587
881
  buffers: ByteBuffer[],
@@ -652,20 +946,41 @@ export default class ReactNativeBleTransport {
652
946
  }
653
947
 
654
948
  try {
655
- const response = await this.runPromise.promise;
949
+ const response = await Promise.race([
950
+ runPromise.promise,
951
+ new Promise<never>((_, reject) => {
952
+ if (options?.timeoutMs) {
953
+ timeout = setTimeout(() => {
954
+ const error = ERRORS.TypedError(
955
+ HardwareErrorCode.BleTimeoutError,
956
+ `BLE response timeout after ${options.timeoutMs}ms for ${name}`
957
+ );
958
+ runPromise.reject(error);
959
+ reject(error);
960
+ }, options.timeoutMs);
961
+ }
962
+ }),
963
+ ]);
656
964
 
657
965
  if (typeof response !== 'string') {
658
966
  throw new Error('Returning data is not string.');
659
967
  }
660
968
 
661
969
  Log?.debug('receive data: ', response);
662
- const jsonData = receiveOne(messages, response);
970
+ const jsonData = ProtocolV1.decodeMessage(messages, response);
663
971
  return check.call(jsonData);
664
972
  } catch (e) {
665
- Log?.error('call error: ', e);
973
+ if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
974
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
975
+ } else {
976
+ Log?.error('call error: ', e);
977
+ }
666
978
  throw e;
667
979
  } finally {
668
- this.runPromise = null;
980
+ if (timeout) clearTimeout(timeout);
981
+ if (this.runPromise === runPromise) {
982
+ this.runPromise = null;
983
+ }
669
984
  }
670
985
  }
671
986
 
@@ -732,6 +1047,8 @@ export default class ReactNativeBleTransport {
732
1047
  if (transportCache[session]) {
733
1048
  delete transportCache[session];
734
1049
  }
1050
+ this.deviceProtocol.delete(session);
1051
+ this.protocolV2Assemblers.delete(session);
735
1052
 
736
1053
  // emit the disconnect event
737
1054
  try {
@@ -754,4 +1071,306 @@ export default class ReactNativeBleTransport {
754
1071
  }
755
1072
  this.runPromise = null;
756
1073
  }
1074
+
1075
+ private getCachedTransport(uuid: string) {
1076
+ const transport = transportCache[uuid];
1077
+ if (!transport) {
1078
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1079
+ }
1080
+ return transport;
1081
+ }
1082
+
1083
+ private createProtocolMismatchError(expected: ProtocolType) {
1084
+ return ERRORS.TypedError(
1085
+ HardwareErrorCode.RuntimeError,
1086
+ `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
1087
+ );
1088
+ }
1089
+
1090
+ private async detectProtocol(
1091
+ uuid: string,
1092
+ expectedProtocol?: ProtocolType
1093
+ ): Promise<ProtocolType> {
1094
+ if (expectedProtocol === 'V1') {
1095
+ if (await this.probeProtocolV1(uuid)) {
1096
+ this.deviceProtocol.set(uuid, 'V1');
1097
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1098
+ return 'V1';
1099
+ }
1100
+ throw this.createProtocolMismatchError(expectedProtocol);
1101
+ }
1102
+
1103
+ if (expectedProtocol === 'V2') {
1104
+ if (await this.probeProtocolV2(uuid)) {
1105
+ this.deviceProtocol.set(uuid, 'V2');
1106
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1107
+ return 'V2';
1108
+ }
1109
+ throw this.createProtocolMismatchError(expectedProtocol);
1110
+ }
1111
+
1112
+ if (this.deviceProtocol.get(uuid) === 'V2' && (await this.probeProtocolV2(uuid))) {
1113
+ this.deviceProtocol.set(uuid, 'V2');
1114
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
1115
+ return 'V2';
1116
+ }
1117
+
1118
+ let protocol: ProtocolType = 'V1';
1119
+ if (!(await this.probeProtocolV1(uuid)) && (await this.probeProtocolV2(uuid))) {
1120
+ protocol = 'V2';
1121
+ }
1122
+ this.deviceProtocol.set(uuid, protocol);
1123
+ Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1124
+ return protocol;
1125
+ }
1126
+
1127
+ private async probeProtocolV1(uuid: string) {
1128
+ if (!this._messages) {
1129
+ return false;
1130
+ }
1131
+
1132
+ try {
1133
+ this.deviceProtocol.set(uuid, 'V1');
1134
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1135
+ return true;
1136
+ } catch (error) {
1137
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1138
+ return false;
1139
+ }
1140
+ }
1141
+
1142
+ private async probeProtocolV2(uuid: string) {
1143
+ if (!this._messages || !this._messagesV2) {
1144
+ return false;
1145
+ }
1146
+
1147
+ this.deviceProtocol.set(uuid, 'V2');
1148
+ this.protocolV2Assemblers.get(uuid)?.reset();
1149
+ return probeProtocolV2Helper({
1150
+ call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
1151
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
1152
+ logger: Log,
1153
+ logPrefix: 'ProtocolV2 RN-BLE',
1154
+ onProbeFailed: () => {
1155
+ this.protocolV2Assemblers.get(uuid)?.reset();
1156
+ this.resetProtocolV2Frames();
1157
+ },
1158
+ });
1159
+ }
1160
+
1161
+ private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1162
+ try {
1163
+ if (!this.runPromise) {
1164
+ this.protocolV2Assemblers.get(uuid)?.reset();
1165
+ this.resetProtocolV2Frames();
1166
+ return;
1167
+ }
1168
+
1169
+ if (data.length === 0) return;
1170
+
1171
+ const assembler = this.protocolV2Assemblers.get(uuid);
1172
+ if (!assembler) return;
1173
+
1174
+ let frameData = assembler.push(data);
1175
+ while (frameData) {
1176
+ this.resolveProtocolV2Frame(frameData);
1177
+ frameData = assembler.push(new Uint8Array(0));
1178
+ }
1179
+ } catch (error) {
1180
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1181
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1182
+ this.runPromise?.reject(notifyError);
1183
+ this.rejectProtocolV2Frame(notifyError);
1184
+ }
1185
+ }
1186
+
1187
+ private resolveProtocolV2Frame(frame: Uint8Array) {
1188
+ if (this.protocolV2FramePromise) {
1189
+ this.protocolV2FramePromise.resolve(frame);
1190
+ this.protocolV2FramePromise = null;
1191
+ return;
1192
+ }
1193
+ this.protocolV2FrameQueue.push(frame);
1194
+ }
1195
+
1196
+ private rejectProtocolV2Frame(error: Error) {
1197
+ this.protocolV2FrameQueue = [];
1198
+ if (this.protocolV2FramePromise) {
1199
+ this.protocolV2FramePromise.reject(error);
1200
+ this.protocolV2FramePromise = null;
1201
+ }
1202
+ }
1203
+
1204
+ private resetProtocolV2Frames() {
1205
+ this.protocolV2FrameQueue = [];
1206
+ this.protocolV2FramePromise = null;
1207
+ }
1208
+
1209
+ private async readProtocolV2Frame() {
1210
+ const queuedFrame = this.protocolV2FrameQueue.shift();
1211
+ if (queuedFrame) {
1212
+ return queuedFrame;
1213
+ }
1214
+
1215
+ const framePromise = createDeferred<Uint8Array>();
1216
+ this.protocolV2FramePromise = framePromise;
1217
+ try {
1218
+ return await framePromise.promise;
1219
+ } finally {
1220
+ if (this.protocolV2FramePromise === framePromise) {
1221
+ this.protocolV2FramePromise = null;
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ private async writeProtocolV2Frame(
1227
+ transport: BleTransport,
1228
+ frame: Uint8Array,
1229
+ options?: { highVolume?: boolean; writeWithResponse?: boolean }
1230
+ ) {
1231
+ const tuning = getProtocolV2BleTuning();
1232
+ const packetCapacity =
1233
+ Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength;
1234
+ const writeWithResponse =
1235
+ !!options?.writeWithResponse ||
1236
+ (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
1237
+ const shouldThrottle = !!options?.highVolume && !writeWithResponse;
1238
+ let packetsWritten = 0;
1239
+
1240
+ try {
1241
+ for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1242
+ const chunk = frame.slice(offset, offset + packetCapacity);
1243
+ const base64 = Buffer.from(chunk).toString('base64');
1244
+ if (writeWithResponse) {
1245
+ await transport.writeCharacteristic.writeWithResponse(base64);
1246
+ } else {
1247
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1248
+ }
1249
+ packetsWritten += 1;
1250
+
1251
+ if (
1252
+ shouldThrottle &&
1253
+ packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1254
+ offset + packetCapacity < frame.length
1255
+ ) {
1256
+ await delay(tuning.highVolumeWritePauseMs);
1257
+ }
1258
+ }
1259
+
1260
+ if (shouldThrottle) {
1261
+ await delay(tuning.highVolumeWriteFlushDelayMs);
1262
+ }
1263
+ } catch (error) {
1264
+ if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
1265
+ Log?.debug(
1266
+ '[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
1267
+ error
1268
+ );
1269
+ await this.writeProtocolV2Frame(transport, frame, {
1270
+ highVolume: true,
1271
+ writeWithResponse: true,
1272
+ });
1273
+ return;
1274
+ }
1275
+ throw error;
1276
+ }
1277
+ }
1278
+
1279
+ private async callProtocolV2(
1280
+ uuid: string,
1281
+ name: string,
1282
+ data: Record<string, unknown>,
1283
+ options?: TransportCallOptions
1284
+ ) {
1285
+ if (!this._messages || !this._messagesV2) {
1286
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1287
+ }
1288
+
1289
+ const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'GetProtoVersion';
1290
+ if (this.runPromise) {
1291
+ if (!forceRun) {
1292
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1293
+ }
1294
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1295
+ this.runPromise.reject(error);
1296
+ this.rejectProtocolV2Frame(error);
1297
+ this.runPromise = null;
1298
+ }
1299
+
1300
+ const transport = this.getCachedTransport(uuid);
1301
+ const runPromise = createDeferred<Uint8Array>();
1302
+ runPromise.promise.catch(() => undefined);
1303
+ this.runPromise = runPromise;
1304
+ this.protocolV2Assemblers.get(uuid)?.reset();
1305
+ this.resetProtocolV2Frames();
1306
+ let completed = false;
1307
+ const callOptions = {
1308
+ ...options,
1309
+ timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1310
+ };
1311
+ const highVolumeWrite = LogBlockCommand.has(name);
1312
+
1313
+ if (highVolumeWrite) {
1314
+ const tuning = getProtocolV2BleTuning();
1315
+ Log?.debug(
1316
+ '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1317
+ name,
1318
+ {
1319
+ packetCapacity:
1320
+ Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1321
+ burstSize: tuning.highVolumeWriteBurstSize,
1322
+ pauseMs: tuning.highVolumeWritePauseMs,
1323
+ flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1324
+ writeWithResponse: tuning.highVolumeWriteWithResponse,
1325
+ }
1326
+ );
1327
+ }
1328
+
1329
+ try {
1330
+ const session = new ProtocolV2Session({
1331
+ schemas: {
1332
+ protocolV1: this._messages,
1333
+ protocolV2: this._messagesV2,
1334
+ },
1335
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1336
+ writeFrame: (frame: Uint8Array) =>
1337
+ this.writeProtocolV2Frame(transport, frame, { highVolume: highVolumeWrite }),
1338
+ readFrame: async () => {
1339
+ const rxFrame = await this.readProtocolV2Frame();
1340
+ if (!(rxFrame instanceof Uint8Array)) {
1341
+ throw new Error('Protocol V2 response is not Uint8Array');
1342
+ }
1343
+ return rxFrame;
1344
+ },
1345
+ logger: Log,
1346
+ logPrefix: 'ProtocolV2 RN-BLE',
1347
+ createTimeoutError: (_messageName: string, timeout: number) =>
1348
+ ERRORS.TypedError(
1349
+ HardwareErrorCode.BleTimeoutError,
1350
+ `BLE response timeout after ${timeout}ms for ${name}`
1351
+ ),
1352
+ });
1353
+
1354
+ const result = await session.call(name, data, callOptions);
1355
+ completed = true;
1356
+ return result;
1357
+ } catch (e) {
1358
+ this.protocolV2Assemblers.get(uuid)?.reset();
1359
+ this.resetProtocolV2Frames();
1360
+ Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1361
+ throw e;
1362
+ } finally {
1363
+ if (!completed) {
1364
+ this.protocolV2Assemblers.get(uuid)?.reset();
1365
+ }
1366
+ this.resetProtocolV2Frames();
1367
+ if (this.runPromise === runPromise) {
1368
+ this.runPromise = null;
1369
+ }
1370
+ }
1371
+ }
1372
+
1373
+ getProtocolType(path: string): ProtocolType {
1374
+ return this.deviceProtocol.get(path) ?? 'V1';
1375
+ }
757
1376
  }