@onekeyfe/hd-transport-react-native 1.2.0-alpha.2 → 1.2.0-alpha.21

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,18 @@ 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,
17
18
  ProtocolV2FrameAssembler,
18
- ProtocolV2Session,
19
+ ProtocolV2LinkManager,
19
20
  type TransportCallOptions,
20
21
  probeProtocolV2 as probeProtocolV2Helper,
21
22
  } from '@onekeyfe/hd-transport';
22
23
  import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
23
24
 
24
25
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
25
- import {
26
- hasWritableCapability,
27
- resolveBleWriteMode,
28
- resolveProtocolV2PacketCapacity,
29
- } from './bleStrategy';
26
+ import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
30
27
  import { subscribeBleOn } from './subscribeBleOn';
31
28
  import {
32
29
  ANDROID_PACKET_LENGTH,
@@ -40,6 +37,7 @@ import { isHeaderChunk } from './utils/validateNotify';
40
37
  import BleTransport from './BleTransport';
41
38
  import timer from './utils/timer';
42
39
  import { bleLogger, setBleLogger } from './logger';
40
+ import { createTransportCallLog } from './transportLog';
43
41
 
44
42
  import type { Deferred } from '@onekeyfe/hd-shared';
45
43
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
@@ -111,17 +109,9 @@ const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
111
109
  const DEVICE_SCAN_TIMEOUT_MS = 8000;
112
110
  const IOS_NOTIFY_READY_DELAY_MS = 150;
113
111
  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
112
  export type ProtocolV2BleTuning = {
119
113
  iosPacketLength?: number;
120
114
  androidPacketLength?: number;
121
- highVolumeWriteBurstSize?: number;
122
- highVolumeWritePauseMs?: number;
123
- highVolumeWriteFlushDelayMs?: number;
124
- highVolumeWriteWithResponse?: boolean;
125
115
  };
126
116
 
127
117
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
@@ -129,10 +119,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
129
119
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
130
120
  iosPacketLength: IOS_PACKET_LENGTH,
131
121
  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
122
  };
137
123
 
138
124
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -153,27 +139,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
153
139
  tuning.androidPacketLength,
154
140
  protocolV2BleTuning.androidPacketLength
155
141
  ),
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
142
  };
171
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
143
+ Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
172
144
  }
173
145
 
174
146
  export function resetProtocolV2BleTuning() {
175
147
  protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
176
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
148
+ Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
177
149
  }
178
150
 
179
151
  export function getProtocolV2BleTuning() {
@@ -222,9 +194,10 @@ const requestAndroidMtu = async (device: Device) => {
222
194
 
223
195
  try {
224
196
  const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
225
- Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
197
+ Log?.debug('[ReactNativeBleTransport] MTU configured', {
198
+ deviceId: device.id,
226
199
  requested: ANDROID_REQUEST_MTU,
227
- mtu: mtuDevice.mtu,
200
+ actual: mtuDevice.mtu,
228
201
  });
229
202
  return mtuDevice;
230
203
  } catch (error) {
@@ -297,9 +270,26 @@ export default class ReactNativeBleTransport {
297
270
 
298
271
  private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
299
272
 
300
- private activeProtocolV2Call: { uuid: string; token: number } | null = null;
301
-
302
- private nextProtocolV2CallToken = 1;
273
+ private protocolV2Links = new ProtocolV2LinkManager<string>({
274
+ getSchemas: () => {
275
+ if (!this._messages || !this._messagesV2) {
276
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
277
+ }
278
+ return {
279
+ protocolV1: this._messages,
280
+ protocolV2: this._messagesV2,
281
+ };
282
+ },
283
+ classifyError: () => 'link-fatal',
284
+ onLinkInvalidated: async (uuid, reason) => {
285
+ this.protocolV2Assemblers.get(uuid)?.reset();
286
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
287
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
288
+ if (reason.startsWith('Protocol V2 link-fatal error:')) {
289
+ await this.release(uuid, true);
290
+ }
291
+ },
292
+ });
303
293
 
304
294
  private monitorTokens: Map<string, number> = new Map();
305
295
 
@@ -322,7 +312,9 @@ export default class ReactNativeBleTransport {
322
312
 
323
313
  configureProtocolV2(signedData: any) {
324
314
  this._messagesV2 = parseConfigure(signedData);
325
- Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
315
+ this.protocolV2Links
316
+ .invalidateAllLinks('Protocol V2 schema reconfigured')
317
+ .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
326
318
  }
327
319
 
328
320
  listen() {
@@ -553,14 +545,13 @@ export default class ReactNativeBleTransport {
553
545
  }
554
546
 
555
547
  blePlxManager.startDeviceScan(
556
- null,
548
+ getBluetoothServiceUuids(),
557
549
  {
558
550
  allowDuplicates: true,
559
551
  scanMode: ScanMode.LowLatency,
560
552
  },
561
553
  (error, device) => {
562
554
  if (error) {
563
- Log?.debug('ble scan manager: ', blePlxManager);
564
555
  Log?.debug('ble scan error: ', error);
565
556
  if (
566
557
  [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
@@ -588,29 +579,8 @@ export default class ReactNativeBleTransport {
588
579
  isOnekeyDevice(device?.name ?? null, device?.id) ||
589
580
  isOnekeyDevice(device?.localName ?? null, device?.id) ||
590
581
  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
582
  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
583
  addDevice(device as unknown as Device);
613
- Log?.debug('search device end ======================\n');
614
584
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
615
585
  Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
616
586
  name: device?.name,
@@ -622,12 +592,19 @@ export default class ReactNativeBleTransport {
622
592
  }
623
593
  );
624
594
 
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);
595
+ getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
596
+ devices => {
597
+ for (const device of devices) {
598
+ const { serviceUUIDs } = device as { serviceUUIDs?: string[] };
599
+ const hasCachedServiceUuid = Boolean(serviceUUIDs?.length);
600
+ const keepDevice = Platform.OS === 'ios' || hasCachedServiceUuid;
601
+ if (keepDevice) {
602
+ Log?.debug('search connected peripheral: ', device.id);
603
+ addDevice(device as unknown as Device);
604
+ }
605
+ }
629
606
  }
630
- });
607
+ );
631
608
 
632
609
  const addDevice = (device: Device) => {
633
610
  if (deviceList.every(d => d.id !== device.id)) {
@@ -641,6 +618,12 @@ export default class ReactNativeBleTransport {
641
618
  name: displayName,
642
619
  commType: 'ble',
643
620
  } as IOneKeyDevice);
621
+ Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
622
+ deviceId: device.id,
623
+ name: displayName,
624
+ serviceUUIDs: device.serviceUUIDs,
625
+ protocolHint,
626
+ });
644
627
  }
645
628
  };
646
629
 
@@ -686,7 +669,6 @@ export default class ReactNativeBleTransport {
686
669
  this.runPromise.reject(error);
687
670
  this.rejectAllProtocolV2Frames(error);
688
671
  this.runPromise = null;
689
- this.activeProtocolV2Call = null;
690
672
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
691
673
  }
692
674
 
@@ -853,6 +835,29 @@ export default class ReactNativeBleTransport {
853
835
  Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
854
836
  return;
855
837
  }
838
+ if (this.deviceProtocol.get(uuid) === 'V2') {
839
+ let errorCode:
840
+ | typeof HardwareErrorCode.BleDeviceBondError
841
+ | typeof HardwareErrorCode.BleCharacteristicNotifyError
842
+ | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
843
+ | typeof HardwareErrorCode.BleTimeoutError =
844
+ HardwareErrorCode.BleCharacteristicNotifyError;
845
+ if (error.reason?.includes('The connection has timed out unexpectedly')) {
846
+ errorCode = HardwareErrorCode.BleTimeoutError;
847
+ } else if (error.reason?.includes('Encryption is insufficient')) {
848
+ errorCode = HardwareErrorCode.BleDeviceBondError;
849
+ } else if (
850
+ error.reason?.includes('Cannot write client characteristic config descriptor') ||
851
+ error.reason?.includes('Cannot find client characteristic config descriptor') ||
852
+ error.reason?.includes('The handle is invalid') ||
853
+ error.reason?.includes('Writing is not permitted') ||
854
+ error.reason?.includes('notify change failed for device')
855
+ ) {
856
+ errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
857
+ }
858
+ this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
859
+ return;
860
+ }
856
861
  if (this.runPromise) {
857
862
  let ERROR:
858
863
  | typeof HardwareErrorCode.BleDeviceBondError
@@ -908,7 +913,7 @@ export default class ReactNativeBleTransport {
908
913
  return;
909
914
  }
910
915
  if (protocol === 'V2') {
911
- this.handleProtocolV2Notification(uuid, new Uint8Array(data));
916
+ this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
912
917
  return;
913
918
  }
914
919
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
@@ -934,8 +939,11 @@ export default class ReactNativeBleTransport {
934
939
  } catch (error) {
935
940
  Log?.debug('monitor data error: ', error);
936
941
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
937
- this.runPromise?.reject(notifyError);
938
- this.rejectAllProtocolV2Frames(notifyError);
942
+ if (this.deviceProtocol.get(uuid) === 'V2') {
943
+ this.rejectProtocolV2Frames(uuid, notifyError);
944
+ } else {
945
+ this.runPromise?.reject(notifyError);
946
+ }
939
947
  }
940
948
  }, notifyTransactionId);
941
949
 
@@ -944,12 +952,12 @@ export default class ReactNativeBleTransport {
944
952
 
945
953
  async release(uuid: string, onclose = false) {
946
954
  const transport = transportCache[uuid];
955
+ await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
947
956
  if (this.runPromise) {
948
957
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
949
958
  this.runPromise.reject(error);
950
959
  this.runPromise = null;
951
960
  this.rejectAllProtocolV2Frames(error);
952
- this.activeProtocolV2Call = null;
953
961
  } else {
954
962
  this.resetProtocolV2Frames(uuid);
955
963
  }
@@ -957,9 +965,6 @@ export default class ReactNativeBleTransport {
957
965
  if (Platform.OS === 'android' && !onclose && transport) {
958
966
  this.protocolV2Assemblers.get(uuid)?.reset();
959
967
  this.resetProtocolV2Frames(uuid);
960
- if (this.activeProtocolV2Call?.uuid === uuid) {
961
- this.activeProtocolV2Call = null;
962
- }
963
968
  return Promise.resolve(true);
964
969
  }
965
970
 
@@ -993,7 +998,7 @@ export default class ReactNativeBleTransport {
993
998
  }
994
999
 
995
1000
  this.deviceProtocol.delete(uuid);
996
- this.deviceProtocolHints.delete(uuid);
1001
+ // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
997
1002
  this.protocolV2Assemblers.get(uuid)?.reset();
998
1003
  this.protocolV2Assemblers.delete(uuid);
999
1004
  this.resetProtocolV2Frames(uuid);
@@ -1025,13 +1030,6 @@ export default class ReactNativeBleTransport {
1025
1030
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1026
1031
  }
1027
1032
 
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
1033
  const protocol = this.getProtocolType(uuid);
1036
1034
  if (!protocol) {
1037
1035
  throw ERRORS.TypedError(
@@ -1039,31 +1037,17 @@ export default class ReactNativeBleTransport {
1039
1037
  `Device protocol has not been detected for ${uuid}`
1040
1038
  );
1041
1039
  }
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
- }
1040
+ Log?.debug('transport call', createTransportCallLog(name, protocol, data));
1062
1041
 
1063
1042
  if (protocol === 'V2') {
1064
1043
  return this.callProtocolV2(uuid, name, data, options);
1065
1044
  }
1066
1045
 
1046
+ const forceRun = name === 'Initialize' || name === 'Cancel';
1047
+ if (this.runPromise && !forceRun) {
1048
+ throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1049
+ }
1050
+
1067
1051
  return this.callProtocolV1(uuid, name, data, options);
1068
1052
  }
1069
1053
 
@@ -1157,14 +1141,13 @@ export default class ReactNativeBleTransport {
1157
1141
  }
1158
1142
  );
1159
1143
  } else if (name === 'FirmwareUpload') {
1160
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
1144
+ Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
1161
1145
  packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
1162
1146
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1163
1147
  pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1164
1148
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
1165
1149
  maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
1166
1150
  });
1167
-
1168
1151
  await writeFirmwareUploadChunkedData(
1169
1152
  buffers,
1170
1153
  async data => {
@@ -1218,7 +1201,6 @@ export default class ReactNativeBleTransport {
1218
1201
  for (const o of buffers) {
1219
1202
  const outData = o.toString('base64');
1220
1203
  // Upload resources on low-end phones may OOM
1221
- // this.Log.debug('send hex strting: ', o.toString('hex'));
1222
1204
  try {
1223
1205
  await transport.writeCharacteristic.writeWithoutResponse(outData);
1224
1206
  } catch (e) {
@@ -1256,7 +1238,6 @@ export default class ReactNativeBleTransport {
1256
1238
  throw new Error('Returning data is not string.');
1257
1239
  }
1258
1240
 
1259
- Log?.debug('receive data: ', response);
1260
1241
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1261
1242
  return check.call(jsonData);
1262
1243
  } catch (e) {
@@ -1279,7 +1260,7 @@ export default class ReactNativeBleTransport {
1279
1260
  }
1280
1261
 
1281
1262
  async disconnect(session: string) {
1282
- Log?.debug('transport-react-native transport resetSession: ', session);
1263
+ await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
1283
1264
  const transport = transportCache[session];
1284
1265
 
1285
1266
  // Clean up disconnect subscription first to prevent onDisconnected callback
@@ -1341,9 +1322,6 @@ export default class ReactNativeBleTransport {
1341
1322
  this.deviceProtocolHints.delete(session);
1342
1323
  this.protocolV2Assemblers.delete(session);
1343
1324
  this.resetProtocolV2Frames(session);
1344
- if (this.activeProtocolV2Call?.uuid === session) {
1345
- this.activeProtocolV2Call = null;
1346
- }
1347
1325
 
1348
1326
  // emit the disconnect event
1349
1327
  try {
@@ -1403,37 +1381,48 @@ export default class ReactNativeBleTransport {
1403
1381
  if (expectedProtocol === 'V1') {
1404
1382
  if (await this.probeProtocolV1(uuid)) {
1405
1383
  this.deviceProtocol.set(uuid, 'V1');
1406
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1384
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1385
+ deviceId: uuid,
1386
+ protocol: 'V1',
1387
+ source: 'expected',
1388
+ });
1407
1389
  return 'V1';
1408
1390
  }
1409
1391
  throw this.createProtocolMismatchError(expectedProtocol);
1410
1392
  }
1411
1393
 
1412
1394
  if (expectedProtocol === 'V2') {
1413
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
1414
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
1395
+ // Skip probing when the caller explicitly confirms V2, such as reconnect after a
1396
+ // firmware reboot where expectedProtocol carries the previously probed result.
1415
1397
  this.deviceProtocol.set(uuid, 'V2');
1416
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1398
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1399
+ deviceId: uuid,
1400
+ protocol: 'V2',
1401
+ source: 'expected',
1402
+ });
1417
1403
  return 'V2';
1418
1404
  }
1419
1405
 
1420
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
1421
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1
1422
- // 不能作为最终结论。
1406
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
1407
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
1423
1408
  const probeOrder: ProtocolType[] =
1424
1409
  protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
1425
1410
 
1426
1411
  for (let i = 0; i < probeOrder.length; i += 1) {
1427
1412
  const protocol = probeOrder[i];
1428
1413
  if (i > 0) {
1429
- // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。
1414
+ // Reset subscriptions and buffers after a failed probe before trying another protocol.
1430
1415
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1431
1416
  }
1432
1417
  const detected =
1433
1418
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
1434
1419
  if (detected) {
1435
1420
  this.deviceProtocol.set(uuid, protocol);
1436
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
1421
+ Log?.debug('[ReactNativeBleTransport] protocol detected', {
1422
+ deviceId: uuid,
1423
+ protocol,
1424
+ source: 'probe',
1425
+ });
1437
1426
  return protocol;
1438
1427
  }
1439
1428
  }
@@ -1444,11 +1433,12 @@ export default class ReactNativeBleTransport {
1444
1433
 
1445
1434
  private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1446
1435
  const transport = transportCache[uuid];
1436
+ await this.protocolV2Links.invalidateLink(
1437
+ uuid,
1438
+ `Reset notify state after Protocol ${protocol} probe`
1439
+ );
1447
1440
  this.protocolV2Assemblers.get(uuid)?.reset();
1448
1441
  this.resetProtocolV2Frames(uuid);
1449
- if (this.activeProtocolV2Call?.uuid === uuid) {
1450
- this.activeProtocolV2Call = null;
1451
- }
1452
1442
  if (this.runPromise) {
1453
1443
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1454
1444
  this.runPromise.reject(error);
@@ -1533,13 +1523,9 @@ export default class ReactNativeBleTransport {
1533
1523
  return detected;
1534
1524
  }
1535
1525
 
1536
- private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1526
+ private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
1537
1527
  try {
1538
- if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1539
- this.protocolV2Assemblers.get(uuid)?.reset();
1540
- this.resetProtocolV2Frames(uuid);
1541
- return;
1542
- }
1528
+ if (this.monitorTokens.get(uuid) !== monitorToken) return;
1543
1529
 
1544
1530
  if (data.length === 0) return;
1545
1531
 
@@ -1552,8 +1538,15 @@ export default class ReactNativeBleTransport {
1552
1538
  } catch (error) {
1553
1539
  Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1554
1540
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1555
- this.runPromise?.reject(notifyError);
1556
- this.rejectAllProtocolV2Frames(notifyError);
1541
+ this.rejectProtocolV2Frames(uuid, notifyError);
1542
+ this.protocolV2Links
1543
+ .invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
1544
+ .catch(invalidateError =>
1545
+ Log?.debug(
1546
+ '[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
1547
+ invalidateError
1548
+ )
1549
+ );
1557
1550
  }
1558
1551
  }
1559
1552
 
@@ -1589,8 +1582,13 @@ export default class ReactNativeBleTransport {
1589
1582
  this.protocolV2FramePromises.delete(uuid);
1590
1583
  }
1591
1584
 
1592
- private isActiveProtocolV2Call(uuid: string, token: number) {
1593
- return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
1585
+ private rejectProtocolV2Frames(uuid: string, error: Error) {
1586
+ this.protocolV2FrameQueues.delete(uuid);
1587
+ const framePromise = this.protocolV2FramePromises.get(uuid);
1588
+ if (framePromise) {
1589
+ this.protocolV2FramePromises.delete(uuid);
1590
+ framePromise.reject(error);
1591
+ }
1594
1592
  }
1595
1593
 
1596
1594
  private async readProtocolV2Frame(uuid: string) {
@@ -1610,11 +1608,7 @@ export default class ReactNativeBleTransport {
1610
1608
  }
1611
1609
  }
1612
1610
 
1613
- private async writeProtocolV2Frame(
1614
- transport: BleTransport,
1615
- frame: Uint8Array,
1616
- options?: { highVolume?: boolean; writeWithResponse?: boolean }
1617
- ) {
1611
+ private async writeProtocolV2Frame(transport: BleTransport, frame: Uint8Array) {
1618
1612
  const tuning = getProtocolV2BleTuning();
1619
1613
  const packetCapacity = resolveProtocolV2PacketCapacity({
1620
1614
  platform: Platform.OS,
@@ -1622,51 +1616,10 @@ export default class ReactNativeBleTransport {
1622
1616
  androidPacketLength: tuning.androidPacketLength,
1623
1617
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1624
1618
  });
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;
1619
+ for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1620
+ const chunk = frame.slice(offset, offset + packetCapacity);
1621
+ const base64 = Buffer.from(chunk).toString('base64');
1622
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1670
1623
  }
1671
1624
  }
1672
1625
 
@@ -1680,27 +1633,6 @@ export default class ReactNativeBleTransport {
1680
1633
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1681
1634
  }
1682
1635
 
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
1636
  const callOptions = {
1705
1637
  ...options,
1706
1638
  timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
@@ -1709,72 +1641,71 @@ export default class ReactNativeBleTransport {
1709
1641
 
1710
1642
  if (highVolumeWrite) {
1711
1643
  const tuning = getProtocolV2BleTuning();
1712
- Log?.debug(
1713
- '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1644
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1714
1645
  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
- );
1646
+ writeMode: 'withoutResponse',
1647
+ packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1648
+ });
1724
1649
  }
1725
1650
 
1726
1651
  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;
1652
+ return await this.protocolV2Links.call(
1653
+ uuid,
1654
+ () => this.createProtocolV2Adapter(uuid),
1655
+ name,
1656
+ data,
1657
+ callOptions
1658
+ );
1757
1659
  } catch (e) {
1758
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1759
- this.protocolV2Assemblers.get(uuid)?.reset();
1760
- this.resetProtocolV2Frames(uuid);
1761
- }
1762
1660
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1763
1661
  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
1662
  }
1776
1663
  }
1777
1664
 
1665
+ private createProtocolV2Adapter(uuid: string) {
1666
+ const generation = this.monitorTokens.get(uuid) ?? 0;
1667
+ const assertCurrentGeneration = () => {
1668
+ if (this.monitorTokens.get(uuid) !== generation) {
1669
+ throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
1670
+ }
1671
+ };
1672
+
1673
+ return {
1674
+ router: PROTOCOL_V2_CHANNEL_BLE_UART,
1675
+ maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
1676
+ generation,
1677
+ prepareCall: () => {
1678
+ assertCurrentGeneration();
1679
+ this.protocolV2Assemblers.get(uuid)?.reset();
1680
+ this.resetProtocolV2Frames(uuid);
1681
+ },
1682
+ writeFrame: async (frame: Uint8Array) => {
1683
+ assertCurrentGeneration();
1684
+ const currentTransport = this.getCachedTransport(uuid);
1685
+ await this.writeProtocolV2Frame(currentTransport, frame);
1686
+ },
1687
+ readFrame: async () => {
1688
+ assertCurrentGeneration();
1689
+ const rxFrame = await this.readProtocolV2Frame(uuid);
1690
+ if (!(rxFrame instanceof Uint8Array)) {
1691
+ throw new Error('Protocol V2 response is not Uint8Array');
1692
+ }
1693
+ return rxFrame;
1694
+ },
1695
+ reset: (reason: string) => {
1696
+ this.protocolV2Assemblers.get(uuid)?.reset();
1697
+ this.rejectProtocolV2Frames(uuid, new Error(reason));
1698
+ },
1699
+ logger: Log,
1700
+ logPrefix: 'ProtocolV2 RN-BLE',
1701
+ createTimeoutError: (messageName: string, timeout: number) =>
1702
+ ERRORS.TypedError(
1703
+ HardwareErrorCode.BleTimeoutError,
1704
+ `BLE response timeout after ${timeout}ms for ${messageName}`
1705
+ ),
1706
+ };
1707
+ }
1708
+
1778
1709
  getProtocolType(path: string): ProtocolType | undefined {
1779
1710
  return this.deviceProtocol.get(path);
1780
1711
  }