@onekeyfe/hd-transport-react-native 1.1.27-alpha.41 → 1.1.27-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -9,150 +9,92 @@ import {
9
9
  } from 'react-native-ble-plx';
10
10
  import ByteBuffer from 'bytebuffer';
11
11
  import transport, {
12
+ COMMON_HEADER_SIZE,
12
13
  LogBlockCommand,
13
14
  type OneKeyDeviceInfoBase,
14
- PROTOCOL_V1_MESSAGE_HEADER_SIZE,
15
- PROTOCOL_V2_CHANNEL_BLE_UART,
16
- type ProtocolType,
17
- ProtocolV2FrameAssembler,
18
- ProtocolV2Session,
19
- type TransportCallOptions,
20
- probeProtocolV2 as probeProtocolV2Helper,
21
15
  } from '@onekeyfe/hd-transport';
22
16
  import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
17
+ import { LoggerNames, getLogger } from '@onekeyfe/hd-core';
23
18
 
24
19
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
25
- import {
26
- hasWritableCapability,
27
- resolveBleWriteMode,
28
- resolveProtocolV2PacketCapacity,
29
- } from './bleStrategy';
30
20
  import { subscribeBleOn } from './subscribeBleOn';
31
21
  import {
32
22
  ANDROID_PACKET_LENGTH,
33
23
  IOS_PACKET_LENGTH,
34
- getBleUuidKey,
35
24
  getBluetoothServiceUuids,
36
25
  getInfosForServiceUuid,
37
- isSameBleUuid,
38
26
  } from './constants';
39
27
  import { isHeaderChunk } from './utils/validateNotify';
40
28
  import BleTransport from './BleTransport';
41
29
  import timer from './utils/timer';
42
- import { bleLogger, setBleLogger } from './logger';
43
30
 
44
31
  import type { Deferred } from '@onekeyfe/hd-shared';
45
32
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
46
33
  import type EventEmitter from 'events';
47
34
  import type { BleAcquireInput, TransportOptions } from './types';
48
35
 
49
- const { check, ProtocolV1, parseConfigure } = transport;
36
+ const { check, buildBuffers, receiveOne, parseConfigure } = transport;
50
37
 
51
- const Log = bleLogger;
38
+ const Log = getLogger(LoggerNames.HdBleTransport);
52
39
 
53
40
  const transportCache: Record<string, BleTransport> = {};
54
- const BLE_RESPONSE_TIMEOUT_MS = 30_000;
55
- const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
56
- const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
57
- const DEVICE_SCAN_TIMEOUT_MS = 8000;
58
- const IOS_NOTIFY_READY_DELAY_MS = 150;
59
- const ANDROID_NOTIFY_READY_DELAY_MS = 300;
60
- const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
61
- const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
62
- const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
41
+ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
42
+ const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
43
+ const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
44
+ const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
45
+ const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
46
+ const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
47
+ const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
48
+ Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
49
+ const ANDROID_GATT_CONGESTED_STATUS = 143;
50
+
51
+ type FirmwareUploadWriteRetryType = 'congested' | 'reconnectable';
52
+ type ResolvedBleCharacteristics = {
53
+ writeCharacteristic: Characteristic;
54
+ notifyCharacteristic: Characteristic;
55
+ };
63
56
 
64
57
  const delay = (ms: number) =>
65
58
  new Promise<void>(resolve => {
66
59
  setTimeout(resolve, ms);
67
60
  });
68
61
 
69
- export type ProtocolV2BleTuning = {
70
- iosPacketLength?: number;
71
- androidPacketLength?: number;
72
- highVolumeWriteBurstSize?: number;
73
- highVolumeWritePauseMs?: number;
74
- highVolumeWriteFlushDelayMs?: number;
75
- highVolumeWriteWithResponse?: boolean;
76
- };
77
-
78
- type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
79
-
80
- const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
81
- iosPacketLength: IOS_PACKET_LENGTH,
82
- androidPacketLength: ANDROID_PACKET_LENGTH,
83
- highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
84
- highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
85
- highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
86
- highVolumeWriteWithResponse: false,
87
- };
88
-
89
- let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
90
-
91
- const normalizePositiveInteger = (value: unknown, fallback: number) => {
92
- const normalized = Number(value);
93
- if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
94
- return Math.floor(normalized);
95
- };
96
-
97
- export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
98
- protocolV2BleTuning = {
99
- iosPacketLength: normalizePositiveInteger(
100
- tuning.iosPacketLength,
101
- protocolV2BleTuning.iosPacketLength
102
- ),
103
- androidPacketLength: normalizePositiveInteger(
104
- tuning.androidPacketLength,
105
- protocolV2BleTuning.androidPacketLength
106
- ),
107
- highVolumeWriteBurstSize: normalizePositiveInteger(
108
- tuning.highVolumeWriteBurstSize,
109
- protocolV2BleTuning.highVolumeWriteBurstSize
110
- ),
111
- highVolumeWritePauseMs: normalizePositiveInteger(
112
- tuning.highVolumeWritePauseMs,
113
- protocolV2BleTuning.highVolumeWritePauseMs
114
- ),
115
- highVolumeWriteFlushDelayMs: normalizePositiveInteger(
116
- tuning.highVolumeWriteFlushDelayMs,
117
- protocolV2BleTuning.highVolumeWriteFlushDelayMs
118
- ),
119
- highVolumeWriteWithResponse:
120
- tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
62
+ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRetryType | null => {
63
+ if (!error || typeof error !== 'object') return null;
64
+ const bleWriteError = error as {
65
+ androidErrorCode?: unknown;
66
+ status?: unknown;
67
+ errorCode?: unknown;
68
+ reason?: unknown;
69
+ message?: unknown;
70
+ name?: unknown;
121
71
  };
122
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
123
- }
124
-
125
- export function resetProtocolV2BleTuning() {
126
- protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
127
- Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
128
- }
129
-
130
- export function getProtocolV2BleTuning() {
131
- return { ...protocolV2BleTuning };
132
- }
133
72
 
134
- function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
135
- return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
136
- }
137
-
138
- function getDeviceDisplayName(device?: Device | null) {
139
- return device?.name || device?.localName || null;
140
- }
73
+ if (
74
+ bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
75
+ bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
76
+ ) {
77
+ return 'reconnectable';
78
+ }
141
79
 
142
- function isGenericBleService(uuid?: string | null) {
143
- return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
144
- }
80
+ if (
81
+ bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
82
+ bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
83
+ ) {
84
+ return 'congested';
85
+ }
145
86
 
146
- function hasKnownOneKeyService(device?: Device | null) {
147
- return (device?.serviceUUIDs ?? []).some(serviceUuid =>
148
- getInfosForServiceUuid(serviceUuid, 'classic')
149
- );
150
- }
87
+ const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
88
+ .filter(value => typeof value === 'string')
89
+ .join(' ');
90
+ return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
91
+ };
151
92
 
152
- const ANDROID_REQUEST_MTU = 256;
93
+ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
94
+ Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
153
95
 
154
- const connectOptions: Record<string, unknown> = {
155
- requestMTU: ANDROID_REQUEST_MTU,
96
+ let connectOptions: Record<string, unknown> = {
97
+ requestMTU: 256,
156
98
  timeout: 3000,
157
99
  refreshGatt: 'OnConnected',
158
100
  };
@@ -161,29 +103,12 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
161
103
 
162
104
  const tryToGetConfiguration = (device: Device) => {
163
105
  if (!device || !device.serviceUUIDs) return null;
164
- const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
165
- if (!serviceUUID) return null;
106
+ const [serviceUUID] = device.serviceUUIDs;
166
107
  const infos = getInfosForServiceUuid(serviceUUID, 'classic');
167
108
  if (!infos) return null;
168
109
  return infos;
169
110
  };
170
111
 
171
- const requestAndroidMtu = async (device: Device) => {
172
- if (Platform.OS !== 'android') return device;
173
-
174
- try {
175
- const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
176
- Log?.debug('[ReactNativeBleTransport] Android MTU requested:', {
177
- requested: ANDROID_REQUEST_MTU,
178
- mtu: mtuDevice.mtu,
179
- });
180
- return mtuDevice;
181
- } catch (error) {
182
- Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
183
- return device;
184
- }
185
- };
186
-
187
112
  type IOBleErrorRemap = Error | BleError | null | undefined;
188
113
 
189
114
  function remapError(error: IOBleErrorRemap) {
@@ -221,45 +146,25 @@ export default class ReactNativeBleTransport {
221
146
 
222
147
  _messages: ReturnType<typeof transport.parseConfigure> | undefined;
223
148
 
224
- _messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
225
-
226
149
  name = 'ReactNativeBleTransport';
227
150
 
228
151
  configured = false;
229
152
 
230
153
  stopped = false;
231
154
 
232
- scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
155
+ scanTimeout = 3000;
233
156
 
234
157
  runPromise: Deferred<any> | null = null;
235
158
 
236
159
  emitter?: EventEmitter;
237
160
 
238
- /** Per-device protocol type detected by active wire-level probe after connect. */
239
- private deviceProtocol: Map<string, ProtocolType> = new Map();
240
-
241
- private deviceProtocolHints: Map<string, ProtocolType> = new Map();
242
-
243
- private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
244
-
245
- private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
246
-
247
- private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
248
-
249
- private activeProtocolV2Call: { uuid: string; token: number } | null = null;
250
-
251
- private nextProtocolV2CallToken = 1;
252
-
253
- private monitorTokens: Map<string, number> = new Map();
254
-
255
- private nextMonitorToken = 1;
161
+ firmwareUploadWriteRecoveryIds = new Set<string>();
256
162
 
257
163
  constructor(options: TransportOptions) {
258
- this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
164
+ this.scanTimeout = options.scanTimeout ?? 3000;
259
165
  }
260
166
 
261
- init(logger: any, emitter: EventEmitter) {
262
- setBleLogger(logger);
167
+ init(_logger: any, emitter: EventEmitter) {
263
168
  this.emitter = emitter;
264
169
  }
265
170
 
@@ -269,11 +174,6 @@ export default class ReactNativeBleTransport {
269
174
  this._messages = messages;
270
175
  }
271
176
 
272
- configureProtocolV2(signedData: any) {
273
- this._messagesV2 = parseConfigure(signedData);
274
- Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
275
- }
276
-
277
177
  listen() {
278
178
  // empty
279
179
  }
@@ -284,6 +184,143 @@ export default class ReactNativeBleTransport {
284
184
  return Promise.resolve(this.blePlxManager);
285
185
  }
286
186
 
187
+ async resolveCharacteristics(device: Device): Promise<ResolvedBleCharacteristics> {
188
+ await device.discoverAllServicesAndCharacteristics();
189
+ let infos = tryToGetConfiguration(device);
190
+ let characteristics: Characteristic[] | undefined;
191
+
192
+ if (!infos) {
193
+ for (const serviceUuid of getBluetoothServiceUuids()) {
194
+ try {
195
+ characteristics = await device.characteristicsForService(serviceUuid);
196
+ infos = getInfosForServiceUuid(serviceUuid, 'classic');
197
+ break;
198
+ } catch (e) {
199
+ Log?.error(e);
200
+ }
201
+ }
202
+ }
203
+
204
+ if (!infos) {
205
+ try {
206
+ Log?.debug('cancel connection when service not found');
207
+ await device.cancelConnection();
208
+ } catch (e) {
209
+ Log?.debug('cancel connection error when service not found: ', e.message || e.reason);
210
+ }
211
+ throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
212
+ }
213
+
214
+ const { serviceUuid, writeUuid, notifyUuid } = infos;
215
+
216
+ if (!characteristics) {
217
+ characteristics = await device.characteristicsForService(serviceUuid);
218
+ }
219
+
220
+ if (!characteristics) {
221
+ throw ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound);
222
+ }
223
+
224
+ let writeCharacteristic;
225
+ let notifyCharacteristic;
226
+ for (const c of characteristics) {
227
+ if (c.uuid === writeUuid) {
228
+ writeCharacteristic = c;
229
+ } else if (c.uuid === notifyUuid) {
230
+ notifyCharacteristic = c;
231
+ }
232
+ }
233
+
234
+ if (!writeCharacteristic) {
235
+ throw ERRORS.TypedError('BLECharacteristicNotFound: write characteristic not found');
236
+ }
237
+
238
+ if (!notifyCharacteristic) {
239
+ throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found');
240
+ }
241
+
242
+ if (!writeCharacteristic.isWritableWithResponse) {
243
+ throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
244
+ }
245
+
246
+ if (!notifyCharacteristic.isNotifiable) {
247
+ throw ERRORS.TypedError(
248
+ 'BLECharacteristicNotNotifiable: notify characteristic not notifiable'
249
+ );
250
+ }
251
+
252
+ return {
253
+ writeCharacteristic,
254
+ notifyCharacteristic,
255
+ };
256
+ }
257
+
258
+ attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
259
+ transport.disconnectSubscription?.remove();
260
+ transport.disconnectSubscription = device.onDisconnected(() => {
261
+ if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
262
+ Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
263
+ return;
264
+ }
265
+
266
+ try {
267
+ Log?.debug('device disconnect: ', device?.id);
268
+ this.emitter?.emit('device-disconnect', {
269
+ name: device?.name,
270
+ id: device?.id,
271
+ connectId: device?.id,
272
+ });
273
+ if (this.runPromise) {
274
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError));
275
+ }
276
+ } catch (e) {
277
+ Log?.debug('device disconnect error: ', e);
278
+ } finally {
279
+ this.release(uuid);
280
+ }
281
+ });
282
+ }
283
+
284
+ async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
285
+ this.firmwareUploadWriteRecoveryIds.add(uuid);
286
+ try {
287
+ transport.disconnectSubscription?.remove();
288
+ transport.disconnectSubscription = undefined;
289
+ transport.notifySubscription?.remove();
290
+ transport.notifySubscription = undefined;
291
+
292
+ let { device } = transport;
293
+ const isConnected = await device.isConnected().catch(() => false);
294
+ if (!isConnected) {
295
+ try {
296
+ device = await device.connect(connectOptions);
297
+ } catch (e) {
298
+ if (
299
+ e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
300
+ e.errorCode === BleErrorCode.OperationCancelled
301
+ ) {
302
+ connectOptions = {};
303
+ device = await device.connect();
304
+ } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
305
+ throw e;
306
+ }
307
+ }
308
+ }
309
+
310
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
311
+ device
312
+ );
313
+
314
+ transport.device = device;
315
+ transport.writeCharacteristic = writeCharacteristic;
316
+ transport.notifyCharacteristic = notifyCharacteristic;
317
+ transport.notifySubscription = this._monitorCharacteristic(notifyCharacteristic, uuid);
318
+ this.attachDisconnectSubscription(transport, device, uuid);
319
+ } finally {
320
+ this.firmwareUploadWriteRecoveryIds.delete(uuid);
321
+ }
322
+ }
323
+
287
324
  /**
288
325
  * 获取设备列表
289
326
  * 在搜索超过超时时间或设备数量大于 5 台时,返回 OneKey 设备,
@@ -323,7 +360,6 @@ export default class ReactNativeBleTransport {
323
360
  blePlxManager.startDeviceScan(
324
361
  null,
325
362
  {
326
- allowDuplicates: true,
327
363
  scanMode: ScanMode.LowLatency,
328
364
  },
329
365
  (error, device) => {
@@ -351,41 +387,14 @@ export default class ReactNativeBleTransport {
351
387
  return;
352
388
  }
353
389
 
354
- const displayName = getDeviceDisplayName(device);
355
- const isOneKey =
356
- isOnekeyDevice(device?.name ?? null, device?.id) ||
357
- isOnekeyDevice(device?.localName ?? null, device?.id) ||
358
- hasKnownOneKeyService(device);
359
- const shouldTraceCandidate =
360
- !!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
361
-
362
- if (shouldTraceCandidate) {
363
- Log?.debug('[ReactNativeBleTransport] scan candidate', {
364
- name: device?.name,
365
- localName: device?.localName,
366
- id: device?.id,
367
- serviceUUIDs: device?.serviceUUIDs,
368
- accepted: isOneKey,
369
- });
370
- }
371
-
372
- if (isOneKey) {
390
+ if (isOnekeyDevice(device?.name ?? null, device?.id)) {
373
391
  Log?.debug('search device start ======================');
374
- const { name, localName, id, serviceUUIDs } = device ?? {};
392
+ const { name, localName, id } = device ?? {};
375
393
  Log?.debug(
376
- `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
377
- id ?? ''
378
- }\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
394
+ `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${id ?? ''}`
379
395
  );
380
396
  addDevice(device as unknown as Device);
381
397
  Log?.debug('search device end ======================\n');
382
- } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
383
- Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
384
- name: device?.name,
385
- localName: device?.localName,
386
- id: device?.id,
387
- serviceUUIDs: device?.serviceUUIDs,
388
- });
389
398
  }
390
399
  }
391
400
  );
@@ -399,16 +408,7 @@ export default class ReactNativeBleTransport {
399
408
 
400
409
  const addDevice = (device: Device) => {
401
410
  if (deviceList.every(d => d.id !== device.id)) {
402
- const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
403
- const protocolHint = inferProtocolHintFromDeviceName(displayName);
404
- if (protocolHint) {
405
- this.deviceProtocolHints.set(device.id, protocolHint);
406
- }
407
- deviceList.push({
408
- ...device,
409
- name: displayName,
410
- commType: 'ble',
411
- } as IOneKeyDevice);
411
+ deviceList.push({ ...device, commType: 'ble' } as IOneKeyDevice);
412
412
  }
413
413
  };
414
414
 
@@ -420,45 +420,25 @@ export default class ReactNativeBleTransport {
420
420
  }
421
421
 
422
422
  async acquire(input: BleAcquireInput) {
423
- const { uuid, forceCleanRunPromise, expectedProtocol } = input;
423
+ const { uuid, forceCleanRunPromise } = input;
424
424
 
425
425
  if (!uuid) {
426
426
  throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
427
427
  }
428
428
 
429
- const cachedTransport = transportCache[uuid];
430
- if (cachedTransport) {
431
- const cachedProtocol = this.deviceProtocol.get(uuid);
432
- const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false);
433
- if (
434
- isCachedDeviceConnected &&
435
- cachedProtocol &&
436
- (!expectedProtocol || cachedProtocol === expectedProtocol)
437
- ) {
438
- Log?.debug(
439
- '[ReactNativeBleTransport] reuse cached BLE transport:',
440
- uuid,
441
- cachedProtocol
442
- );
443
- return { uuid, protocolType: cachedProtocol };
444
- }
429
+ let device: Device | null = null;
445
430
 
431
+ if (transportCache[uuid]) {
446
432
  /**
447
- * If the transport is not reusable due to a protocol mismatch or stale
448
- * connection, clean it up before creating a new transport instance.
433
+ * If the transport is not released due to an exception operation
434
+ * it will be handled again here
449
435
  */
450
- Log?.debug('transport not reusable, will release: ', uuid);
451
- await this.release(uuid, true);
436
+ Log?.debug('transport not be released, will release: ', uuid);
437
+ await this.release(uuid);
452
438
  }
453
439
 
454
- let device: Device | null = null;
455
-
456
440
  if (forceCleanRunPromise && this.runPromise) {
457
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
458
- this.runPromise.reject(error);
459
- this.rejectAllProtocolV2Frames(error);
460
- this.runPromise = null;
461
- this.activeProtocolV2Call = null;
441
+ this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
462
442
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
463
443
  }
464
444
 
@@ -470,12 +450,11 @@ export default class ReactNativeBleTransport {
470
450
  throw error;
471
451
  }
472
452
 
453
+ // check device is bonded
473
454
  if (Platform.OS === 'android') {
474
455
  const bondState = await pairDevice(uuid);
475
456
  if (bondState.bonding) {
476
457
  await onDeviceBondState(uuid);
477
- } else if (!bondState.bonded) {
478
- throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
479
458
  }
480
459
  }
481
460
 
@@ -501,6 +480,7 @@ export default class ReactNativeBleTransport {
501
480
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
502
481
  e.errorCode === BleErrorCode.OperationCancelled
503
482
  ) {
483
+ connectOptions = {};
504
484
  Log?.debug('first try to reconnect without params');
505
485
  device = await blePlxManager.connectToDevice(uuid);
506
486
  } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
@@ -520,16 +500,17 @@ export default class ReactNativeBleTransport {
520
500
  Log?.debug('not connected, try to connect to device: ', uuid);
521
501
 
522
502
  try {
523
- device = await device.connect(connectOptions);
503
+ await device.connect(connectOptions);
524
504
  } catch (e) {
525
505
  Log?.debug('not connected, try to connect to device has error: ', e);
526
506
  if (
527
507
  e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
528
508
  e.errorCode === BleErrorCode.OperationCancelled
529
509
  ) {
510
+ connectOptions = {};
530
511
  Log?.debug('second try to reconnect without params');
531
512
  try {
532
- device = await device.connect();
513
+ await device.connect();
533
514
  } catch (e) {
534
515
  Log?.debug('last try to reconnect error: ', e);
535
516
  // last try to reconnect device if this issue exists
@@ -537,7 +518,7 @@ export default class ReactNativeBleTransport {
537
518
  if (e.errorCode === BleErrorCode.OperationCancelled) {
538
519
  Log?.debug('last try to reconnect');
539
520
  await device.cancelConnection();
540
- device = await device.connect();
521
+ await device.connect();
541
522
  }
542
523
  }
543
524
  } else {
@@ -546,194 +527,41 @@ export default class ReactNativeBleTransport {
546
527
  }
547
528
  }
548
529
 
549
- device = await requestAndroidMtu(device);
550
-
551
- await device.discoverAllServicesAndCharacteristics();
552
- let infos = tryToGetConfiguration(device);
553
- let characteristics;
554
- let fallbackServiceUuid: string | undefined;
555
-
556
- if (!infos) {
557
- for (const serviceUuid of getBluetoothServiceUuids()) {
558
- try {
559
- characteristics = await device.characteristicsForService(serviceUuid);
560
- infos = getInfosForServiceUuid(serviceUuid, 'classic');
561
- break;
562
- } catch (e) {
563
- Log?.error(e);
564
- }
565
- }
566
- }
567
-
568
- if (!infos) {
569
- const services = await device.services();
570
- Log?.debug(
571
- '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
572
- services?.map(service => service.uuid)
573
- );
574
-
575
- const knownService = services.find(service =>
576
- getInfosForServiceUuid(service.uuid, 'classic')
577
- );
578
- const fallbackService =
579
- knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
580
-
581
- if (fallbackService) {
582
- fallbackServiceUuid = fallbackService.uuid;
583
- characteristics = await device.characteristicsForService(fallbackService.uuid);
584
- Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
585
- }
586
- }
587
-
588
- if (!infos) {
589
- if (!fallbackServiceUuid) {
590
- try {
591
- Log?.debug('cancel connection when service not found');
592
- await device.cancelConnection();
593
- } catch (e) {
594
- Log?.debug('cancel connection error when service not found: ', e.message || e.reason);
595
- }
596
- throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
597
- }
598
- }
599
-
600
- const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
601
- const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
602
- const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
603
-
604
- if (!serviceUuid) {
605
- throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
606
- }
607
-
608
- if (!characteristics) {
609
- characteristics = await device.characteristicsForService(serviceUuid);
610
- }
611
-
612
- if (!characteristics) {
613
- throw ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound);
614
- }
615
-
616
- let writeCharacteristic;
617
- let notifyCharacteristic;
618
- for (const c of characteristics) {
619
- if (isSameBleUuid(c.uuid, writeUuid)) {
620
- writeCharacteristic = c;
621
- } else if (isSameBleUuid(c.uuid, notifyUuid)) {
622
- notifyCharacteristic = c;
623
- }
624
- }
625
-
626
- if (!writeCharacteristic) {
627
- throw ERRORS.TypedError('BLECharacteristicNotFound: write characteristic not found');
628
- }
629
-
630
- if (!notifyCharacteristic) {
631
- throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found');
632
- }
633
-
634
- if (!hasWritableCapability(writeCharacteristic)) {
635
- throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
636
- }
637
-
638
- if (!notifyCharacteristic.isNotifiable) {
639
- throw ERRORS.TypedError(
640
- 'BLECharacteristicNotNotifiable: notify characteristic not notifiable'
641
- );
642
- }
643
-
644
- const protocolHint = expectedProtocol
645
- ? undefined
646
- : this.deviceProtocolHints.get(uuid) ??
647
- inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
530
+ const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
648
531
 
649
532
  // release transport before new transport instance
650
- await this.release(uuid, true);
651
- if (protocolHint) {
652
- this.deviceProtocolHints.set(uuid, protocolHint);
653
- }
533
+ await this.release(uuid);
654
534
 
655
535
  const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
656
- if (Platform.OS === 'android') {
657
- transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
658
- }
659
- const monitorToken = this.nextMonitorToken;
660
- this.nextMonitorToken += 1;
661
- const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
662
- transport.monitorToken = monitorToken;
663
- transport.notifyTransactionId = notifyTransactionId;
664
- this.monitorTokens.set(uuid, monitorToken);
665
536
  transport.notifySubscription = this._monitorCharacteristic(
666
537
  transport.notifyCharacteristic,
667
- uuid,
668
- monitorToken,
669
- notifyTransactionId
538
+ uuid
670
539
  );
671
540
  transportCache[uuid] = transport;
672
541
 
673
- this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
674
-
675
- if (Platform.OS === 'ios') {
676
- await new Promise<void>(resolve => {
677
- setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
678
- });
679
- } else if (Platform.OS === 'android') {
680
- await delay(ANDROID_NOTIFY_READY_DELAY_MS);
681
- }
682
-
683
- const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
684
-
685
542
  this.emitter?.emit('device-connect', {
686
543
  name: device.name,
687
544
  id: device.id,
688
545
  connectId: device.id,
689
546
  });
690
547
 
691
- transport.disconnectSubscription = device.onDisconnected(() => {
692
- if (transportCache[uuid] !== transport) {
693
- Log?.debug('device disconnect ignored for stale transport: ', device?.id);
694
- return;
695
- }
696
-
697
- try {
698
- Log?.debug('device disconnect: ', device?.id);
699
- this.emitter?.emit('device-disconnect', {
700
- name: device?.name,
701
- id: device?.id,
702
- connectId: device?.id,
703
- });
704
- if (this.runPromise) {
705
- const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
706
- this.runPromise.reject(error);
707
- this.rejectAllProtocolV2Frames(error);
708
- }
709
- } catch (e) {
710
- Log?.debug('device disconnect error: ', e);
711
- } finally {
712
- this.release(uuid, true);
713
- }
714
- });
548
+ this.attachDisconnectSubscription(transport, device, uuid);
715
549
 
716
- return { uuid, protocolType };
550
+ return { uuid };
717
551
  }
718
552
 
719
- _monitorCharacteristic(
720
- characteristic: Characteristic,
721
- uuid: string,
722
- monitorToken: number,
723
- notifyTransactionId: string
724
- ): Subscription {
553
+ _monitorCharacteristic(characteristic: Characteristic, uuid: string): Subscription {
725
554
  let bufferLength = 0;
726
555
  let buffer: any[] = [];
727
556
  const subscription = characteristic.monitor((error, c) => {
728
- const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
729
557
  if (error) {
730
558
  Log?.debug(
731
559
  `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
732
560
  error as unknown as string
733
561
  }`
734
562
  );
735
- if (!isCurrentMonitor) {
736
- Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
563
+ if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
564
+ Log?.debug('notify error ignored during FirmwareUpload write recovery: ', uuid);
737
565
  return;
738
566
  }
739
567
  if (this.runPromise) {
@@ -755,45 +583,27 @@ export default class ReactNativeBleTransport {
755
583
  error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
756
584
  error.reason?.includes('notify change failed for device')
757
585
  ) {
758
- const notifyError = ERRORS.TypedError(
759
- HardwareErrorCode.BleCharacteristicNotifyChangeFailure
586
+ this.runPromise.reject(
587
+ ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure)
760
588
  );
761
- this.runPromise.reject(notifyError);
762
- this.rejectAllProtocolV2Frames(notifyError);
763
589
  Log?.debug(
764
590
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
765
591
  );
766
592
  return;
767
593
  }
768
- const notifyError = ERRORS.TypedError(ERROR);
769
- this.runPromise.reject(notifyError);
770
- this.rejectAllProtocolV2Frames(notifyError);
594
+ this.runPromise.reject(ERRORS.TypedError(ERROR));
771
595
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
772
596
  }
773
597
 
774
598
  return;
775
599
  }
776
600
 
777
- if (!isCurrentMonitor) {
778
- Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
779
- return;
780
- }
781
-
782
601
  if (!c) {
783
602
  throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
784
603
  }
785
604
 
786
605
  try {
787
606
  const data = Buffer.from(c.value as string, 'base64');
788
- const protocol = this.deviceProtocol.get(uuid);
789
- if (!protocol) {
790
- Log?.debug('monitor data ignored before protocol detection: ', uuid);
791
- return;
792
- }
793
- if (protocol === 'V2') {
794
- this.handleProtocolV2Notification(uuid, new Uint8Array(data));
795
- return;
796
- }
797
607
  // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
798
608
  if (isHeaderChunk(data)) {
799
609
  bufferLength = data.readInt32BE(5);
@@ -802,7 +612,7 @@ export default class ReactNativeBleTransport {
802
612
  buffer = buffer.concat([...data]);
803
613
  }
804
614
 
805
- if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
615
+ if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) {
806
616
  const value = Buffer.from(buffer);
807
617
  // console.log(
808
618
  // '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
@@ -816,41 +626,17 @@ export default class ReactNativeBleTransport {
816
626
  }
817
627
  } catch (error) {
818
628
  Log?.debug('monitor data error: ', error);
819
- const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
820
- this.runPromise?.reject(notifyError);
821
- this.rejectAllProtocolV2Frames(notifyError);
629
+ this.runPromise?.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError));
822
630
  }
823
- }, notifyTransactionId);
631
+ }, uuid);
824
632
 
825
633
  return subscription;
826
634
  }
827
635
 
828
- async release(uuid: string, onclose = false) {
636
+ async release(uuid: string) {
829
637
  const transport = transportCache[uuid];
830
- if (this.runPromise) {
831
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
832
- this.runPromise.reject(error);
833
- this.runPromise = null;
834
- this.rejectAllProtocolV2Frames(error);
835
- this.activeProtocolV2Call = null;
836
- } else {
837
- this.resetProtocolV2Frames(uuid);
838
- }
839
-
840
- if (Platform.OS === 'android' && !onclose && transport) {
841
- this.protocolV2Assemblers.get(uuid)?.reset();
842
- this.resetProtocolV2Frames(uuid);
843
- if (this.activeProtocolV2Call?.uuid === uuid) {
844
- this.activeProtocolV2Call = null;
845
- }
846
- return Promise.resolve(true);
847
- }
848
638
 
849
639
  if (transport) {
850
- if (this.monitorTokens.get(uuid) === transport.monitorToken) {
851
- this.monitorTokens.delete(uuid);
852
- }
853
-
854
640
  // Clean up disconnect subscription first to prevent callbacks on released transport
855
641
  Log?.debug('release: removing disconnect subscription for device: ', uuid);
856
642
  transport.disconnectSubscription?.remove();
@@ -864,27 +650,12 @@ export default class ReactNativeBleTransport {
864
650
  transport.notifySubscription?.remove();
865
651
  transport.notifySubscription = undefined;
866
652
 
867
- if (transport.notifyTransactionId) {
868
- try {
869
- await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
870
- } catch (e) {
871
- Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
872
- }
873
- }
874
-
875
653
  delete transportCache[uuid];
876
- }
877
654
 
878
- this.deviceProtocol.delete(uuid);
879
- this.deviceProtocolHints.delete(uuid);
880
- this.protocolV2Assemblers.get(uuid)?.reset();
881
- this.protocolV2Assemblers.delete(uuid);
882
- this.resetProtocolV2Frames(uuid);
883
-
884
- try {
885
- await this.blePlxManager?.cancelTransaction(uuid);
886
- } catch (e) {
887
- Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
655
+ // Temporary close the Android disconnect after each request
656
+ if (Platform.OS === 'android') {
657
+ // await this.blePlxManager?.cancelDeviceConnection(uuid);
658
+ }
888
659
  }
889
660
 
890
661
  return Promise.resolve(true);
@@ -894,12 +665,7 @@ export default class ReactNativeBleTransport {
894
665
  await this.call(session, name, data);
895
666
  }
896
667
 
897
- async call(
898
- uuid: string,
899
- name: string,
900
- data: Record<string, unknown>,
901
- options?: TransportCallOptions
902
- ) {
668
+ async call(uuid: string, name: string, data: Record<string, unknown>) {
903
669
  if (this.stopped) {
904
670
  // eslint-disable-next-line prefer-promise-reject-errors
905
671
  return Promise.reject(ERRORS.TypedError('Transport stopped.'));
@@ -915,13 +681,13 @@ export default class ReactNativeBleTransport {
915
681
  throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
916
682
  }
917
683
 
918
- const protocol = this.getProtocolType(uuid);
919
- if (!protocol) {
920
- throw ERRORS.TypedError(
921
- HardwareErrorCode.RuntimeError,
922
- `Device protocol has not been detected for ${uuid}`
923
- );
684
+ const transport = transportCache[uuid];
685
+ if (!transport) {
686
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
924
687
  }
688
+
689
+ this.runPromise = createDeferred();
690
+ const messages = this._messages;
925
691
  // Upload resources on low-end phones may OOM
926
692
  if (name === 'ResourceUpdate' || name === 'ResourceAck') {
927
693
  Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
@@ -929,44 +695,12 @@ export default class ReactNativeBleTransport {
929
695
  hash: data?.hash,
930
696
  });
931
697
  } else if (LogBlockCommand.has(name)) {
932
- Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
698
+ Log?.debug('transport-react-native', 'call-', ' name: ', name);
933
699
  } else {
934
- Log?.debug(
935
- 'transport-react-native',
936
- 'call-',
937
- ' name: ',
938
- name,
939
- ' data: ',
940
- data,
941
- ' protocol: ',
942
- protocol
943
- );
944
- }
945
-
946
- if (protocol === 'V2') {
947
- return this.callProtocolV2(uuid, name, data, options);
948
- }
949
-
950
- return this.callProtocolV1(uuid, name, data, options);
951
- }
952
-
953
- private async callProtocolV1(
954
- uuid: string,
955
- name: string,
956
- data: Record<string, unknown>,
957
- options?: TransportCallOptions
958
- ) {
959
- if (!this._messages) {
960
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
700
+ Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data);
961
701
  }
962
702
 
963
- const transport = this.getCachedTransport(uuid);
964
- const runPromise = createDeferred<string>();
965
- runPromise.promise.catch(() => undefined);
966
- this.runPromise = runPromise;
967
- const messages = this._messages;
968
- const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
969
- let timeout: ReturnType<typeof setTimeout> | undefined;
703
+ const buffers = buildBuffers(messages, name, data);
970
704
 
971
705
  async function writeChunkedData(
972
706
  buffers: ByteBuffer[],
@@ -995,6 +729,41 @@ export default class ReactNativeBleTransport {
995
729
  }
996
730
  }
997
731
 
732
+ async function writeFirmwareUploadChunkedData(
733
+ buffers: ByteBuffer[],
734
+ writeFunction: (data: string) => Promise<void>,
735
+ onError: (e: any) => void
736
+ ) {
737
+ let index = 0;
738
+ let packetsWritten = 0;
739
+ let chunk = ByteBuffer.allocate(FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY);
740
+
741
+ while (index < buffers.length) {
742
+ const buffer = buffers[index].toBuffer();
743
+ chunk.append(buffer);
744
+ index += 1;
745
+
746
+ if (chunk.offset === FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY || index >= buffers.length) {
747
+ chunk.reset();
748
+ try {
749
+ await writeFunction(chunk.toString('base64'));
750
+ packetsWritten += 1;
751
+ chunk = ByteBuffer.allocate(FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY);
752
+ if (packetsWritten % FIRMWARE_UPLOAD_WRITE_BURST_SIZE === 0 && index < buffers.length) {
753
+ await delay(FIRMWARE_UPLOAD_WRITE_PAUSE_MS);
754
+ }
755
+ } catch (e) {
756
+ onError(e);
757
+ throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
758
+ }
759
+ }
760
+ }
761
+
762
+ if (packetsWritten > 0) {
763
+ await delay(FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS);
764
+ }
765
+ }
766
+
998
767
  if (name === 'EmmcFileWrite') {
999
768
  await writeChunkedData(
1000
769
  buffers,
@@ -1005,10 +774,57 @@ export default class ReactNativeBleTransport {
1005
774
  }
1006
775
  );
1007
776
  } else if (name === 'FirmwareUpload') {
1008
- await writeChunkedData(
777
+ Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', {
778
+ packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
779
+ burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
780
+ pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
781
+ flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
782
+ maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
783
+ });
784
+
785
+ await writeFirmwareUploadChunkedData(
1009
786
  buffers,
1010
787
  async data => {
1011
- await transport.writeCharacteristic.writeWithoutResponse(data);
788
+ let attempt = 0;
789
+ // Retry only congestion. Other write errors should surface immediately.
790
+ // GATT_CONGESTED is usually transient backpressure from the Android BLE queue.
791
+ // eslint-disable-next-line no-constant-condition
792
+ while (true) {
793
+ try {
794
+ await transport.writeCharacteristic.writeWithoutResponse(data);
795
+ return;
796
+ } catch (error) {
797
+ const retryType = getFirmwareUploadWriteRetryType(error);
798
+ if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
799
+ throw error;
800
+ }
801
+ const shouldReconnect = retryType === 'reconnectable';
802
+ const delayMs = shouldReconnect
803
+ ? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
804
+ : resolveFirmwareUploadRetryDelay(attempt);
805
+ Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
806
+ attempt: attempt + 1,
807
+ delayMs,
808
+ reconnect: shouldReconnect,
809
+ error,
810
+ });
811
+ if (shouldReconnect) {
812
+ this.firmwareUploadWriteRecoveryIds.add(uuid);
813
+ }
814
+ await delay(delayMs);
815
+ attempt += 1;
816
+ if (shouldReconnect) {
817
+ try {
818
+ await this.reconnectFirmwareUploadTransport(uuid, transport);
819
+ } catch (e) {
820
+ Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
821
+ if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
822
+ throw e;
823
+ }
824
+ }
825
+ }
826
+ }
827
+ }
1012
828
  },
1013
829
  e => {
1014
830
  this.runPromise = null;
@@ -1037,41 +853,20 @@ export default class ReactNativeBleTransport {
1037
853
  }
1038
854
 
1039
855
  try {
1040
- const response = await Promise.race([
1041
- runPromise.promise,
1042
- new Promise<never>((_, reject) => {
1043
- if (options?.timeoutMs) {
1044
- timeout = setTimeout(() => {
1045
- const error = ERRORS.TypedError(
1046
- HardwareErrorCode.BleTimeoutError,
1047
- `BLE response timeout after ${options.timeoutMs}ms for ${name}`
1048
- );
1049
- runPromise.reject(error);
1050
- reject(error);
1051
- }, options.timeoutMs);
1052
- }
1053
- }),
1054
- ]);
856
+ const response = await this.runPromise.promise;
1055
857
 
1056
858
  if (typeof response !== 'string') {
1057
859
  throw new Error('Returning data is not string.');
1058
860
  }
1059
861
 
1060
862
  Log?.debug('receive data: ', response);
1061
- const jsonData = ProtocolV1.decodeMessage(messages, response);
863
+ const jsonData = receiveOne(messages, response);
1062
864
  return check.call(jsonData);
1063
865
  } catch (e) {
1064
- if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1065
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1066
- } else {
1067
- Log?.error('call error: ', e);
1068
- }
866
+ Log?.error('call error: ', e);
1069
867
  throw e;
1070
868
  } finally {
1071
- if (timeout) clearTimeout(timeout);
1072
- if (this.runPromise === runPromise) {
1073
- this.runPromise = null;
1074
- }
869
+ this.runPromise = null;
1075
870
  }
1076
871
  }
1077
872
 
@@ -1138,13 +933,6 @@ export default class ReactNativeBleTransport {
1138
933
  if (transportCache[session]) {
1139
934
  delete transportCache[session];
1140
935
  }
1141
- this.deviceProtocol.delete(session);
1142
- this.deviceProtocolHints.delete(session);
1143
- this.protocolV2Assemblers.delete(session);
1144
- this.resetProtocolV2Frames(session);
1145
- if (this.activeProtocolV2Call?.uuid === session) {
1146
- this.activeProtocolV2Call = null;
1147
- }
1148
936
 
1149
937
  // emit the disconnect event
1150
938
  try {
@@ -1167,421 +955,4 @@ export default class ReactNativeBleTransport {
1167
955
  }
1168
956
  this.runPromise = null;
1169
957
  }
1170
-
1171
- private getCachedTransport(uuid: string) {
1172
- const transport = transportCache[uuid];
1173
- if (!transport) {
1174
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
1175
- }
1176
- return transport;
1177
- }
1178
-
1179
- private createProtocolMismatchError(expected: ProtocolType) {
1180
- return ERRORS.TypedError(
1181
- HardwareErrorCode.RuntimeError,
1182
- `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
1183
- );
1184
- }
1185
-
1186
- private createProtocolDetectionError() {
1187
- return ERRORS.TypedError(
1188
- HardwareErrorCode.BleTimeoutError,
1189
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1190
- );
1191
- }
1192
-
1193
- private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
1194
- if (this.deviceProtocol.get(uuid) === protocol) {
1195
- this.deviceProtocol.delete(uuid);
1196
- }
1197
- }
1198
-
1199
- private async detectProtocol(
1200
- uuid: string,
1201
- expectedProtocol?: ProtocolType,
1202
- protocolHint?: ProtocolType
1203
- ): Promise<ProtocolType> {
1204
- if (expectedProtocol === 'V1') {
1205
- if (await this.probeProtocolV1(uuid)) {
1206
- this.deviceProtocol.set(uuid, 'V1');
1207
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
1208
- return 'V1';
1209
- }
1210
- throw this.createProtocolMismatchError(expectedProtocol);
1211
- }
1212
-
1213
- if (expectedProtocol === 'V2') {
1214
- this.deviceProtocol.set(uuid, 'V2');
1215
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
1216
- return 'V2';
1217
- }
1218
-
1219
- if (protocolHint === 'V2') {
1220
- this.deviceProtocol.set(uuid, 'V2');
1221
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (hint)`);
1222
- return 'V2';
1223
- }
1224
-
1225
- if (this.deviceProtocol.get(uuid) === 'V2') {
1226
- this.deviceProtocol.set(uuid, 'V2');
1227
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
1228
- return 'V2';
1229
- }
1230
-
1231
- const protocolV1Detected = await this.probeProtocolV1(uuid);
1232
- if (protocolV1Detected) {
1233
- this.deviceProtocol.set(uuid, 'V1');
1234
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1`);
1235
- return 'V1';
1236
- }
1237
-
1238
- await this.resetProbeStateAfterProtocolProbe(uuid, 'V1');
1239
- if (await this.probeProtocolV2(uuid)) {
1240
- this.deviceProtocol.set(uuid, 'V2');
1241
- Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2`);
1242
- return 'V2';
1243
- }
1244
-
1245
- this.deviceProtocol.delete(uuid);
1246
- throw this.createProtocolDetectionError();
1247
- }
1248
-
1249
- private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
1250
- const transport = transportCache[uuid];
1251
- this.protocolV2Assemblers.get(uuid)?.reset();
1252
- this.resetProtocolV2Frames(uuid);
1253
- if (this.activeProtocolV2Call?.uuid === uuid) {
1254
- this.activeProtocolV2Call = null;
1255
- }
1256
- if (this.runPromise) {
1257
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1258
- this.runPromise.reject(error);
1259
- this.runPromise = null;
1260
- }
1261
-
1262
- if (!transport) return;
1263
-
1264
- const previousNotifyTransactionId = transport.notifyTransactionId;
1265
- if (this.monitorTokens.get(uuid) === transport.monitorToken) {
1266
- this.monitorTokens.delete(uuid);
1267
- }
1268
- transport.notifySubscription?.remove();
1269
- transport.notifySubscription = undefined;
1270
- if (previousNotifyTransactionId) {
1271
- try {
1272
- await this.blePlxManager?.cancelTransaction(previousNotifyTransactionId);
1273
- } catch (error) {
1274
- Log?.debug(
1275
- `[ReactNativeBleTransport] cancel notify after Protocol ${protocol} probe failed:`,
1276
- error?.message || error
1277
- );
1278
- }
1279
- }
1280
-
1281
- const monitorToken = this.nextMonitorToken;
1282
- this.nextMonitorToken += 1;
1283
- const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
1284
- transport.monitorToken = monitorToken;
1285
- transport.notifyTransactionId = notifyTransactionId;
1286
- this.monitorTokens.set(uuid, monitorToken);
1287
- transport.notifySubscription = this._monitorCharacteristic(
1288
- transport.notifyCharacteristic,
1289
- uuid,
1290
- monitorToken,
1291
- notifyTransactionId
1292
- );
1293
- if (Platform.OS === 'ios') {
1294
- await new Promise<void>(resolve => {
1295
- setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
1296
- });
1297
- }
1298
- }
1299
-
1300
- private async probeProtocolV1(uuid: string) {
1301
- if (!this._messages) {
1302
- return false;
1303
- }
1304
-
1305
- try {
1306
- this.deviceProtocol.set(uuid, 'V1');
1307
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1308
- return true;
1309
- } catch (error) {
1310
- this.clearProbeProtocol(uuid, 'V1');
1311
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1312
- return false;
1313
- }
1314
- }
1315
-
1316
- private async probeProtocolV2(uuid: string) {
1317
- if (!this._messages || !this._messagesV2) {
1318
- return false;
1319
- }
1320
-
1321
- this.deviceProtocol.set(uuid, 'V2');
1322
- this.protocolV2Assemblers.get(uuid)?.reset();
1323
- const detected = await probeProtocolV2Helper({
1324
- call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
1325
- this.callProtocolV2(uuid, name, data, options),
1326
- timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
1327
- logger: Log,
1328
- logPrefix: 'ProtocolV2 RN-BLE',
1329
- onProbeFailed: () => {
1330
- this.protocolV2Assemblers.get(uuid)?.reset();
1331
- this.resetProtocolV2Frames(uuid);
1332
- },
1333
- });
1334
- if (!detected) {
1335
- this.clearProbeProtocol(uuid, 'V2');
1336
- }
1337
- return detected;
1338
- }
1339
-
1340
- private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
1341
- try {
1342
- if (!this.runPromise || this.activeProtocolV2Call?.uuid !== uuid) {
1343
- this.protocolV2Assemblers.get(uuid)?.reset();
1344
- this.resetProtocolV2Frames(uuid);
1345
- return;
1346
- }
1347
-
1348
- if (data.length === 0) return;
1349
-
1350
- const assembler = this.protocolV2Assemblers.get(uuid);
1351
- if (!assembler) return;
1352
-
1353
- let frameData = assembler.push(data);
1354
- while (frameData) {
1355
- this.resolveProtocolV2Frame(uuid, frameData);
1356
- frameData = assembler.push(new Uint8Array(0));
1357
- }
1358
- } catch (error) {
1359
- Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
1360
- const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
1361
- this.runPromise?.reject(notifyError);
1362
- this.rejectAllProtocolV2Frames(notifyError);
1363
- }
1364
- }
1365
-
1366
- private getProtocolV2FrameQueue(uuid: string) {
1367
- let queue = this.protocolV2FrameQueues.get(uuid);
1368
- if (!queue) {
1369
- queue = [];
1370
- this.protocolV2FrameQueues.set(uuid, queue);
1371
- }
1372
- return queue;
1373
- }
1374
-
1375
- private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
1376
- const framePromise = this.protocolV2FramePromises.get(uuid);
1377
- if (framePromise) {
1378
- framePromise.resolve(frame);
1379
- this.protocolV2FramePromises.delete(uuid);
1380
- return;
1381
- }
1382
- this.getProtocolV2FrameQueue(uuid).push(frame);
1383
- }
1384
-
1385
- private rejectAllProtocolV2Frames(error: Error) {
1386
- this.protocolV2FrameQueues.clear();
1387
- for (const framePromise of this.protocolV2FramePromises.values()) {
1388
- framePromise.reject(error);
1389
- }
1390
- this.protocolV2FramePromises.clear();
1391
- }
1392
-
1393
- private resetProtocolV2Frames(uuid: string) {
1394
- this.protocolV2FrameQueues.delete(uuid);
1395
- this.protocolV2FramePromises.delete(uuid);
1396
- }
1397
-
1398
- private isActiveProtocolV2Call(uuid: string, token: number) {
1399
- return this.activeProtocolV2Call?.uuid === uuid && this.activeProtocolV2Call.token === token;
1400
- }
1401
-
1402
- private async readProtocolV2Frame(uuid: string) {
1403
- const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
1404
- if (queuedFrame) {
1405
- return queuedFrame;
1406
- }
1407
-
1408
- const framePromise = createDeferred<Uint8Array>();
1409
- this.protocolV2FramePromises.set(uuid, framePromise);
1410
- try {
1411
- return await framePromise.promise;
1412
- } finally {
1413
- if (this.protocolV2FramePromises.get(uuid) === framePromise) {
1414
- this.protocolV2FramePromises.delete(uuid);
1415
- }
1416
- }
1417
- }
1418
-
1419
- private async writeProtocolV2Frame(
1420
- transport: BleTransport,
1421
- frame: Uint8Array,
1422
- options?: { highVolume?: boolean; writeWithResponse?: boolean }
1423
- ) {
1424
- const tuning = getProtocolV2BleTuning();
1425
- const packetCapacity = resolveProtocolV2PacketCapacity({
1426
- platform: Platform.OS,
1427
- iosPacketLength: tuning.iosPacketLength,
1428
- androidPacketLength: tuning.androidPacketLength,
1429
- mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1430
- });
1431
- const writeWithResponse =
1432
- !!options?.writeWithResponse || (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
1433
- const writeMode = resolveBleWriteMode(
1434
- transport.writeCharacteristic,
1435
- writeWithResponse ? 'withResponse' : 'withoutResponse'
1436
- );
1437
- const shouldThrottle = !!options?.highVolume && writeMode === 'withoutResponse';
1438
- let packetsWritten = 0;
1439
-
1440
- try {
1441
- for (let offset = 0; offset < frame.length; offset += packetCapacity) {
1442
- const chunk = frame.slice(offset, offset + packetCapacity);
1443
- const base64 = Buffer.from(chunk).toString('base64');
1444
- if (writeMode === 'withResponse') {
1445
- await transport.writeCharacteristic.writeWithResponse(base64);
1446
- } else {
1447
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1448
- }
1449
- packetsWritten += 1;
1450
-
1451
- if (
1452
- shouldThrottle &&
1453
- packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
1454
- offset + packetCapacity < frame.length
1455
- ) {
1456
- await delay(tuning.highVolumeWritePauseMs);
1457
- }
1458
- }
1459
-
1460
- if (shouldThrottle) {
1461
- await delay(tuning.highVolumeWriteFlushDelayMs);
1462
- }
1463
- } catch (error) {
1464
- if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
1465
- Log?.debug(
1466
- '[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
1467
- error
1468
- );
1469
- await this.writeProtocolV2Frame(transport, frame, {
1470
- highVolume: true,
1471
- writeWithResponse: true,
1472
- });
1473
- return;
1474
- }
1475
- throw error;
1476
- }
1477
- }
1478
-
1479
- private async callProtocolV2(
1480
- uuid: string,
1481
- name: string,
1482
- data: Record<string, unknown>,
1483
- options?: TransportCallOptions
1484
- ) {
1485
- if (!this._messages || !this._messagesV2) {
1486
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
1487
- }
1488
-
1489
- const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'GetProtoVersion';
1490
- if (this.runPromise) {
1491
- if (!forceRun) {
1492
- throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
1493
- }
1494
- const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
1495
- this.runPromise.reject(error);
1496
- this.rejectAllProtocolV2Frames(error);
1497
- this.runPromise = null;
1498
- this.activeProtocolV2Call = null;
1499
- }
1500
-
1501
- const transport = this.getCachedTransport(uuid);
1502
- const runPromise = createDeferred<Uint8Array>();
1503
- runPromise.promise.catch(() => undefined);
1504
- this.runPromise = runPromise;
1505
- const callToken = this.nextProtocolV2CallToken++;
1506
- this.activeProtocolV2Call = { uuid, token: callToken };
1507
- this.protocolV2Assemblers.get(uuid)?.reset();
1508
- this.resetProtocolV2Frames(uuid);
1509
- let completed = false;
1510
- const callOptions = {
1511
- ...options,
1512
- timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
1513
- };
1514
- const highVolumeWrite = LogBlockCommand.has(name);
1515
-
1516
- if (highVolumeWrite) {
1517
- const tuning = getProtocolV2BleTuning();
1518
- Log?.debug(
1519
- '[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
1520
- name,
1521
- {
1522
- packetCapacity:
1523
- Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1524
- burstSize: tuning.highVolumeWriteBurstSize,
1525
- pauseMs: tuning.highVolumeWritePauseMs,
1526
- flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
1527
- writeWithResponse: tuning.highVolumeWriteWithResponse,
1528
- }
1529
- );
1530
- }
1531
-
1532
- try {
1533
- const session = new ProtocolV2Session({
1534
- schemas: {
1535
- protocolV1: this._messages,
1536
- protocolV2: this._messagesV2,
1537
- },
1538
- router: PROTOCOL_V2_CHANNEL_BLE_UART,
1539
- writeFrame: async (frame: Uint8Array) => {
1540
- await this.writeProtocolV2Frame(transport, frame, {
1541
- highVolume: highVolumeWrite,
1542
- });
1543
- },
1544
- readFrame: async () => {
1545
- const rxFrame = await this.readProtocolV2Frame(uuid);
1546
- if (!(rxFrame instanceof Uint8Array)) {
1547
- throw new Error('Protocol V2 response is not Uint8Array');
1548
- }
1549
- return rxFrame;
1550
- },
1551
- logger: Log,
1552
- logPrefix: 'ProtocolV2 RN-BLE',
1553
- createTimeoutError: (_messageName: string, timeout: number) =>
1554
- ERRORS.TypedError(
1555
- HardwareErrorCode.BleTimeoutError,
1556
- `BLE response timeout after ${timeout}ms for ${name}`
1557
- ),
1558
- });
1559
-
1560
- const result = await session.call(name, data, callOptions);
1561
- completed = true;
1562
- return result;
1563
- } catch (e) {
1564
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1565
- this.protocolV2Assemblers.get(uuid)?.reset();
1566
- this.resetProtocolV2Frames(uuid);
1567
- }
1568
- Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
1569
- throw e;
1570
- } finally {
1571
- if (this.isActiveProtocolV2Call(uuid, callToken)) {
1572
- if (!completed) {
1573
- this.protocolV2Assemblers.get(uuid)?.reset();
1574
- }
1575
- this.resetProtocolV2Frames(uuid);
1576
- this.activeProtocolV2Call = null;
1577
- }
1578
- if (this.runPromise === runPromise) {
1579
- this.runPromise = null;
1580
- }
1581
- }
1582
- }
1583
-
1584
- getProtocolType(path: string): ProtocolType | undefined {
1585
- return this.deviceProtocol.get(path);
1586
- }
1587
958
  }