@onekeyfe/hd-transport-react-native 1.2.0-alpha.70 → 1.2.0-alpha.72

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
@@ -5,11 +5,11 @@ import {
5
5
  BleError,
6
6
  BleErrorCode,
7
7
  BleManager as BlePlxManager,
8
+ ConnectionPriority,
8
9
  ScanMode,
9
10
  } from 'react-native-ble-plx';
10
11
  import ByteBuffer from 'bytebuffer';
11
12
  import transport, {
12
- LogBlockCommand,
13
13
  type OneKeyDeviceInfoBase,
14
14
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
15
15
  PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
@@ -20,6 +20,7 @@ import transport, {
20
20
  ProtocolV2LinkManager,
21
21
  TRANSPORT_EVENT,
22
22
  type TransportCallOptions,
23
+ isProtocolV2HighThroughputCall,
23
24
  probeProtocolV2 as probeProtocolV2Helper,
24
25
  writeProtocolV2BleFrame,
25
26
  } from '@onekeyfe/hd-transport';
@@ -32,11 +33,18 @@ import {
32
33
  } from '@onekeyfe/hd-shared';
33
34
 
34
35
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
35
- import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
36
+ import {
37
+ hasWritableCapability,
38
+ resolveProtocolV2PacketCapacity,
39
+ shouldRefreshNegotiatedMtu,
40
+ shouldWriteProtocolV2WithResponse,
41
+ } from './bleStrategy';
36
42
  import { subscribeBleOn } from './subscribeBleOn';
37
43
  import {
38
44
  ANDROID_PACKET_LENGTH,
45
+ ANDROID_PROTOCOL_V2_PACKET_LENGTH,
39
46
  IOS_PACKET_LENGTH,
47
+ IOS_PROTOCOL_V2_PACKET_LENGTH,
40
48
  getBluetoothServiceUuids,
41
49
  getInfosForServiceUuid,
42
50
  isSameBleUuid,
@@ -61,7 +69,6 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
61
69
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
62
70
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
63
71
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
64
- const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
65
72
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
66
73
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
67
74
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
@@ -162,8 +169,8 @@ export type ProtocolV2BleTuning = {
162
169
  type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
163
170
 
164
171
  const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
165
- iosPacketLength: IOS_PACKET_LENGTH,
166
- androidPacketLength: ANDROID_PACKET_LENGTH,
172
+ iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
173
+ androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
167
174
  };
168
175
 
169
176
  let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
@@ -205,12 +212,18 @@ function getDeviceDisplayName(device?: Device | null) {
205
212
  return device?.name || device?.localName || null;
206
213
  }
207
214
 
208
- const ANDROID_REQUEST_MTU = 256;
215
+ const IOS_REQUEST_MTU = 247;
216
+ const ANDROID_REQUEST_MTU = 517;
217
+ const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
218
+ const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
219
+
220
+ const getRequestedBleMtu = () =>
221
+ Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
209
222
 
210
223
  const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
211
224
 
212
225
  const connectOptions: Record<string, unknown> = {
213
- requestMTU: ANDROID_REQUEST_MTU,
226
+ requestMTU: getRequestedBleMtu(),
214
227
  timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
215
228
  refreshGatt: 'OnConnected',
216
229
  };
@@ -264,23 +277,32 @@ const tryToGetConfiguration = (device: Device) => {
264
277
  return infos;
265
278
  };
266
279
 
267
- const requestAndroidMtu = async (device: Device) => {
268
- if (Platform.OS !== 'android') return device;
280
+ const requestNegotiatedMtu = async (
281
+ device: Device,
282
+ stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
283
+ attempt: number
284
+ ) => {
285
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
269
286
 
270
287
  try {
271
- const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
272
- Log?.debug('[ReactNativeBleTransport] MTU configured', {
273
- deviceId: device.id,
274
- requested: ANDROID_REQUEST_MTU,
275
- actual: mtuDevice.mtu,
276
- });
288
+ // iOS ignores the requested value but react-native-ble-plx returns a fresh
289
+ // Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
290
+ const mtuDevice = await device.requestMTU(getRequestedBleMtu());
277
291
  return mtuDevice;
278
292
  } catch (error) {
279
- Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
293
+ Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
294
+ platform: Platform.OS,
295
+ stage,
296
+ attempt,
297
+ actual: device.mtu,
298
+ error: error instanceof Error ? error.message : String(error),
299
+ });
280
300
  return device;
281
301
  }
282
302
  };
283
303
 
304
+ const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
305
+
284
306
  type IOBleErrorRemap = Error | BleError | null | undefined;
285
307
 
286
308
  function remapError(error: IOBleErrorRemap) {
@@ -394,6 +416,12 @@ export default class ReactNativeBleTransport {
394
416
 
395
417
  private disconnectEventTokens: Map<string, number> = new Map();
396
418
 
419
+ private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
420
+
421
+ private androidHighPriorityDevices: Set<string> = new Set();
422
+
423
+ private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
424
+
397
425
  private nextMonitorToken = 1;
398
426
 
399
427
  constructor(options: TransportOptions) {
@@ -775,9 +803,7 @@ export default class ReactNativeBleTransport {
775
803
  const { writeCharacteristic, notifyCharacteristic } =
776
804
  characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
777
805
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
778
- if (Platform.OS === 'android') {
779
- transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
780
- }
806
+ transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
781
807
  const monitorToken = this.nextMonitorToken;
782
808
  this.nextMonitorToken += 1;
783
809
  const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
@@ -791,6 +817,7 @@ export default class ReactNativeBleTransport {
791
817
  notifyTransactionId
792
818
  );
793
819
  transportCache[uuid] = transport;
820
+ this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
794
821
  this.protocolV2Assemblers.set(
795
822
  uuid,
796
823
  new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
@@ -804,6 +831,40 @@ export default class ReactNativeBleTransport {
804
831
  await delay(ANDROID_NOTIFY_READY_DELAY_MS);
805
832
  }
806
833
 
834
+ const initialMtu = transport.mtuSize;
835
+ let refreshAttempts = 0;
836
+ if (
837
+ (Platform.OS === 'ios' || Platform.OS === 'android') &&
838
+ shouldRefreshNegotiatedMtu(transport.mtuSize)
839
+ ) {
840
+ refreshAttempts += 1;
841
+ let refreshedDevice = await requestNegotiatedMtu(
842
+ transport.device,
843
+ 'servicesAndNotifyReady',
844
+ 1
845
+ );
846
+ transport.device = refreshedDevice;
847
+ transport.mtuSize =
848
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
849
+
850
+ if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
851
+ await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
852
+ refreshAttempts += 1;
853
+ refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
854
+ transport.device = refreshedDevice;
855
+ transport.mtuSize =
856
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
857
+ }
858
+ }
859
+
860
+ Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
861
+ platform: Platform.OS,
862
+ requested: getRequestedBleMtu(),
863
+ initial: initialMtu,
864
+ actual: transport.mtuSize,
865
+ refreshAttempts,
866
+ });
867
+
807
868
  return transport;
808
869
  }
809
870
 
@@ -946,7 +1007,7 @@ export default class ReactNativeBleTransport {
946
1007
  }
947
1008
  }
948
1009
 
949
- device = await requestAndroidMtu(device);
1010
+ device = await resolveNegotiatedMtu(device);
950
1011
  const acquiredDevice = device;
951
1012
  const { writeCharacteristic, notifyCharacteristic } =
952
1013
  await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
@@ -981,7 +1042,7 @@ export default class ReactNativeBleTransport {
981
1042
  if (!currentTransport) {
982
1043
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
983
1044
  }
984
- this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
1045
+ this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
985
1046
  return { uuid, protocolType };
986
1047
  } catch (error) {
987
1048
  await this.release(uuid, true);
@@ -1151,6 +1212,8 @@ export default class ReactNativeBleTransport {
1151
1212
  return Promise.resolve(true);
1152
1213
  }
1153
1214
 
1215
+ await this.restoreAndroidConnectionPriority(uuid, transport);
1216
+
1154
1217
  if (transport) {
1155
1218
  if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1156
1219
  this.monitorTokens.delete(uuid);
@@ -1180,6 +1243,8 @@ export default class ReactNativeBleTransport {
1180
1243
  delete transportCache[uuid];
1181
1244
  }
1182
1245
 
1246
+ this.protocolV2HighVolumeLogSignatures.delete(uuid);
1247
+
1183
1248
  this.deviceProtocol.delete(uuid);
1184
1249
  this.probingProtocols.delete(uuid);
1185
1250
  // Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
@@ -2106,9 +2171,12 @@ export default class ReactNativeBleTransport {
2106
2171
  context: ProtocolV2CallContext,
2107
2172
  assertCurrentGeneration: () => void
2108
2173
  ) {
2109
- const shouldUseWriteWithResponse =
2110
- transport.writeCharacteristic.isWritableWithResponse &&
2111
- (context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
2174
+ const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
2175
+ platform: Platform.OS,
2176
+ highThroughput: context.highThroughput,
2177
+ requestedWithResponse: context.writeWithResponse,
2178
+ characteristic: transport.writeCharacteristic,
2179
+ });
2112
2180
  let attempt = 0;
2113
2181
  for (;;) {
2114
2182
  assertCurrentGeneration();
@@ -2167,24 +2235,14 @@ export default class ReactNativeBleTransport {
2167
2235
  platform: Platform.OS,
2168
2236
  iosPacketLength: tuning.iosPacketLength,
2169
2237
  androidPacketLength: tuning.androidPacketLength,
2170
- mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
2238
+ mtu: transport.mtuSize,
2171
2239
  });
2172
- // Match Desktop BLE pacing so Pro2 firmware can finish the previous response
2173
- // before the next single-packet control command is written.
2174
- const initialDelayMs =
2175
- Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
2176
- ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
2177
- : 0;
2178
2240
  await writeProtocolV2BleFrame({
2179
2241
  frame,
2180
2242
  packetCapacity,
2181
2243
  assertActive: assertCurrentGeneration,
2182
2244
  signal: context.signal,
2183
2245
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
2184
- initialDelayMs,
2185
- burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
2186
- burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
2187
- flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
2188
2246
  wait: delay,
2189
2247
  writePacket: packet =>
2190
2248
  this.writeProtocolV2Packet(
@@ -2208,15 +2266,44 @@ export default class ReactNativeBleTransport {
2208
2266
  }
2209
2267
 
2210
2268
  const callOptions = options;
2211
- const highVolumeWrite = LogBlockCommand.has(name);
2269
+ const highThroughputWrite = isProtocolV2HighThroughputCall(name);
2212
2270
 
2213
- if (highVolumeWrite) {
2271
+ if (highThroughputWrite) {
2272
+ await this.ensureProtocolV2HighThroughputMtu(uuid);
2214
2273
  const tuning = getProtocolV2BleTuning();
2215
- Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
2216
- name,
2217
- writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
2218
- packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
2274
+ const currentTransport = this.getCachedTransport(uuid);
2275
+ const writeWithResponse = shouldWriteProtocolV2WithResponse({
2276
+ platform: Platform.OS,
2277
+ highThroughput: true,
2278
+ requestedWithResponse: options?.writeWithResponse,
2279
+ characteristic: currentTransport.writeCharacteristic,
2280
+ });
2281
+ const packetCapacity = resolveProtocolV2PacketCapacity({
2282
+ platform: Platform.OS,
2283
+ iosPacketLength: tuning.iosPacketLength,
2284
+ androidPacketLength: tuning.androidPacketLength,
2285
+ mtu: currentTransport.mtuSize,
2219
2286
  });
2287
+ const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
2288
+ const logSignature = `${name}:${writeMode}:${String(
2289
+ currentTransport.mtuSize
2290
+ )}:${packetCapacity}`;
2291
+ const loggedSignatures =
2292
+ this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
2293
+ if (!loggedSignatures.has(logSignature)) {
2294
+ loggedSignatures.add(logSignature);
2295
+ this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
2296
+ Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
2297
+ name,
2298
+ writeMode,
2299
+ reportedMtu: currentTransport.mtuSize,
2300
+ packetCapacity,
2301
+ });
2302
+ }
2303
+ }
2304
+
2305
+ if (highThroughputWrite) {
2306
+ await this.enableAndroidHighConnectionPriority(uuid);
2220
2307
  }
2221
2308
 
2222
2309
  try {
@@ -2230,6 +2317,90 @@ export default class ReactNativeBleTransport {
2230
2317
  } catch (e) {
2231
2318
  Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
2232
2319
  throw e;
2320
+ } finally {
2321
+ if (highThroughputWrite) {
2322
+ this.scheduleAndroidBalancedConnectionPriority(uuid);
2323
+ }
2324
+ }
2325
+ }
2326
+
2327
+ private async ensureProtocolV2HighThroughputMtu(uuid: string) {
2328
+ const transport = this.getCachedTransport(uuid);
2329
+ if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
2330
+
2331
+ const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
2332
+ transport.device = refreshedDevice;
2333
+ transport.mtuSize =
2334
+ typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
2335
+
2336
+ if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
2337
+ throw ERRORS.TypedError(
2338
+ HardwareErrorCode.BleConnectedError,
2339
+ `Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
2340
+ );
2341
+ }
2342
+ }
2343
+
2344
+ private clearAndroidPriorityResetTimer(uuid: string) {
2345
+ const timerId = this.androidPriorityResetTimers.get(uuid);
2346
+ if (timerId !== undefined) {
2347
+ clearTimeout(timerId);
2348
+ this.androidPriorityResetTimers.delete(uuid);
2349
+ }
2350
+ }
2351
+
2352
+ private async enableAndroidHighConnectionPriority(uuid: string) {
2353
+ if (Platform.OS !== 'android') return;
2354
+
2355
+ this.clearAndroidPriorityResetTimer(uuid);
2356
+ if (this.androidHighPriorityDevices.has(uuid)) return;
2357
+
2358
+ const transport = transportCache[uuid];
2359
+ if (!transport) return;
2360
+
2361
+ try {
2362
+ transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
2363
+ this.androidHighPriorityDevices.add(uuid);
2364
+ Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
2365
+ priority: 'high',
2366
+ });
2367
+ } catch (error) {
2368
+ Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
2369
+ error: error instanceof Error ? error.message : String(error),
2370
+ });
2371
+ }
2372
+ }
2373
+
2374
+ private scheduleAndroidBalancedConnectionPriority(uuid: string) {
2375
+ if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
2376
+
2377
+ this.clearAndroidPriorityResetTimer(uuid);
2378
+ const timerId = setTimeout(() => {
2379
+ this.androidPriorityResetTimers.delete(uuid);
2380
+ this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
2381
+ Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
2382
+ );
2383
+ }, ANDROID_HIGH_PRIORITY_IDLE_MS);
2384
+ this.androidPriorityResetTimers.set(uuid, timerId);
2385
+ }
2386
+
2387
+ private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
2388
+ this.clearAndroidPriorityResetTimer(uuid);
2389
+ if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
2390
+ return;
2391
+ }
2392
+
2393
+ try {
2394
+ transport.device = await transport.device.requestConnectionPriority(
2395
+ ConnectionPriority.Balanced
2396
+ );
2397
+ Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
2398
+ priority: 'balanced',
2399
+ });
2400
+ } catch (error) {
2401
+ Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
2402
+ error: error instanceof Error ? error.message : String(error),
2403
+ });
2233
2404
  }
2234
2405
  }
2235
2406