@onekeyfe/hd-transport-react-native 1.2.0-alpha.67 → 1.2.0-alpha.68

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.
@@ -1 +1 @@
1
- {"version":3,"file":"BleTransport.d.ts","sourceRoot":"","sources":["../src/BleTransport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjF,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,EAAE,EAAE,MAAM,CAAC;IAEX,IAAI,SAA6B;IAEjC,MAAM,EAAE,MAAM,CAAC;IAEf,OAAO,SAAM;IAEb,mBAAmB,EAAE,cAAc,CAAC;IAEpC,oBAAoB,EAAE,cAAc,CAAC;IAErC,kBAAkB,CAAC,EAAE,YAAY,CAAC;IAElC,sBAAsB,CAAC,EAAE,YAAY,CAAC;IAEtC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,YAAY,CAAC,EAAE,MAAM,CAAC;gBAGpB,MAAM,EAAE,MAAM,EACd,mBAAmB,EAAE,cAAc,EACnC,oBAAoB,EAAE,cAAc;IAQhC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlD"}
1
+ {"version":3,"file":"BleTransport.d.ts","sourceRoot":"","sources":["../src/BleTransport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjF,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,EAAE,EAAE,MAAM,CAAC;IAEX,IAAI,SAA6B;IAEjC,MAAM,EAAE,MAAM,CAAC;IAEf,OAAO,SAAM;IAEb,mBAAmB,EAAE,cAAc,CAAC;IAEpC,oBAAoB,EAAE,cAAc,CAAC;IAErC,kBAAkB,CAAC,EAAE,YAAY,CAAC;IAElC,sBAAsB,CAAC,EAAE,YAAY,CAAC;IAEtC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,YAAY,CAAC,EAAE,MAAM,CAAC;gBAGpB,MAAM,EAAE,MAAM,EACd,mBAAmB,EAAE,cAAc,EACnC,oBAAoB,EAAE,cAAc;IAQhC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -210,6 +210,10 @@ class BleTransport {
210
210
  }
211
211
  writeWithRetry(data) {
212
212
  return __awaiter(this, void 0, void 0, function* () {
213
+ if (reactNative.Platform.OS === 'ios' && this.writeCharacteristic.isWritableWithResponse) {
214
+ yield this.writeCharacteristic.writeWithResponse(data);
215
+ return;
216
+ }
213
217
  yield this.writeCharacteristic.writeWithoutResponse(data);
214
218
  });
215
219
  }
@@ -222,9 +226,36 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = reactNative.Platform.OS === 'ios' ? 4 :
222
226
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = reactNative.Platform.OS === 'ios' ? 8 : 10;
223
227
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = reactNative.Platform.OS === 'ios' ? 24 : 30;
224
228
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
229
+ const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
225
230
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
226
231
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY = reactNative.Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
227
232
  const ANDROID_GATT_CONGESTED_STATUS = 143;
233
+ const isAsciiWhitespace = (code) => code === 0x09 ||
234
+ code === 0x0a ||
235
+ code === 0x0b ||
236
+ code === 0x0c ||
237
+ code === 0x0d ||
238
+ code === 0x20;
239
+ const hasGattCongestedStatus = (text) => {
240
+ let searchFrom = 0;
241
+ while (searchFrom < text.length) {
242
+ const statusIndex = text.indexOf('status', searchFrom);
243
+ if (statusIndex < 0)
244
+ return false;
245
+ let cursor = statusIndex + 'status'.length;
246
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor)))
247
+ cursor += 1;
248
+ if (text[cursor] === ':' || text[cursor] === '=') {
249
+ cursor += 1;
250
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor)))
251
+ cursor += 1;
252
+ }
253
+ if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor))
254
+ return true;
255
+ searchFrom = statusIndex + 'status'.length;
256
+ }
257
+ return false;
258
+ };
228
259
  const delay = (ms) => new Promise(resolve => {
229
260
  setTimeout(resolve, ms);
230
261
  });
@@ -239,7 +270,7 @@ const getFirmwareUploadWriteRetryType = (error) => {
239
270
  const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
240
271
  .filter(value => typeof value === 'string')
241
272
  .join(' ');
242
- return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
273
+ return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
243
274
  };
244
275
  const resolveFirmwareUploadRetryDelay = (attempt, baseDelayMs = 200, maxDelayMs = 1200) => Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
245
276
  const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
@@ -638,12 +669,17 @@ class ReactNativeBleTransport {
638
669
  return;
639
670
  }
640
671
  const displayName = getDeviceDisplayName(device);
641
- const isOneKey = hdShared.isOnekeyBluetoothDevice({
642
- id: device === null || device === void 0 ? void 0 : device.id,
643
- name: device === null || device === void 0 ? void 0 : device.name,
644
- localName: device === null || device === void 0 ? void 0 : device.localName,
645
- serviceUuids: device === null || device === void 0 ? void 0 : device.serviceUUIDs,
646
- });
672
+ const isUnnamedIOSPeripheral = reactNative.Platform.OS === 'ios' && !(displayName === null || displayName === void 0 ? void 0 : displayName.trim());
673
+ const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.name) ||
674
+ hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.localName);
675
+ const isOneKey = !isUnnamedIOSPeripheral &&
676
+ !isFindMyPeripheral &&
677
+ hdShared.isOnekeyBluetoothDevice({
678
+ id: device === null || device === void 0 ? void 0 : device.id,
679
+ name: device === null || device === void 0 ? void 0 : device.name,
680
+ localName: device === null || device === void 0 ? void 0 : device.localName,
681
+ serviceUuids: device === null || device === void 0 ? void 0 : device.serviceUUIDs,
682
+ });
647
683
  if (isOneKey) {
648
684
  addDevice(device);
649
685
  }
@@ -661,12 +697,15 @@ class ReactNativeBleTransport {
661
697
  const localName = 'localName' in device && typeof device.localName === 'string'
662
698
  ? device.localName
663
699
  : null;
664
- if (hdShared.isOnekeyBluetoothDevice({
665
- id: device.id,
666
- name: device.name,
667
- localName,
668
- serviceUuids: device.serviceUUIDs,
669
- })) {
700
+ const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device.name) ||
701
+ hdShared.isPro2FindMyAdvertisementName(localName);
702
+ if (!isFindMyPeripheral &&
703
+ hdShared.isOnekeyBluetoothDevice({
704
+ id: device.id,
705
+ name: device.name,
706
+ localName,
707
+ serviceUuids: device.serviceUUIDs,
708
+ })) {
670
709
  Log === null || Log === void 0 ? void 0 : Log.debug('search connected peripheral: ', device.id);
671
710
  addDevice(device);
672
711
  }
@@ -1160,7 +1199,7 @@ class ReactNativeBleTransport {
1160
1199
  let attempt = 0;
1161
1200
  while (true) {
1162
1201
  try {
1163
- yield this.writeBlePacket(uuid, data, payload => transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
1202
+ yield this.writeBlePacket(uuid, data, payload => transport.writeWithRetry(payload), isCurrentOwner);
1164
1203
  return;
1165
1204
  }
1166
1205
  catch (error) {
@@ -1187,7 +1226,10 @@ class ReactNativeBleTransport {
1187
1226
  for (const o of buffers) {
1188
1227
  const outData = o.toString('base64');
1189
1228
  try {
1190
- yield this.writeBlePacket(uuid, outData, payload => transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
1229
+ const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1230
+ yield this.writeBlePacket(uuid, outData, payload => shouldUseWriteWithResponse
1231
+ ? transport.writeCharacteristic.writeWithResponse(payload)
1232
+ : transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
1191
1233
  }
1192
1234
  catch (e) {
1193
1235
  Log === null || Log === void 0 ? void 0 : Log.debug('writeCharacteristic write error: ', e);
@@ -1520,6 +1562,15 @@ class ReactNativeBleTransport {
1520
1562
  detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
1521
1563
  var _a;
1522
1564
  return __awaiter(this, void 0, void 0, function* () {
1565
+ if (reactNative.Platform.OS === 'ios' && expectedProtocol) {
1566
+ this.deviceProtocol.set(uuid, expectedProtocol);
1567
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol selected', {
1568
+ deviceId: uuid,
1569
+ protocol: expectedProtocol,
1570
+ source: 'expected',
1571
+ });
1572
+ return expectedProtocol;
1573
+ }
1523
1574
  if (expectedProtocol === 'V1') {
1524
1575
  if (yield this.probeProtocolV1(uuid)) {
1525
1576
  this.deviceProtocol.set(uuid, 'V1');
@@ -1749,6 +1800,8 @@ class ReactNativeBleTransport {
1749
1800
  }
1750
1801
  writeProtocolV2Packet(uuid, transport, base64, context, assertCurrentGeneration) {
1751
1802
  return __awaiter(this, void 0, void 0, function* () {
1803
+ const shouldUseWriteWithResponse = transport.writeCharacteristic.isWritableWithResponse &&
1804
+ (context.writeWithResponse === true || (reactNative.Platform.OS === 'ios' && !context.highVolume));
1752
1805
  let attempt = 0;
1753
1806
  for (;;) {
1754
1807
  assertCurrentGeneration();
@@ -1756,7 +1809,9 @@ class ReactNativeBleTransport {
1756
1809
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1757
1810
  }
1758
1811
  try {
1759
- yield this.writeBlePacket(uuid, base64, payload => transport.writeCharacteristic.writeWithoutResponse(payload), () => {
1812
+ yield this.writeBlePacket(uuid, base64, payload => shouldUseWriteWithResponse
1813
+ ? transport.writeCharacteristic.writeWithResponse(payload)
1814
+ : transport.writeCharacteristic.writeWithoutResponse(payload), () => {
1760
1815
  try {
1761
1816
  assertCurrentGeneration();
1762
1817
  return !context.signal.aborted;
@@ -1794,12 +1849,16 @@ class ReactNativeBleTransport {
1794
1849
  androidPacketLength: tuning.androidPacketLength,
1795
1850
  mtu: reactNative.Platform.OS === 'android' ? transport$1.mtuSize : undefined,
1796
1851
  });
1852
+ const initialDelayMs = reactNative.Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
1853
+ ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
1854
+ : 0;
1797
1855
  yield transport.writeProtocolV2BleFrame({
1798
1856
  frame,
1799
1857
  packetCapacity,
1800
1858
  assertActive: assertCurrentGeneration,
1801
1859
  signal: context.signal,
1802
1860
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1861
+ initialDelayMs,
1803
1862
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1804
1863
  burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1805
1864
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
@@ -1819,7 +1878,7 @@ class ReactNativeBleTransport {
1819
1878
  const tuning = getProtocolV2BleTuning();
1820
1879
  Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
1821
1880
  name,
1822
- writeMode: 'withoutResponse',
1881
+ writeMode: (options === null || options === void 0 ? void 0 : options.writeWithResponse) ? 'withResponse' : 'withoutResponse',
1823
1882
  packetCapacity: reactNative.Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
1824
1883
  });
1825
1884
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.0-alpha.67",
3
+ "version": "1.2.0-alpha.68",
4
4
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,11 +20,11 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-core": "1.2.0-alpha.67",
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.67",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.67",
23
+ "@onekeyfe/hd-core": "1.2.0-alpha.68",
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.68",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.68",
26
26
  "@onekeyfe/react-native-ble-utils": "^0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "5fea66e8c65a1fab2ccc099b2811c8749b2487e0"
29
+ "gitHead": "6e8a931d08768980cfe7fa918bba447ed73e2724"
30
30
  }
@@ -1,3 +1,5 @@
1
+ import { Platform } from 'react-native';
2
+
1
3
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
2
4
 
3
5
  export default class BleTransport {
@@ -33,6 +35,10 @@ export default class BleTransport {
33
35
  }
34
36
 
35
37
  async writeWithRetry(data: string): Promise<void> {
38
+ if (Platform.OS === 'ios' && this.writeCharacteristic.isWritableWithResponse) {
39
+ await this.writeCharacteristic.writeWithResponse(data);
40
+ return;
41
+ }
36
42
  await this.writeCharacteristic.writeWithoutResponse(data);
37
43
  }
38
44
  }
@@ -1,7 +1,16 @@
1
1
  import { BleErrorCode } from 'react-native-ble-plx';
2
+ import { Platform } from 'react-native';
2
3
 
3
4
  import BleTransport from '../BleTransport';
4
5
 
6
+ jest.mock(
7
+ 'react-native',
8
+ () => ({
9
+ Platform: { OS: 'ios' },
10
+ }),
11
+ { virtual: true }
12
+ );
13
+
5
14
  jest.mock(
6
15
  'react-native-ble-plx',
7
16
  () => ({
@@ -19,12 +28,18 @@ jest.mock('@onekeyfe/hd-shared', () => ({
19
28
  }));
20
29
 
21
30
  describe('BleTransport side-effecting writes', () => {
31
+ beforeEach(() => {
32
+ Platform.OS = 'ios';
33
+ });
34
+
22
35
  test('does not reconnect or replay after the device disconnects', async () => {
23
36
  const error = Object.assign(new Error('device disconnected'), {
24
37
  errorCode: BleErrorCode.DeviceDisconnected,
25
38
  });
26
39
  const writeCharacteristic = {
27
- writeWithoutResponse: jest.fn(() => Promise.reject(error)),
40
+ isWritableWithResponse: true,
41
+ writeWithResponse: jest.fn(() => Promise.reject(error)),
42
+ writeWithoutResponse: jest.fn(() => Promise.resolve()),
28
43
  };
29
44
  const device = {
30
45
  id: 'classic-id',
@@ -35,7 +50,44 @@ describe('BleTransport side-effecting writes', () => {
35
50
 
36
51
  await expect(transport.writeWithRetry('payload')).rejects.toBe(error);
37
52
 
38
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
53
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
39
54
  expect(device.connect).not.toHaveBeenCalled();
40
55
  });
56
+
57
+ test('keeps Android Protocol V1 writes on writeWithoutResponse', async () => {
58
+ Platform.OS = 'android';
59
+ const writeCharacteristic = {
60
+ isWritableWithResponse: true,
61
+ writeWithResponse: jest.fn(() => Promise.resolve()),
62
+ writeWithoutResponse: jest.fn(() => Promise.resolve()),
63
+ };
64
+ const transport = new BleTransport(
65
+ { id: 'classic-id' } as any,
66
+ writeCharacteristic as any,
67
+ {} as any
68
+ );
69
+
70
+ await transport.writeWithRetry('payload');
71
+
72
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
73
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledWith('payload');
74
+ });
75
+
76
+ test('uses writeWithoutResponse on iOS when the characteristic lacks response support', async () => {
77
+ const writeCharacteristic = {
78
+ isWritableWithResponse: false,
79
+ writeWithResponse: jest.fn(() => Promise.resolve()),
80
+ writeWithoutResponse: jest.fn(() => Promise.resolve()),
81
+ };
82
+ const transport = new BleTransport(
83
+ { id: 'classic-id' } as any,
84
+ writeCharacteristic as any,
85
+ {} as any
86
+ );
87
+
88
+ await transport.writeWithRetry('payload');
89
+
90
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
91
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledWith('payload');
92
+ });
41
93
  });
@@ -0,0 +1,132 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ import { getConnectedDeviceIds } from '../BleManager';
4
+ import ReactNativeBleTransport from '../index';
5
+
6
+ jest.mock(
7
+ 'react-native',
8
+ () => ({
9
+ PermissionsAndroid: {},
10
+ Platform: { OS: 'ios' },
11
+ }),
12
+ { virtual: true }
13
+ );
14
+
15
+ jest.mock('react-native-ble-plx', () => ({
16
+ BleError: class BleError extends Error {},
17
+ BleErrorCode: {},
18
+ BleManager: jest.fn(),
19
+ ScanMode: { LowLatency: 2 },
20
+ }));
21
+
22
+ jest.mock('../BleManager', () => ({
23
+ getConnectedDeviceIds: jest.fn(),
24
+ onDeviceBondState: jest.fn(),
25
+ pairDevice: jest.fn(),
26
+ }));
27
+
28
+ jest.mock('../subscribeBleOn', () => ({
29
+ subscribeBleOn: jest.fn(() => Promise.resolve()),
30
+ }));
31
+
32
+ const ONEKEY_SERVICE_UUID = '00000001-0000-1000-8000-00805f9b34fb';
33
+
34
+ describe('ReactNativeBleTransport iOS discovery', () => {
35
+ test('filters a bonded Pro2 Find My peripheral while keeping the wallet peripheral', async () => {
36
+ jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([
37
+ {
38
+ id: 'find-my-peripheral',
39
+ name: 'Pro2 6E9E - Find My',
40
+ localName: null,
41
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
42
+ },
43
+ {
44
+ id: 'wallet-peripheral',
45
+ name: 'Pro2 6E9E',
46
+ localName: 'Pro2 6E9E',
47
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
48
+ },
49
+ ] as never);
50
+ const blePlxManager = {
51
+ startDeviceScan: jest.fn(),
52
+ stopDeviceScan: jest.fn(),
53
+ };
54
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
55
+ transport.blePlxManager = blePlxManager as never;
56
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
57
+
58
+ const devices = await transport.enumerate();
59
+
60
+ expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
61
+ });
62
+
63
+ test('filters a scanned Pro2 Find My peripheral by name when localName is null', async () => {
64
+ jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
65
+ const blePlxManager = {
66
+ startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
67
+ queueMicrotask(() => {
68
+ listener(null, {
69
+ id: 'find-my-peripheral',
70
+ name: 'Pro2 6E9E - Find My',
71
+ localName: null,
72
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
73
+ });
74
+ listener(null, {
75
+ id: 'wallet-peripheral',
76
+ name: 'Pro2 6E9E',
77
+ localName: 'Pro2 6E9E',
78
+ serviceUUIDs: [ONEKEY_SERVICE_UUID],
79
+ });
80
+ });
81
+ }),
82
+ stopDeviceScan: jest.fn(),
83
+ };
84
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
85
+ transport.blePlxManager = blePlxManager as never;
86
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
87
+
88
+ const devices = await transport.enumerate();
89
+
90
+ expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
91
+ });
92
+
93
+ test('ignores an unnamed scanned advertisement while keeping the named wallet peripheral', async () => {
94
+ jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
95
+ const blePlxManager = {
96
+ startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
97
+ queueMicrotask(() => {
98
+ listener(null, {
99
+ id: 'unnamed-peripheral',
100
+ name: null,
101
+ localName: null,
102
+ serviceUUIDs: [
103
+ '0000180a-0000-1000-8000-00805f9b34fb',
104
+ '0000180f-0000-1000-8000-00805f9b34fb',
105
+ '0000fffd-0000-1000-8000-00805f9b34fb',
106
+ ONEKEY_SERVICE_UUID,
107
+ ],
108
+ });
109
+ listener(null, {
110
+ id: 'wallet-peripheral',
111
+ name: 'Pro2 769D',
112
+ localName: 'Pro2 769D',
113
+ serviceUUIDs: [
114
+ '0000180a-0000-1000-8000-00805f9b34fb',
115
+ '0000180f-0000-1000-8000-00805f9b34fb',
116
+ '0000fffd-0000-1000-8000-00805f9b34fb',
117
+ ONEKEY_SERVICE_UUID,
118
+ ],
119
+ });
120
+ });
121
+ }),
122
+ stopDeviceScan: jest.fn(),
123
+ };
124
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
125
+ transport.blePlxManager = blePlxManager as never;
126
+ transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
127
+
128
+ const devices = await transport.enumerate();
129
+
130
+ expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
131
+ });
132
+ });
@@ -3,7 +3,6 @@ import transportPackage, {
3
3
  PROTOCOL_V2_CHANNEL_BLE_UART,
4
4
  ProtocolV2,
5
5
  TRANSPORT_EVENT,
6
- bytesToHex,
7
6
  } from '@onekeyfe/hd-transport';
8
7
  import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
9
8
 
@@ -46,6 +45,11 @@ jest.mock('../subscribeBleOn', () => ({
46
45
  subscribeBleOn: jest.fn(() => Promise.resolve()),
47
46
  }));
48
47
 
48
+ const setPlatformOS = (os: 'ios' | 'android') => {
49
+ const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
50
+ reactNative.Platform.OS = os;
51
+ };
52
+
49
53
  const { parseConfigure } = transportPackage;
50
54
 
51
55
  const protocolV1Schema = {
@@ -69,11 +73,13 @@ const protocolV1Schema = {
69
73
 
70
74
  const protocolV2Schema = {
71
75
  nested: {
76
+ ProtocolInfoRequest: { fields: {} },
72
77
  Ping: {
73
78
  fields: {
74
79
  message: { type: 'string', id: 1 },
75
80
  },
76
81
  },
82
+ DeviceInfoGet: { fields: {} },
77
83
  FileWrite: { fields: {} },
78
84
  Success: {
79
85
  fields: {
@@ -82,8 +88,10 @@ const protocolV2Schema = {
82
88
  },
83
89
  MessageType: {
84
90
  values: {
91
+ MessageType_ProtocolInfoRequest: 60200,
85
92
  MessageType_Ping: 60206,
86
93
  MessageType_Success: 60207,
94
+ MessageType_DeviceInfoGet: 60600,
87
95
  MessageType_FileWrite: 60805,
88
96
  },
89
97
  },
@@ -95,7 +103,13 @@ const schemas = {
95
103
  protocolV2: parseConfigure(protocolV2Schema),
96
104
  };
97
105
 
98
- const createHarness = () => {
106
+ const createHarness = ({
107
+ deviceName = 'OneKey Pro 2',
108
+ isWritableWithResponse = true,
109
+ }: {
110
+ deviceName?: string;
111
+ isWritableWithResponse?: boolean;
112
+ } = {}) => {
99
113
  const uuid = 'rn-pro2-id';
100
114
  const sentSeqs: number[] = [];
101
115
  let responseSeq = 0;
@@ -134,15 +148,15 @@ const createHarness = () => {
134
148
  const writeCharacteristic = {
135
149
  uuid: '0002',
136
150
  deviceID: uuid,
137
- isWritableWithResponse: true,
151
+ isWritableWithResponse,
138
152
  isWritableWithoutResponse: true,
139
153
  writeWithResponse: jest.fn(handleWrite),
140
154
  writeWithoutResponse: jest.fn(handleWrite),
141
155
  };
142
156
  const device = {
143
157
  id: uuid,
144
- name: 'OneKey Pro 2',
145
- localName: 'OneKey Pro 2',
158
+ name: deviceName,
159
+ localName: deviceName,
146
160
  serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
147
161
  isConnected: jest.fn(() => Promise.resolve(true)),
148
162
  cancelConnection: jest.fn(() => Promise.resolve()),
@@ -184,7 +198,13 @@ const createHarness = () => {
184
198
  };
185
199
  };
186
200
 
187
- const createV1Harness = () => {
201
+ const createV1Harness = ({
202
+ respondOnWriteCount = 1,
203
+ isWritableWithResponse = true,
204
+ }: {
205
+ respondOnWriteCount?: number | number[];
206
+ isWritableWithResponse?: boolean;
207
+ } = {}) => {
188
208
  const uuid = 'rn-classic-id';
189
209
  const notifySubscriptionRemovers: jest.Mock[] = [];
190
210
  const disconnectSubscriptionRemovers: jest.Mock[] = [];
@@ -203,20 +223,25 @@ const createV1Harness = () => {
203
223
  }),
204
224
  };
205
225
  let writeCount = 0;
226
+ const responseWriteCounts = new Set(
227
+ Array.isArray(respondOnWriteCount) ? respondOnWriteCount : [respondOnWriteCount]
228
+ );
229
+ const handleWrite = () => {
230
+ writeCount += 1;
231
+ if (responseWriteCounts.has(writeCount)) {
232
+ notifyCallback?.(null, {
233
+ value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
234
+ });
235
+ }
236
+ return Promise.resolve();
237
+ };
206
238
  const writeCharacteristic = {
207
239
  uuid: '0002',
208
240
  deviceID: uuid,
209
- isWritableWithResponse: true,
241
+ isWritableWithResponse,
210
242
  isWritableWithoutResponse: true,
211
- writeWithoutResponse: jest.fn(() => {
212
- writeCount += 1;
213
- if (writeCount === 1) {
214
- notifyCallback?.(null, {
215
- value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
216
- });
217
- }
218
- return Promise.resolve();
219
- }),
243
+ writeWithResponse: jest.fn(handleWrite),
244
+ writeWithoutResponse: jest.fn(handleWrite),
220
245
  };
221
246
  const device = {
222
247
  id: uuid,
@@ -249,6 +274,7 @@ const createV1Harness = () => {
249
274
  uuid,
250
275
  device,
251
276
  bleManager,
277
+ writeCharacteristic,
252
278
  notifySubscriptionRemovers,
253
279
  disconnectSubscriptionRemovers,
254
280
  };
@@ -264,6 +290,19 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
264
290
  ).toBeNull();
265
291
  });
266
292
 
293
+ test.each(['status 143', 'status:143', 'status = 143', 'GATT_CONGESTED'])(
294
+ 'classifies %s as transient GATT congestion',
295
+ message => {
296
+ expect(getFirmwareUploadWriteRetryType({ message })).toBe('congested');
297
+ }
298
+ );
299
+
300
+ test('handles long uncontrolled status messages without a backtracking regular expression', () => {
301
+ const message = `status${' '.repeat(100_000)}142`;
302
+
303
+ expect(getFirmwareUploadWriteRetryType({ message })).toBeNull();
304
+ });
305
+
267
306
  test('keeps another device reader when releasing a device with an active V1 call', async () => {
268
307
  const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
269
308
  const activeV1Call = createDeferred<string>();
@@ -302,12 +341,101 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
302
341
  expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
303
342
  });
304
343
 
344
+ test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
345
+ const { transport, uuid, writeCharacteristic } = createV1Harness({
346
+ respondOnWriteCount: [1, 2],
347
+ });
348
+
349
+ await expect(transport.acquire({ uuid, expectedProtocol: 'V1' })).resolves.toEqual({
350
+ uuid,
351
+ protocolType: 'V1',
352
+ });
353
+ const releaseNative = jest.spyOn(transport as any, 'releaseNative');
354
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
355
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
356
+
357
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
358
+ await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
359
+
360
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
361
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
362
+ expect(releaseNative).not.toHaveBeenCalled();
363
+ await transport.release(uuid, true);
364
+ });
365
+
366
+ test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
367
+ const { transport, uuid, writeCharacteristic } = createV1Harness({
368
+ isWritableWithResponse: false,
369
+ });
370
+
371
+ await transport.acquire({ uuid, expectedProtocol: 'V1' });
372
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
373
+
374
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
375
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
376
+ await transport.release(uuid, true);
377
+ });
378
+
379
+ test('does not resend a failed iOS Protocol V1 control write without response', async () => {
380
+ const { transport, uuid, writeCharacteristic } = createV1Harness();
381
+ const writeError = new Error('write with response failed');
382
+
383
+ await transport.acquire({ uuid, expectedProtocol: 'V1' });
384
+ writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
385
+
386
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
387
+ errorCode: HardwareErrorCode.BleWriteCharacteristicError,
388
+ });
389
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
390
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
391
+ await transport.release(uuid, true);
392
+ });
393
+
394
+ test('actively probes Protocol V2 on iOS when only a name-derived hint is available', async () => {
395
+ const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
396
+ deviceName: 'Pro2 6E9E',
397
+ });
398
+
399
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
400
+ uuid,
401
+ protocolType: 'V2',
402
+ });
403
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
404
+
405
+ await expect(
406
+ transport.call(uuid, 'Ping', { message: 'first-core-command' })
407
+ ).resolves.toBeDefined();
408
+ expect(sentSeqs).toEqual([1, 2]);
409
+ await transport.release(uuid, true);
410
+ });
411
+
412
+ test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
413
+ const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
414
+ const probeProtocolV1 = jest
415
+ .spyOn(transport as any, 'probeProtocolV1')
416
+ .mockResolvedValue(false);
417
+ const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
418
+
419
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
420
+ uuid,
421
+ protocolType: 'V2',
422
+ });
423
+
424
+ expect(probeProtocolV1).toHaveBeenCalledTimes(1);
425
+ expect(probeProtocolV2).toHaveBeenCalledTimes(1);
426
+ expect(probeProtocolV1.mock.invocationCallOrder[0]).toBeLessThan(
427
+ probeProtocolV2.mock.invocationCallOrder[0]
428
+ );
429
+ await transport.release(uuid, true);
430
+ });
431
+
305
432
  test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
433
+ setPlatformOS('android');
306
434
  const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
307
435
  createV1Harness();
308
436
  const probeProtocolV2 = jest
309
437
  .spyOn(transport as any, 'probeProtocolV2')
310
- .mockImplementationOnce(async () => {
438
+ .mockImplementation(async () => {
311
439
  await (transport as any).releaseNative(uuid, true);
312
440
  return false;
313
441
  });
@@ -333,8 +461,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
333
461
  });
334
462
 
335
463
  test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
464
+ setPlatformOS('android');
336
465
  const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
337
- jest.spyOn(transport as any, 'probeProtocolV2').mockImplementationOnce(async () => {
466
+ jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
338
467
  await (transport as any).releaseNative(uuid, true);
339
468
  return false;
340
469
  });
@@ -353,7 +482,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
353
482
  });
354
483
 
355
484
  test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
356
- const { transport, uuid, device } = createV1Harness();
485
+ const { transport, uuid, device } = createV1Harness({
486
+ respondOnWriteCount: Number.POSITIVE_INFINITY,
487
+ });
357
488
 
358
489
  await transport.acquire({ uuid, expectedProtocol: 'V1' });
359
490
  await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
@@ -365,29 +496,31 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
365
496
  });
366
497
 
367
498
  afterEach(() => {
499
+ setPlatformOS('ios');
368
500
  resetProtocolV2BleTuning();
369
501
  });
370
502
 
371
- test('keeps the Protocol V2 sequence across probe and the next call', async () => {
503
+ test('uses the first Protocol V2 sequence for the first Core call when protocol is known', async () => {
372
504
  const { transport, uuid, sentSeqs } = createHarness();
373
505
 
374
- await transport.acquire({ uuid });
375
- await transport.call(uuid, 'Ping', { message: 'after-probe' });
506
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
507
+ await transport.call(uuid, 'Ping', { message: 'first-core-command' });
376
508
 
377
- expect(sentSeqs).toEqual([1, 2]);
378
- expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
509
+ expect(sentSeqs).toEqual([1]);
379
510
  await transport.release(uuid, true);
380
511
  });
381
512
 
382
513
  test('rejects the active Protocol V2 reader when the current monitor errors', async () => {
383
514
  const harness = createHarness();
384
515
  const { transport, uuid, sentSeqs } = harness;
385
- await transport.acquire({ uuid });
516
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
386
517
  harness.setShouldRespond(false);
387
518
 
388
519
  const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
389
- while (sentSeqs.length < 2) {
390
- await Promise.resolve();
520
+ while (sentSeqs.length < 1) {
521
+ await new Promise(resolve => {
522
+ setTimeout(resolve, 0);
523
+ });
391
524
  }
392
525
  await new Promise(resolve => {
393
526
  setTimeout(resolve, 0);
@@ -402,40 +535,149 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
402
535
  test('retains the sequence cursor when a new monitor generation is acquired', async () => {
403
536
  const { transport, uuid, sentSeqs } = createHarness();
404
537
 
405
- await transport.acquire({ uuid });
538
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
539
+ await transport.call(uuid, 'Ping', { message: 'first-generation' });
406
540
  await transport.release(uuid, true);
407
- await transport.acquire({ uuid });
541
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
542
+ await transport.call(uuid, 'Ping', { message: 'second-generation' });
408
543
 
409
544
  expect(sentSeqs).toEqual([1, 2]);
410
545
  await transport.release(uuid, true);
411
546
  });
412
547
 
413
- test('uses withoutResponse for normal and high-volume calls', async () => {
548
+ test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
414
549
  const { transport, uuid, writeCharacteristic } = createHarness();
415
550
 
416
- await transport.acquire({ uuid });
417
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
551
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
552
+ const releaseNative = jest.spyOn(transport as any, 'releaseNative');
553
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
418
554
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
419
555
 
420
- await transport.call(uuid, 'Ping', { message: 'normal' });
421
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
422
- expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
556
+ await transport.call(uuid, 'DeviceInfoGet', {});
557
+ await transport.call(uuid, 'ProtocolInfoRequest', {});
558
+
559
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
560
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
561
+ expect(releaseNative).not.toHaveBeenCalled();
562
+
563
+ await transport.release(uuid, true);
564
+ });
565
+
566
+ test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
567
+ const { transport, uuid, writeCharacteristic } = createHarness();
568
+
569
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
423
570
 
424
571
  await transport.call(uuid, 'FileWrite', {});
425
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
572
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
426
573
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
427
574
  await transport.release(uuid, true);
428
575
  });
429
576
 
577
+ test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
578
+ const { transport, uuid, writeCharacteristic } = createHarness();
579
+
580
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
581
+
582
+ await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
583
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
584
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
585
+ await transport.release(uuid, true);
586
+ });
587
+
588
+ test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
589
+ const { transport, uuid, writeCharacteristic } = createHarness({
590
+ isWritableWithResponse: false,
591
+ });
592
+
593
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
594
+ await transport.call(uuid, 'ProtocolInfoRequest', {});
595
+
596
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
597
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
598
+ await transport.release(uuid, true);
599
+ });
600
+
601
+ test('does not resend a failed iOS Protocol V2 control write without response', async () => {
602
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
603
+ const writeError = new Error('write with response failed');
604
+ const writeWithResponse = jest.fn().mockRejectedValue(writeError);
605
+ const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
606
+ const context = {
607
+ messageName: 'ProtocolInfoRequest',
608
+ timeoutMs: 1000,
609
+ highVolume: false,
610
+ generation: 1,
611
+ signal: new AbortController().signal,
612
+ };
613
+
614
+ await expect(
615
+ transport.writeProtocolV2Packet(
616
+ 'test-device',
617
+ {
618
+ writeCharacteristic: {
619
+ isWritableWithResponse: true,
620
+ writeWithResponse,
621
+ writeWithoutResponse,
622
+ },
623
+ },
624
+ Buffer.from('control').toString('base64'),
625
+ context,
626
+ jest.fn()
627
+ )
628
+ ).rejects.toBe(writeError);
629
+ expect(writeWithResponse).toHaveBeenCalledTimes(1);
630
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
631
+ });
632
+
633
+ test('paces a one-packet Protocol V2 control write on iOS', async () => {
634
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
635
+ const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
636
+ const bleTransport = {
637
+ mtuSize: 23,
638
+ writeCharacteristic: { writeWithoutResponse },
639
+ };
640
+ const context = {
641
+ messageName: 'ProtocolInfoRequest',
642
+ timeoutMs: 1000,
643
+ highVolume: false,
644
+ generation: 1,
645
+ signal: new AbortController().signal,
646
+ };
647
+ configureProtocolV2BleTuning({ iosPacketLength: 20 });
648
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
649
+
650
+ try {
651
+ const call = transport.writeProtocolV2Frame(
652
+ 'test-device',
653
+ bleTransport,
654
+ new Uint8Array(10),
655
+ context,
656
+ jest.fn()
657
+ );
658
+
659
+ await Promise.resolve();
660
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
661
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
662
+
663
+ await call;
664
+ expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
665
+ } finally {
666
+ setTimeoutSpy.mockRestore();
667
+ }
668
+ });
669
+
430
670
  test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
431
671
  const harness = createHarness();
432
672
  const { transport, uuid, sentSeqs } = harness;
433
- await transport.acquire({ uuid });
673
+ await transport.acquire({ uuid, expectedProtocol: 'V2' });
434
674
  harness.setShouldRespond(false);
435
675
 
436
676
  const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
437
- while (sentSeqs.length < 2) {
438
- await Promise.resolve();
677
+ while (sentSeqs.length < 1) {
678
+ await new Promise(resolve => {
679
+ setTimeout(resolve, 0);
680
+ });
439
681
  }
440
682
 
441
683
  const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
@@ -448,7 +690,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
448
690
  const disconnectListener = jest.fn();
449
691
  harness.emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnectListener);
450
692
 
451
- await harness.transport.acquire({ uuid: harness.uuid });
693
+ await harness.transport.acquire({ uuid: harness.uuid, expectedProtocol: 'V2' });
452
694
  harness.emitDisconnect();
453
695
  await harness.transport.disconnect(harness.uuid);
454
696
 
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  HardwareErrorCode,
29
29
  createDeferred,
30
30
  isOnekeyBluetoothDevice,
31
+ isPro2FindMyAdvertisementName,
31
32
  } from '@onekeyfe/hd-shared';
32
33
 
33
34
  import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
@@ -60,6 +61,7 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
60
61
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
61
62
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
62
63
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
64
+ const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
63
65
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
64
66
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
65
67
  Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
@@ -71,6 +73,33 @@ type ResolvedBleCharacteristics = {
71
73
  notifyCharacteristic: Characteristic;
72
74
  };
73
75
 
76
+ const isAsciiWhitespace = (code: number) =>
77
+ code === 0x09 ||
78
+ code === 0x0a ||
79
+ code === 0x0b ||
80
+ code === 0x0c ||
81
+ code === 0x0d ||
82
+ code === 0x20;
83
+
84
+ const hasGattCongestedStatus = (text: string) => {
85
+ let searchFrom = 0;
86
+ while (searchFrom < text.length) {
87
+ const statusIndex = text.indexOf('status', searchFrom);
88
+ if (statusIndex < 0) return false;
89
+
90
+ let cursor = statusIndex + 'status'.length;
91
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
92
+ if (text[cursor] === ':' || text[cursor] === '=') {
93
+ cursor += 1;
94
+ while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
95
+ }
96
+ if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
97
+
98
+ searchFrom = statusIndex + 'status'.length;
99
+ }
100
+ return false;
101
+ };
102
+
74
103
  const delay = (ms: number) =>
75
104
  new Promise<void>(resolve => {
76
105
  setTimeout(resolve, ms);
@@ -99,7 +128,7 @@ export const getFirmwareUploadWriteRetryType = (
99
128
  const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
100
129
  .filter(value => typeof value === 'string')
101
130
  .join(' ');
102
- return /GATT_CONGESTED|status\s*[:=]?\s*143/.test(text) ? 'congested' : null;
131
+ return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
103
132
  };
104
133
 
105
134
  const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
@@ -655,12 +684,21 @@ export default class ReactNativeBleTransport {
655
684
  }
656
685
 
657
686
  const displayName = getDeviceDisplayName(device);
658
- const isOneKey = isOnekeyBluetoothDevice({
659
- id: device?.id,
660
- name: device?.name,
661
- localName: device?.localName,
662
- serviceUuids: device?.serviceUUIDs,
663
- });
687
+ // iOS may report a service-only advertisement before the named scan response.
688
+ // Do not cache that incomplete advertisement as an unknown device.
689
+ const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
690
+ const isFindMyPeripheral =
691
+ isPro2FindMyAdvertisementName(device?.name) ||
692
+ isPro2FindMyAdvertisementName(device?.localName);
693
+ const isOneKey =
694
+ !isUnnamedIOSPeripheral &&
695
+ !isFindMyPeripheral &&
696
+ isOnekeyBluetoothDevice({
697
+ id: device?.id,
698
+ name: device?.name,
699
+ localName: device?.localName,
700
+ serviceUuids: device?.serviceUUIDs,
701
+ });
664
702
  if (isOneKey) {
665
703
  addDevice(device as unknown as Device);
666
704
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
@@ -681,7 +719,12 @@ export default class ReactNativeBleTransport {
681
719
  'localName' in device && typeof device.localName === 'string'
682
720
  ? device.localName
683
721
  : null;
722
+ const isFindMyPeripheral =
723
+ isPro2FindMyAdvertisementName(device.name) ||
724
+ isPro2FindMyAdvertisementName(localName);
725
+
684
726
  if (
727
+ !isFindMyPeripheral &&
685
728
  isOnekeyBluetoothDevice({
686
729
  id: device.id,
687
730
  name: device.name,
@@ -1331,7 +1374,7 @@ export default class ReactNativeBleTransport {
1331
1374
  await this.writeBlePacket(
1332
1375
  uuid,
1333
1376
  data,
1334
- payload => transport.writeCharacteristic.writeWithoutResponse(payload),
1377
+ payload => transport.writeWithRetry(payload),
1335
1378
  isCurrentOwner
1336
1379
  );
1337
1380
  return;
@@ -1361,10 +1404,15 @@ export default class ReactNativeBleTransport {
1361
1404
  const outData = o.toString('base64');
1362
1405
  // Upload resources on low-end phones may OOM
1363
1406
  try {
1407
+ const shouldUseWriteWithResponse =
1408
+ Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1364
1409
  await this.writeBlePacket(
1365
1410
  uuid,
1366
1411
  outData,
1367
- payload => transport.writeCharacteristic.writeWithoutResponse(payload),
1412
+ payload =>
1413
+ shouldUseWriteWithResponse
1414
+ ? transport.writeCharacteristic.writeWithResponse(payload)
1415
+ : transport.writeCharacteristic.writeWithoutResponse(payload),
1368
1416
  isCurrentOwner
1369
1417
  );
1370
1418
  } catch (e) {
@@ -1780,6 +1828,16 @@ export default class ReactNativeBleTransport {
1780
1828
  protocolHint?: ProtocolType,
1781
1829
  rebuildTransport?: () => Promise<void>
1782
1830
  ): Promise<ProtocolType> {
1831
+ if (Platform.OS === 'ios' && expectedProtocol) {
1832
+ this.deviceProtocol.set(uuid, expectedProtocol);
1833
+ Log?.debug('[ReactNativeBleTransport] protocol selected', {
1834
+ deviceId: uuid,
1835
+ protocol: expectedProtocol,
1836
+ source: 'expected',
1837
+ });
1838
+ return expectedProtocol;
1839
+ }
1840
+
1783
1841
  if (expectedProtocol === 'V1') {
1784
1842
  if (await this.probeProtocolV1(uuid)) {
1785
1843
  this.deviceProtocol.set(uuid, 'V1');
@@ -2048,6 +2106,9 @@ export default class ReactNativeBleTransport {
2048
2106
  context: ProtocolV2CallContext,
2049
2107
  assertCurrentGeneration: () => void
2050
2108
  ) {
2109
+ const shouldUseWriteWithResponse =
2110
+ transport.writeCharacteristic.isWritableWithResponse &&
2111
+ (context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
2051
2112
  let attempt = 0;
2052
2113
  for (;;) {
2053
2114
  assertCurrentGeneration();
@@ -2058,7 +2119,10 @@ export default class ReactNativeBleTransport {
2058
2119
  await this.writeBlePacket(
2059
2120
  uuid,
2060
2121
  base64,
2061
- payload => transport.writeCharacteristic.writeWithoutResponse(payload),
2122
+ payload =>
2123
+ shouldUseWriteWithResponse
2124
+ ? transport.writeCharacteristic.writeWithResponse(payload)
2125
+ : transport.writeCharacteristic.writeWithoutResponse(payload),
2062
2126
  // Same rule as Protocol V1: a write from a superseded generation must not
2063
2127
  // tear down the link that the current generation is using.
2064
2128
  () => {
@@ -2105,12 +2169,19 @@ export default class ReactNativeBleTransport {
2105
2169
  androidPacketLength: tuning.androidPacketLength,
2106
2170
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
2107
2171
  });
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;
2108
2178
  await writeProtocolV2BleFrame({
2109
2179
  frame,
2110
2180
  packetCapacity,
2111
2181
  assertActive: assertCurrentGeneration,
2112
2182
  signal: context.signal,
2113
2183
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
2184
+ initialDelayMs,
2114
2185
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
2115
2186
  burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
2116
2187
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
@@ -2143,7 +2214,7 @@ export default class ReactNativeBleTransport {
2143
2214
  const tuning = getProtocolV2BleTuning();
2144
2215
  Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
2145
2216
  name,
2146
- writeMode: 'withoutResponse',
2217
+ writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
2147
2218
  packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
2148
2219
  });
2149
2220
  }