@onekeyfe/hd-transport-react-native 1.2.0-alpha.3 → 1.2.0-alpha.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -12,21 +12,21 @@ 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
26
  import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
23
27
 
24
28
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
25
- import {
26
- hasWritableCapability,
27
- resolveBleWriteMode,
28
- resolveProtocolV2PacketCapacity,
29
- } from './bleStrategy';
29
+ import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
30
30
  import { subscribeBleOn } from './subscribeBleOn';
31
31
  import {
32
32
  ANDROID_PACKET_LENGTH,
@@ -40,6 +40,7 @@ import { isHeaderChunk } from './utils/validateNotify';
40
40
  import BleTransport from './BleTransport';
41
41
  import timer from './utils/timer';
42
42
  import { bleLogger, setBleLogger } from './logger';
43
+ import { createTransportCallLog } from './transportLog';
43
44
 
44
45
  import type { Deferred } from '@onekeyfe/hd-shared';
45
46
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
@@ -105,23 +106,14 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
105
106
 
106
107
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
107
108
  Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
108
- const BLE_RESPONSE_TIMEOUT_MS = 30_000;
109
109
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
110
110
  const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
111
- const DEVICE_SCAN_TIMEOUT_MS = 8000;
111
+ const DEVICE_SCAN_TIMEOUT_MS = 3000;
112
112
  const IOS_NOTIFY_READY_DELAY_MS = 150;
113
113
  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
114
  export type ProtocolV2BleTuning = {
119
115
  iosPacketLength?: number;
120
116
  androidPacketLength?: number;
121
- highVolumeWriteBurstSize?: number;
122
- highVolumeWritePauseMs?: number;
123
- highVolumeWriteFlushDelayMs?: number;
124
- highVolumeWriteWithResponse?: boolean;
125
117
  };
126
118
 
127
119
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
@@ -129,10 +121,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
129
121
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
130
122
  iosPacketLength: IOS_PACKET_LENGTH,
131
123
  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
124
  };
137
125
 
138
126
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -153,27 +141,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
153
141
  tuning.androidPacketLength,
154
142
  protocolV2BleTuning.androidPacketLength
155
143
  ),
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
144
  };
171
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
145
+ Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
172
146
  }
173
147
 
174
148
  export function resetProtocolV2BleTuning() {
175
149
  protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
176
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
150
+ Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
177
151
  }
178
152
 
179
153
  export function getProtocolV2BleTuning() {
@@ -222,9 +196,10 @@ const requestAndroidMtu = async (device: Device) => {
222
196
 
223
197
  try {
224
198
  const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
225
- Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
199
+ Log?.debug('[ReactNativeBleTransport] MTU configured', {
200
+ deviceId: device.id,
226
201
  requested: ANDROID_REQUEST_MTU,
227
- mtu: mtuDevice.mtu,
202
+ actual: mtuDevice.mtu,
228
203
  });
229
204
  return mtuDevice;
230
205
  } catch (error) {
@@ -272,6 +247,8 @@ export default class ReactNativeBleTransport {
272
247
 
273
248
  _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
274
249
 
250
+ private protocolV2SchemaConfiguration: string | undefined;
251
+
275
252
  name = 'ReactNativeBleTransport';
276
253
 
277
254
  configured = false;
@@ -297,12 +274,31 @@ export default class ReactNativeBleTransport {
297
274
 
298
275
  private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
299
276
 
300
- private activeProtocolV2Call: { uuid: string; token: number } | null = null;
301
-
302
- private nextProtocolV2CallToken = 1;
277
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
278
+ getSchemas: () => {
279
+ if (!this._messages || !this._messagesV2) {
280
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
281
+ }
282
+ return {
283
+ protocolV1: this._messages,
284
+ protocolV2: this._messagesV2,
285
+ };
286
+ },
287
+ classifyError: () => 'link-fatal',
288
+ onLinkInvalidated: async (uuid, reason) => {
289
+ this.protocolV2Assemblers.get(uuid)?.reset();
290
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
291
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
292
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
293
+ await this.release(uuid, true);
294
+ }
295
+ },
296
+ });
303
297
 
304
298
  private monitorTokens: Map<string, number> = new Map();
305
299
 
300
+ private disconnectEventTokens: Map<string, number> = new Map();
301
+
306
302
  private nextMonitorToken = 1;
307
303
 
308
304
  constructor(options: TransportOptions) {
@@ -321,8 +317,19 @@ export default class ReactNativeBleTransport {
321
317
  }
322
318
 
323
319
  configureProtocolV2(signedData: any) {
320
+ const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
321
+ if (this.protocolV2SchemaConfiguration === configuration) {
322
+ return;
323
+ }
324
+
325
+ const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
324
326
  this._messagesV2 = parseConfigure(signedData);
325
- Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
327
+ this.protocolV2SchemaConfiguration = configuration;
328
+ if (isReconfiguration) {
329
+ this.protocolV2Links
330
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
331
+ .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
332
+ }
326
333
  }
327
334
 
328
335
  listen() {
@@ -436,6 +443,7 @@ export default class ReactNativeBleTransport {
436
443
 
437
444
  attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
438
445
  transport.disconnectSubscription?.remove();
446
+ const { monitorToken } = transport;
439
447
  transport.disconnectSubscription = device.onDisconnected(() => {
440
448
  if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
441
449
  Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
@@ -445,14 +453,14 @@ export default class ReactNativeBleTransport {
445
453
  Log?.debug('device disconnect ignored for stale transport: ', device?.id);
446
454
  return;
447
455
  }
456
+ if (this.monitorTokens.get(uuid) !== monitorToken) {
457
+ Log?.debug('device disconnect ignored for stale generation: ', device?.id);
458
+ return;
459
+ }
448
460
 
449
461
  try {
450
462
  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
- });
463
+ this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
456
464
  if (this.runPromise) {
457
465
  const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
458
466
  this.runPromise.reject(error);
@@ -466,6 +474,22 @@ export default class ReactNativeBleTransport {
466
474
  });
467
475
  }
468
476
 
477
+ private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
478
+ if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
479
+ return;
480
+ }
481
+ if (this.monitorTokens.get(uuid) !== token) {
482
+ Log?.debug('device disconnect event ignored for stale generation: ', uuid);
483
+ return;
484
+ }
485
+ this.disconnectEventTokens.set(uuid, token);
486
+ this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
487
+ name,
488
+ id: uuid,
489
+ connectId: uuid,
490
+ });
491
+ }
492
+
469
493
  async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
470
494
  this.firmwareUploadWriteRecoveryIds.add(uuid);
471
495
  try {
@@ -553,14 +577,13 @@ export default class ReactNativeBleTransport {
553
577
  }
554
578
 
555
579
  blePlxManager.startDeviceScan(
556
- null,
580
+ getBluetoothServiceUuids(),
557
581
  {
558
582
  allowDuplicates: true,
559
583
  scanMode: ScanMode.LowLatency,
560
584
  },
561
585
  (error, device) => {
562
586
  if (error) {
563
- Log?.debug('ble scan manager: ', blePlxManager);
564
587
  Log?.debug('ble scan error: ', error);
565
588
  if (
566
589
  [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
@@ -588,29 +611,8 @@ export default class ReactNativeBleTransport {
588
611
  isOnekeyDevice(device?.name ?? null, device?.id) ||
589
612
  isOnekeyDevice(device?.localName ?? null, device?.id) ||
590
613
  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', {
596
- name: device?.name,
597
- localName: device?.localName,
598
- id: device?.id,
599
- serviceUUIDs: device?.serviceUUIDs,
600
- accepted: isOneKey,
601
- });
602
- }
603
-
604
614
  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
615
  addDevice(device as unknown as Device);
613
- Log?.debug('search device end ======================\n');
614
616
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
615
617
  Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
616
618
  name: device?.name,
@@ -622,12 +624,19 @@ export default class ReactNativeBleTransport {
622
624
  }
623
625
  );
624
626
 
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);
627
+ getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
628
+ devices => {
629
+ for (const device of devices) {
630
+ const { serviceUUIDs } = device as { serviceUUIDs?: string[] };
631
+ const hasCachedServiceUuid = Boolean(serviceUUIDs?.length);
632
+ const keepDevice = Platform.OS === 'ios' || hasCachedServiceUuid;
633
+ if (keepDevice) {
634
+ Log?.debug('search connected peripheral: ', device.id);
635
+ addDevice(device as unknown as Device);
636
+ }
637
+ }
629
638
  }
630
- });
639
+ );
631
640
 
632
641
  const addDevice = (device: Device) => {
633
642
  if (deviceList.every(d => d.id !== device.id)) {
@@ -641,6 +650,12 @@ export default class ReactNativeBleTransport {
641
650
  name: displayName,
642
651
  commType: 'ble',
643
652
  } as IOneKeyDevice);
653
+ Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
654
+ deviceId: device.id,
655
+ name: displayName,
656
+ serviceUUIDs: device.serviceUUIDs,
657
+ protocolHint,
658
+ });
644
659
  }
645
660
  };
646
661
 
@@ -686,7 +701,6 @@ export default class ReactNativeBleTransport {
686
701
  this.runPromise.reject(error);
687
702
  this.rejectAllProtocolV2Frames(error);
688
703
  this.runPromise = null;
689
- this.activeProtocolV2Call = null;
690
704
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
691
705
  }
692
706
 
@@ -806,7 +820,10 @@ export default class ReactNativeBleTransport {
806
820
  );
807
821
  transportCache[uuid] = transport;
808
822
 
809
- this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
823
+ this.protocolV2Assemblers.set(
824
+ uuid,
825
+ new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
826
+ );
810
827
 
811
828
  if (Platform.OS === 'ios') {
812
829
  await new Promise<void>(resolve => {
@@ -818,12 +835,6 @@ export default class ReactNativeBleTransport {
818
835
 
819
836
  const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
820
837
 
821
- this.emitter?.emit('device-connect', {
822
- name: device.name,
823
- id: device.id,
824
- connectId: device.id,
825
- });
826
-
827
838
  this.attachDisconnectSubscription(transport, device, uuid);
828
839
 
829
840
  return { uuid, protocolType };
@@ -853,6 +864,29 @@ export default class ReactNativeBleTransport {
853
864
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
854
865
  return;
855
866
  }
867
+ if (this.deviceProtocol.get(uuid) === 'V2') {
868
+ let errorCode:
869
+ | typeof HardwareErrorCode.BleDeviceBondError
870
+ | typeof HardwareErrorCode.BleCharacteristicNotifyError
871
+ | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
872
+ | typeof HardwareErrorCode.BleTimeoutError =
873
+ HardwareErrorCode.BleCharacteristicNotifyError;
874
+ if (error.reason?.includes('The connection has timed out unexpectedly')) {
875
+ errorCode = HardwareErrorCode.BleTimeoutError;
876
+ } else if (error.reason?.includes('Encryption is insufficient')) {
877
+ errorCode = HardwareErrorCode.BleDeviceBondError;
878
+ } else if (
879
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
880
+ error.reason?.includes('Cannot find client characteristic config descriptor') ||
881
+ error.reason?.includes('The handle is invalid') ||
882
+ error.reason?.includes('Writing is not permitted') ||
883
+ error.reason?.includes('notify change failed for device')
884
+ ) {
885
+ errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
886
+ }
887
+ this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
888
+ return;
889
+ }
856
890
  if (this.runPromise) {
857
891
  let ERROR:
858
892
  | typeof HardwareErrorCode.BleDeviceBondError
@@ -908,7 +942,7 @@ export default class ReactNativeBleTransport {
908
942
  return;
909
943
  }
910
944
  if (protocol === 'V2') {
911
- this.handleProtocolV2Notification(uuid, new Uint8Array(data));
945
+ this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
912
946
  return;
913
947
  }
914
948
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
@@ -934,8 +968,11 @@ export default class ReactNativeBleTransport {
934
968
  } catch (error) {
935
969
  Log?.debug('monitor data error: ', error);
936
970
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
937
- this.runPromise?.reject(notifyError);
938
- this.rejectAllProtocolV2Frames(notifyError);
971
+ if (this.deviceProtocol.get(uuid) === 'V2') {
972
+ this.rejectProtocolV2Frames(uuid, notifyError);
973
+ } else {
974
+ this.runPromise?.reject(notifyError);
975
+ }
939
976
  }
940
977
  }, notifyTransactionId);
941
978
 
@@ -944,12 +981,12 @@ export default class ReactNativeBleTransport {
944
981
 
945
982
  async release(uuid: string, onclose = false) {
946
983
  const transport = transportCache[uuid];
984
+ await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
947
985
  if (this.runPromise) {
948
986
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
949
987
  this.runPromise.reject(error);
950
988
  this.runPromise = null;
951
989
  this.rejectAllProtocolV2Frames(error);
952
- this.activeProtocolV2Call = null;
953
990
  } else {
954
991
  this.resetProtocolV2Frames(uuid);
955
992
  }
@@ -957,9 +994,6 @@ export default class ReactNativeBleTransport {
957
994
  if (Platform.OS === 'android' && !onclose && transport) {
958
995
  this.protocolV2Assemblers.get(uuid)?.reset();
959
996
  this.resetProtocolV2Frames(uuid);
960
- if (this.activeProtocolV2Call?.uuid === uuid) {
961
- this.activeProtocolV2Call = null;
962
- }
963
997
  return Promise.resolve(true);
964
998
  }
965
999
 
@@ -993,7 +1027,7 @@ export default class ReactNativeBleTransport {
993
1027
  }
994
1028
 
995
1029
  this.deviceProtocol.delete(uuid);
996
- this.deviceProtocolHints.delete(uuid);
1030
+ // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
997
1031
  this.protocolV2Assemblers.get(uuid)?.reset();
998
1032
  this.protocolV2Assemblers.delete(uuid);
999
1033
  this.resetProtocolV2Frames(uuid);
@@ -1025,13 +1059,6 @@ export default class ReactNativeBleTransport {
1025
1059
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1026
1060
  }
1027
1061
 
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
1062
  const protocol = this.getProtocolType(uuid);
1036
1063
  if (!protocol) {
1037
1064
  throw ERRORS.TypedError(
@@ -1039,31 +1066,17 @@ export default class ReactNativeBleTransport {
1039
1066
  `Device protocol has not been detected for ${uuid}`
1040
1067
  );
1041
1068
  }
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
- }
1069
+ Log?.debug('transport call', createTransportCallLog(name, protocol, data));
1062
1070
 
1063
1071
  if (protocol === 'V2') {
1064
1072
  return this.callProtocolV2(uuid, name, data, options);
1065
1073
  }
1066
1074
 
1075
+ const forceRun = name === 'Initialize' || name === 'Cancel';
1076
+ if (this.runPromise && !forceRun) {
1077
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1078
+ }
1079
+
1067
1080
  return this.callProtocolV1(uuid, name, data, options);
1068
1081
  }
1069
1082
 
@@ -1157,14 +1170,13 @@ export default class ReactNativeBleTransport {
1157
1170
  }
1158
1171
  );
1159
1172
  } else if (name === 'FirmwareUpload') {
1160
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
1173
+ Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
1161
1174
  packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
1162
1175
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1163
1176
  pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1164
1177
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1165
1178
  maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
1166
1179
  });
1167
-
1168
1180
  await writeFirmwareUploadChunkedData(
1169
1181
  buffers,
1170
1182
  async data => {
@@ -1218,7 +1230,6 @@ export default class ReactNativeBleTransport {
1218
1230
  for (const o of buffers) {
1219
1231
  const outData = o.toString('base64');
1220
1232
  // Upload resources on low-end phones may OOM
1221
- // this.Log.debug('send hex strting: ', o.toString('hex'));
1222
1233
  try {
1223
1234
  await transport.writeCharacteristic.writeWithoutResponse(outData);
1224
1235
  } catch (e) {
@@ -1256,7 +1267,6 @@ export default class ReactNativeBleTransport {
1256
1267
  throw new Error('Returning data is not string.');
1257
1268
  }
1258
1269
 
1259
- Log?.debug('receive data: ', response);
1260
1270
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1261
1271
  return check.call(jsonData);
1262
1272
  } catch (e) {
@@ -1279,8 +1289,9 @@ export default class ReactNativeBleTransport {
1279
1289
  }
1280
1290
 
1281
1291
  async disconnect(session: string) {
1282
- Log?.debug('transport-react-native transport resetSession: ', session);
1292
+ await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
1283
1293
  const transport = transportCache[session];
1294
+ const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
1284
1295
 
1285
1296
  // Clean up disconnect subscription first to prevent onDisconnected callback
1286
1297
  // from being triggered when we cancel the connection below
@@ -1341,20 +1352,16 @@ export default class ReactNativeBleTransport {
1341
1352
  this.deviceProtocolHints.delete(session);
1342
1353
  this.protocolV2Assemblers.delete(session);
1343
1354
  this.resetProtocolV2Frames(session);
1344
- if (this.activeProtocolV2Call?.uuid === session) {
1345
- this.activeProtocolV2Call = null;
1346
- }
1347
1355
 
1348
1356
  // emit the disconnect event
1349
1357
  try {
1350
- this.emitter?.emit('device-disconnect', {
1351
- name: transport?.device?.name,
1352
- id: session,
1353
- connectId: session,
1354
- });
1358
+ this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
1355
1359
  } catch (e) {
1356
1360
  Log?.error('resetSession: emit disconnect event error: ', e);
1357
1361
  }
1362
+ if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
1363
+ this.monitorTokens.delete(session);
1364
+ }
1358
1365
  // eslint-disable-next-line no-promise-executor-return
1359
1366
  await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
1360
1367
  }
@@ -1403,37 +1410,49 @@ export default class ReactNativeBleTransport {
1403
1410
  if (expectedProtocol === 'V1') {
1404
1411
  if (await this.probeProtocolV1(uuid)) {
1405
1412
  this.deviceProtocol.set(uuid, 'V1');
1406
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1413
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1414
+ deviceId: uuid,
1415
+ protocol: 'V1',
1416
+ source: 'expected',
1417
+ });
1407
1418
  return 'V1';
1408
1419
  }
1409
1420
  throw this.createProtocolMismatchError(expectedProtocol);
1410
1421
  }
1411
1422
 
1412
1423
  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';
1424
+ if (await this.probeProtocolV2(uuid)) {
1425
+ this.deviceProtocol.set(uuid, 'V2');
1426
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1427
+ deviceId: uuid,
1428
+ protocol: 'V2',
1429
+ source: 'expected',
1430
+ });
1431
+ return 'V2';
1432
+ }
1433
+ throw this.createProtocolMismatchError(expectedProtocol);
1418
1434
  }
1419
1435
 
1420
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
1421
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1
1422
- // 不能作为最终结论。
1436
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
1437
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
1423
1438
  const probeOrder: ProtocolType[] =
1424
1439
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1425
1440
 
1426
1441
  for (let i = 0; i < probeOrder.length; i += 1) {
1427
1442
  const protocol = probeOrder[i];
1428
1443
  if (i > 0) {
1429
- // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
1444
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
1430
1445
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1431
1446
  }
1432
1447
  const detected =
1433
1448
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1434
1449
  if (detected) {
1435
1450
  this.deviceProtocol.set(uuid, protocol);
1436
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1451
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1452
+ deviceId: uuid,
1453
+ protocol,
1454
+ source: 'probe',
1455
+ });
1437
1456
  return protocol;
1438
1457
  }
1439
1458
  }
@@ -1444,11 +1463,12 @@ export default class ReactNativeBleTransport {
1444
1463
 
1445
1464
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1446
1465
  const transport = transportCache[uuid];
1466
+ await this.protocolV2Links.invalidateLink(
1467
+ uuid,
1468
+ `Reset notify state after Protocol ${protocol} probe`
1469
+ );
1447
1470
  this.protocolV2Assemblers.get(uuid)?.reset();
1448
1471
  this.resetProtocolV2Frames(uuid);
1449
- if (this.activeProtocolV2Call?.uuid === uuid) {
1450
- this.activeProtocolV2Call = null;
1451
- }
1452
1472
  if (this.runPromise) {
1453
1473
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1454
1474
  this.runPromise.reject(error);
@@ -1533,13 +1553,9 @@ export default class ReactNativeBleTransport {
1533
1553
  return detected;
1534
1554
  }
1535
1555
 
1536
- private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1556
+ private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
1537
1557
  try {
1538
- if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1539
- this.protocolV2Assemblers.get(uuid)?.reset();
1540
- this.resetProtocolV2Frames(uuid);
1541
- return;
1542
- }
1558
+ if (this.monitorTokens.get(uuid) !== monitorToken) return;
1543
1559
 
1544
1560
  if (data.length === 0) return;
1545
1561
 
@@ -1552,8 +1568,15 @@ export default class ReactNativeBleTransport {
1552
1568
  } catch (error) {
1553
1569
  Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1554
1570
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1555
- this.runPromise?.reject(notifyError);
1556
- this.rejectAllProtocolV2Frames(notifyError);
1571
+ this.rejectProtocolV2Frames(uuid, notifyError);
1572
+ this.protocolV2Links
1573
+ .invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
1574
+ .catch(invalidateError =>
1575
+ Log?.debug(
1576
+ '[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
1577
+ invalidateError
1578
+ )
1579
+ );
1557
1580
  }
1558
1581
  }
1559
1582
 
@@ -1589,8 +1612,13 @@ export default class ReactNativeBleTransport {
1589
1612
  this.protocolV2FramePromises.delete(uuid);
1590
1613
  }
1591
1614
 
1592
- private isActiveProtocolV2Call(uuid: string, token: number) {
1593
- return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
1615
+ private rejectProtocolV2Frames(uuid: string, error: Error) {
1616
+ this.protocolV2FrameQueues.delete(uuid);
1617
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1618
+ if (framePromise) {
1619
+ this.protocolV2FramePromises.delete(uuid);
1620
+ framePromise.reject(error);
1621
+ }
1594
1622
  }
1595
1623
 
1596
1624
  private async readProtocolV2Frame(uuid: string) {
@@ -1610,10 +1638,46 @@ export default class ReactNativeBleTransport {
1610
1638
  }
1611
1639
  }
1612
1640
 
1641
+ private async writeProtocolV2Packet(
1642
+ transport: BleTransport,
1643
+ base64: string,
1644
+ context: ProtocolV2CallContext,
1645
+ assertCurrentGeneration: () => void
1646
+ ) {
1647
+ let attempt = 0;
1648
+ for (;;) {
1649
+ assertCurrentGeneration();
1650
+ if (context.signal.aborted) {
1651
+ throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1652
+ }
1653
+ try {
1654
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1655
+ assertCurrentGeneration();
1656
+ return;
1657
+ } catch (error) {
1658
+ if (
1659
+ getFirmwareUploadWriteRetryType(error) !== 'congested' ||
1660
+ attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
1661
+ ) {
1662
+ throw error;
1663
+ }
1664
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
1665
+ attempt += 1;
1666
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
1667
+ name: context.messageName,
1668
+ attempt,
1669
+ delayMs,
1670
+ });
1671
+ await delay(delayMs);
1672
+ }
1673
+ }
1674
+ }
1675
+
1613
1676
  private async writeProtocolV2Frame(
1614
1677
  transport: BleTransport,
1615
1678
  frame: Uint8Array,
1616
- options?: { highVolume?: boolean; writeWithResponse?: boolean }
1679
+ context: ProtocolV2CallContext,
1680
+ assertCurrentGeneration: () => void
1617
1681
  ) {
1618
1682
  const tuning = getProtocolV2BleTuning();
1619
1683
  const packetCapacity = resolveProtocolV2PacketCapacity({
@@ -1622,52 +1686,24 @@ export default class ReactNativeBleTransport {
1622
1686
  androidPacketLength: tuning.androidPacketLength,
1623
1687
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1624
1688
  });
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
- try {
1635
- for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1636
- const chunk = frame.slice(offset, offset + packetCapacity);
1637
- const base64 = Buffer.from(chunk).toString('base64');
1638
- if (writeMode === 'withResponse') {
1639
- await transport.writeCharacteristic.writeWithResponse(base64);
1640
- } else {
1641
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1642
- }
1643
- packetsWritten += 1;
1644
-
1645
- if (
1646
- shouldThrottle &&
1647
- packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1648
- offset + packetCapacity < frame.length
1649
- ) {
1650
- await delay(tuning.highVolumeWritePauseMs);
1651
- }
1652
- }
1653
-
1654
- if (shouldThrottle) {
1655
- await delay(tuning.highVolumeWriteFlushDelayMs);
1656
- }
1657
- } catch (error) {
1658
- if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
1659
- Log?.debug(
1660
- '[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
1661
- error
1662
- );
1663
- await this.writeProtocolV2Frame(transport, frame, {
1664
- highVolume: true,
1665
- writeWithResponse: true,
1666
- });
1667
- return;
1668
- }
1669
- throw error;
1670
- }
1689
+ await writeProtocolV2BleFrame({
1690
+ frame,
1691
+ packetCapacity,
1692
+ assertActive: assertCurrentGeneration,
1693
+ signal: context.signal,
1694
+ abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1695
+ burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1696
+ burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1697
+ flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1698
+ wait: delay,
1699
+ writePacket: packet =>
1700
+ this.writeProtocolV2Packet(
1701
+ transport,
1702
+ Buffer.from(packet).toString('base64'),
1703
+ context,
1704
+ assertCurrentGeneration
1705
+ ),
1706
+ });
1671
1707
  }
1672
1708
 
1673
1709
  private async callProtocolV2(
@@ -1680,101 +1716,76 @@ export default class ReactNativeBleTransport {
1680
1716
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1681
1717
  }
1682
1718
 
1683
- const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'Ping';
1684
- if (this.runPromise) {
1685
- if (!forceRun) {
1686
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1687
- }
1688
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1689
- this.runPromise.reject(error);
1690
- this.rejectAllProtocolV2Frames(error);
1691
- this.runPromise = null;
1692
- this.activeProtocolV2Call = null;
1693
- }
1694
-
1695
- const transport = this.getCachedTransport(uuid);
1696
- const runPromise = createDeferred<Uint8Array>();
1697
- runPromise.promise.catch(() => undefined);
1698
- this.runPromise = runPromise;
1699
- const callToken = this.nextProtocolV2CallToken++;
1700
- this.activeProtocolV2Call = { uuid, token: callToken };
1701
- this.protocolV2Assemblers.get(uuid)?.reset();
1702
- this.resetProtocolV2Frames(uuid);
1703
- let completed = false;
1704
- const callOptions = {
1705
- ...options,
1706
- timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1707
- };
1719
+ const callOptions = options;
1708
1720
  const highVolumeWrite = LogBlockCommand.has(name);
1709
1721
 
1710
1722
  if (highVolumeWrite) {
1711
1723
  const tuning = getProtocolV2BleTuning();
1712
- Log?.debug(
1713
- '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1724
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1714
1725
  name,
1715
- {
1716
- packetCapacity:
1717
- Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1718
- burstSize: tuning.highVolumeWriteBurstSize,
1719
- pauseMs: tuning.highVolumeWritePauseMs,
1720
- flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1721
- writeWithResponse: tuning.highVolumeWriteWithResponse,
1722
- }
1723
- );
1726
+ writeMode: 'withoutResponse',
1727
+ packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1728
+ });
1724
1729
  }
1725
1730
 
1726
1731
  try {
1727
- const session = new ProtocolV2Session({
1728
- schemas: {
1729
- protocolV1: this._messages,
1730
- protocolV2: this._messagesV2,
1731
- },
1732
- router: PROTOCOL_V2_CHANNEL_BLE_UART,
1733
- writeFrame: async (frame: Uint8Array) => {
1734
- await this.writeProtocolV2Frame(transport, frame, {
1735
- highVolume: highVolumeWrite,
1736
- });
1737
- },
1738
- readFrame: async () => {
1739
- const rxFrame = await this.readProtocolV2Frame(uuid);
1740
- if (!(rxFrame instanceof Uint8Array)) {
1741
- throw new Error('Protocol V2 response is not Uint8Array');
1742
- }
1743
- return rxFrame;
1744
- },
1745
- logger: Log,
1746
- logPrefix: 'ProtocolV2 RN-BLE',
1747
- createTimeoutError: (_messageName: string, timeout: number) =>
1748
- ERRORS.TypedError(
1749
- HardwareErrorCode.BleTimeoutError,
1750
- `BLE response timeout after ${timeout}ms for ${name}`
1751
- ),
1752
- });
1753
-
1754
- const result = await session.call(name, data, callOptions);
1755
- completed = true;
1756
- return result;
1732
+ return await this.protocolV2Links.call(
1733
+ uuid,
1734
+ () => this.createProtocolV2Adapter(uuid),
1735
+ name,
1736
+ data,
1737
+ callOptions
1738
+ );
1757
1739
  } catch (e) {
1758
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1759
- this.protocolV2Assemblers.get(uuid)?.reset();
1760
- this.resetProtocolV2Frames(uuid);
1761
- }
1762
1740
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1763
1741
  throw e;
1764
- } finally {
1765
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1766
- if (!completed) {
1767
- this.protocolV2Assemblers.get(uuid)?.reset();
1768
- }
1769
- this.resetProtocolV2Frames(uuid);
1770
- this.activeProtocolV2Call = null;
1771
- }
1772
- if (this.runPromise === runPromise) {
1773
- this.runPromise = null;
1774
- }
1775
1742
  }
1776
1743
  }
1777
1744
 
1745
+ private createProtocolV2Adapter(uuid: string) {
1746
+ const generation = this.monitorTokens.get(uuid) ?? 0;
1747
+ const assertCurrentGeneration = () => {
1748
+ if (this.monitorTokens.get(uuid) !== generation) {
1749
+ throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
1750
+ }
1751
+ };
1752
+
1753
+ return {
1754
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1755
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1756
+ generation,
1757
+ prepareCall: () => {
1758
+ assertCurrentGeneration();
1759
+ this.protocolV2Assemblers.get(uuid)?.reset();
1760
+ this.resetProtocolV2Frames(uuid);
1761
+ },
1762
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
1763
+ assertCurrentGeneration();
1764
+ const currentTransport = this.getCachedTransport(uuid);
1765
+ await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
1766
+ },
1767
+ readFrame: async () => {
1768
+ assertCurrentGeneration();
1769
+ const rxFrame = await this.readProtocolV2Frame(uuid);
1770
+ if (!(rxFrame instanceof Uint8Array)) {
1771
+ throw new Error('Protocol V2 response is not Uint8Array');
1772
+ }
1773
+ return rxFrame;
1774
+ },
1775
+ reset: (reason: string) => {
1776
+ this.protocolV2Assemblers.get(uuid)?.reset();
1777
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
1778
+ },
1779
+ logger: Log,
1780
+ logPrefix: 'ProtocolV2 RN-BLE',
1781
+ createTimeoutError: (messageName: string, timeout: number) =>
1782
+ ERRORS.TypedError(
1783
+ HardwareErrorCode.BleTimeoutError,
1784
+ `BLE response timeout after ${timeout}ms for ${messageName}`
1785
+ ),
1786
+ };
1787
+ }
1788
+
1778
1789
  getProtocolType(path: string): ProtocolType | undefined {
1779
1790
  return this.deviceProtocol.get(path);
1780
1791
  }