@onekeyfe/hd-transport-react-native 1.1.26 → 1.1.27-alpha.31

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