@onekeyfe/hd-transport-react-native 1.2.0-alpha.6 → 1.2.0-alpha.61

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
@@ -12,26 +12,31 @@ import transport, {
12
12
  LogBlockCommand,
13
13
  type OneKeyDeviceInfoBase,
14
14
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
15
+ PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
15
16
  PROTOCOL_V2_CHANNEL_BLE_UART,
16
17
  type ProtocolType,
18
+ type ProtocolV2CallContext,
17
19
  ProtocolV2FrameAssembler,
18
- ProtocolV2Session,
20
+ ProtocolV2LinkManager,
21
+ TRANSPORT_EVENT,
19
22
  type TransportCallOptions,
20
23
  probeProtocolV2 as probeProtocolV2Helper,
24
+ writeProtocolV2BleFrame,
21
25
  } from '@onekeyfe/hd-transport';
22
- import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
26
+ import {
27
+ ERRORS,
28
+ HardwareErrorCode,
29
+ createDeferred,
30
+ isOnekeyBluetoothDevice,
31
+ isPro2FindMyAdvertisementName,
32
+ } from '@onekeyfe/hd-shared';
23
33
 
24
34
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
25
- import {
26
- hasWritableCapability,
27
- resolveBleWriteMode,
28
- resolveProtocolV2PacketCapacity,
29
- } from './bleStrategy';
35
+ import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
30
36
  import { subscribeBleOn } from './subscribeBleOn';
31
37
  import {
32
38
  ANDROID_PACKET_LENGTH,
33
39
  IOS_PACKET_LENGTH,
34
- getBleUuidKey,
35
40
  getBluetoothServiceUuids,
36
41
  getInfosForServiceUuid,
37
42
  isSameBleUuid,
@@ -40,6 +45,7 @@ import { isHeaderChunk } from './utils/validateNotify';
40
45
  import BleTransport from './BleTransport';
41
46
  import timer from './utils/timer';
42
47
  import { bleLogger, setBleLogger } from './logger';
48
+ import { createTransportCallLog } from './transportLog';
43
49
 
44
50
  import type { Deferred } from '@onekeyfe/hd-shared';
45
51
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
@@ -55,24 +61,53 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
55
61
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
56
62
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
57
63
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
58
- const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
64
+ const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
59
65
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
60
66
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
61
67
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
62
68
  const ANDROID_GATT_CONGESTED_STATUS = 143;
63
69
 
64
- type FirmwareUploadWriteRetryType = 'congested' | 'reconnectable';
70
+ type FirmwareUploadWriteRetryType = 'congested';
65
71
  type ResolvedBleCharacteristics = {
66
72
  writeCharacteristic: Characteristic;
67
73
  notifyCharacteristic: Characteristic;
68
74
  };
69
75
 
76
+ const isAsciiWhitespace = (code: number) =>
77
+ code === 0x09 ||
78
+ code === 0x0a ||
79
+ code === 0x0b ||
80
+ code === 0x0c ||
81
+ code === 0x0d ||
82
+ code === 0x20;
83
+
84
+ const hasGattCongestedStatus = (text: string) => {
85
+ let searchFrom = 0;
86
+ while (searchFrom < text.length) {
87
+ const statusIndex = text.indexOf('status', searchFrom);
88
+ if (statusIndex < 0) return false;
89
+
90
+ let cursor = statusIndex + 'status'.length;
91
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
92
+ if (text[cursor] === ':' || text[cursor] === '=') {
93
+ cursor += 1;
94
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
95
+ }
96
+ if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
97
+
98
+ searchFrom = statusIndex + 'status'.length;
99
+ }
100
+ return false;
101
+ };
102
+
70
103
  const delay = (ms: number) =>
71
104
  new Promise<void>(resolve => {
72
105
  setTimeout(resolve, ms);
73
106
  });
74
107
 
75
- const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRetryType | null => {
108
+ export const getFirmwareUploadWriteRetryType = (
109
+ error: unknown
110
+ ): FirmwareUploadWriteRetryType | null => {
76
111
  if (!error || typeof error !== 'object') return null;
77
112
  const bleWriteError = error as {
78
113
  androidErrorCode?: unknown;
@@ -83,13 +118,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
83
118
  name?: unknown;
84
119
  };
85
120
 
86
- if (
87
- bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
88
- bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
89
- ) {
90
- return 'reconnectable';
91
- }
92
-
93
121
  if (
94
122
  bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
95
123
  bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
@@ -100,28 +128,19 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
100
128
  const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
101
129
  .filter(value => typeof value === 'string')
102
130
  .join(' ');
103
- return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
131
+ return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
104
132
  };
105
133
 
106
134
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
107
135
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
108
- const BLE_RESPONSE_TIMEOUT_MS = 30_000;
109
136
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
110
137
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
111
- const DEVICE_SCAN_TIMEOUT_MS = 8000;
138
+ const DEVICE_SCAN_TIMEOUT_MS = 3000;
112
139
  const IOS_NOTIFY_READY_DELAY_MS = 150;
113
140
  const ANDROID_NOTIFY_READY_DELAY_MS = 300;
114
- const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
115
- const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
116
- const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
117
-
118
141
  export type ProtocolV2BleTuning = {
119
142
  iosPacketLength?: number;
120
143
  androidPacketLength?: number;
121
- highVolumeWriteBurstSize?: number;
122
- highVolumeWritePauseMs?: number;
123
- highVolumeWriteFlushDelayMs?: number;
124
- highVolumeWriteWithResponse?: boolean;
125
144
  };
126
145
 
127
146
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
@@ -129,10 +148,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
129
148
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
130
149
  iosPacketLength: IOS_PACKET_LENGTH,
131
150
  androidPacketLength: ANDROID_PACKET_LENGTH,
132
- highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
133
- highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
134
- highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
135
- highVolumeWriteWithResponse: false,
136
151
  };
137
152
 
138
153
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -153,27 +168,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
153
168
  tuning.androidPacketLength,
154
169
  protocolV2BleTuning.androidPacketLength
155
170
  ),
156
- highVolumeWriteBurstSize: normalizePositiveInteger(
157
- tuning.highVolumeWriteBurstSize,
158
- protocolV2BleTuning.highVolumeWriteBurstSize
159
- ),
160
- highVolumeWritePauseMs: normalizePositiveInteger(
161
- tuning.highVolumeWritePauseMs,
162
- protocolV2BleTuning.highVolumeWritePauseMs
163
- ),
164
- highVolumeWriteFlushDelayMs: normalizePositiveInteger(
165
- tuning.highVolumeWriteFlushDelayMs,
166
- protocolV2BleTuning.highVolumeWriteFlushDelayMs
167
- ),
168
- highVolumeWriteWithResponse:
169
- tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
170
171
  };
171
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
172
+ Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
172
173
  }
173
174
 
174
175
  export function resetProtocolV2BleTuning() {
175
176
  protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
176
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
177
+ Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
177
178
  }
178
179
 
179
180
  export function getProtocolV2BleTuning() {
@@ -188,16 +189,6 @@ function getDeviceDisplayName(device?: Device | null) {
188
189
  return device?.name || device?.localName || null;
189
190
  }
190
191
 
191
- function isGenericBleService(uuid?: string | null) {
192
- return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
193
- }
194
-
195
- function hasKnownOneKeyService(device?: Device | null) {
196
- return (device?.serviceUUIDs ?? []).some(serviceUuid =>
197
- getInfosForServiceUuid(serviceUuid, 'classic')
198
- );
199
- }
200
-
201
192
  const ANDROID_REQUEST_MTU = 256;
202
193
 
203
194
  const connectOptions: Record<string, unknown> = {
@@ -222,9 +213,10 @@ const requestAndroidMtu = async (device: Device) => {
222
213
 
223
214
  try {
224
215
  const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
225
- Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
216
+ Log?.debug('[ReactNativeBleTransport] MTU configured', {
217
+ deviceId: device.id,
226
218
  requested: ANDROID_REQUEST_MTU,
227
- mtu: mtuDevice.mtu,
219
+ actual: mtuDevice.mtu,
228
220
  });
229
221
  return mtuDevice;
230
222
  } catch (error) {
@@ -272,6 +264,8 @@ export default class ReactNativeBleTransport {
272
264
 
273
265
  _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
274
266
 
267
+ private protocolV2SchemaConfiguration: string | undefined;
268
+
275
269
  name = 'ReactNativeBleTransport';
276
270
 
277
271
  configured = false;
@@ -282,6 +276,8 @@ export default class ReactNativeBleTransport {
282
276
 
283
277
  runPromise: Deferred<any> | null = null;
284
278
 
279
+ private runPromiseDeviceId: string | null = null;
280
+
285
281
  emitter?: EventEmitter;
286
282
 
287
283
  firmwareUploadWriteRecoveryIds = new Set<string>();
@@ -297,12 +293,31 @@ export default class ReactNativeBleTransport {
297
293
 
298
294
  private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
299
295
 
300
- private activeProtocolV2Call: { uuid: string; token: number } | null = null;
301
-
302
- private nextProtocolV2CallToken = 1;
296
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
297
+ getSchemas: () => {
298
+ if (!this._messages || !this._messagesV2) {
299
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
300
+ }
301
+ return {
302
+ protocolV1: this._messages,
303
+ protocolV2: this._messagesV2,
304
+ };
305
+ },
306
+ classifyError: () => 'link-fatal',
307
+ onLinkInvalidated: async (uuid, reason) => {
308
+ this.protocolV2Assemblers.get(uuid)?.reset();
309
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
310
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
311
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
312
+ await this.releaseNative(uuid, true);
313
+ }
314
+ },
315
+ });
303
316
 
304
317
  private monitorTokens: Map<string, number> = new Map();
305
318
 
319
+ private disconnectEventTokens: Map<string, number> = new Map();
320
+
306
321
  private nextMonitorToken = 1;
307
322
 
308
323
  constructor(options: TransportOptions) {
@@ -321,8 +336,19 @@ export default class ReactNativeBleTransport {
321
336
  }
322
337
 
323
338
  configureProtocolV2(signedData: any) {
339
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
340
+ if (this.protocolV2SchemaConfiguration === configuration) {
341
+ return;
342
+ }
343
+
344
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
324
345
  this._messagesV2 = parseConfigure(signedData);
325
- Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
346
+ this.protocolV2SchemaConfiguration = configuration;
347
+ if (isReconfiguration) {
348
+ this.protocolV2Links
349
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
350
+ .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
351
+ }
326
352
  }
327
353
 
328
354
  listen() {
@@ -352,29 +378,15 @@ export default class ReactNativeBleTransport {
352
378
  }
353
379
  }
354
380
 
355
- let fallbackServiceUuid: string | undefined;
356
-
357
381
  if (!infos) {
358
382
  const services = await device.services();
359
383
  Log?.debug(
360
384
  '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
361
385
  services?.map(service => service.uuid)
362
386
  );
363
-
364
- const knownService = services.find(service =>
365
- getInfosForServiceUuid(service.uuid, 'classic')
366
- );
367
- const fallbackService =
368
- knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
369
-
370
- if (fallbackService) {
371
- fallbackServiceUuid = fallbackService.uuid;
372
- characteristics = await device.characteristicsForService(fallbackService.uuid);
373
- Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
374
- }
375
387
  }
376
388
 
377
- if (!infos && !fallbackServiceUuid) {
389
+ if (!infos) {
378
390
  try {
379
391
  Log?.debug('cancel connection when service not found');
380
392
  await device.cancelConnection();
@@ -384,9 +396,7 @@ export default class ReactNativeBleTransport {
384
396
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
385
397
  }
386
398
 
387
- const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
388
- const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
389
- const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
399
+ const { serviceUuid, writeUuid, notifyUuid } = infos;
390
400
 
391
401
  if (!serviceUuid) {
392
402
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
@@ -436,6 +446,7 @@ export default class ReactNativeBleTransport {
436
446
 
437
447
  attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
438
448
  transport.disconnectSubscription?.remove();
449
+ const { monitorToken } = transport;
439
450
  transport.disconnectSubscription = device.onDisconnected(() => {
440
451
  if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
441
452
  Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
@@ -445,18 +456,17 @@ export default class ReactNativeBleTransport {
445
456
  Log?.debug('device disconnect ignored for stale transport: ', device?.id);
446
457
  return;
447
458
  }
459
+ if (this.monitorTokens.get(uuid) !== monitorToken) {
460
+ Log?.debug('device disconnect ignored for stale generation: ', device?.id);
461
+ return;
462
+ }
448
463
 
449
464
  try {
450
465
  Log?.debug('device disconnect: ', device?.id);
451
- this.emitter?.emit('device-disconnect', {
452
- name: device?.name,
453
- id: device?.id,
454
- connectId: device?.id,
455
- });
456
- if (this.runPromise) {
466
+ this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
467
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
457
468
  const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
458
469
  this.runPromise.reject(error);
459
- this.rejectAllProtocolV2Frames(error);
460
470
  }
461
471
  } catch (e) {
462
472
  Log?.debug('device disconnect error: ', e);
@@ -466,6 +476,22 @@ export default class ReactNativeBleTransport {
466
476
  });
467
477
  }
468
478
 
479
+ private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
480
+ if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
481
+ return;
482
+ }
483
+ if (this.monitorTokens.get(uuid) !== token) {
484
+ Log?.debug('device disconnect event ignored for stale generation: ', uuid);
485
+ return;
486
+ }
487
+ this.disconnectEventTokens.set(uuid, token);
488
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
489
+ name,
490
+ id: uuid,
491
+ connectId: uuid,
492
+ });
493
+ }
494
+
469
495
  async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
470
496
  this.firmwareUploadWriteRecoveryIds.add(uuid);
471
497
  try {
@@ -553,14 +579,13 @@ export default class ReactNativeBleTransport {
553
579
  }
554
580
 
555
581
  blePlxManager.startDeviceScan(
556
- null,
582
+ getBluetoothServiceUuids(),
557
583
  {
558
584
  allowDuplicates: true,
559
585
  scanMode: ScanMode.LowLatency,
560
586
  },
561
587
  (error, device) => {
562
588
  if (error) {
563
- Log?.debug('ble scan manager: ', blePlxManager);
564
589
  Log?.debug('ble scan error: ', error);
565
590
  if (
566
591
  [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
@@ -584,33 +609,23 @@ export default class ReactNativeBleTransport {
584
609
  }
585
610
 
586
611
  const displayName = getDeviceDisplayName(device);
612
+ // iOS may report a service-only advertisement before the named scan response.
613
+ // Do not cache that incomplete advertisement as an unknown device.
614
+ const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
615
+ const isFindMyPeripheral =
616
+ isPro2FindMyAdvertisementName(device?.name) ||
617
+ isPro2FindMyAdvertisementName(device?.localName);
587
618
  const isOneKey =
588
- isOnekeyDevice(device?.name ?? null, device?.id) ||
589
- isOnekeyDevice(device?.localName ?? null, device?.id) ||
590
- hasKnownOneKeyService(device);
591
- const shouldTraceCandidate =
592
- !!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
593
-
594
- if (shouldTraceCandidate) {
595
- Log?.debug('[ReactNativeBleTransport] scan candidate', {
619
+ !isUnnamedIOSPeripheral &&
620
+ !isFindMyPeripheral &&
621
+ isOnekeyBluetoothDevice({
622
+ id: device?.id,
596
623
  name: device?.name,
597
624
  localName: device?.localName,
598
- id: device?.id,
599
- serviceUUIDs: device?.serviceUUIDs,
600
- accepted: isOneKey,
625
+ serviceUuids: device?.serviceUUIDs,
601
626
  });
602
- }
603
-
604
627
  if (isOneKey) {
605
- Log?.debug('search device start ======================');
606
- const { name, localName, id, serviceUUIDs } = device ?? {};
607
- Log?.debug(
608
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
609
- id ?? ''
610
- }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
611
- );
612
628
  addDevice(device as unknown as Device);
613
- Log?.debug('search device end ======================\n');
614
629
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
615
630
  Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
616
631
  name: device?.name,
@@ -622,12 +637,32 @@ export default class ReactNativeBleTransport {
622
637
  }
623
638
  );
624
639
 
625
- getConnectedDeviceIds(getBluetoothServiceUuids()).then(devices => {
626
- for (const device of devices) {
627
- Log?.debug('search connected peripheral: ', device.id);
628
- addDevice(device as unknown as Device);
640
+ getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
641
+ devices => {
642
+ for (const device of devices) {
643
+ const localName =
644
+ 'localName' in device && typeof device.localName === 'string'
645
+ ? device.localName
646
+ : null;
647
+ const isFindMyPeripheral =
648
+ isPro2FindMyAdvertisementName(device.name) ||
649
+ isPro2FindMyAdvertisementName(localName);
650
+
651
+ if (
652
+ !isFindMyPeripheral &&
653
+ isOnekeyBluetoothDevice({
654
+ id: device.id,
655
+ name: device.name,
656
+ localName,
657
+ serviceUuids: device.serviceUUIDs,
658
+ })
659
+ ) {
660
+ Log?.debug('search connected peripheral: ', device.id);
661
+ addDevice(device as unknown as Device);
662
+ }
663
+ }
629
664
  }
630
- });
665
+ );
631
666
 
632
667
  const addDevice = (device: Device) => {
633
668
  if (deviceList.every(d => d.id !== device.id)) {
@@ -641,6 +676,12 @@ export default class ReactNativeBleTransport {
641
676
  name: displayName,
642
677
  commType: 'ble',
643
678
  } as IOneKeyDevice);
679
+ Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
680
+ deviceId: device.id,
681
+ name: displayName,
682
+ serviceUUIDs: device.serviceUUIDs,
683
+ protocolHint,
684
+ });
644
685
  }
645
686
  };
646
687
 
@@ -651,6 +692,46 @@ export default class ReactNativeBleTransport {
651
692
  });
652
693
  }
653
694
 
695
+ private async installTransportForAcquire(
696
+ uuid: string,
697
+ device: Device,
698
+ characteristics?: ResolvedBleCharacteristics
699
+ ) {
700
+ const { writeCharacteristic, notifyCharacteristic } =
701
+ characteristics ?? (await this.resolveCharacteristics(device));
702
+ const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
703
+ if (Platform.OS === 'android') {
704
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
705
+ }
706
+ const monitorToken = this.nextMonitorToken;
707
+ this.nextMonitorToken += 1;
708
+ const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
709
+ transport.monitorToken = monitorToken;
710
+ transport.notifyTransactionId = notifyTransactionId;
711
+ this.monitorTokens.set(uuid, monitorToken);
712
+ transport.notifySubscription = this._monitorCharacteristic(
713
+ transport.notifyCharacteristic,
714
+ uuid,
715
+ monitorToken,
716
+ notifyTransactionId
717
+ );
718
+ transportCache[uuid] = transport;
719
+ this.protocolV2Assemblers.set(
720
+ uuid,
721
+ new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
722
+ );
723
+
724
+ if (Platform.OS === 'ios') {
725
+ await new Promise<void>(resolve => {
726
+ setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
727
+ });
728
+ } else if (Platform.OS === 'android') {
729
+ await delay(ANDROID_NOTIFY_READY_DELAY_MS);
730
+ }
731
+
732
+ return transport;
733
+ }
734
+
654
735
  async acquire(input: BleAcquireInput) {
655
736
  const { uuid, forceCleanRunPromise, expectedProtocol } = input;
656
737
 
@@ -684,9 +765,8 @@ export default class ReactNativeBleTransport {
684
765
  if (forceCleanRunPromise && this.runPromise) {
685
766
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
686
767
  this.runPromise.reject(error);
687
- this.rejectAllProtocolV2Frames(error);
688
768
  this.runPromise = null;
689
- this.activeProtocolV2Call = null;
769
+ this.runPromiseDeviceId = null;
690
770
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
691
771
  }
692
772
 
@@ -775,12 +855,16 @@ export default class ReactNativeBleTransport {
775
855
  }
776
856
 
777
857
  device = await requestAndroidMtu(device);
778
- const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
858
+ const acquiredDevice = device;
859
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
860
+ acquiredDevice
861
+ );
779
862
 
780
863
  const protocolHint = expectedProtocol
781
864
  ? undefined
782
- : this.deviceProtocolHints.get(uuid) ??
783
- inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
865
+ : input.protocolHint ??
866
+ this.deviceProtocolHints.get(uuid) ??
867
+ inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
784
868
 
785
869
  // release transport before new transport instance
786
870
  await this.release(uuid, true);
@@ -788,45 +872,30 @@ export default class ReactNativeBleTransport {
788
872
  this.deviceProtocolHints.set(uuid, protocolHint);
789
873
  }
790
874
 
791
- const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
792
- if (Platform.OS === 'android') {
793
- transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
794
- }
795
- const monitorToken = this.nextMonitorToken;
796
- this.nextMonitorToken += 1;
797
- const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
798
- transport.monitorToken = monitorToken;
799
- transport.notifyTransactionId = notifyTransactionId;
800
- this.monitorTokens.set(uuid, monitorToken);
801
- transport.notifySubscription = this._monitorCharacteristic(
802
- transport.notifyCharacteristic,
803
- uuid,
804
- monitorToken,
805
- notifyTransactionId
806
- );
807
- transportCache[uuid] = transport;
808
-
809
- this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
810
-
811
- if (Platform.OS === 'ios') {
812
- await new Promise<void>(resolve => {
813
- setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
814
- });
815
- } else if (Platform.OS === 'android') {
816
- await delay(ANDROID_NOTIFY_READY_DELAY_MS);
817
- }
818
-
819
- const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
820
-
821
- this.emitter?.emit('device-connect', {
822
- name: device.name,
823
- id: device.id,
824
- connectId: device.id,
875
+ await this.installTransportForAcquire(uuid, acquiredDevice, {
876
+ writeCharacteristic,
877
+ notifyCharacteristic,
825
878
  });
826
879
 
827
- this.attachDisconnectSubscription(transport, device, uuid);
828
-
829
- return { uuid, protocolType };
880
+ try {
881
+ const protocolType = await this.detectProtocol(
882
+ uuid,
883
+ expectedProtocol,
884
+ protocolHint,
885
+ async () => {
886
+ await this.installTransportForAcquire(uuid, acquiredDevice);
887
+ }
888
+ );
889
+ const currentTransport = transportCache[uuid];
890
+ if (!currentTransport) {
891
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
892
+ }
893
+ this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
894
+ return { uuid, protocolType };
895
+ } catch (error) {
896
+ await this.release(uuid, true);
897
+ throw error;
898
+ }
830
899
  }
831
900
 
832
901
  _monitorCharacteristic(
@@ -853,7 +922,30 @@ export default class ReactNativeBleTransport {
853
922
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
854
923
  return;
855
924
  }
856
- if (this.runPromise) {
925
+ if (this.deviceProtocol.get(uuid) === 'V2') {
926
+ let errorCode:
927
+ | typeof HardwareErrorCode.BleDeviceBondError
928
+ | typeof HardwareErrorCode.BleCharacteristicNotifyError
929
+ | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
930
+ | typeof HardwareErrorCode.BleTimeoutError =
931
+ HardwareErrorCode.BleCharacteristicNotifyError;
932
+ if (error.reason?.includes('The connection has timed out unexpectedly')) {
933
+ errorCode = HardwareErrorCode.BleTimeoutError;
934
+ } else if (error.reason?.includes('Encryption is insufficient')) {
935
+ errorCode = HardwareErrorCode.BleDeviceBondError;
936
+ } else if (
937
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
938
+ error.reason?.includes('Cannot find client characteristic config descriptor') ||
939
+ error.reason?.includes('The handle is invalid') ||
940
+ error.reason?.includes('Writing is not permitted') ||
941
+ error.reason?.includes('notify change failed for device')
942
+ ) {
943
+ errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
944
+ }
945
+ this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
946
+ return;
947
+ }
948
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
857
949
  let ERROR:
858
950
  | typeof HardwareErrorCode.BleDeviceBondError
859
951
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -876,7 +968,6 @@ export default class ReactNativeBleTransport {
876
968
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure
877
969
  );
878
970
  this.runPromise.reject(notifyError);
879
- this.rejectAllProtocolV2Frames(notifyError);
880
971
  Log?.debug(
881
972
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
882
973
  );
@@ -884,7 +975,6 @@ export default class ReactNativeBleTransport {
884
975
  }
885
976
  const notifyError = ERRORS.TypedError(ERROR);
886
977
  this.runPromise.reject(notifyError);
887
- this.rejectAllProtocolV2Frames(notifyError);
888
978
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
889
979
  }
890
980
 
@@ -908,7 +998,7 @@ export default class ReactNativeBleTransport {
908
998
  return;
909
999
  }
910
1000
  if (protocol === 'V2') {
911
- this.handleProtocolV2Notification(uuid, new Uint8Array(data));
1001
+ this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
912
1002
  return;
913
1003
  }
914
1004
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
@@ -929,13 +1019,18 @@ export default class ReactNativeBleTransport {
929
1019
  // );
930
1020
  bufferLength = 0;
931
1021
  buffer = [];
932
- this.runPromise?.resolve(value.toString('hex'));
1022
+ if (this.runPromiseDeviceId === uuid) {
1023
+ this.runPromise?.resolve(value.toString('hex'));
1024
+ }
933
1025
  }
934
1026
  } catch (error) {
935
1027
  Log?.debug('monitor data error: ', error);
936
1028
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
937
- this.runPromise?.reject(notifyError);
938
- this.rejectAllProtocolV2Frames(notifyError);
1029
+ if (this.deviceProtocol.get(uuid) === 'V2') {
1030
+ this.rejectProtocolV2Frames(uuid, notifyError);
1031
+ } else if (this.runPromiseDeviceId === uuid) {
1032
+ this.runPromise?.reject(notifyError);
1033
+ }
939
1034
  }
940
1035
  }, notifyTransactionId);
941
1036
 
@@ -943,13 +1038,18 @@ export default class ReactNativeBleTransport {
943
1038
  }
944
1039
 
945
1040
  async release(uuid: string, onclose = false) {
1041
+ await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
1042
+ return this.releaseNative(uuid, onclose);
1043
+ }
1044
+
1045
+ private async releaseNative(uuid: string, onclose = false) {
946
1046
  const transport = transportCache[uuid];
947
- if (this.runPromise) {
1047
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
948
1048
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
949
1049
  this.runPromise.reject(error);
950
1050
  this.runPromise = null;
951
- this.rejectAllProtocolV2Frames(error);
952
- this.activeProtocolV2Call = null;
1051
+ this.runPromiseDeviceId = null;
1052
+ this.rejectProtocolV2Frames(uuid, error);
953
1053
  } else {
954
1054
  this.resetProtocolV2Frames(uuid);
955
1055
  }
@@ -957,9 +1057,6 @@ export default class ReactNativeBleTransport {
957
1057
  if (Platform.OS === 'android' && !onclose && transport) {
958
1058
  this.protocolV2Assemblers.get(uuid)?.reset();
959
1059
  this.resetProtocolV2Frames(uuid);
960
- if (this.activeProtocolV2Call?.uuid === uuid) {
961
- this.activeProtocolV2Call = null;
962
- }
963
1060
  return Promise.resolve(true);
964
1061
  }
965
1062
 
@@ -993,7 +1090,7 @@ export default class ReactNativeBleTransport {
993
1090
  }
994
1091
 
995
1092
  this.deviceProtocol.delete(uuid);
996
- this.deviceProtocolHints.delete(uuid);
1093
+ // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
997
1094
  this.protocolV2Assemblers.get(uuid)?.reset();
998
1095
  this.protocolV2Assemblers.delete(uuid);
999
1096
  this.resetProtocolV2Frames(uuid);
@@ -1025,13 +1122,6 @@ export default class ReactNativeBleTransport {
1025
1122
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1026
1123
  }
1027
1124
 
1028
- const forceRun = name === 'Initialize' || name === 'Cancel';
1029
-
1030
- Log?.debug('transport-react-native call this.runPromise', this.runPromise);
1031
- if (this.runPromise && !forceRun) {
1032
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1033
- }
1034
-
1035
1125
  const protocol = this.getProtocolType(uuid);
1036
1126
  if (!protocol) {
1037
1127
  throw ERRORS.TypedError(
@@ -1039,31 +1129,17 @@ export default class ReactNativeBleTransport {
1039
1129
  `Device protocol has not been detected for ${uuid}`
1040
1130
  );
1041
1131
  }
1042
- // Upload resources on low-end phones may OOM
1043
- if (name === 'ResourceUpdate' || name === 'ResourceAck') {
1044
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
1045
- file_name: data?.file_name,
1046
- hash: data?.hash,
1047
- });
1048
- } else if (LogBlockCommand.has(name)) {
1049
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
1050
- } else {
1051
- Log?.debug(
1052
- 'transport-react-native',
1053
- 'call-',
1054
- ' name: ',
1055
- name,
1056
- ' data: ',
1057
- data,
1058
- ' protocol: ',
1059
- protocol
1060
- );
1061
- }
1132
+ Log?.debug('transport call', createTransportCallLog(name, protocol, data));
1062
1133
 
1063
1134
  if (protocol === 'V2') {
1064
1135
  return this.callProtocolV2(uuid, name, data, options);
1065
1136
  }
1066
1137
 
1138
+ const forceRun = name === 'Initialize' || name === 'Cancel';
1139
+ if (this.runPromise && !forceRun) {
1140
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1141
+ }
1142
+
1067
1143
  return this.callProtocolV1(uuid, name, data, options);
1068
1144
  }
1069
1145
 
@@ -1081,6 +1157,7 @@ export default class ReactNativeBleTransport {
1081
1157
  const runPromise = createDeferred<string>();
1082
1158
  runPromise.promise.catch(() => undefined);
1083
1159
  this.runPromise = runPromise;
1160
+ this.runPromiseDeviceId = uuid;
1084
1161
  const messages = this._messages;
1085
1162
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1086
1163
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1157,14 +1234,13 @@ export default class ReactNativeBleTransport {
1157
1234
  }
1158
1235
  );
1159
1236
  } else if (name === 'FirmwareUpload') {
1160
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
1237
+ Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
1161
1238
  packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
1162
1239
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1163
1240
  pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1164
1241
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1165
1242
  maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
1166
1243
  });
1167
-
1168
1244
  await writeFirmwareUploadChunkedData(
1169
1245
  buffers,
1170
1246
  async data => {
@@ -1174,38 +1250,21 @@ export default class ReactNativeBleTransport {
1174
1250
  // eslint-disable-next-line no-constant-condition
1175
1251
  while (true) {
1176
1252
  try {
1177
- await transport.writeCharacteristic.writeWithoutResponse(data);
1253
+ await transport.writeWithRetry(data);
1178
1254
  return;
1179
1255
  } catch (error) {
1180
1256
  const retryType = getFirmwareUploadWriteRetryType(error);
1181
1257
  if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1182
1258
  throw error;
1183
1259
  }
1184
- const shouldReconnect = retryType === 'reconnectable';
1185
- const delayMs = shouldReconnect
1186
- ? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
1187
- : resolveFirmwareUploadRetryDelay(attempt);
1260
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
1188
1261
  Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
1189
1262
  attempt: attempt + 1,
1190
1263
  delayMs,
1191
- reconnect: shouldReconnect,
1192
1264
  error,
1193
1265
  });
1194
- if (shouldReconnect) {
1195
- this.firmwareUploadWriteRecoveryIds.add(uuid);
1196
- }
1197
1266
  await delay(delayMs);
1198
1267
  attempt += 1;
1199
- if (shouldReconnect) {
1200
- try {
1201
- await this.reconnectFirmwareUploadTransport(uuid, transport);
1202
- } catch (e) {
1203
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
1204
- if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1205
- throw e;
1206
- }
1207
- }
1208
- }
1209
1268
  }
1210
1269
  }
1211
1270
  },
@@ -1218,9 +1277,14 @@ export default class ReactNativeBleTransport {
1218
1277
  for (const o of buffers) {
1219
1278
  const outData = o.toString('base64');
1220
1279
  // Upload resources on low-end phones may OOM
1221
- // this.Log.debug('send hex strting: ', o.toString('hex'));
1222
1280
  try {
1223
- await transport.writeCharacteristic.writeWithoutResponse(outData);
1281
+ const shouldUseWriteWithResponse =
1282
+ Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1283
+ if (shouldUseWriteWithResponse) {
1284
+ await transport.writeCharacteristic.writeWithResponse(outData);
1285
+ } else {
1286
+ await transport.writeCharacteristic.writeWithoutResponse(outData);
1287
+ }
1224
1288
  } catch (e) {
1225
1289
  Log?.debug('writeCharacteristic write error: ', e);
1226
1290
  this.runPromise = null;
@@ -1256,20 +1320,28 @@ export default class ReactNativeBleTransport {
1256
1320
  throw new Error('Returning data is not string.');
1257
1321
  }
1258
1322
 
1259
- Log?.debug('receive data: ', response);
1260
1323
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1261
1324
  return check.call(jsonData);
1262
1325
  } catch (e) {
1263
- if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1264
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1326
+ if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1327
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
1265
1328
  } else {
1266
1329
  Log?.error('call error: ', e);
1267
1330
  }
1331
+ const isProbeTimeout =
1332
+ name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1333
+ if (
1334
+ !isProbeTimeout &&
1335
+ (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1336
+ ) {
1337
+ await this.disconnect(uuid);
1338
+ }
1268
1339
  throw e;
1269
1340
  } finally {
1270
1341
  if (timeout) clearTimeout(timeout);
1271
1342
  if (this.runPromise === runPromise) {
1272
1343
  this.runPromise = null;
1344
+ this.runPromiseDeviceId = null;
1273
1345
  }
1274
1346
  }
1275
1347
  }
@@ -1279,8 +1351,9 @@ export default class ReactNativeBleTransport {
1279
1351
  }
1280
1352
 
1281
1353
  async disconnect(session: string) {
1282
- Log?.debug('transport-react-native transport resetSession: ', session);
1354
+ await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
1283
1355
  const transport = transportCache[session];
1356
+ const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
1284
1357
 
1285
1358
  // Clean up disconnect subscription first to prevent onDisconnected callback
1286
1359
  // from being triggered when we cancel the connection below
@@ -1341,20 +1414,16 @@ export default class ReactNativeBleTransport {
1341
1414
  this.deviceProtocolHints.delete(session);
1342
1415
  this.protocolV2Assemblers.delete(session);
1343
1416
  this.resetProtocolV2Frames(session);
1344
- if (this.activeProtocolV2Call?.uuid === session) {
1345
- this.activeProtocolV2Call = null;
1346
- }
1347
1417
 
1348
1418
  // emit the disconnect event
1349
1419
  try {
1350
- this.emitter?.emit('device-disconnect', {
1351
- name: transport?.device?.name,
1352
- id: session,
1353
- connectId: session,
1354
- });
1420
+ this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
1355
1421
  } catch (e) {
1356
1422
  Log?.error('resetSession: emit disconnect event error: ', e);
1357
1423
  }
1424
+ if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
1425
+ this.monitorTokens.delete(session);
1426
+ }
1358
1427
  // eslint-disable-next-line no-promise-executor-return
1359
1428
  await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
1360
1429
  }
@@ -1365,6 +1434,7 @@ export default class ReactNativeBleTransport {
1365
1434
  // this.runPromise.reject(new Error('Transport_CallCanceled'));
1366
1435
  }
1367
1436
  this.runPromise = null;
1437
+ this.runPromiseDeviceId = null;
1368
1438
  }
1369
1439
 
1370
1440
  private getCachedTransport(uuid: string) {
@@ -1385,7 +1455,7 @@ export default class ReactNativeBleTransport {
1385
1455
  private createProtocolDetectionError() {
1386
1456
  return ERRORS.TypedError(
1387
1457
  HardwareErrorCode.BleTimeoutError,
1388
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1458
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
1389
1459
  );
1390
1460
  }
1391
1461
 
@@ -1398,42 +1468,71 @@ export default class ReactNativeBleTransport {
1398
1468
  private async detectProtocol(
1399
1469
  uuid: string,
1400
1470
  expectedProtocol?: ProtocolType,
1401
- protocolHint?: ProtocolType
1471
+ protocolHint?: ProtocolType,
1472
+ rebuildTransport?: () => Promise<void>
1402
1473
  ): Promise<ProtocolType> {
1474
+ if (Platform.OS === 'ios' && expectedProtocol) {
1475
+ this.deviceProtocol.set(uuid, expectedProtocol);
1476
+ Log?.debug('[ReactNativeBleTransport] protocol selected', {
1477
+ deviceId: uuid,
1478
+ protocol: expectedProtocol,
1479
+ source: 'expected',
1480
+ });
1481
+ return expectedProtocol;
1482
+ }
1483
+
1403
1484
  if (expectedProtocol === 'V1') {
1404
1485
  if (await this.probeProtocolV1(uuid)) {
1405
1486
  this.deviceProtocol.set(uuid, 'V1');
1406
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1487
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1488
+ deviceId: uuid,
1489
+ protocol: 'V1',
1490
+ source: 'expected',
1491
+ });
1407
1492
  return 'V1';
1408
1493
  }
1409
1494
  throw this.createProtocolMismatchError(expectedProtocol);
1410
1495
  }
1411
1496
 
1412
1497
  if (expectedProtocol === 'V2') {
1413
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
1414
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
1415
- this.deviceProtocol.set(uuid, 'V2');
1416
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1417
- return 'V2';
1498
+ if (await this.probeProtocolV2(uuid)) {
1499
+ this.deviceProtocol.set(uuid, 'V2');
1500
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1501
+ deviceId: uuid,
1502
+ protocol: 'V2',
1503
+ source: 'expected',
1504
+ });
1505
+ return 'V2';
1506
+ }
1507
+ throw this.createProtocolMismatchError(expectedProtocol);
1418
1508
  }
1419
1509
 
1420
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
1421
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1
1422
- // 不能作为最终结论。
1510
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
1511
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
1423
1512
  const probeOrder: ProtocolType[] =
1424
1513
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1425
1514
 
1426
1515
  for (let i = 0; i < probeOrder.length; i += 1) {
1427
1516
  const protocol = probeOrder[i];
1428
1517
  if (i > 0) {
1429
- // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
1518
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
1430
1519
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1520
+ if (!transportCache[uuid]) {
1521
+ if (!rebuildTransport) {
1522
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1523
+ }
1524
+ await rebuildTransport();
1525
+ }
1431
1526
  }
1432
1527
  const detected =
1433
1528
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1434
1529
  if (detected) {
1435
1530
  this.deviceProtocol.set(uuid, protocol);
1436
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1531
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1532
+ deviceId: uuid,
1533
+ protocol,
1534
+ source: 'probe',
1535
+ });
1437
1536
  return protocol;
1438
1537
  }
1439
1538
  }
@@ -1444,11 +1543,12 @@ export default class ReactNativeBleTransport {
1444
1543
 
1445
1544
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1446
1545
  const transport = transportCache[uuid];
1546
+ await this.protocolV2Links.invalidateLink(
1547
+ uuid,
1548
+ `Reset notify state after Protocol ${protocol} probe`
1549
+ );
1447
1550
  this.protocolV2Assemblers.get(uuid)?.reset();
1448
1551
  this.resetProtocolV2Frames(uuid);
1449
- if (this.activeProtocolV2Call?.uuid === uuid) {
1450
- this.activeProtocolV2Call = null;
1451
- }
1452
1552
  if (this.runPromise) {
1453
1553
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1454
1554
  this.runPromise.reject(error);
@@ -1500,11 +1600,13 @@ export default class ReactNativeBleTransport {
1500
1600
 
1501
1601
  try {
1502
1602
  this.deviceProtocol.set(uuid, 'V1');
1503
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1603
+ // GetFeatures identifies Protocol V1 without resetting an existing wallet
1604
+ // session before Core has a chance to restore a hidden wallet.
1605
+ await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1504
1606
  return true;
1505
1607
  } catch (error) {
1506
1608
  this.clearProbeProtocol(uuid, 'V1');
1507
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1609
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
1508
1610
  return false;
1509
1611
  }
1510
1612
  }
@@ -1533,13 +1635,9 @@ export default class ReactNativeBleTransport {
1533
1635
  return detected;
1534
1636
  }
1535
1637
 
1536
- private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1638
+ private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
1537
1639
  try {
1538
- if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1539
- this.protocolV2Assemblers.get(uuid)?.reset();
1540
- this.resetProtocolV2Frames(uuid);
1541
- return;
1542
- }
1640
+ if (this.monitorTokens.get(uuid) !== monitorToken) return;
1543
1641
 
1544
1642
  if (data.length === 0) return;
1545
1643
 
@@ -1552,8 +1650,15 @@ export default class ReactNativeBleTransport {
1552
1650
  } catch (error) {
1553
1651
  Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1554
1652
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1555
- this.runPromise?.reject(notifyError);
1556
- this.rejectAllProtocolV2Frames(notifyError);
1653
+ this.rejectProtocolV2Frames(uuid, notifyError);
1654
+ this.protocolV2Links
1655
+ .invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
1656
+ .catch(invalidateError =>
1657
+ Log?.debug(
1658
+ '[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
1659
+ invalidateError
1660
+ )
1661
+ );
1557
1662
  }
1558
1663
  }
1559
1664
 
@@ -1576,21 +1681,17 @@ export default class ReactNativeBleTransport {
1576
1681
  this.getProtocolV2FrameQueue(uuid).push(frame);
1577
1682
  }
1578
1683
 
1579
- private rejectAllProtocolV2Frames(error: Error) {
1580
- this.protocolV2FrameQueues.clear();
1581
- for (const framePromise of this.protocolV2FramePromises.values()) {
1582
- framePromise.reject(error);
1583
- }
1584
- this.protocolV2FramePromises.clear();
1585
- }
1586
-
1587
1684
  private resetProtocolV2Frames(uuid: string) {
1588
- this.protocolV2FrameQueues.delete(uuid);
1589
- this.protocolV2FramePromises.delete(uuid);
1685
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1590
1686
  }
1591
1687
 
1592
- private isActiveProtocolV2Call(uuid: string, token: number) {
1593
- return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
1688
+ private rejectProtocolV2Frames(uuid: string, error: Error) {
1689
+ this.protocolV2FrameQueues.delete(uuid);
1690
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1691
+ if (framePromise) {
1692
+ this.protocolV2FramePromises.delete(uuid);
1693
+ framePromise.reject(error);
1694
+ }
1594
1695
  }
1595
1696
 
1596
1697
  private async readProtocolV2Frame(uuid: string) {
@@ -1610,10 +1711,53 @@ export default class ReactNativeBleTransport {
1610
1711
  }
1611
1712
  }
1612
1713
 
1714
+ private async writeProtocolV2Packet(
1715
+ transport: BleTransport,
1716
+ base64: string,
1717
+ context: ProtocolV2CallContext,
1718
+ assertCurrentGeneration: () => void
1719
+ ) {
1720
+ const shouldUseWriteWithResponse =
1721
+ transport.writeCharacteristic.isWritableWithResponse &&
1722
+ (context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
1723
+ let attempt = 0;
1724
+ for (;;) {
1725
+ assertCurrentGeneration();
1726
+ if (context.signal.aborted) {
1727
+ throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1728
+ }
1729
+ try {
1730
+ if (shouldUseWriteWithResponse) {
1731
+ await transport.writeCharacteristic.writeWithResponse(base64);
1732
+ } else {
1733
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1734
+ }
1735
+ assertCurrentGeneration();
1736
+ return;
1737
+ } catch (error) {
1738
+ if (
1739
+ getFirmwareUploadWriteRetryType(error) !== 'congested' ||
1740
+ attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
1741
+ ) {
1742
+ throw error;
1743
+ }
1744
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
1745
+ attempt += 1;
1746
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
1747
+ name: context.messageName,
1748
+ attempt,
1749
+ delayMs,
1750
+ });
1751
+ await delay(delayMs);
1752
+ }
1753
+ }
1754
+ }
1755
+
1613
1756
  private async writeProtocolV2Frame(
1614
1757
  transport: BleTransport,
1615
1758
  frame: Uint8Array,
1616
- options?: { highVolume?: boolean; writeWithResponse?: boolean }
1759
+ context: ProtocolV2CallContext,
1760
+ assertCurrentGeneration: () => void
1617
1761
  ) {
1618
1762
  const tuning = getProtocolV2BleTuning();
1619
1763
  const packetCapacity = resolveProtocolV2PacketCapacity({
@@ -1622,37 +1766,31 @@ export default class ReactNativeBleTransport {
1622
1766
  androidPacketLength: tuning.androidPacketLength,
1623
1767
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1624
1768
  });
1625
- const writeWithResponse =
1626
- !!options?.writeWithResponse || (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
1627
- const writeMode = resolveBleWriteMode(
1628
- transport.writeCharacteristic,
1629
- writeWithResponse ? 'withResponse' : 'withoutResponse'
1630
- );
1631
- const shouldThrottle = !!options?.highVolume && writeMode === 'withoutResponse';
1632
- let packetsWritten = 0;
1633
-
1634
- for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1635
- const chunk = frame.slice(offset, offset + packetCapacity);
1636
- const base64 = Buffer.from(chunk).toString('base64');
1637
- if (writeMode === 'withResponse') {
1638
- await transport.writeCharacteristic.writeWithResponse(base64);
1639
- } else {
1640
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1641
- }
1642
- packetsWritten += 1;
1643
-
1644
- if (
1645
- shouldThrottle &&
1646
- packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1647
- offset + packetCapacity < frame.length
1648
- ) {
1649
- await delay(tuning.highVolumeWritePauseMs);
1650
- }
1651
- }
1652
-
1653
- if (shouldThrottle) {
1654
- await delay(tuning.highVolumeWriteFlushDelayMs);
1655
- }
1769
+ // Match Desktop BLE pacing so Pro2 firmware can finish the previous response
1770
+ // before the next single-packet control command is written.
1771
+ const initialDelayMs =
1772
+ Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
1773
+ ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
1774
+ : 0;
1775
+ await writeProtocolV2BleFrame({
1776
+ frame,
1777
+ packetCapacity,
1778
+ assertActive: assertCurrentGeneration,
1779
+ signal: context.signal,
1780
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1781
+ initialDelayMs,
1782
+ burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1783
+ burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1784
+ flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1785
+ wait: delay,
1786
+ writePacket: packet =>
1787
+ this.writeProtocolV2Packet(
1788
+ transport,
1789
+ Buffer.from(packet).toString('base64'),
1790
+ context,
1791
+ assertCurrentGeneration
1792
+ ),
1793
+ });
1656
1794
  }
1657
1795
 
1658
1796
  private async callProtocolV2(
@@ -1665,101 +1803,76 @@ export default class ReactNativeBleTransport {
1665
1803
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1666
1804
  }
1667
1805
 
1668
- const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'Ping';
1669
- if (this.runPromise) {
1670
- if (!forceRun) {
1671
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1672
- }
1673
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1674
- this.runPromise.reject(error);
1675
- this.rejectAllProtocolV2Frames(error);
1676
- this.runPromise = null;
1677
- this.activeProtocolV2Call = null;
1678
- }
1679
-
1680
- const transport = this.getCachedTransport(uuid);
1681
- const runPromise = createDeferred<Uint8Array>();
1682
- runPromise.promise.catch(() => undefined);
1683
- this.runPromise = runPromise;
1684
- const callToken = this.nextProtocolV2CallToken++;
1685
- this.activeProtocolV2Call = { uuid, token: callToken };
1686
- this.protocolV2Assemblers.get(uuid)?.reset();
1687
- this.resetProtocolV2Frames(uuid);
1688
- let completed = false;
1689
- const callOptions = {
1690
- ...options,
1691
- timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1692
- };
1806
+ const callOptions = options;
1693
1807
  const highVolumeWrite = LogBlockCommand.has(name);
1694
1808
 
1695
1809
  if (highVolumeWrite) {
1696
1810
  const tuning = getProtocolV2BleTuning();
1697
- Log?.debug(
1698
- '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1811
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1699
1812
  name,
1700
- {
1701
- packetCapacity:
1702
- Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1703
- burstSize: tuning.highVolumeWriteBurstSize,
1704
- pauseMs: tuning.highVolumeWritePauseMs,
1705
- flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1706
- writeWithResponse: tuning.highVolumeWriteWithResponse,
1707
- }
1708
- );
1813
+ writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
1814
+ packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1815
+ });
1709
1816
  }
1710
1817
 
1711
1818
  try {
1712
- const session = new ProtocolV2Session({
1713
- schemas: {
1714
- protocolV1: this._messages,
1715
- protocolV2: this._messagesV2,
1716
- },
1717
- router: PROTOCOL_V2_CHANNEL_BLE_UART,
1718
- writeFrame: async (frame: Uint8Array) => {
1719
- await this.writeProtocolV2Frame(transport, frame, {
1720
- highVolume: highVolumeWrite,
1721
- });
1722
- },
1723
- readFrame: async () => {
1724
- const rxFrame = await this.readProtocolV2Frame(uuid);
1725
- if (!(rxFrame instanceof Uint8Array)) {
1726
- throw new Error('Protocol V2 response is not Uint8Array');
1727
- }
1728
- return rxFrame;
1729
- },
1730
- logger: Log,
1731
- logPrefix: 'ProtocolV2 RN-BLE',
1732
- createTimeoutError: (_messageName: string, timeout: number) =>
1733
- ERRORS.TypedError(
1734
- HardwareErrorCode.BleTimeoutError,
1735
- `BLE response timeout after ${timeout}ms for ${name}`
1736
- ),
1737
- });
1738
-
1739
- const result = await session.call(name, data, callOptions);
1740
- completed = true;
1741
- return result;
1819
+ return await this.protocolV2Links.call(
1820
+ uuid,
1821
+ () => this.createProtocolV2Adapter(uuid),
1822
+ name,
1823
+ data,
1824
+ callOptions
1825
+ );
1742
1826
  } catch (e) {
1743
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1744
- this.protocolV2Assemblers.get(uuid)?.reset();
1745
- this.resetProtocolV2Frames(uuid);
1746
- }
1747
1827
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1748
1828
  throw e;
1749
- } finally {
1750
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1751
- if (!completed) {
1752
- this.protocolV2Assemblers.get(uuid)?.reset();
1753
- }
1754
- this.resetProtocolV2Frames(uuid);
1755
- this.activeProtocolV2Call = null;
1756
- }
1757
- if (this.runPromise === runPromise) {
1758
- this.runPromise = null;
1759
- }
1760
1829
  }
1761
1830
  }
1762
1831
 
1832
+ private createProtocolV2Adapter(uuid: string) {
1833
+ const generation = this.monitorTokens.get(uuid) ?? 0;
1834
+ const assertCurrentGeneration = () => {
1835
+ if (this.monitorTokens.get(uuid) !== generation) {
1836
+ throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
1837
+ }
1838
+ };
1839
+
1840
+ return {
1841
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1842
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1843
+ generation,
1844
+ prepareCall: () => {
1845
+ assertCurrentGeneration();
1846
+ this.protocolV2Assemblers.get(uuid)?.reset();
1847
+ this.resetProtocolV2Frames(uuid);
1848
+ },
1849
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
1850
+ assertCurrentGeneration();
1851
+ const currentTransport = this.getCachedTransport(uuid);
1852
+ await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
1853
+ },
1854
+ readFrame: async () => {
1855
+ assertCurrentGeneration();
1856
+ const rxFrame = await this.readProtocolV2Frame(uuid);
1857
+ if (!(rxFrame instanceof Uint8Array)) {
1858
+ throw new Error('Protocol V2 response is not Uint8Array');
1859
+ }
1860
+ return rxFrame;
1861
+ },
1862
+ reset: (reason: string) => {
1863
+ this.protocolV2Assemblers.get(uuid)?.reset();
1864
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
1865
+ },
1866
+ logger: Log,
1867
+ logPrefix: 'ProtocolV2 RN-BLE',
1868
+ createTimeoutError: (messageName: string, timeout: number) =>
1869
+ ERRORS.TypedError(
1870
+ HardwareErrorCode.BleTimeoutError,
1871
+ `BLE response timeout after ${timeout}ms for ${messageName}`
1872
+ ),
1873
+ };
1874
+ }
1875
+
1763
1876
  getProtocolType(path: string): ProtocolType | undefined {
1764
1877
  return this.deviceProtocol.get(path);
1765
1878
  }