@onekeyfe/hd-transport-react-native 1.2.0-alpha.33 → 1.2.0-alpha.34

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.
@@ -0,0 +1,41 @@
1
+ import { BleErrorCode } from 'react-native-ble-plx';
2
+
3
+ import BleTransport from '../BleTransport';
4
+
5
+ jest.mock(
6
+ 'react-native-ble-plx',
7
+ () => ({
8
+ BleErrorCode: {
9
+ DeviceDisconnected: 205,
10
+ CharacteristicNotFound: 404,
11
+ },
12
+ }),
13
+ { virtual: true }
14
+ );
15
+
16
+ jest.mock('@onekeyfe/hd-shared', () => ({
17
+ ...jest.requireActual('@onekeyfe/hd-shared'),
18
+ wait: jest.fn(() => Promise.resolve()),
19
+ }));
20
+
21
+ describe('BleTransport side-effecting writes', () => {
22
+ test('does not reconnect or replay after the device disconnects', async () => {
23
+ const error = Object.assign(new Error('device disconnected'), {
24
+ errorCode: BleErrorCode.DeviceDisconnected,
25
+ });
26
+ const writeCharacteristic = {
27
+ writeWithoutResponse: jest.fn(() => Promise.reject(error)),
28
+ };
29
+ const device = {
30
+ id: 'classic-id',
31
+ connect: jest.fn(() => Promise.resolve()),
32
+ discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
33
+ };
34
+ const transport = new BleTransport(device as any, writeCharacteristic as any, {} as any);
35
+
36
+ await expect(transport.writeWithRetry('payload')).rejects.toBe(error);
37
+
38
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
39
+ expect(device.connect).not.toHaveBeenCalled();
40
+ });
41
+ });
@@ -0,0 +1,20 @@
1
+ import { getBluetoothServiceUuids, getInfosForServiceUuid } from '../constants';
2
+
3
+ describe('React Native BLE service filters', () => {
4
+ test('does not scan or configure the ignored FFFD service', () => {
5
+ expect(getBluetoothServiceUuids()).not.toContain('fffd');
6
+ expect(getInfosForServiceUuid('fffd', 'classic')).toBeNull();
7
+ expect(getInfosForServiceUuid('0000fffd-0000-1000-8000-00805f9b34fb', 'classic')).toBeNull();
8
+ });
9
+
10
+ test('keeps the OneKey communication service configured', () => {
11
+ expect(getBluetoothServiceUuids()).toContain('00000001-0000-1000-8000-00805f9b34fb');
12
+ expect(getInfosForServiceUuid('0001', 'classic')).toMatchObject({
13
+ serviceUuid: '00000001-0000-1000-8000-00805f9b34fb',
14
+ });
15
+ });
16
+
17
+ test('does not match a vendor-specific UUID containing the OneKey short key', () => {
18
+ expect(getInfosForServiceUuid('abcd0001-1234-5678-9012-abcdefabcdef', 'classic')).toBeNull();
19
+ });
20
+ });
@@ -5,10 +5,11 @@ import transportPackage, {
5
5
  TRANSPORT_EVENT,
6
6
  bytesToHex,
7
7
  } from '@onekeyfe/hd-transport';
8
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
8
+ import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
9
9
 
10
10
  import ReactNativeBleTransport, {
11
11
  configureProtocolV2BleTuning,
12
+ getFirmwareUploadWriteRetryType,
12
13
  resetProtocolV2BleTuning,
13
14
  } from '../index';
14
15
 
@@ -50,6 +51,7 @@ const { parseConfigure } = transportPackage;
50
51
  const protocolV1Schema = {
51
52
  nested: {
52
53
  Initialize: { fields: {} },
54
+ GetFeatures: { fields: {} },
53
55
  Success: {
54
56
  fields: {
55
57
  message: { type: 'string', id: 1 },
@@ -59,6 +61,7 @@ const protocolV1Schema = {
59
61
  values: {
60
62
  MessageType_Initialize: 1,
61
63
  MessageType_Success: 2,
64
+ MessageType_GetFeatures: 55,
62
65
  },
63
66
  },
64
67
  },
@@ -140,7 +143,7 @@ const createHarness = () => {
140
143
  id: uuid,
141
144
  name: 'OneKey Pro 2',
142
145
  localName: 'OneKey Pro 2',
143
- serviceUUIDs: ['fffd'],
146
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
144
147
  isConnected: jest.fn(() => Promise.resolve(true)),
145
148
  cancelConnection: jest.fn(() => Promise.resolve()),
146
149
  onDisconnected: jest.fn(callback => {
@@ -181,11 +184,140 @@ const createHarness = () => {
181
184
  };
182
185
  };
183
186
 
187
+ const createV1Harness = () => {
188
+ const uuid = 'rn-classic-id';
189
+ let notifyCallback:
190
+ | ((error: Error | null, characteristic: { value: string } | null) => void)
191
+ | undefined;
192
+ const notifyCharacteristic = {
193
+ uuid: '0003',
194
+ deviceID: uuid,
195
+ isNotifiable: true,
196
+ monitor: jest.fn(callback => {
197
+ notifyCallback = callback;
198
+ return { remove: jest.fn() };
199
+ }),
200
+ };
201
+ let writeCount = 0;
202
+ const writeCharacteristic = {
203
+ uuid: '0002',
204
+ deviceID: uuid,
205
+ isWritableWithResponse: true,
206
+ isWritableWithoutResponse: true,
207
+ writeWithoutResponse: jest.fn(() => {
208
+ writeCount += 1;
209
+ if (writeCount === 1) {
210
+ notifyCallback?.(null, {
211
+ value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
212
+ });
213
+ }
214
+ return Promise.resolve();
215
+ }),
216
+ };
217
+ const device = {
218
+ id: uuid,
219
+ name: 'OneKey Classic',
220
+ localName: 'OneKey Classic',
221
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
222
+ isConnected: jest.fn(() => Promise.resolve(true)),
223
+ cancelConnection: jest.fn(() => Promise.resolve()),
224
+ onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
225
+ };
226
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
227
+ transport.blePlxManager = {
228
+ devices: jest.fn(() => Promise.resolve([device])),
229
+ connectedDevices: jest.fn(() => Promise.resolve([])),
230
+ cancelTransaction: jest.fn(() => Promise.resolve()),
231
+ } as any;
232
+ transport.resolveCharacteristics = jest.fn(() =>
233
+ Promise.resolve({ writeCharacteristic, notifyCharacteristic })
234
+ );
235
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
236
+ transport.configure(protocolV1Schema);
237
+ transport.configureProtocolV2(protocolV2Schema);
238
+ return { transport, uuid, device };
239
+ };
240
+
184
241
  describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
242
+ test('does not classify disconnects as retryable firmware writes', () => {
243
+ expect(
244
+ getFirmwareUploadWriteRetryType({
245
+ errorCode: 205,
246
+ message: 'Device disconnected after write',
247
+ })
248
+ ).toBeNull();
249
+ });
250
+
251
+ test('keeps another device reader when releasing a device with an active V1 call', async () => {
252
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
253
+ const activeV1Call = createDeferred<string>();
254
+ const otherDeviceReader = createDeferred<Uint8Array>();
255
+ activeV1Call.promise.catch(() => undefined);
256
+ otherDeviceReader.promise.catch(() => undefined);
257
+ transport.runPromise = activeV1Call;
258
+ transport.runPromiseDeviceId = 'device-a';
259
+ transport.protocolV2FramePromises.set('device-b', otherDeviceReader);
260
+
261
+ await transport.releaseNative('device-a', true);
262
+
263
+ expect(transport.protocolV2FramePromises.get('device-b')).toBe(otherDeviceReader);
264
+ });
265
+
266
+ test('rejects a pending reader when its device frame state resets', async () => {
267
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
268
+ const reader = createDeferred<Uint8Array>();
269
+ transport.protocolV2FramePromises.set('device-a', reader);
270
+ const result = Promise.race([
271
+ reader.promise.then(
272
+ () => 'resolved',
273
+ () => 'rejected'
274
+ ),
275
+ new Promise(resolve => {
276
+ setTimeout(() => resolve('pending'), 20);
277
+ }),
278
+ ]);
279
+
280
+ transport.resetProtocolV2Frames('device-a');
281
+
282
+ await expect(result).resolves.toBe('rejected');
283
+ });
284
+
185
285
  test('keeps the legacy default BLE scan timeout', () => {
186
286
  expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
187
287
  });
188
288
 
289
+ test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
290
+ const { transport, uuid } = createV1Harness();
291
+ const probeProtocolV2 = jest
292
+ .spyOn(transport as any, 'probeProtocolV2')
293
+ .mockImplementationOnce(async () => {
294
+ await (transport as any).releaseNative(uuid, true);
295
+ return false;
296
+ });
297
+ const resolveCharacteristics = jest.spyOn(transport as any, 'resolveCharacteristics');
298
+
299
+ await expect(transport.acquire({ uuid, protocolHint: 'V2' })).resolves.toEqual({
300
+ uuid,
301
+ protocolType: 'V1',
302
+ });
303
+
304
+ expect(probeProtocolV2).toHaveBeenCalledTimes(1);
305
+ expect(resolveCharacteristics).toHaveBeenCalledTimes(2);
306
+ expect(transport.getProtocolType(uuid)).toBe('V1');
307
+ });
308
+
309
+ test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
310
+ const { transport, uuid, device } = createV1Harness();
311
+
312
+ await transport.acquire({ uuid, expectedProtocol: 'V1' });
313
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
314
+ errorCode: HardwareErrorCode.BleTimeoutError,
315
+ });
316
+
317
+ expect(device.cancelConnection).toHaveBeenCalled();
318
+ expect(transport.getProtocolType(uuid)).toBeUndefined();
319
+ });
320
+
189
321
  afterEach(() => {
190
322
  resetProtocolV2BleTuning();
191
323
  });
package/src/constants.ts CHANGED
@@ -12,7 +12,6 @@ type BluetoothServices = Record<
12
12
  >;
13
13
 
14
14
  const ClassicServiceUUID = '00000001-0000-1000-8000-00805f9b34fb';
15
- const Pro2ServiceUUID = 'fffd';
16
15
 
17
16
  const OneKeyServices: Record<string, BluetoothServices> = {
18
17
  classic: {
@@ -21,11 +20,6 @@ const OneKeyServices: Record<string, BluetoothServices> = {
21
20
  writeUuid: '00000002-0000-1000-8000-00805f9b34fb',
22
21
  notifyUuid: '00000003-0000-1000-8000-00805f9b34fb',
23
22
  },
24
- [Pro2ServiceUUID]: {
25
- serviceUuid: Pro2ServiceUUID,
26
- writeUuid: '00000002-0000-1000-8000-00805f9b34fb',
27
- notifyUuid: '00000003-0000-1000-8000-00805f9b34fb',
28
- },
29
23
  },
30
24
  };
31
25
 
@@ -48,7 +42,7 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic
48
42
  Object.values(services).find(
49
43
  item =>
50
44
  normalizeBleUuid(item.serviceUuid) === normalizedServiceUuid ||
51
- getBleUuidKey(item.serviceUuid) === getBleUuidKey(serviceUuid)
45
+ matchesKnownBleUuid(serviceUuid, createKnownBleUuidAliases(item.serviceUuid))
52
46
  );
53
47
  if (!service) {
54
48
  return null;
@@ -59,17 +53,8 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic
59
53
  export const normalizeBleUuid = (uuid?: string | null) =>
60
54
  (uuid ?? '').replace(/-/g, '').toLowerCase();
61
55
 
62
- export const getBleUuidKey = (uuid?: string | null) => {
63
- const normalized = normalizeBleUuid(uuid);
64
- return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
65
- };
66
-
67
56
  export const isSameBleUuid = (left?: string | null, right?: string | null) => {
68
- const normalizedLeft = normalizeBleUuid(left);
69
- const normalizedRight = normalizeBleUuid(right);
70
-
71
- return (
72
- normalizedLeft === normalizedRight ||
73
- (getBleUuidKey(left) !== '' && getBleUuidKey(left) === getBleUuidKey(right))
74
- );
57
+ if (!left || !right) return false;
58
+ return matchesKnownBleUuid(left, createKnownBleUuidAliases(right));
75
59
  };
60
+ import { createKnownBleUuidAliases, matchesKnownBleUuid } from '@onekeyfe/hd-shared';
package/src/index.ts CHANGED
@@ -23,7 +23,12 @@ import transport, {
23
23
  probeProtocolV2 as probeProtocolV2Helper,
24
24
  writeProtocolV2BleFrame,
25
25
  } from '@onekeyfe/hd-transport';
26
- import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
26
+ import {
27
+ ERRORS,
28
+ HardwareErrorCode,
29
+ createDeferred,
30
+ isOnekeyBluetoothDevice,
31
+ } from '@onekeyfe/hd-shared';
27
32
 
28
33
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
29
34
  import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
@@ -31,7 +36,6 @@ import { subscribeBleOn } from './subscribeBleOn';
31
36
  import {
32
37
  ANDROID_PACKET_LENGTH,
33
38
  IOS_PACKET_LENGTH,
34
- getBleUuidKey,
35
39
  getBluetoothServiceUuids,
36
40
  getInfosForServiceUuid,
37
41
  isSameBleUuid,
@@ -56,13 +60,12 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
56
60
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
57
61
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
58
62
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
59
- const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
60
63
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
61
64
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
62
65
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
63
66
  const ANDROID_GATT_CONGESTED_STATUS = 143;
64
67
 
65
- type FirmwareUploadWriteRetryType = 'congested' | 'reconnectable';
68
+ type FirmwareUploadWriteRetryType = 'congested';
66
69
  type ResolvedBleCharacteristics = {
67
70
  writeCharacteristic: Characteristic;
68
71
  notifyCharacteristic: Characteristic;
@@ -73,7 +76,9 @@ const delay = (ms: number) =>
73
76
  setTimeout(resolve, ms);
74
77
  });
75
78
 
76
- const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRetryType | null => {
79
+ export const getFirmwareUploadWriteRetryType = (
80
+ error: unknown
81
+ ): FirmwareUploadWriteRetryType | null => {
77
82
  if (!error || typeof error !== 'object') return null;
78
83
  const bleWriteError = error as {
79
84
  androidErrorCode?: unknown;
@@ -84,13 +89,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
84
89
  name?: unknown;
85
90
  };
86
91
 
87
- if (
88
- bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
89
- bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
90
- ) {
91
- return 'reconnectable';
92
- }
93
-
94
92
  if (
95
93
  bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
96
94
  bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
@@ -162,16 +160,6 @@ function getDeviceDisplayName(device?: Device | null) {
162
160
  return device?.name || device?.localName || null;
163
161
  }
164
162
 
165
- function isGenericBleService(uuid?: string | null) {
166
- return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
167
- }
168
-
169
- function hasKnownOneKeyService(device?: Device | null) {
170
- return (device?.serviceUUIDs ?? []).some(serviceUuid =>
171
- getInfosForServiceUuid(serviceUuid, 'classic')
172
- );
173
- }
174
-
175
163
  const ANDROID_REQUEST_MTU = 256;
176
164
 
177
165
  const connectOptions: Record<string, unknown> = {
@@ -259,6 +247,8 @@ export default class ReactNativeBleTransport {
259
247
 
260
248
  runPromise: Deferred<any> | null = null;
261
249
 
250
+ private runPromiseDeviceId: string | null = null;
251
+
262
252
  emitter?: EventEmitter;
263
253
 
264
254
  firmwareUploadWriteRecoveryIds = new Set<string>();
@@ -359,29 +349,15 @@ export default class ReactNativeBleTransport {
359
349
  }
360
350
  }
361
351
 
362
- let fallbackServiceUuid: string | undefined;
363
-
364
352
  if (!infos) {
365
353
  const services = await device.services();
366
354
  Log?.debug(
367
355
  '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
368
356
  services?.map(service => service.uuid)
369
357
  );
370
-
371
- const knownService = services.find(service =>
372
- getInfosForServiceUuid(service.uuid, 'classic')
373
- );
374
- const fallbackService =
375
- knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
376
-
377
- if (fallbackService) {
378
- fallbackServiceUuid = fallbackService.uuid;
379
- characteristics = await device.characteristicsForService(fallbackService.uuid);
380
- Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
381
- }
382
358
  }
383
359
 
384
- if (!infos && !fallbackServiceUuid) {
360
+ if (!infos) {
385
361
  try {
386
362
  Log?.debug('cancel connection when service not found');
387
363
  await device.cancelConnection();
@@ -391,9 +367,7 @@ export default class ReactNativeBleTransport {
391
367
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
392
368
  }
393
369
 
394
- const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
395
- const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
396
- const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
370
+ const { serviceUuid, writeUuid, notifyUuid } = infos;
397
371
 
398
372
  if (!serviceUuid) {
399
373
  throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
@@ -461,10 +435,9 @@ export default class ReactNativeBleTransport {
461
435
  try {
462
436
  Log?.debug('device disconnect: ', device?.id);
463
437
  this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
464
- if (this.runPromise) {
438
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
465
439
  const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
466
440
  this.runPromise.reject(error);
467
- this.rejectAllProtocolV2Frames(error);
468
441
  }
469
442
  } catch (e) {
470
443
  Log?.debug('device disconnect error: ', e);
@@ -607,10 +580,12 @@ export default class ReactNativeBleTransport {
607
580
  }
608
581
 
609
582
  const displayName = getDeviceDisplayName(device);
610
- const isOneKey =
611
- isOnekeyDevice(device?.name ?? null, device?.id) ||
612
- isOnekeyDevice(device?.localName ?? null, device?.id) ||
613
- hasKnownOneKeyService(device);
583
+ const isOneKey = isOnekeyBluetoothDevice({
584
+ id: device?.id,
585
+ name: device?.name,
586
+ localName: device?.localName,
587
+ serviceUuids: device?.serviceUUIDs,
588
+ });
614
589
  if (isOneKey) {
615
590
  addDevice(device as unknown as Device);
616
591
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
@@ -627,10 +602,18 @@ export default class ReactNativeBleTransport {
627
602
  getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
628
603
  devices => {
629
604
  for (const device of devices) {
630
- const { serviceUUIDs } = device as { serviceUUIDs?: string[] };
631
- const hasCachedServiceUuid = Boolean(serviceUUIDs?.length);
632
- const keepDevice = Platform.OS === 'ios' || hasCachedServiceUuid;
633
- if (keepDevice) {
605
+ const localName =
606
+ 'localName' in device && typeof device.localName === 'string'
607
+ ? device.localName
608
+ : null;
609
+ if (
610
+ isOnekeyBluetoothDevice({
611
+ id: device.id,
612
+ name: device.name,
613
+ localName,
614
+ serviceUuids: device.serviceUUIDs,
615
+ })
616
+ ) {
634
617
  Log?.debug('search connected peripheral: ', device.id);
635
618
  addDevice(device as unknown as Device);
636
619
  }
@@ -699,8 +682,8 @@ export default class ReactNativeBleTransport {
699
682
  if (forceCleanRunPromise && this.runPromise) {
700
683
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
701
684
  this.runPromise.reject(error);
702
- this.rejectAllProtocolV2Frames(error);
703
685
  this.runPromise = null;
686
+ this.runPromiseDeviceId = null;
704
687
  Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
705
688
  }
706
689
 
@@ -793,7 +776,8 @@ export default class ReactNativeBleTransport {
793
776
 
794
777
  const protocolHint = expectedProtocol
795
778
  ? undefined
796
- : this.deviceProtocolHints.get(uuid) ??
779
+ : input.protocolHint ??
780
+ this.deviceProtocolHints.get(uuid) ??
797
781
  inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
798
782
 
799
783
  // release transport before new transport instance
@@ -887,7 +871,7 @@ export default class ReactNativeBleTransport {
887
871
  this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
888
872
  return;
889
873
  }
890
- if (this.runPromise) {
874
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
891
875
  let ERROR:
892
876
  | typeof HardwareErrorCode.BleDeviceBondError
893
877
  | typeof HardwareErrorCode.BleCharacteristicNotifyError
@@ -910,7 +894,6 @@ export default class ReactNativeBleTransport {
910
894
  HardwareErrorCode.BleCharacteristicNotifyChangeFailure
911
895
  );
912
896
  this.runPromise.reject(notifyError);
913
- this.rejectAllProtocolV2Frames(notifyError);
914
897
  Log?.debug(
915
898
  `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
916
899
  );
@@ -918,7 +901,6 @@ export default class ReactNativeBleTransport {
918
901
  }
919
902
  const notifyError = ERRORS.TypedError(ERROR);
920
903
  this.runPromise.reject(notifyError);
921
- this.rejectAllProtocolV2Frames(notifyError);
922
904
  Log?.debug(': monitor notify error, and has unreleased Promise', Error);
923
905
  }
924
906
 
@@ -963,14 +945,16 @@ export default class ReactNativeBleTransport {
963
945
  // );
964
946
  bufferLength = 0;
965
947
  buffer = [];
966
- this.runPromise?.resolve(value.toString('hex'));
948
+ if (this.runPromiseDeviceId === uuid) {
949
+ this.runPromise?.resolve(value.toString('hex'));
950
+ }
967
951
  }
968
952
  } catch (error) {
969
953
  Log?.debug('monitor data error: ', error);
970
954
  const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
971
955
  if (this.deviceProtocol.get(uuid) === 'V2') {
972
956
  this.rejectProtocolV2Frames(uuid, notifyError);
973
- } else {
957
+ } else if (this.runPromiseDeviceId === uuid) {
974
958
  this.runPromise?.reject(notifyError);
975
959
  }
976
960
  }
@@ -986,11 +970,12 @@ export default class ReactNativeBleTransport {
986
970
 
987
971
  private async releaseNative(uuid: string, onclose = false) {
988
972
  const transport = transportCache[uuid];
989
- if (this.runPromise) {
973
+ if (this.runPromise && this.runPromiseDeviceId === uuid) {
990
974
  const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
991
975
  this.runPromise.reject(error);
992
976
  this.runPromise = null;
993
- this.rejectAllProtocolV2Frames(error);
977
+ this.runPromiseDeviceId = null;
978
+ this.rejectProtocolV2Frames(uuid, error);
994
979
  } else {
995
980
  this.resetProtocolV2Frames(uuid);
996
981
  }
@@ -1098,6 +1083,7 @@ export default class ReactNativeBleTransport {
1098
1083
  const runPromise = createDeferred<string>();
1099
1084
  runPromise.promise.catch(() => undefined);
1100
1085
  this.runPromise = runPromise;
1086
+ this.runPromiseDeviceId = uuid;
1101
1087
  const messages = this._messages;
1102
1088
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1103
1089
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1197,31 +1183,14 @@ export default class ReactNativeBleTransport {
1197
1183
  if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1198
1184
  throw error;
1199
1185
  }
1200
- const shouldReconnect = retryType === 'reconnectable';
1201
- const delayMs = shouldReconnect
1202
- ? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
1203
- : resolveFirmwareUploadRetryDelay(attempt);
1186
+ const delayMs = resolveFirmwareUploadRetryDelay(attempt);
1204
1187
  Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
1205
1188
  attempt: attempt + 1,
1206
1189
  delayMs,
1207
- reconnect: shouldReconnect,
1208
1190
  error,
1209
1191
  });
1210
- if (shouldReconnect) {
1211
- this.firmwareUploadWriteRecoveryIds.add(uuid);
1212
- }
1213
1192
  await delay(delayMs);
1214
1193
  attempt += 1;
1215
- if (shouldReconnect) {
1216
- try {
1217
- await this.reconnectFirmwareUploadTransport(uuid, transport);
1218
- } catch (e) {
1219
- Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
1220
- if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
1221
- throw e;
1222
- }
1223
- }
1224
- }
1225
1194
  }
1226
1195
  }
1227
1196
  },
@@ -1274,16 +1243,25 @@ export default class ReactNativeBleTransport {
1274
1243
  const jsonData = ProtocolV1.decodeMessage(messages, response);
1275
1244
  return check.call(jsonData);
1276
1245
  } catch (e) {
1277
- if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1278
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
1246
+ if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
1247
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
1279
1248
  } else {
1280
1249
  Log?.error('call error: ', e);
1281
1250
  }
1251
+ const isProbeTimeout =
1252
+ name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1253
+ if (
1254
+ !isProbeTimeout &&
1255
+ (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1256
+ ) {
1257
+ await this.disconnect(uuid);
1258
+ }
1282
1259
  throw e;
1283
1260
  } finally {
1284
1261
  if (timeout) clearTimeout(timeout);
1285
1262
  if (this.runPromise === runPromise) {
1286
1263
  this.runPromise = null;
1264
+ this.runPromiseDeviceId = null;
1287
1265
  }
1288
1266
  }
1289
1267
  }
@@ -1376,6 +1354,7 @@ export default class ReactNativeBleTransport {
1376
1354
  // this.runPromise.reject(new Error('Transport_CallCanceled'));
1377
1355
  }
1378
1356
  this.runPromise = null;
1357
+ this.runPromiseDeviceId = null;
1379
1358
  }
1380
1359
 
1381
1360
  private getCachedTransport(uuid: string) {
@@ -1396,7 +1375,7 @@ export default class ReactNativeBleTransport {
1396
1375
  private createProtocolDetectionError() {
1397
1376
  return ERRORS.TypedError(
1398
1377
  HardwareErrorCode.BleTimeoutError,
1399
- 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
1378
+ 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
1400
1379
  );
1401
1380
  }
1402
1381
 
@@ -1447,6 +1426,13 @@ export default class ReactNativeBleTransport {
1447
1426
  if (i > 0) {
1448
1427
  // Reset subscriptions and buffers after a failed probe before trying another protocol.
1449
1428
  await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
1429
+ if (!transportCache[uuid]) {
1430
+ const reacquired = await this.acquire({
1431
+ uuid,
1432
+ expectedProtocol: protocol,
1433
+ });
1434
+ return reacquired.protocolType;
1435
+ }
1450
1436
  }
1451
1437
  const detected =
1452
1438
  protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
@@ -1524,11 +1510,13 @@ export default class ReactNativeBleTransport {
1524
1510
 
1525
1511
  try {
1526
1512
  this.deviceProtocol.set(uuid, 'V1');
1527
- await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1513
+ // GetFeatures identifies Protocol V1 without resetting an existing wallet
1514
+ // session before Core has a chance to restore a hidden wallet.
1515
+ await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
1528
1516
  return true;
1529
1517
  } catch (error) {
1530
1518
  this.clearProbeProtocol(uuid, 'V1');
1531
- Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
1519
+ Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
1532
1520
  return false;
1533
1521
  }
1534
1522
  }
@@ -1603,17 +1591,8 @@ export default class ReactNativeBleTransport {
1603
1591
  this.getProtocolV2FrameQueue(uuid).push(frame);
1604
1592
  }
1605
1593
 
1606
- private rejectAllProtocolV2Frames(error: Error) {
1607
- this.protocolV2FrameQueues.clear();
1608
- for (const framePromise of this.protocolV2FramePromises.values()) {
1609
- framePromise.reject(error);
1610
- }
1611
- this.protocolV2FramePromises.clear();
1612
- }
1613
-
1614
1594
  private resetProtocolV2Frames(uuid: string) {
1615
- this.protocolV2FrameQueues.delete(uuid);
1616
- this.protocolV2FramePromises.delete(uuid);
1595
+ this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
1617
1596
  }
1618
1597
 
1619
1598
  private rejectProtocolV2Frames(uuid: string, error: Error) {
package/src/types.ts CHANGED
@@ -10,4 +10,5 @@ export type BleAcquireInput = {
10
10
  uuid: string;
11
11
  forceCleanRunPromise?: boolean;
12
12
  expectedProtocol?: ProtocolType;
13
+ protocolHint?: ProtocolType;
13
14
  };