@onekeyfe/hd-transport-react-native 1.1.34-alpha.2 → 1.1.34-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -9,33 +9,44 @@ 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_BLE_FRAME_MAX_BYTES,
16
+ PROTOCOL_V2_CHANNEL_BLE_UART,
17
+ type ProtocolType,
18
+ ProtocolV2FrameAssembler,
19
+ ProtocolV2LinkManager,
20
+ type TransportCallOptions,
21
+ probeProtocolV2 as probeProtocolV2Helper,
15
22
  } from '@onekeyfe/hd-transport';
16
23
  import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
17
- import { LoggerNames, getLogger } from '@onekeyfe/hd-core';
18
24
 
19
25
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
26
+ import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
20
27
  import { subscribeBleOn } from './subscribeBleOn';
21
28
  import {
22
29
  ANDROID_PACKET_LENGTH,
23
30
  IOS_PACKET_LENGTH,
31
+ getBleUuidKey,
24
32
  getBluetoothServiceUuids,
25
33
  getInfosForServiceUuid,
34
+ isSameBleUuid,
26
35
  } from './constants';
27
36
  import { isHeaderChunk } from './utils/validateNotify';
28
37
  import BleTransport from './BleTransport';
29
38
  import timer from './utils/timer';
39
+ import { bleLogger, setBleLogger } from './logger';
40
+ import { createTransportCallLog } from './transportLog';
30
41
 
31
42
  import type { Deferred } from '@onekeyfe/hd-shared';
32
43
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
33
44
  import type EventEmitter from 'events';
34
45
  import type { BleAcquireInput, TransportOptions } from './types';
35
46
 
36
- const { check, buildBuffers, receiveOne, parseConfigure } = transport;
47
+ const { check, ProtocolV1, parseConfigure } = transport;
37
48
 
38
- const Log = getLogger(LoggerNames.HdBleTransport);
49
+ const Log = bleLogger;
39
50
 
40
51
  const transportCache: Record<string, BleTransport> = {};
41
52
  const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
@@ -54,11 +65,6 @@ type ResolvedBleCharacteristics = {
54
65
  notifyCharacteristic: Characteristic;
55
66
  };
56
67
 
57
- const getBleIdentityName = (device?: { name?: string | null } | null): string | null => {
58
- const localName = (device as { localName?: string | null } | undefined)?.localName;
59
- return device?.name ?? localName ?? null;
60
- };
61
-
62
68
  const delay = (ms: number) =>
63
69
  new Promise<void>(resolve => {
64
70
  setTimeout(resolve, ms);
@@ -97,9 +103,76 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
97
103
 
98
104
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
99
105
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
106
+ const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
107
+ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
108
+ const DEVICE_SCAN_TIMEOUT_MS = 8000;
109
+ const IOS_NOTIFY_READY_DELAY_MS = 150;
110
+ const ANDROID_NOTIFY_READY_DELAY_MS = 300;
111
+ export type ProtocolV2BleTuning = {
112
+ iosPacketLength?: number;
113
+ androidPacketLength?: number;
114
+ };
115
+
116
+ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
117
+
118
+ const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
119
+ iosPacketLength: IOS_PACKET_LENGTH,
120
+ androidPacketLength: ANDROID_PACKET_LENGTH,
121
+ };
122
+
123
+ let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
124
+
125
+ const normalizePositiveInteger = (value: unknown, fallback: number) => {
126
+ const normalized = Number(value);
127
+ if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
128
+ return Math.floor(normalized);
129
+ };
130
+
131
+ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
132
+ protocolV2BleTuning = {
133
+ iosPacketLength: normalizePositiveInteger(
134
+ tuning.iosPacketLength,
135
+ protocolV2BleTuning.iosPacketLength
136
+ ),
137
+ androidPacketLength: normalizePositiveInteger(
138
+ tuning.androidPacketLength,
139
+ protocolV2BleTuning.androidPacketLength
140
+ ),
141
+ };
142
+ Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
143
+ }
144
+
145
+ export function resetProtocolV2BleTuning() {
146
+ protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
147
+ Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
148
+ }
149
+
150
+ export function getProtocolV2BleTuning() {
151
+ return { ...protocolV2BleTuning };
152
+ }
153
+
154
+ function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
155
+ return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
156
+ }
157
+
158
+ function getDeviceDisplayName(device?: Device | null) {
159
+ return device?.name || device?.localName || null;
160
+ }
161
+
162
+ function isGenericBleService(uuid?: string | null) {
163
+ return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
164
+ }
100
165
 
101
- let connectOptions: Record<string, unknown> = {
102
- requestMTU: 256,
166
+ function hasKnownOneKeyService(device?: Device | null) {
167
+ return (device?.serviceUUIDs ?? []).some(serviceUuid =>
168
+ getInfosForServiceUuid(serviceUuid, 'classic')
169
+ );
170
+ }
171
+
172
+ const ANDROID_REQUEST_MTU = 256;
173
+
174
+ const connectOptions: Record<string, unknown> = {
175
+ requestMTU: ANDROID_REQUEST_MTU,
103
176
  timeout: 3000,
104
177
  refreshGatt: 'OnConnected',
105
178
  };
@@ -108,12 +181,30 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
108
181
 
109
182
  const tryToGetConfiguration = (device: Device) => {
110
183
  if (!device || !device.serviceUUIDs) return null;
111
- const [serviceUUID] = device.serviceUUIDs;
184
+ const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
185
+ if (!serviceUUID) return null;
112
186
  const infos = getInfosForServiceUuid(serviceUUID, 'classic');
113
187
  if (!infos) return null;
114
188
  return infos;
115
189
  };
116
190
 
191
+ const requestAndroidMtu = async (device: Device) => {
192
+ if (Platform.OS !== 'android') return device;
193
+
194
+ try {
195
+ const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
196
+ Log?.debug('[ReactNativeBleTransport] MTU configured', {
197
+ deviceId: device.id,
198
+ requested: ANDROID_REQUEST_MTU,
199
+ actual: mtuDevice.mtu,
200
+ });
201
+ return mtuDevice;
202
+ } catch (error) {
203
+ Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
204
+ return device;
205
+ }
206
+ };
207
+
117
208
  type IOBleErrorRemap = Error | BleError | null | undefined;
118
209
 
119
210
  function remapError(error: IOBleErrorRemap) {
@@ -151,13 +242,15 @@ export default class ReactNativeBleTransport {
151
242
 
152
243
  _messages: ReturnType<typeof transport.parseConfigure> | undefined;
153
244
 
245
+ _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
246
+
154
247
  name = 'ReactNativeBleTransport';
155
248
 
156
249
  configured = false;
157
250
 
158
251
  stopped = false;
159
252
 
160
- scanTimeout = 3000;
253
+ scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
161
254
 
162
255
  runPromise: Deferred<any> | null = null;
163
256
 
@@ -165,11 +258,48 @@ export default class ReactNativeBleTransport {
165
258
 
166
259
  firmwareUploadWriteRecoveryIds = new Set<string>();
167
260
 
261
+ /** Per-device protocol type detected by active wire-level probe after connect. */
262
+ private deviceProtocol: Map<string, ProtocolType> = new Map();
263
+
264
+ private deviceProtocolHints: Map<string, ProtocolType> = new Map();
265
+
266
+ private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
267
+
268
+ private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
269
+
270
+ private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
271
+
272
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
273
+ getSchemas: () => {
274
+ if (!this._messages || !this._messagesV2) {
275
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
276
+ }
277
+ return {
278
+ protocolV1: this._messages,
279
+ protocolV2: this._messagesV2,
280
+ };
281
+ },
282
+ classifyError: () => 'link-fatal',
283
+ onLinkInvalidated: async (uuid, reason) => {
284
+ this.protocolV2Assemblers.get(uuid)?.reset();
285
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
286
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
287
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
288
+ await this.release(uuid, true);
289
+ }
290
+ },
291
+ });
292
+
293
+ private monitorTokens: Map<string, number> = new Map();
294
+
295
+ private nextMonitorToken = 1;
296
+
168
297
  constructor(options: TransportOptions) {
169
- this.scanTimeout = options.scanTimeout ?? 3000;
298
+ this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
170
299
  }
171
300
 
172
- init(_logger: any, emitter: EventEmitter) {
301
+ init(logger: any, emitter: EventEmitter) {
302
+ setBleLogger(logger);
173
303
  this.emitter = emitter;
174
304
  }
175
305
 
@@ -179,6 +309,13 @@ export default class ReactNativeBleTransport {
179
309
  this._messages = messages;
180
310
  }
181
311
 
312
+ configureProtocolV2(signedData: any) {
313
+ this._messagesV2 = parseConfigure(signedData);
314
+ this.protocolV2Links
315
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
316
+ .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
317
+ }
318
+
182
319
  listen() {
183
320
  // empty
184
321
  }
@@ -206,7 +343,29 @@ export default class ReactNativeBleTransport {
206
343
  }
207
344
  }
208
345
 
346
+ let fallbackServiceUuid: string | undefined;
347
+
209
348
  if (!infos) {
349
+ const services = await device.services();
350
+ Log?.debug(
351
+ '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
352
+ services?.map(service => service.uuid)
353
+ );
354
+
355
+ const knownService = services.find(service =>
356
+ getInfosForServiceUuid(service.uuid, 'classic')
357
+ );
358
+ const fallbackService =
359
+ knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
360
+
361
+ if (fallbackService) {
362
+ fallbackServiceUuid = fallbackService.uuid;
363
+ characteristics = await device.characteristicsForService(fallbackService.uuid);
364
+ Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
365
+ }
366
+ }
367
+
368
+ if (!infos && !fallbackServiceUuid) {
210
369
  try {
211
370
  Log?.debug('cancel connection when service not found');
212
371
  await device.cancelConnection();
@@ -216,7 +375,13 @@ export default class ReactNativeBleTransport {
216
375
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
217
376
  }
218
377
 
219
- const { serviceUuid, writeUuid, notifyUuid } = infos;
378
+ const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
379
+ const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
380
+ const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
381
+
382
+ if (!serviceUuid) {
383
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
384
+ }
220
385
 
221
386
  if (!characteristics) {
222
387
  characteristics = await device.characteristicsForService(serviceUuid);
@@ -229,9 +394,9 @@ export default class ReactNativeBleTransport {
229
394
  let writeCharacteristic;
230
395
  let notifyCharacteristic;
231
396
  for (const c of characteristics) {
232
- if (c.uuid === writeUuid) {
397
+ if (isSameBleUuid(c.uuid, writeUuid)) {
233
398
  writeCharacteristic = c;
234
- } else if (c.uuid === notifyUuid) {
399
+ } else if (isSameBleUuid(c.uuid, notifyUuid)) {
235
400
  notifyCharacteristic = c;
236
401
  }
237
402
  }
@@ -244,7 +409,7 @@ export default class ReactNativeBleTransport {
244
409
  throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found');
245
410
  }
246
411
 
247
- if (!writeCharacteristic.isWritableWithResponse) {
412
+ if (!hasWritableCapability(writeCharacteristic)) {
248
413
  throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
249
414
  }
250
415
 
@@ -267,6 +432,10 @@ export default class ReactNativeBleTransport {
267
432
  Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
268
433
  return;
269
434
  }
435
+ if (transportCache[uuid] !== transport) {
436
+ Log?.debug('device disconnect ignored for stale transport: ', device?.id);
437
+ return;
438
+ }
270
439
 
271
440
  try {
272
441
  Log?.debug('device disconnect: ', device?.id);
@@ -276,12 +445,14 @@ export default class ReactNativeBleTransport {
276
445
  connectId: device?.id,
277
446
  });
278
447
  if (this.runPromise) {
279
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError));
448
+ const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
449
+ this.runPromise.reject(error);
450
+ this.rejectAllProtocolV2Frames(error);
280
451
  }
281
452
  } catch (e) {
282
453
  Log?.debug('device disconnect error: ', e);
283
454
  } finally {
284
- this.release(uuid);
455
+ this.release(uuid, true);
285
456
  }
286
457
  });
287
458
  }
@@ -304,7 +475,6 @@ export default class ReactNativeBleTransport {
304
475
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
305
476
  e.errorCode === BleErrorCode.OperationCancelled
306
477
  ) {
307
- connectOptions = {};
308
478
  device = await device.connect();
309
479
  } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
310
480
  throw e;
@@ -319,7 +489,18 @@ export default class ReactNativeBleTransport {
319
489
  transport.device = device;
320
490
  transport.writeCharacteristic = writeCharacteristic;
321
491
  transport.notifyCharacteristic = notifyCharacteristic;
322
- transport.notifySubscription = this._monitorCharacteristic(notifyCharacteristic, uuid);
492
+ const monitorToken = this.nextMonitorToken;
493
+ this.nextMonitorToken += 1;
494
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
495
+ transport.monitorToken = monitorToken;
496
+ transport.notifyTransactionId = notifyTransactionId;
497
+ this.monitorTokens.set(uuid, monitorToken);
498
+ transport.notifySubscription = this._monitorCharacteristic(
499
+ notifyCharacteristic,
500
+ uuid,
501
+ monitorToken,
502
+ notifyTransactionId
503
+ );
323
504
  this.attachDisconnectSubscription(transport, device, uuid);
324
505
  } finally {
325
506
  this.firmwareUploadWriteRecoveryIds.delete(uuid);
@@ -365,11 +546,11 @@ export default class ReactNativeBleTransport {
365
546
  blePlxManager.startDeviceScan(
366
547
  getBluetoothServiceUuids(),
367
548
  {
549
+ allowDuplicates: true,
368
550
  scanMode: ScanMode.LowLatency,
369
551
  },
370
552
  (error, device) => {
371
553
  if (error) {
372
- Log?.debug('ble scan manager: ', blePlxManager);
373
554
  Log?.debug('ble scan error: ', error);
374
555
  if (
375
556
  [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
@@ -392,14 +573,20 @@ export default class ReactNativeBleTransport {
392
573
  return;
393
574
  }
394
575
 
395
- if (isOnekeyDevice(getBleIdentityName(device), device?.id)) {
396
- Log?.debug('search device start ======================');
397
- const { name, localName, id } = device ?? {};
398
- Log?.debug(
399
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${id ?? ''}`
400
- );
576
+ const displayName = getDeviceDisplayName(device);
577
+ const isOneKey =
578
+ isOnekeyDevice(device?.name ?? null, device?.id) ||
579
+ isOnekeyDevice(device?.localName ?? null, device?.id) ||
580
+ hasKnownOneKeyService(device);
581
+ if (isOneKey) {
401
582
  addDevice(device as unknown as Device);
402
- Log?.debug('search device end ======================\n');
583
+ } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
584
+ Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
585
+ name: device?.name,
586
+ localName: device?.localName,
587
+ id: device?.id,
588
+ serviceUUIDs: device?.serviceUUIDs,
589
+ });
403
590
  }
404
591
  }
405
592
  );
@@ -411,7 +598,6 @@ export default class ReactNativeBleTransport {
411
598
  const hasCachedServiceUuid = Boolean(serviceUUIDs?.length);
412
599
  const keepDevice = Platform.OS === 'ios' || hasCachedServiceUuid;
413
600
  if (keepDevice) {
414
- Log?.debug('search connected peripheral: ', device.id);
415
601
  addDevice(device as unknown as Device);
416
602
  }
417
603
  }
@@ -420,7 +606,22 @@ export default class ReactNativeBleTransport {
420
606
 
421
607
  const addDevice = (device: Device) => {
422
608
  if (deviceList.every(d => d.id !== device.id)) {
423
- deviceList.push({ ...device, commType: 'ble' } as IOneKeyDevice);
609
+ const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
610
+ const protocolHint = inferProtocolHintFromDeviceName(displayName);
611
+ if (protocolHint) {
612
+ this.deviceProtocolHints.set(device.id, protocolHint);
613
+ }
614
+ deviceList.push({
615
+ ...device,
616
+ name: displayName,
617
+ commType: 'ble',
618
+ } as IOneKeyDevice);
619
+ Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
620
+ deviceId: device.id,
621
+ name: displayName,
622
+ serviceUUIDs: device.serviceUUIDs,
623
+ protocolHint,
624
+ });
424
625
  }
425
626
  };
426
627
 
@@ -432,25 +633,40 @@ export default class ReactNativeBleTransport {
432
633
  }
433
634
 
434
635
  async acquire(input: BleAcquireInput) {
435
- const { uuid, forceCleanRunPromise } = input;
636
+ const { uuid, forceCleanRunPromise, expectedProtocol } = input;
436
637
 
437
638
  if (!uuid) {
438
639
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
439
640
  }
440
641
 
441
- let device: Device | null = null;
642
+ const cachedTransport = transportCache[uuid];
643
+ if (cachedTransport) {
644
+ const cachedProtocol = this.deviceProtocol.get(uuid);
645
+ const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false);
646
+ if (
647
+ isCachedDeviceConnected &&
648
+ cachedProtocol &&
649
+ (!expectedProtocol || cachedProtocol === expectedProtocol)
650
+ ) {
651
+ Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
652
+ return { uuid, protocolType: cachedProtocol };
653
+ }
442
654
 
443
- if (transportCache[uuid]) {
444
655
  /**
445
- * If the transport is not released due to an exception operation
446
- * it will be handled again here
656
+ * If the transport is not reusable due to a protocol mismatch or stale
657
+ * connection, clean it up before creating a new transport instance.
447
658
  */
448
- Log?.debug('transport not be released, will release: ', uuid);
449
- await this.release(uuid);
659
+ Log?.debug('transport not reusable, will release: ', uuid);
660
+ await this.release(uuid, true);
450
661
  }
451
662
 
663
+ let device: Device | null = null;
664
+
452
665
  if (forceCleanRunPromise && this.runPromise) {
453
- this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
666
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
667
+ this.runPromise.reject(error);
668
+ this.rejectAllProtocolV2Frames(error);
669
+ this.runPromise = null;
454
670
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
455
671
  }
456
672
 
@@ -462,11 +678,12 @@ export default class ReactNativeBleTransport {
462
678
  throw error;
463
679
  }
464
680
 
465
- // check device is bonded
466
681
  if (Platform.OS === 'android') {
467
682
  const bondState = await pairDevice(uuid);
468
683
  if (bondState.bonding) {
469
684
  await onDeviceBondState(uuid);
685
+ } else if (!bondState.bonded) {
686
+ throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
470
687
  }
471
688
  }
472
689
 
@@ -492,7 +709,6 @@ export default class ReactNativeBleTransport {
492
709
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
493
710
  e.errorCode === BleErrorCode.OperationCancelled
494
711
  ) {
495
- connectOptions = {};
496
712
  Log?.debug('first try to reconnect without params');
497
713
  device = await blePlxManager.connectToDevice(uuid);
498
714
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
@@ -512,17 +728,16 @@ export default class ReactNativeBleTransport {
512
728
  Log?.debug('not connected, try to connect to device: ', uuid);
513
729
 
514
730
  try {
515
- await device.connect(connectOptions);
731
+ device = await device.connect(connectOptions);
516
732
  } catch (e) {
517
733
  Log?.debug('not connected, try to connect to device has error: ', e);
518
734
  if (
519
735
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
520
736
  e.errorCode === BleErrorCode.OperationCancelled
521
737
  ) {
522
- connectOptions = {};
523
738
  Log?.debug('second try to reconnect without params');
524
739
  try {
525
- await device.connect();
740
+ device = await device.connect();
526
741
  } catch (e) {
527
742
  Log?.debug('last try to reconnect error: ', e);
528
743
  // last try to reconnect device if this issue exists
@@ -530,7 +745,7 @@ export default class ReactNativeBleTransport {
530
745
  if (e.errorCode === BleErrorCode.OperationCancelled) {
531
746
  Log?.debug('last try to reconnect');
532
747
  await device.cancelConnection();
533
- await device.connect();
748
+ device = await device.connect();
534
749
  }
535
750
  }
536
751
  } else {
@@ -539,18 +754,50 @@ export default class ReactNativeBleTransport {
539
754
  }
540
755
  }
541
756
 
757
+ device = await requestAndroidMtu(device);
542
758
  const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
543
759
 
760
+ const protocolHint = expectedProtocol
761
+ ? undefined
762
+ : this.deviceProtocolHints.get(uuid) ??
763
+ inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
764
+
544
765
  // release transport before new transport instance
545
- await this.release(uuid);
766
+ await this.release(uuid, true);
767
+ if (protocolHint) {
768
+ this.deviceProtocolHints.set(uuid, protocolHint);
769
+ }
546
770
 
547
771
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
772
+ if (Platform.OS === 'android') {
773
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
774
+ }
775
+ const monitorToken = this.nextMonitorToken;
776
+ this.nextMonitorToken += 1;
777
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
778
+ transport.monitorToken = monitorToken;
779
+ transport.notifyTransactionId = notifyTransactionId;
780
+ this.monitorTokens.set(uuid, monitorToken);
548
781
  transport.notifySubscription = this._monitorCharacteristic(
549
782
  transport.notifyCharacteristic,
550
- uuid
783
+ uuid,
784
+ monitorToken,
785
+ notifyTransactionId
551
786
  );
552
787
  transportCache[uuid] = transport;
553
788
 
789
+ this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
790
+
791
+ if (Platform.OS === 'ios') {
792
+ await new Promise<void>(resolve => {
793
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
794
+ });
795
+ } else if (Platform.OS === 'android') {
796
+ await delay(ANDROID_NOTIFY_READY_DELAY_MS);
797
+ }
798
+
799
+ const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
800
+
554
801
  this.emitter?.emit('device-connect', {
555
802
  name: device.name,
556
803
  id: device.id,
@@ -559,13 +806,19 @@ export default class ReactNativeBleTransport {
559
806
 
560
807
  this.attachDisconnectSubscription(transport, device, uuid);
561
808
 
562
- return { uuid };
809
+ return { uuid, protocolType };
563
810
  }
564
811
 
565
- _monitorCharacteristic(characteristic: Characteristic, uuid: string): Subscription {
812
+ _monitorCharacteristic(
813
+ characteristic: Characteristic,
814
+ uuid: string,
815
+ monitorToken: number,
816
+ notifyTransactionId: string
817
+ ): Subscription {
566
818
  let bufferLength = 0;
567
819
  let buffer: any[] = [];
568
820
  const subscription = characteristic.monitor((error, c) => {
821
+ const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
569
822
  if (error) {
570
823
  Log?.debug(
571
824
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
@@ -576,6 +829,33 @@ export default class ReactNativeBleTransport {
576
829
  Log?.debug('notify error ignored during FirmwareUpload write recovery: ', uuid);
577
830
  return;
578
831
  }
832
+ if (!isCurrentMonitor) {
833
+ Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
834
+ return;
835
+ }
836
+ if (this.deviceProtocol.get(uuid) === 'V2') {
837
+ let errorCode:
838
+ | typeof HardwareErrorCode.BleDeviceBondError
839
+ | typeof HardwareErrorCode.BleCharacteristicNotifyError
840
+ | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
841
+ | typeof HardwareErrorCode.BleTimeoutError =
842
+ HardwareErrorCode.BleCharacteristicNotifyError;
843
+ if (error.reason?.includes('The connection has timed out unexpectedly')) {
844
+ errorCode = HardwareErrorCode.BleTimeoutError;
845
+ } else if (error.reason?.includes('Encryption is insufficient')) {
846
+ errorCode = HardwareErrorCode.BleDeviceBondError;
847
+ } else if (
848
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
849
+ error.reason?.includes('Cannot find client characteristic config descriptor') ||
850
+ error.reason?.includes('The handle is invalid') ||
851
+ error.reason?.includes('Writing is not permitted') ||
852
+ error.reason?.includes('notify change failed for device')
853
+ ) {
854
+ errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
855
+ }
856
+ this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
857
+ return;
858
+ }
579
859
  if (this.runPromise) {
580
860
  let ERROR:
581
861
  | typeof HardwareErrorCode.BleDeviceBondError
@@ -595,27 +875,45 @@ export default class ReactNativeBleTransport {
595
875
  error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
596
876
  error.reason?.includes('notify change failed for device')
597
877
  ) {
598
- this.runPromise.reject(
599
- ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure)
878
+ const notifyError = ERRORS.TypedError(
879
+ HardwareErrorCode.BleCharacteristicNotifyChangeFailure
600
880
  );
881
+ this.runPromise.reject(notifyError);
882
+ this.rejectAllProtocolV2Frames(notifyError);
601
883
  Log?.debug(
602
884
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
603
885
  );
604
886
  return;
605
887
  }
606
- this.runPromise.reject(ERRORS.TypedError(ERROR));
888
+ const notifyError = ERRORS.TypedError(ERROR);
889
+ this.runPromise.reject(notifyError);
890
+ this.rejectAllProtocolV2Frames(notifyError);
607
891
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
608
892
  }
609
893
 
610
894
  return;
611
895
  }
612
896
 
897
+ if (!isCurrentMonitor) {
898
+ Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
899
+ return;
900
+ }
901
+
613
902
  if (!c) {
614
903
  throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
615
904
  }
616
905
 
617
906
  try {
618
907
  const data = Buffer.from(c.value as string, 'base64');
908
+ const protocol = this.deviceProtocol.get(uuid);
909
+ if (!protocol) {
910
+ Log?.debug('monitor data ignored before protocol detection: ', uuid);
911
+ return;
912
+ }
913
+ if (protocol === 'V2') {
914
+ this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
915
+ return;
916
+ }
619
917
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
620
918
  if (isHeaderChunk(data)) {
621
919
  bufferLength = data.readInt32BE(5);
@@ -624,7 +922,7 @@ export default class ReactNativeBleTransport {
624
922
  buffer = buffer.concat([...data]);
625
923
  }
626
924
 
627
- if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) {
925
+ if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
628
926
  const value = Buffer.from(buffer);
629
927
  // console.log(
630
928
  // '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
@@ -638,17 +936,41 @@ export default class ReactNativeBleTransport {
638
936
  }
639
937
  } catch (error) {
640
938
  Log?.debug('monitor data error: ', error);
641
- this.runPromise?.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
939
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
940
+ if (this.deviceProtocol.get(uuid) === 'V2') {
941
+ this.rejectProtocolV2Frames(uuid, notifyError);
942
+ } else {
943
+ this.runPromise?.reject(notifyError);
944
+ }
642
945
  }
643
- }, uuid);
946
+ }, notifyTransactionId);
644
947
 
645
948
  return subscription;
646
949
  }
647
950
 
648
- async release(uuid: string) {
951
+ async release(uuid: string, onclose = false) {
649
952
  const transport = transportCache[uuid];
953
+ await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
954
+ if (this.runPromise) {
955
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
956
+ this.runPromise.reject(error);
957
+ this.runPromise = null;
958
+ this.rejectAllProtocolV2Frames(error);
959
+ } else {
960
+ this.resetProtocolV2Frames(uuid);
961
+ }
962
+
963
+ if (Platform.OS === 'android' && !onclose && transport) {
964
+ this.protocolV2Assemblers.get(uuid)?.reset();
965
+ this.resetProtocolV2Frames(uuid);
966
+ return Promise.resolve(true);
967
+ }
650
968
 
651
969
  if (transport) {
970
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
971
+ this.monitorTokens.delete(uuid);
972
+ }
973
+
652
974
  // Clean up disconnect subscription first to prevent callbacks on released transport
653
975
  Log?.debug('release: removing disconnect subscription for device: ', uuid);
654
976
  transport.disconnectSubscription?.remove();
@@ -662,12 +984,27 @@ export default class ReactNativeBleTransport {
662
984
  transport.notifySubscription?.remove();
663
985
  transport.notifySubscription = undefined;
664
986
 
987
+ if (transport.notifyTransactionId) {
988
+ try {
989
+ await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
990
+ } catch (e) {
991
+ Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
992
+ }
993
+ }
994
+
665
995
  delete transportCache[uuid];
996
+ }
666
997
 
667
- // Temporary close the Android disconnect after each request
668
- if (Platform.OS === 'android') {
669
- // await this.blePlxManager?.cancelDeviceConnection(uuid);
670
- }
998
+ this.deviceProtocol.delete(uuid);
999
+ // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
1000
+ this.protocolV2Assemblers.get(uuid)?.reset();
1001
+ this.protocolV2Assemblers.delete(uuid);
1002
+ this.resetProtocolV2Frames(uuid);
1003
+
1004
+ try {
1005
+ await this.blePlxManager?.cancelTransaction(uuid);
1006
+ } catch (e) {
1007
+ Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
671
1008
  }
672
1009
 
673
1010
  return Promise.resolve(true);
@@ -677,7 +1014,12 @@ export default class ReactNativeBleTransport {
677
1014
  await this.call(session, name, data);
678
1015
  }
679
1016
 
680
- async call(uuid: string, name: string, data: Record<string, unknown>) {
1017
+ async call(
1018
+ uuid: string,
1019
+ name: string,
1020
+ data: Record<string, unknown>,
1021
+ options?: TransportCallOptions
1022
+ ) {
681
1023
  if (this.stopped) {
682
1024
  // eslint-disable-next-line prefer-promise-reject-errors
683
1025
  return Promise.reject(ERRORS.TypedError('Transport stopped.'));
@@ -686,33 +1028,44 @@ export default class ReactNativeBleTransport {
686
1028
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
687
1029
  }
688
1030
 
689
- const forceRun = name === 'Initialize' || name === 'Cancel';
1031
+ const protocol = this.getProtocolType(uuid);
1032
+ if (!protocol) {
1033
+ throw ERRORS.TypedError(
1034
+ HardwareErrorCode.RuntimeError,
1035
+ `Device protocol has not been detected for ${uuid}`
1036
+ );
1037
+ }
1038
+ Log?.debug('transport call', createTransportCallLog(name, protocol, data));
1039
+
1040
+ if (protocol === 'V2') {
1041
+ return this.callProtocolV2(uuid, name, data, options);
1042
+ }
690
1043
 
691
- Log?.debug('transport-react-native call this.runPromise', this.runPromise);
1044
+ const forceRun = name === 'Initialize' || name === 'Cancel';
692
1045
  if (this.runPromise && !forceRun) {
693
1046
  throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
694
1047
  }
695
1048
 
696
- const transport = transportCache[uuid];
697
- if (!transport) {
698
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
699
- }
1049
+ return this.callProtocolV1(uuid, name, data, options);
1050
+ }
700
1051
 
701
- this.runPromise = createDeferred();
702
- const messages = this._messages;
703
- // Upload resources on low-end phones may OOM
704
- if (name === 'ResourceUpdate' || name === 'ResourceAck') {
705
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
706
- file_name: data?.file_name,
707
- hash: data?.hash,
708
- });
709
- } else if (LogBlockCommand.has(name)) {
710
- Log?.debug('transport-react-native', 'call-', ' name: ', name);
711
- } else {
712
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data);
1052
+ private async callProtocolV1(
1053
+ uuid: string,
1054
+ name: string,
1055
+ data: Record<string, unknown>,
1056
+ options?: TransportCallOptions
1057
+ ) {
1058
+ if (!this._messages) {
1059
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
713
1060
  }
714
1061
 
715
- const buffers = buildBuffers(messages, name, data);
1062
+ const transport = this.getCachedTransport(uuid);
1063
+ const runPromise = createDeferred<string>();
1064
+ runPromise.promise.catch(() => undefined);
1065
+ this.runPromise = runPromise;
1066
+ const messages = this._messages;
1067
+ const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1068
+ let timeout: ReturnType<typeof setTimeout> | undefined;
716
1069
 
717
1070
  async function writeChunkedData(
718
1071
  buffers: ByteBuffer[],
@@ -786,14 +1139,13 @@ export default class ReactNativeBleTransport {
786
1139
  }
787
1140
  );
788
1141
  } else if (name === 'FirmwareUpload') {
789
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
1142
+ Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
790
1143
  packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
791
1144
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
792
1145
  pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
793
1146
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
794
1147
  maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
795
1148
  });
796
-
797
1149
  await writeFirmwareUploadChunkedData(
798
1150
  buffers,
799
1151
  async data => {
@@ -847,7 +1199,6 @@ export default class ReactNativeBleTransport {
847
1199
  for (const o of buffers) {
848
1200
  const outData = o.toString('base64');
849
1201
  // Upload resources on low-end phones may OOM
850
- // this.Log.debug('send hex strting: ', o.toString('hex'));
851
1202
  try {
852
1203
  await transport.writeCharacteristic.writeWithoutResponse(outData);
853
1204
  } catch (e) {
@@ -865,20 +1216,40 @@ export default class ReactNativeBleTransport {
865
1216
  }
866
1217
 
867
1218
  try {
868
- const response = await this.runPromise.promise;
1219
+ const response = await Promise.race([
1220
+ runPromise.promise,
1221
+ new Promise<never>((_, reject) => {
1222
+ if (options?.timeoutMs) {
1223
+ timeout = setTimeout(() => {
1224
+ const error = ERRORS.TypedError(
1225
+ HardwareErrorCode.BleTimeoutError,
1226
+ `BLE response timeout after ${options.timeoutMs}ms for ${name}`
1227
+ );
1228
+ runPromise.reject(error);
1229
+ reject(error);
1230
+ }, options.timeoutMs);
1231
+ }
1232
+ }),
1233
+ ]);
869
1234
 
870
1235
  if (typeof response !== 'string') {
871
1236
  throw new Error('Returning data is not string.');
872
1237
  }
873
1238
 
874
- Log?.debug('receive data: ', response);
875
- const jsonData = receiveOne(messages, response);
1239
+ const jsonData = ProtocolV1.decodeMessage(messages, response);
876
1240
  return check.call(jsonData);
877
1241
  } catch (e) {
878
- Log?.error('call error: ', e);
1242
+ if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1243
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1244
+ } else {
1245
+ Log?.error('call error: ', e);
1246
+ }
879
1247
  throw e;
880
1248
  } finally {
881
- this.runPromise = null;
1249
+ if (timeout) clearTimeout(timeout);
1250
+ if (this.runPromise === runPromise) {
1251
+ this.runPromise = null;
1252
+ }
882
1253
  }
883
1254
  }
884
1255
 
@@ -887,7 +1258,7 @@ export default class ReactNativeBleTransport {
887
1258
  }
888
1259
 
889
1260
  async disconnect(session: string) {
890
- Log?.debug('transport-react-native transport resetSession: ', session);
1261
+ await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
891
1262
  const transport = transportCache[session];
892
1263
 
893
1264
  // Clean up disconnect subscription first to prevent onDisconnected callback
@@ -945,6 +1316,10 @@ export default class ReactNativeBleTransport {
945
1316
  if (transportCache[session]) {
946
1317
  delete transportCache[session];
947
1318
  }
1319
+ this.deviceProtocol.delete(session);
1320
+ this.deviceProtocolHints.delete(session);
1321
+ this.protocolV2Assemblers.delete(session);
1322
+ this.resetProtocolV2Frames(session);
948
1323
 
949
1324
  // emit the disconnect event
950
1325
  try {
@@ -967,4 +1342,374 @@ export default class ReactNativeBleTransport {
967
1342
  }
968
1343
  this.runPromise = null;
969
1344
  }
1345
+
1346
+ private getCachedTransport(uuid: string) {
1347
+ const transport = transportCache[uuid];
1348
+ if (!transport) {
1349
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1350
+ }
1351
+ return transport;
1352
+ }
1353
+
1354
+ private createProtocolMismatchError(expected: ProtocolType) {
1355
+ return ERRORS.TypedError(
1356
+ HardwareErrorCode.RuntimeError,
1357
+ `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
1358
+ );
1359
+ }
1360
+
1361
+ private createProtocolDetectionError() {
1362
+ return ERRORS.TypedError(
1363
+ HardwareErrorCode.BleTimeoutError,
1364
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1365
+ );
1366
+ }
1367
+
1368
+ private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1369
+ if (this.deviceProtocol.get(uuid) === protocol) {
1370
+ this.deviceProtocol.delete(uuid);
1371
+ }
1372
+ }
1373
+
1374
+ private async detectProtocol(
1375
+ uuid: string,
1376
+ expectedProtocol?: ProtocolType,
1377
+ protocolHint?: ProtocolType
1378
+ ): Promise<ProtocolType> {
1379
+ if (expectedProtocol === 'V1') {
1380
+ if (await this.probeProtocolV1(uuid)) {
1381
+ this.deviceProtocol.set(uuid, 'V1');
1382
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1383
+ deviceId: uuid,
1384
+ protocol: 'V1',
1385
+ source: 'expected',
1386
+ });
1387
+ return 'V1';
1388
+ }
1389
+ throw this.createProtocolMismatchError(expectedProtocol);
1390
+ }
1391
+
1392
+ if (expectedProtocol === 'V2') {
1393
+ // Skip probing when the caller explicitly confirms V2, such as reconnect after a
1394
+ // firmware reboot where expectedProtocol carries the previously probed result.
1395
+ this.deviceProtocol.set(uuid, 'V2');
1396
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1397
+ deviceId: uuid,
1398
+ protocol: 'V2',
1399
+ source: 'expected',
1400
+ });
1401
+ return 'V2';
1402
+ }
1403
+
1404
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
1405
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
1406
+ const probeOrder: ProtocolType[] =
1407
+ protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1408
+
1409
+ for (let i = 0; i < probeOrder.length; i += 1) {
1410
+ const protocol = probeOrder[i];
1411
+ if (i > 0) {
1412
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
1413
+ await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1414
+ }
1415
+ const detected =
1416
+ protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1417
+ if (detected) {
1418
+ this.deviceProtocol.set(uuid, protocol);
1419
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1420
+ deviceId: uuid,
1421
+ protocol,
1422
+ source: 'probe',
1423
+ });
1424
+ return protocol;
1425
+ }
1426
+ }
1427
+
1428
+ this.deviceProtocol.delete(uuid);
1429
+ throw this.createProtocolDetectionError();
1430
+ }
1431
+
1432
+ private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1433
+ const transport = transportCache[uuid];
1434
+ await this.protocolV2Links.invalidateLink(
1435
+ uuid,
1436
+ `Reset notify state after Protocol ${protocol} probe`
1437
+ );
1438
+ this.protocolV2Assemblers.get(uuid)?.reset();
1439
+ this.resetProtocolV2Frames(uuid);
1440
+ if (this.runPromise) {
1441
+ const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1442
+ this.runPromise.reject(error);
1443
+ this.runPromise = null;
1444
+ }
1445
+
1446
+ if (!transport) return;
1447
+
1448
+ const previousNotifyTransactionId = transport.notifyTransactionId;
1449
+ if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1450
+ this.monitorTokens.delete(uuid);
1451
+ }
1452
+ transport.notifySubscription?.remove();
1453
+ transport.notifySubscription = undefined;
1454
+ if (previousNotifyTransactionId) {
1455
+ try {
1456
+ await this.blePlxManager?.cancelTransaction(previousNotifyTransactionId);
1457
+ } catch (error) {
1458
+ Log?.debug(
1459
+ `[ReactNativeBleTransport] cancel notify after Protocol ${protocol} probe failed:`,
1460
+ error?.message || error
1461
+ );
1462
+ }
1463
+ }
1464
+
1465
+ const monitorToken = this.nextMonitorToken;
1466
+ this.nextMonitorToken += 1;
1467
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
1468
+ transport.monitorToken = monitorToken;
1469
+ transport.notifyTransactionId = notifyTransactionId;
1470
+ this.monitorTokens.set(uuid, monitorToken);
1471
+ transport.notifySubscription = this._monitorCharacteristic(
1472
+ transport.notifyCharacteristic,
1473
+ uuid,
1474
+ monitorToken,
1475
+ notifyTransactionId
1476
+ );
1477
+ if (Platform.OS === 'ios') {
1478
+ await new Promise<void>(resolve => {
1479
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
1480
+ });
1481
+ }
1482
+ }
1483
+
1484
+ private async probeProtocolV1(uuid: string) {
1485
+ if (!this._messages) {
1486
+ return false;
1487
+ }
1488
+
1489
+ try {
1490
+ this.deviceProtocol.set(uuid, 'V1');
1491
+ await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1492
+ return true;
1493
+ } catch (error) {
1494
+ this.clearProbeProtocol(uuid, 'V1');
1495
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1496
+ return false;
1497
+ }
1498
+ }
1499
+
1500
+ private async probeProtocolV2(uuid: string) {
1501
+ if (!this._messages || !this._messagesV2) {
1502
+ return false;
1503
+ }
1504
+
1505
+ this.deviceProtocol.set(uuid, 'V2');
1506
+ this.protocolV2Assemblers.get(uuid)?.reset();
1507
+ const detected = await probeProtocolV2Helper({
1508
+ call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
1509
+ this.callProtocolV2(uuid, name, data, options),
1510
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
1511
+ logger: Log,
1512
+ logPrefix: 'ProtocolV2 RN-BLE',
1513
+ onProbeFailed: () => {
1514
+ this.protocolV2Assemblers.get(uuid)?.reset();
1515
+ this.resetProtocolV2Frames(uuid);
1516
+ },
1517
+ });
1518
+ if (!detected) {
1519
+ this.clearProbeProtocol(uuid, 'V2');
1520
+ }
1521
+ return detected;
1522
+ }
1523
+
1524
+ private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
1525
+ try {
1526
+ if (this.monitorTokens.get(uuid) !== monitorToken) return;
1527
+
1528
+ if (data.length === 0) return;
1529
+
1530
+ const assembler = this.protocolV2Assemblers.get(uuid);
1531
+ if (!assembler) return;
1532
+
1533
+ for (const frameData of assembler.drain(data)) {
1534
+ this.resolveProtocolV2Frame(uuid, frameData);
1535
+ }
1536
+ } catch (error) {
1537
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1538
+ const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1539
+ this.rejectProtocolV2Frames(uuid, notifyError);
1540
+ this.protocolV2Links
1541
+ .invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
1542
+ .catch(invalidateError =>
1543
+ Log?.debug(
1544
+ '[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
1545
+ invalidateError
1546
+ )
1547
+ );
1548
+ }
1549
+ }
1550
+
1551
+ private getProtocolV2FrameQueue(uuid: string) {
1552
+ let queue = this.protocolV2FrameQueues.get(uuid);
1553
+ if (!queue) {
1554
+ queue = [];
1555
+ this.protocolV2FrameQueues.set(uuid, queue);
1556
+ }
1557
+ return queue;
1558
+ }
1559
+
1560
+ private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
1561
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1562
+ if (framePromise) {
1563
+ framePromise.resolve(frame);
1564
+ this.protocolV2FramePromises.delete(uuid);
1565
+ return;
1566
+ }
1567
+ this.getProtocolV2FrameQueue(uuid).push(frame);
1568
+ }
1569
+
1570
+ private rejectAllProtocolV2Frames(error: Error) {
1571
+ this.protocolV2FrameQueues.clear();
1572
+ for (const framePromise of this.protocolV2FramePromises.values()) {
1573
+ framePromise.reject(error);
1574
+ }
1575
+ this.protocolV2FramePromises.clear();
1576
+ }
1577
+
1578
+ private resetProtocolV2Frames(uuid: string) {
1579
+ this.protocolV2FrameQueues.delete(uuid);
1580
+ this.protocolV2FramePromises.delete(uuid);
1581
+ }
1582
+
1583
+ private rejectProtocolV2Frames(uuid: string, error: Error) {
1584
+ this.protocolV2FrameQueues.delete(uuid);
1585
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1586
+ if (framePromise) {
1587
+ this.protocolV2FramePromises.delete(uuid);
1588
+ framePromise.reject(error);
1589
+ }
1590
+ }
1591
+
1592
+ private async readProtocolV2Frame(uuid: string) {
1593
+ const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
1594
+ if (queuedFrame) {
1595
+ return queuedFrame;
1596
+ }
1597
+
1598
+ const framePromise = createDeferred<Uint8Array>();
1599
+ this.protocolV2FramePromises.set(uuid, framePromise);
1600
+ try {
1601
+ return await framePromise.promise;
1602
+ } finally {
1603
+ if (this.protocolV2FramePromises.get(uuid) === framePromise) {
1604
+ this.protocolV2FramePromises.delete(uuid);
1605
+ }
1606
+ }
1607
+ }
1608
+
1609
+ private async writeProtocolV2Frame(transport: BleTransport, frame: Uint8Array) {
1610
+ const tuning = getProtocolV2BleTuning();
1611
+ const packetCapacity = resolveProtocolV2PacketCapacity({
1612
+ platform: Platform.OS,
1613
+ iosPacketLength: tuning.iosPacketLength,
1614
+ androidPacketLength: tuning.androidPacketLength,
1615
+ mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1616
+ });
1617
+ for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1618
+ const chunk = frame.slice(offset, offset + packetCapacity);
1619
+ const base64 = Buffer.from(chunk).toString('base64');
1620
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1621
+ }
1622
+ }
1623
+
1624
+ private async callProtocolV2(
1625
+ uuid: string,
1626
+ name: string,
1627
+ data: Record<string, unknown>,
1628
+ options?: TransportCallOptions
1629
+ ) {
1630
+ if (!this._messages || !this._messagesV2) {
1631
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1632
+ }
1633
+
1634
+ const callOptions = {
1635
+ ...options,
1636
+ // Align with V1 BLE and the USB V2 base transport: only arm a watchdog when the caller
1637
+ // explicitly passes timeoutMs. Interactive acks (ButtonAck/PinMatrixAck/PassphraseAck)
1638
+ // have their timeoutMs removed by DeviceCommands.stripInteractiveAckTimeout, so we must
1639
+ // not fill in a 30s default here — that would hard-cap the time a user spends confirming
1640
+ // a transaction or entering a PIN/passphrase on the device and tear down the BLE link.
1641
+ timeoutMs: options?.timeoutMs,
1642
+ };
1643
+ const highVolumeWrite = LogBlockCommand.has(name);
1644
+
1645
+ if (highVolumeWrite) {
1646
+ const tuning = getProtocolV2BleTuning();
1647
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1648
+ name,
1649
+ writeMode: 'withoutResponse',
1650
+ packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1651
+ });
1652
+ }
1653
+
1654
+ try {
1655
+ return await this.protocolV2Links.call(
1656
+ uuid,
1657
+ () => this.createProtocolV2Adapter(uuid),
1658
+ name,
1659
+ data,
1660
+ callOptions
1661
+ );
1662
+ } catch (e) {
1663
+ Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1664
+ throw e;
1665
+ }
1666
+ }
1667
+
1668
+ private createProtocolV2Adapter(uuid: string) {
1669
+ const generation = this.monitorTokens.get(uuid) ?? 0;
1670
+ const assertCurrentGeneration = () => {
1671
+ if (this.monitorTokens.get(uuid) !== generation) {
1672
+ throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
1673
+ }
1674
+ };
1675
+
1676
+ return {
1677
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1678
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1679
+ generation,
1680
+ prepareCall: () => {
1681
+ assertCurrentGeneration();
1682
+ this.protocolV2Assemblers.get(uuid)?.reset();
1683
+ this.resetProtocolV2Frames(uuid);
1684
+ },
1685
+ writeFrame: async (frame: Uint8Array) => {
1686
+ assertCurrentGeneration();
1687
+ const currentTransport = this.getCachedTransport(uuid);
1688
+ await this.writeProtocolV2Frame(currentTransport, frame);
1689
+ },
1690
+ readFrame: async () => {
1691
+ assertCurrentGeneration();
1692
+ const rxFrame = await this.readProtocolV2Frame(uuid);
1693
+ if (!(rxFrame instanceof Uint8Array)) {
1694
+ throw new Error('Protocol V2 response is not Uint8Array');
1695
+ }
1696
+ return rxFrame;
1697
+ },
1698
+ reset: (reason: string) => {
1699
+ this.protocolV2Assemblers.get(uuid)?.reset();
1700
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
1701
+ },
1702
+ logger: Log,
1703
+ logPrefix: 'ProtocolV2 RN-BLE',
1704
+ createTimeoutError: (messageName: string, timeout: number) =>
1705
+ ERRORS.TypedError(
1706
+ HardwareErrorCode.BleTimeoutError,
1707
+ `BLE response timeout after ${timeout}ms for ${messageName}`
1708
+ ),
1709
+ };
1710
+ }
1711
+
1712
+ getProtocolType(path: string): ProtocolType | undefined {
1713
+ return this.deviceProtocol.get(path);
1714
+ }
970
1715
  }