@onekeyfe/hd-transport-react-native 1.2.0-alpha.51 → 1.2.0-alpha.53

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":"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
@@ -222,6 +222,7 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = reactNative.Platform.OS === 'ios' ? 4 :
222
222
  const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = reactNative.Platform.OS === 'ios' ? 8 : 10;
223
223
  const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = reactNative.Platform.OS === 'ios' ? 24 : 30;
224
224
  const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
225
+ const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
225
226
  const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
226
227
  const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY = reactNative.Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
227
228
  const ANDROID_GATT_CONGESTED_STATUS = 143;
@@ -614,12 +615,17 @@ class ReactNativeBleTransport {
614
615
  return;
615
616
  }
616
617
  const displayName = getDeviceDisplayName(device);
617
- const isOneKey = hdShared.isOnekeyBluetoothDevice({
618
- id: device === null || device === void 0 ? void 0 : device.id,
619
- name: device === null || device === void 0 ? void 0 : device.name,
620
- localName: device === null || device === void 0 ? void 0 : device.localName,
621
- serviceUuids: device === null || device === void 0 ? void 0 : device.serviceUUIDs,
622
- });
618
+ const isUnnamedIOSPeripheral = reactNative.Platform.OS === 'ios' && !(displayName === null || displayName === void 0 ? void 0 : displayName.trim());
619
+ const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.name) ||
620
+ hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.localName);
621
+ const isOneKey = !isUnnamedIOSPeripheral &&
622
+ !isFindMyPeripheral &&
623
+ hdShared.isOnekeyBluetoothDevice({
624
+ id: device === null || device === void 0 ? void 0 : device.id,
625
+ name: device === null || device === void 0 ? void 0 : device.name,
626
+ localName: device === null || device === void 0 ? void 0 : device.localName,
627
+ serviceUuids: device === null || device === void 0 ? void 0 : device.serviceUUIDs,
628
+ });
623
629
  if (isOneKey) {
624
630
  addDevice(device);
625
631
  }
@@ -637,12 +643,15 @@ class ReactNativeBleTransport {
637
643
  const localName = 'localName' in device && typeof device.localName === 'string'
638
644
  ? device.localName
639
645
  : null;
640
- if (hdShared.isOnekeyBluetoothDevice({
641
- id: device.id,
642
- name: device.name,
643
- localName,
644
- serviceUuids: device.serviceUUIDs,
645
- })) {
646
+ const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device.name) ||
647
+ hdShared.isPro2FindMyAdvertisementName(localName);
648
+ if (!isFindMyPeripheral &&
649
+ hdShared.isOnekeyBluetoothDevice({
650
+ id: device.id,
651
+ name: device.name,
652
+ localName,
653
+ serviceUuids: device.serviceUUIDs,
654
+ })) {
646
655
  Log === null || Log === void 0 ? void 0 : Log.debug('search connected peripheral: ', device.id);
647
656
  addDevice(device);
648
657
  }
@@ -1035,18 +1044,8 @@ class ReactNativeBleTransport {
1035
1044
  const transport = this.getCachedTransport(uuid);
1036
1045
  const runPromise = hdShared.createDeferred();
1037
1046
  runPromise.promise.catch(() => undefined);
1038
- const supersededRunPromise = this.runPromise;
1039
- if (supersededRunPromise) {
1040
- supersededRunPromise.reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleForceCleanRunPromise));
1041
- }
1042
1047
  this.runPromise = runPromise;
1043
1048
  this.runPromiseDeviceId = uuid;
1044
- const releaseOwnershipIfCurrent = () => {
1045
- if (this.runPromise === runPromise) {
1046
- this.runPromise = null;
1047
- this.runPromiseDeviceId = null;
1048
- }
1049
- };
1050
1049
  const messages = this._messages;
1051
1050
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1052
1051
  let timeout;
@@ -1105,7 +1104,7 @@ class ReactNativeBleTransport {
1105
1104
  }
1106
1105
  if (name === 'EmmcFileWrite') {
1107
1106
  yield writeChunkedData(buffers, data => transport.writeWithRetry(data), e => {
1108
- releaseOwnershipIfCurrent();
1107
+ this.runPromise = null;
1109
1108
  Log === null || Log === void 0 ? void 0 : Log.error('writeCharacteristic write error: ', e);
1110
1109
  });
1111
1110
  }
@@ -1140,7 +1139,7 @@ class ReactNativeBleTransport {
1140
1139
  }
1141
1140
  }
1142
1141
  }), e => {
1143
- releaseOwnershipIfCurrent();
1142
+ this.runPromise = null;
1144
1143
  Log === null || Log === void 0 ? void 0 : Log.error('writeCharacteristic write error: ', e);
1145
1144
  });
1146
1145
  }
@@ -1148,11 +1147,17 @@ class ReactNativeBleTransport {
1148
1147
  for (const o of buffers) {
1149
1148
  const outData = o.toString('base64');
1150
1149
  try {
1151
- yield transport.writeCharacteristic.writeWithoutResponse(outData);
1150
+ const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1151
+ if (shouldUseWriteWithResponse) {
1152
+ yield transport.writeCharacteristic.writeWithResponse(outData);
1153
+ }
1154
+ else {
1155
+ yield transport.writeCharacteristic.writeWithoutResponse(outData);
1156
+ }
1152
1157
  }
1153
1158
  catch (e) {
1154
1159
  Log === null || Log === void 0 ? void 0 : Log.debug('writeCharacteristic write error: ', e);
1155
- releaseOwnershipIfCurrent();
1160
+ this.runPromise = null;
1156
1161
  if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceDisconnected) {
1157
1162
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded);
1158
1163
  }
@@ -1192,9 +1197,7 @@ class ReactNativeBleTransport {
1192
1197
  Log === null || Log === void 0 ? void 0 : Log.error('call error: ', e);
1193
1198
  }
1194
1199
  const isProbeTimeout = name === 'GetFeatures' && (options === null || options === void 0 ? void 0 : options.timeoutMs) === PROTOCOL_PROBE_TIMEOUT_MS;
1195
- const isStaleCall = this.runPromise !== runPromise;
1196
1200
  if (!isProbeTimeout &&
1197
- !isStaleCall &&
1198
1201
  (e === null || e === void 0 ? void 0 : e.errorCode) === hdShared.HardwareErrorCode.BleTimeoutError) {
1199
1202
  yield this.disconnect(uuid);
1200
1203
  }
@@ -1305,7 +1308,25 @@ class ReactNativeBleTransport {
1305
1308
  }
1306
1309
  }
1307
1310
  detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
1311
+ var _a;
1308
1312
  return __awaiter(this, void 0, void 0, function* () {
1313
+ if (reactNative.Platform.OS === 'ios') {
1314
+ const protocol = (_a = expectedProtocol !== null && expectedProtocol !== void 0 ? expectedProtocol : protocolHint) !== null && _a !== void 0 ? _a : 'V1';
1315
+ let source = 'ios-legacy-default';
1316
+ if (expectedProtocol) {
1317
+ source = 'expected';
1318
+ }
1319
+ else if (protocolHint) {
1320
+ source = 'hint';
1321
+ }
1322
+ this.deviceProtocol.set(uuid, protocol);
1323
+ Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol selected', {
1324
+ deviceId: uuid,
1325
+ protocol,
1326
+ source,
1327
+ });
1328
+ return protocol;
1329
+ }
1309
1330
  if (expectedProtocol === 'V1') {
1310
1331
  if (yield this.probeProtocolV1(uuid)) {
1311
1332
  this.deviceProtocol.set(uuid, 'V1');
@@ -1511,6 +1532,9 @@ class ReactNativeBleTransport {
1511
1532
  }
1512
1533
  writeProtocolV2Packet(transport, base64, context, assertCurrentGeneration) {
1513
1534
  return __awaiter(this, void 0, void 0, function* () {
1535
+ const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' &&
1536
+ !context.highVolume &&
1537
+ transport.writeCharacteristic.isWritableWithResponse;
1514
1538
  let attempt = 0;
1515
1539
  for (;;) {
1516
1540
  assertCurrentGeneration();
@@ -1518,7 +1542,12 @@ class ReactNativeBleTransport {
1518
1542
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1519
1543
  }
1520
1544
  try {
1521
- yield transport.writeCharacteristic.writeWithoutResponse(base64);
1545
+ if (shouldUseWriteWithResponse) {
1546
+ yield transport.writeCharacteristic.writeWithResponse(base64);
1547
+ }
1548
+ else {
1549
+ yield transport.writeCharacteristic.writeWithoutResponse(base64);
1550
+ }
1522
1551
  assertCurrentGeneration();
1523
1552
  return;
1524
1553
  }
@@ -1548,12 +1577,16 @@ class ReactNativeBleTransport {
1548
1577
  androidPacketLength: tuning.androidPacketLength,
1549
1578
  mtu: reactNative.Platform.OS === 'android' ? transport$1.mtuSize : undefined,
1550
1579
  });
1580
+ const initialDelayMs = reactNative.Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
1581
+ ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
1582
+ : 0;
1551
1583
  yield transport.writeProtocolV2BleFrame({
1552
1584
  frame,
1553
1585
  packetCapacity,
1554
1586
  assertActive: assertCurrentGeneration,
1555
1587
  signal: context.signal,
1556
1588
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1589
+ initialDelayMs,
1557
1590
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1558
1591
  burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1559
1592
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
package/jest.config.js CHANGED
@@ -2,9 +2,4 @@ module.exports = {
2
2
  preset: '../../jest.config.js',
3
3
  testEnvironment: 'node',
4
4
  modulePathIgnorePatterns: ['node_modules', '<rootDir>/dist'],
5
- moduleNameMapper: {
6
- // The workspace symlinks resolve to prebuilt dist outputs that can lag behind src.
7
- '^@onekeyfe/hd-transport$': '<rootDir>/../hd-transport/src/index.ts',
8
- '^@onekeyfe/hd-shared$': '<rootDir>/../shared/src/index.ts',
9
- },
10
5
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.0-alpha.51",
3
+ "version": "1.2.0-alpha.53",
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.51",
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.51",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.51",
23
+ "@onekeyfe/hd-core": "1.2.0-alpha.53",
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.53",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.53",
26
26
  "@onekeyfe/react-native-ble-utils": "^0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "c947101d26542c9fd27264b9ddff7f7bba0ad24d"
29
+ "gitHead": "196b7301f81d730460045c65c90d22706434b04d"
30
30
  }
@@ -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
  };
@@ -302,12 +328,81 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
302
328
  expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
303
329
  });
304
330
 
331
+ test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
332
+ const { transport, uuid, writeCharacteristic } = createV1Harness({
333
+ respondOnWriteCount: [1, 2],
334
+ });
335
+
336
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
337
+ uuid,
338
+ protocolType: 'V1',
339
+ });
340
+ const releaseNative = jest.spyOn(transport as any, 'releaseNative');
341
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
342
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
343
+
344
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
345
+ await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
346
+
347
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
348
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
349
+ expect(releaseNative).not.toHaveBeenCalled();
350
+ await transport.release(uuid, true);
351
+ });
352
+
353
+ test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
354
+ const { transport, uuid, writeCharacteristic } = createV1Harness({
355
+ isWritableWithResponse: false,
356
+ });
357
+
358
+ await transport.acquire({ uuid });
359
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
360
+
361
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
362
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
363
+ await transport.release(uuid, true);
364
+ });
365
+
366
+ test('does not resend a failed iOS Protocol V1 control write without response', async () => {
367
+ const { transport, uuid, writeCharacteristic } = createV1Harness();
368
+ const writeError = new Error('write with response failed');
369
+
370
+ await transport.acquire({ uuid });
371
+ writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
372
+
373
+ await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
374
+ errorCode: HardwareErrorCode.BleWriteCharacteristicError,
375
+ });
376
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
377
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
378
+ await transport.release(uuid, true);
379
+ });
380
+
381
+ test('keeps the first Core command as the first iOS BLE request for a Protocol V2 device', async () => {
382
+ const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
383
+ deviceName: 'Pro2 6E9E',
384
+ });
385
+
386
+ await expect(transport.acquire({ uuid })).resolves.toEqual({
387
+ uuid,
388
+ protocolType: 'V2',
389
+ });
390
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
391
+
392
+ await expect(
393
+ transport.call(uuid, 'Ping', { message: 'first-core-command' })
394
+ ).resolves.toBeDefined();
395
+ expect(sentSeqs).toEqual([1]);
396
+ await transport.release(uuid, true);
397
+ });
398
+
305
399
  test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
400
+ setPlatformOS('android');
306
401
  const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
307
402
  createV1Harness();
308
403
  const probeProtocolV2 = jest
309
404
  .spyOn(transport as any, 'probeProtocolV2')
310
- .mockImplementationOnce(async () => {
405
+ .mockImplementation(async () => {
311
406
  await (transport as any).releaseNative(uuid, true);
312
407
  return false;
313
408
  });
@@ -333,8 +428,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
333
428
  });
334
429
 
335
430
  test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
431
+ setPlatformOS('android');
336
432
  const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
337
- jest.spyOn(transport as any, 'probeProtocolV2').mockImplementationOnce(async () => {
433
+ jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
338
434
  await (transport as any).releaseNative(uuid, true);
339
435
  return false;
340
436
  });
@@ -353,7 +449,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
353
449
  });
354
450
 
355
451
  test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
356
- const { transport, uuid, device } = createV1Harness();
452
+ const { transport, uuid, device } = createV1Harness({
453
+ respondOnWriteCount: Number.POSITIVE_INFINITY,
454
+ });
357
455
 
358
456
  await transport.acquire({ uuid, expectedProtocol: 'V1' });
359
457
  await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
@@ -365,17 +463,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
365
463
  });
366
464
 
367
465
  afterEach(() => {
466
+ setPlatformOS('ios');
368
467
  resetProtocolV2BleTuning();
369
468
  });
370
469
 
371
- test('keeps the Protocol V2 sequence across probe and the next call', async () => {
470
+ test('starts the Protocol V2 sequence with the first Core call on iOS', async () => {
372
471
  const { transport, uuid, sentSeqs } = createHarness();
373
472
 
374
473
  await transport.acquire({ uuid });
375
- await transport.call(uuid, 'Ping', { message: 'after-probe' });
474
+ await transport.call(uuid, 'Ping', { message: 'first-core-command' });
376
475
 
377
- expect(sentSeqs).toEqual([1, 2]);
378
- expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
476
+ expect(sentSeqs).toEqual([1]);
379
477
  await transport.release(uuid, true);
380
478
  });
381
479
 
@@ -386,8 +484,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
386
484
  harness.setShouldRespond(false);
387
485
 
388
486
  const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
389
- while (sentSeqs.length < 2) {
390
- await Promise.resolve();
487
+ while (sentSeqs.length < 1) {
488
+ await new Promise(resolve => {
489
+ setTimeout(resolve, 0);
490
+ });
391
491
  }
392
492
  await new Promise(resolve => {
393
493
  setTimeout(resolve, 0);
@@ -403,30 +503,124 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
403
503
  const { transport, uuid, sentSeqs } = createHarness();
404
504
 
405
505
  await transport.acquire({ uuid });
506
+ await transport.call(uuid, 'Ping', { message: 'first-generation' });
406
507
  await transport.release(uuid, true);
407
508
  await transport.acquire({ uuid });
509
+ await transport.call(uuid, 'Ping', { message: 'second-generation' });
408
510
 
409
511
  expect(sentSeqs).toEqual([1, 2]);
410
512
  await transport.release(uuid, true);
411
513
  });
412
514
 
413
- test('uses withoutResponse for normal and high-volume calls', async () => {
515
+ test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
414
516
  const { transport, uuid, writeCharacteristic } = createHarness();
415
517
 
416
518
  await transport.acquire({ uuid });
417
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
519
+ const releaseNative = jest.spyOn(transport as any, 'releaseNative');
520
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
418
521
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
419
522
 
420
- await transport.call(uuid, 'Ping', { message: 'normal' });
421
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
422
- expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
523
+ await transport.call(uuid, 'DeviceInfoGet', {});
524
+ await transport.call(uuid, 'ProtocolInfoRequest', {});
525
+
526
+ expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
527
+ expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
528
+ expect(releaseNative).not.toHaveBeenCalled();
529
+
530
+ await transport.release(uuid, true);
531
+ });
532
+
533
+ test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
534
+ const { transport, uuid, writeCharacteristic } = createHarness();
535
+
536
+ await transport.acquire({ uuid });
423
537
 
424
538
  await transport.call(uuid, 'FileWrite', {});
425
- expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
539
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
540
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
541
+ await transport.release(uuid, true);
542
+ });
543
+
544
+ test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
545
+ const { transport, uuid, writeCharacteristic } = createHarness({
546
+ isWritableWithResponse: false,
547
+ });
548
+
549
+ await transport.acquire({ uuid });
550
+ await transport.call(uuid, 'ProtocolInfoRequest', {});
551
+
426
552
  expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
553
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
427
554
  await transport.release(uuid, true);
428
555
  });
429
556
 
557
+ test('does not resend a failed iOS Protocol V2 control write without response', async () => {
558
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
559
+ const writeError = new Error('write with response failed');
560
+ const writeWithResponse = jest.fn().mockRejectedValue(writeError);
561
+ const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
562
+ const context = {
563
+ messageName: 'ProtocolInfoRequest',
564
+ timeoutMs: 1000,
565
+ highVolume: false,
566
+ generation: 1,
567
+ signal: new AbortController().signal,
568
+ };
569
+
570
+ await expect(
571
+ transport.writeProtocolV2Packet(
572
+ {
573
+ writeCharacteristic: {
574
+ isWritableWithResponse: true,
575
+ writeWithResponse,
576
+ writeWithoutResponse,
577
+ },
578
+ },
579
+ Buffer.from('control').toString('base64'),
580
+ context,
581
+ jest.fn()
582
+ )
583
+ ).rejects.toBe(writeError);
584
+ expect(writeWithResponse).toHaveBeenCalledTimes(1);
585
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
586
+ });
587
+
588
+ test('paces a one-packet Protocol V2 control write on iOS', async () => {
589
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
590
+ const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
591
+ const bleTransport = {
592
+ mtuSize: 23,
593
+ writeCharacteristic: { writeWithoutResponse },
594
+ };
595
+ const context = {
596
+ messageName: 'ProtocolInfoRequest',
597
+ timeoutMs: 1000,
598
+ highVolume: false,
599
+ generation: 1,
600
+ signal: new AbortController().signal,
601
+ };
602
+ configureProtocolV2BleTuning({ iosPacketLength: 20 });
603
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
604
+
605
+ try {
606
+ const call = transport.writeProtocolV2Frame(
607
+ bleTransport,
608
+ new Uint8Array(10),
609
+ context,
610
+ jest.fn()
611
+ );
612
+
613
+ await Promise.resolve();
614
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
615
+ expect(writeWithoutResponse).not.toHaveBeenCalled();
616
+
617
+ await call;
618
+ expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
619
+ } finally {
620
+ setTimeoutSpy.mockRestore();
621
+ }
622
+ });
623
+
430
624
  test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
431
625
  const harness = createHarness();
432
626
  const { transport, uuid, sentSeqs } = harness;
@@ -434,8 +628,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
434
628
  harness.setShouldRespond(false);
435
629
 
436
630
  const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
437
- while (sentSeqs.length < 2) {
438
- await Promise.resolve();
631
+ while (sentSeqs.length < 1) {
632
+ await new Promise(resolve => {
633
+ setTimeout(resolve, 0);
634
+ });
439
635
  }
440
636
 
441
637
  const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
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;
@@ -580,12 +582,21 @@ export default class ReactNativeBleTransport {
580
582
  }
581
583
 
582
584
  const displayName = getDeviceDisplayName(device);
583
- const isOneKey = isOnekeyBluetoothDevice({
584
- id: device?.id,
585
- name: device?.name,
586
- localName: device?.localName,
587
- serviceUuids: device?.serviceUUIDs,
588
- });
585
+ // iOS may report a service-only advertisement before the named scan response.
586
+ // Do not cache that incomplete advertisement as an unknown device.
587
+ const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
588
+ const isFindMyPeripheral =
589
+ isPro2FindMyAdvertisementName(device?.name) ||
590
+ isPro2FindMyAdvertisementName(device?.localName);
591
+ const isOneKey =
592
+ !isUnnamedIOSPeripheral &&
593
+ !isFindMyPeripheral &&
594
+ isOnekeyBluetoothDevice({
595
+ id: device?.id,
596
+ name: device?.name,
597
+ localName: device?.localName,
598
+ serviceUuids: device?.serviceUUIDs,
599
+ });
589
600
  if (isOneKey) {
590
601
  addDevice(device as unknown as Device);
591
602
  } else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
@@ -606,7 +617,12 @@ export default class ReactNativeBleTransport {
606
617
  'localName' in device && typeof device.localName === 'string'
607
618
  ? device.localName
608
619
  : null;
620
+ const isFindMyPeripheral =
621
+ isPro2FindMyAdvertisementName(device.name) ||
622
+ isPro2FindMyAdvertisementName(localName);
623
+
609
624
  if (
625
+ !isFindMyPeripheral &&
610
626
  isOnekeyBluetoothDevice({
611
627
  id: device.id,
612
628
  name: device.name,
@@ -1113,24 +1129,8 @@ export default class ReactNativeBleTransport {
1113
1129
  const transport = this.getCachedTransport(uuid);
1114
1130
  const runPromise = createDeferred<string>();
1115
1131
  runPromise.promise.catch(() => undefined);
1116
- const supersededRunPromise = this.runPromise;
1117
- if (supersededRunPromise) {
1118
- // Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
1119
- // the superseded deferred now so its response race resolves and its finally block
1120
- // clears its timeout timer; an orphaned timer would otherwise fire much later and
1121
- // tear down the shared connection while another call is using it.
1122
- supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
1123
- }
1124
1132
  this.runPromise = runPromise;
1125
1133
  this.runPromiseDeviceId = uuid;
1126
- // A superseded call's late write failure must not clear the successor's ownership;
1127
- // only the call that still owns the slot may release it.
1128
- const releaseOwnershipIfCurrent = () => {
1129
- if (this.runPromise === runPromise) {
1130
- this.runPromise = null;
1131
- this.runPromiseDeviceId = null;
1132
- }
1133
- };
1134
1134
  const messages = this._messages;
1135
1135
  const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
1136
1136
  let timeout: ReturnType<typeof setTimeout> | undefined;
@@ -1202,7 +1202,7 @@ export default class ReactNativeBleTransport {
1202
1202
  buffers,
1203
1203
  data => transport.writeWithRetry(data),
1204
1204
  e => {
1205
- releaseOwnershipIfCurrent();
1205
+ this.runPromise = null;
1206
1206
  Log?.error('writeCharacteristic write error: ', e);
1207
1207
  }
1208
1208
  );
@@ -1242,7 +1242,7 @@ export default class ReactNativeBleTransport {
1242
1242
  }
1243
1243
  },
1244
1244
  e => {
1245
- releaseOwnershipIfCurrent();
1245
+ this.runPromise = null;
1246
1246
  Log?.error('writeCharacteristic write error: ', e);
1247
1247
  }
1248
1248
  );
@@ -1251,10 +1251,16 @@ export default class ReactNativeBleTransport {
1251
1251
  const outData = o.toString('base64');
1252
1252
  // Upload resources on low-end phones may OOM
1253
1253
  try {
1254
- await transport.writeCharacteristic.writeWithoutResponse(outData);
1254
+ const shouldUseWriteWithResponse =
1255
+ Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
1256
+ if (shouldUseWriteWithResponse) {
1257
+ await transport.writeCharacteristic.writeWithResponse(outData);
1258
+ } else {
1259
+ await transport.writeCharacteristic.writeWithoutResponse(outData);
1260
+ }
1255
1261
  } catch (e) {
1256
1262
  Log?.debug('writeCharacteristic write error: ', e);
1257
- releaseOwnershipIfCurrent();
1263
+ this.runPromise = null;
1258
1264
  if (e.errorCode === BleErrorCode.DeviceDisconnected) {
1259
1265
  throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
1260
1266
  } else if (e.errorCode === BleErrorCode.OperationStartFailed) {
@@ -1297,13 +1303,8 @@ export default class ReactNativeBleTransport {
1297
1303
  }
1298
1304
  const isProbeTimeout =
1299
1305
  name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
1300
- // A call that has been superseded (forceRun) or cleaned up no longer owns the
1301
- // transport; its late timeout must not tear down the connection the current
1302
- // call is actively using.
1303
- const isStaleCall = this.runPromise !== runPromise;
1304
1306
  if (
1305
1307
  !isProbeTimeout &&
1306
- !isStaleCall &&
1307
1308
  (e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
1308
1309
  ) {
1309
1310
  await this.disconnect(uuid);
@@ -1443,6 +1444,23 @@ export default class ReactNativeBleTransport {
1443
1444
  protocolHint?: ProtocolType,
1444
1445
  rebuildTransport?: () => Promise<void>
1445
1446
  ): Promise<ProtocolType> {
1447
+ if (Platform.OS === 'ios') {
1448
+ const protocol = expectedProtocol ?? protocolHint ?? 'V1';
1449
+ let source = 'ios-legacy-default';
1450
+ if (expectedProtocol) {
1451
+ source = 'expected';
1452
+ } else if (protocolHint) {
1453
+ source = 'hint';
1454
+ }
1455
+ this.deviceProtocol.set(uuid, protocol);
1456
+ Log?.debug('[ReactNativeBleTransport] protocol selected', {
1457
+ deviceId: uuid,
1458
+ protocol,
1459
+ source,
1460
+ });
1461
+ return protocol;
1462
+ }
1463
+
1446
1464
  if (expectedProtocol === 'V1') {
1447
1465
  if (await this.probeProtocolV1(uuid)) {
1448
1466
  this.deviceProtocol.set(uuid, 'V1');
@@ -1679,6 +1697,10 @@ export default class ReactNativeBleTransport {
1679
1697
  context: ProtocolV2CallContext,
1680
1698
  assertCurrentGeneration: () => void
1681
1699
  ) {
1700
+ const shouldUseWriteWithResponse =
1701
+ Platform.OS === 'ios' &&
1702
+ !context.highVolume &&
1703
+ transport.writeCharacteristic.isWritableWithResponse;
1682
1704
  let attempt = 0;
1683
1705
  for (;;) {
1684
1706
  assertCurrentGeneration();
@@ -1686,7 +1708,11 @@ export default class ReactNativeBleTransport {
1686
1708
  throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
1687
1709
  }
1688
1710
  try {
1689
- await transport.writeCharacteristic.writeWithoutResponse(base64);
1711
+ if (shouldUseWriteWithResponse) {
1712
+ await transport.writeCharacteristic.writeWithResponse(base64);
1713
+ } else {
1714
+ await transport.writeCharacteristic.writeWithoutResponse(base64);
1715
+ }
1690
1716
  assertCurrentGeneration();
1691
1717
  return;
1692
1718
  } catch (error) {
@@ -1721,12 +1747,19 @@ export default class ReactNativeBleTransport {
1721
1747
  androidPacketLength: tuning.androidPacketLength,
1722
1748
  mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
1723
1749
  });
1750
+ // Match Desktop BLE pacing so Pro2 firmware can finish the previous response
1751
+ // before the next single-packet control command is written.
1752
+ const initialDelayMs =
1753
+ Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
1754
+ ? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
1755
+ : 0;
1724
1756
  await writeProtocolV2BleFrame({
1725
1757
  frame,
1726
1758
  packetCapacity,
1727
1759
  assertActive: assertCurrentGeneration,
1728
1760
  signal: context.signal,
1729
1761
  abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
1762
+ initialDelayMs,
1730
1763
  burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
1731
1764
  burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
1732
1765
  flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
@@ -1,211 +0,0 @@
1
- import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
-
3
- import ReactNativeBleTransport from '../index';
4
-
5
- import messages from '@onekeyfe/hd-transport/messages.json';
6
-
7
- jest.mock(
8
- 'react-native',
9
- () => ({
10
- Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
11
- PermissionsAndroid: {
12
- PERMISSIONS: {},
13
- RESULTS: {},
14
- request: jest.fn(),
15
- requestMultiple: jest.fn(),
16
- },
17
- }),
18
- { virtual: true }
19
- );
20
-
21
- jest.mock('react-native-ble-plx', () => ({
22
- BleATTErrorCode: { InvalidHandle: 1 },
23
- BleError: Error,
24
- BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
25
- BleManager: jest.fn(),
26
- ScanMode: { LowLatency: 2 },
27
- }));
28
-
29
- jest.mock('@onekeyfe/react-native-ble-utils', () => ({
30
- __esModule: true,
31
- default: {
32
- getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
33
- getBondedPeripherals: jest.fn(() => Promise.resolve([])),
34
- pairDevice: jest.fn(() => Promise.resolve()),
35
- },
36
- }));
37
-
38
- const UUID = 'stale-timeout-device';
39
-
40
- const flush = () =>
41
- new Promise(resolve => {
42
- setImmediate(resolve);
43
- });
44
-
45
- function createHarness() {
46
- const t = new ReactNativeBleTransport({});
47
- t.configure(messages);
48
- (t as any).deviceProtocol.set(UUID, 'V1');
49
- const writeWithoutResponse = jest.fn(() => Promise.resolve());
50
- const fakeBleTransport = {
51
- writeCharacteristic: { writeWithoutResponse },
52
- writeWithRetry: jest.fn(() => Promise.resolve()),
53
- };
54
- (t as any).getCachedTransport = () => fakeBleTransport;
55
- const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
56
- return { t, disconnectSpy, writeWithoutResponse };
57
- }
58
-
59
- describe('Protocol V1 stale call timeout', () => {
60
- beforeAll(() => {
61
- jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
62
- });
63
-
64
- afterAll(() => {
65
- jest.useRealTimers();
66
- });
67
-
68
- afterEach(() => {
69
- jest.clearAllTimers();
70
- jest.restoreAllMocks();
71
- });
72
-
73
- test('superseded Initialize timeout does not tear down the shared transport', async () => {
74
- const { t, disconnectSpy } = createHarness();
75
-
76
- // Initialize #1: written while the device reboots, never answered.
77
- const firstErrors: unknown[] = [];
78
- const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
79
- first.catch(e => firstErrors.push(e));
80
- await flush();
81
-
82
- // Initialize #2 (forceRun) supersedes #1 three seconds later.
83
- jest.advanceTimersByTime(3000);
84
- const secondErrors: unknown[] = [];
85
- const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
86
- second.catch(e => secondErrors.push(e));
87
- await flush();
88
-
89
- // The device answers Initialize #2 (settle its deferred at the transport seam).
90
- expect(t.runPromise).not.toBeNull();
91
- t.runPromise?.reject(new Error('settled by device response'));
92
- await flush();
93
-
94
- // FirmwareUpload is now the active call on the same transport, awaiting its response.
95
- const uploadErrors: unknown[] = [];
96
- const upload = t.call(UUID, 'FirmwareUpload', { payload: Buffer.alloc(300) });
97
- upload.catch(e => uploadErrors.push(e));
98
- await flush();
99
- jest.advanceTimersByTime(100); // firmware upload flush delay
100
- await flush();
101
-
102
- // Initialize #1's 25s response timer elapses while the upload is in flight.
103
- jest.advanceTimersByTime(25000);
104
- await flush();
105
-
106
- expect(disconnectSpy).not.toHaveBeenCalled();
107
- expect(firstErrors).toHaveLength(1);
108
- expect(uploadErrors).toHaveLength(0);
109
- });
110
-
111
- test('forceRun supersede settles the previous pending call immediately', async () => {
112
- const { t } = createHarness();
113
-
114
- const firstErrors: Array<{ errorCode?: unknown }> = [];
115
- const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
116
- first.catch(e => firstErrors.push(e));
117
- await flush();
118
-
119
- const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
120
- second.catch(() => undefined);
121
- await flush();
122
-
123
- expect(firstErrors).toHaveLength(1);
124
- expect(firstErrors[0]?.errorCode).toBe(HardwareErrorCode.BleForceCleanRunPromise);
125
-
126
- t.runPromise?.reject(new Error('settle second call'));
127
- await flush();
128
- });
129
-
130
- test('late write failure of a superseded call keeps the successor as owner', async () => {
131
- const { t, disconnectSpy } = createHarness();
132
-
133
- // First call's write hangs; its promise is controlled by the test.
134
- let rejectFirstWrite: ((e: Error) => void) | undefined;
135
- const fakeBleTransport = {
136
- writeCharacteristic: {
137
- writeWithoutResponse: jest
138
- .fn()
139
- .mockImplementationOnce(
140
- () =>
141
- new Promise((_resolve, reject) => {
142
- rejectFirstWrite = reject;
143
- })
144
- )
145
- .mockImplementation(() => Promise.resolve()),
146
- },
147
- writeWithRetry: jest.fn(() => Promise.resolve()),
148
- };
149
- (t as any).getCachedTransport = () => fakeBleTransport;
150
-
151
- const firstErrors: unknown[] = [];
152
- const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
153
- first.catch(e => firstErrors.push(e));
154
- await flush();
155
-
156
- // forceRun successor takes ownership while the first call is stuck writing.
157
- const secondErrors: unknown[] = [];
158
- const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 5000 });
159
- second.catch(e => secondErrors.push(e));
160
- await flush();
161
-
162
- // The first call's write now fails late; it must not clear the successor's slot.
163
- rejectFirstWrite?.(new Error('late write failure'));
164
- await flush();
165
-
166
- expect(t.runPromise).not.toBeNull();
167
-
168
- // The successor's genuine timeout must still tear the connection down.
169
- jest.advanceTimersByTime(5000);
170
- await flush();
171
-
172
- expect(disconnectSpy).toHaveBeenCalledTimes(1);
173
- expect(secondErrors).toHaveLength(1);
174
- });
175
-
176
- test('orphan timer left behind by cancel() must not disconnect the transport', async () => {
177
- const { t, disconnectSpy } = createHarness();
178
-
179
- // cancel() nulls the ownership slot without settling the deferred, so the
180
- // call's response timer stays armed (reachable via DeviceCommands.dispose).
181
- const errors: Array<{ errorCode?: unknown }> = [];
182
- const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
183
- p.catch(e => errors.push(e));
184
- await flush();
185
-
186
- t.cancel();
187
-
188
- jest.advanceTimersByTime(5000);
189
- await flush();
190
-
191
- expect(disconnectSpy).not.toHaveBeenCalled();
192
- expect(errors).toHaveLength(1);
193
- expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
194
- });
195
-
196
- test('timeout on the active call still disconnects the transport', async () => {
197
- const { t, disconnectSpy } = createHarness();
198
-
199
- const errors: Array<{ errorCode?: unknown }> = [];
200
- const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
201
- p.catch(e => errors.push(e));
202
- await flush();
203
-
204
- jest.advanceTimersByTime(5000);
205
- await flush();
206
-
207
- expect(disconnectSpy).toHaveBeenCalledTimes(1);
208
- expect(errors).toHaveLength(1);
209
- expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
210
- });
211
- });