@onekeyfe/hd-transport-react-native 1.2.0-alpha.75 → 1.2.0-alpha.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-react-native",
3
- "version": "1.2.0-alpha.75",
3
+ "version": "1.2.0-alpha.76",
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.75",
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.75",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.75",
23
+ "@onekeyfe/hd-core": "1.2.0-alpha.76",
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.76",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.76",
26
26
  "@onekeyfe/react-native-ble-utils": "^0.1.6",
27
27
  "react-native-ble-plx": "3.5.1"
28
28
  },
29
- "gitHead": "a9b3ab1cb53f87121ababb2b0ce45789be8fe5ad"
29
+ "gitHead": "9c85b3757bef3483d84ead72974af4718ca019d0"
30
30
  }
@@ -1,5 +1,3 @@
1
- import { Platform } from 'react-native';
2
-
3
1
  import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
4
2
 
5
3
  export default class BleTransport {
@@ -34,11 +32,16 @@ export default class BleTransport {
34
32
  this.notifyCharacteristic = notifyCharacteristic;
35
33
  }
36
34
 
35
+ /**
36
+ * Bulk-transfer write (Protocol V1 FirmwareUpload / EmmcFileWrite only).
37
+ *
38
+ * Must stay writeWithoutResponse on every platform. writeWithResponse serialises
39
+ * each packet into its own connection-interval round trip, which on iOS turned a
40
+ * 1.7MB firmware upload (~13.5k packets) into a >10 minute transfer that never
41
+ * finished before the app-level timeout. Protocol V2 and ordinary V1 control
42
+ * messages pick their write type separately and are unaffected by this method.
43
+ */
37
44
  async writeWithRetry(data: string): Promise<void> {
38
- if (Platform.OS === 'ios' && this.writeCharacteristic.isWritableWithResponse) {
39
- await this.writeCharacteristic.writeWithResponse(data);
40
- return;
41
- }
42
45
  await this.writeCharacteristic.writeWithoutResponse(data);
43
46
  }
44
47
  }
@@ -38,8 +38,8 @@ describe('BleTransport side-effecting writes', () => {
38
38
  });
39
39
  const writeCharacteristic = {
40
40
  isWritableWithResponse: true,
41
- writeWithResponse: jest.fn(() => Promise.reject(error)),
42
- writeWithoutResponse: jest.fn(() => Promise.resolve()),
41
+ writeWithResponse: jest.fn(() => Promise.resolve()),
42
+ writeWithoutResponse: jest.fn(() => Promise.reject(error)),
43
43
  };
44
44
  const device = {
45
45
  id: 'classic-id',
@@ -50,10 +50,31 @@ describe('BleTransport side-effecting writes', () => {
50
50
 
51
51
  await expect(transport.writeWithRetry('payload')).rejects.toBe(error);
52
52
 
53
- expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
53
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
54
54
  expect(device.connect).not.toHaveBeenCalled();
55
55
  });
56
56
 
57
+ test('keeps iOS bulk transfer on writeWithoutResponse even when the characteristic supports responses', async () => {
58
+ // writeWithResponse serialises every packet into its own connection-interval
59
+ // round trip. On a ~1.7MB firmware (~13.5k packets) that stalled the upload past
60
+ // the 600s app-level timeout, so bulk transfer must never opt into it.
61
+ const writeCharacteristic = {
62
+ isWritableWithResponse: true,
63
+ writeWithResponse: jest.fn(() => Promise.resolve()),
64
+ writeWithoutResponse: jest.fn(() => Promise.resolve()),
65
+ };
66
+ const transport = new BleTransport(
67
+ { id: 'classic-id' } as any,
68
+ writeCharacteristic as any,
69
+ {} as any
70
+ );
71
+
72
+ await transport.writeWithRetry('payload');
73
+
74
+ expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
75
+ expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledWith('payload');
76
+ });
77
+
57
78
  test('keeps Android Protocol V1 writes on writeWithoutResponse', async () => {
58
79
  Platform.OS = 'android';
59
80
  const writeCharacteristic = {
@@ -0,0 +1,280 @@
1
+ import { EventEmitter } from 'events';
2
+ import { BleErrorCode } from 'react-native-ble-plx';
3
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
4
+
5
+ import ReactNativeBleTransport, {
6
+ BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
7
+ BLE_CONNECT_TIMEOUT_MS,
8
+ BLE_GATT_SETUP_TIMEOUT_MS,
9
+ } from '../index';
10
+ import protocolV1Schema from './protocolV1SchemaFixture';
11
+
12
+ jest.mock(
13
+ 'react-native',
14
+ () => ({
15
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
16
+ PermissionsAndroid: {
17
+ PERMISSIONS: {},
18
+ RESULTS: {},
19
+ request: jest.fn(),
20
+ requestMultiple: jest.fn(),
21
+ },
22
+ }),
23
+ { virtual: true }
24
+ );
25
+
26
+ jest.mock('react-native-ble-plx', () => ({
27
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
28
+ BleError: Error,
29
+ BleErrorCode: {
30
+ DeviceDisconnected: 201,
31
+ OperationStartFailed: 601,
32
+ DeviceMTUChangeFailed: 401,
33
+ OperationCancelled: 2,
34
+ OperationTimedOut: 3,
35
+ DeviceAlreadyConnected: 203,
36
+ },
37
+ BleManager: jest.fn(),
38
+ ScanMode: { LowLatency: 2 },
39
+ }));
40
+
41
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
42
+ __esModule: true,
43
+ default: {
44
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
45
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
46
+ pairDevice: jest.fn(() => Promise.resolve()),
47
+ },
48
+ }));
49
+
50
+ const UUID = 'stalled-connect-device';
51
+
52
+ const flush = () =>
53
+ new Promise(resolve => {
54
+ setImmediate(resolve);
55
+ });
56
+
57
+ async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 250) {
58
+ for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
59
+ jest.advanceTimersByTime(stepMs);
60
+ // eslint-disable-next-line no-await-in-loop
61
+ await flush();
62
+ if (settled()) return;
63
+ }
64
+ throw new Error(`fake timers exhausted after ${totalMs}ms before the connect settled`);
65
+ }
66
+
67
+ /** A device whose native connect() never settles — the observed iOS failure mode. */
68
+ function createHarness(connectImpl: () => Promise<unknown>) {
69
+ const connect = jest.fn(connectImpl);
70
+ const writeCharacteristic = {
71
+ uuid: '00000002-0000-1000-8000-00805f9b34fb',
72
+ isWritableWithResponse: true,
73
+ };
74
+ const notifyCharacteristic = {
75
+ uuid: '00000003-0000-1000-8000-00805f9b34fb',
76
+ isNotifiable: true,
77
+ };
78
+ const device = {
79
+ id: UUID,
80
+ name: 'OneKey Classic',
81
+ localName: 'OneKey Classic',
82
+ serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
83
+ isConnected: jest.fn(() => Promise.resolve(false)),
84
+ cancelConnection: jest.fn(() => Promise.resolve()),
85
+ connect,
86
+ discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
87
+ characteristicsForService: jest.fn(() =>
88
+ Promise.resolve([writeCharacteristic, notifyCharacteristic])
89
+ ),
90
+ services: jest.fn(() => Promise.resolve([])),
91
+ onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
92
+ };
93
+ const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
94
+ const bleManager = {
95
+ devices: jest.fn(() => Promise.resolve([device])),
96
+ connectedDevices: jest.fn(() => Promise.resolve([])),
97
+ connectToDevice: jest.fn(connectImpl),
98
+ cancelTransaction: jest.fn(() => Promise.resolve()),
99
+ cancelDeviceConnection: jest.fn(() => Promise.resolve()),
100
+ onStateChange: jest.fn((listener: (state: string) => void) => {
101
+ // Dispatch asynchronously: subscribeBleOn wires its own cleanup after
102
+ // registering, so a synchronous callback would run before it is ready.
103
+ setImmediate(() => listener('PoweredOn'));
104
+ return { remove: jest.fn() };
105
+ }),
106
+ state: jest.fn(() => Promise.resolve('PoweredOn')),
107
+ startDeviceScan: jest.fn(),
108
+ stopDeviceScan: jest.fn(),
109
+ };
110
+ (transport as any).blePlxManager = bleManager;
111
+ transport.init(
112
+ { debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
113
+ new EventEmitter()
114
+ );
115
+ transport.configure(protocolV1Schema);
116
+ return { transport, device, bleManager, connect };
117
+ }
118
+
119
+ describe('BLE connect timeout', () => {
120
+ beforeAll(() => {
121
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
122
+ });
123
+
124
+ afterAll(() => {
125
+ jest.useRealTimers();
126
+ });
127
+
128
+ afterEach(() => {
129
+ jest.clearAllTimers();
130
+ jest.restoreAllMocks();
131
+ });
132
+
133
+ test('a native connect that never settles is bounded instead of blocking forever', async () => {
134
+ // iOS applies its own connect timeout on a serial queue; when that queue is busy
135
+ // the timeout never fires and acquire() blocks until the app-level 60s timeout.
136
+ const { transport } = createHarness(
137
+ () =>
138
+ new Promise(() => {
139
+ // never settles
140
+ })
141
+ );
142
+
143
+ const errors: Array<{ errorCode?: unknown }> = [];
144
+ let settled = false;
145
+ transport.acquire({ uuid: UUID }).catch(e => {
146
+ errors.push(e);
147
+ settled = true;
148
+ });
149
+ await flush();
150
+
151
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
152
+
153
+ expect(errors).toHaveLength(1);
154
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
155
+ });
156
+
157
+ test('the connect budget leaves generous headroom over a healthy connect', () => {
158
+ // Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
159
+ // fires when the native timeout itself fails to.
160
+ expect(BLE_CONNECT_TIMEOUT_MS).toBeGreaterThanOrEqual(6000);
161
+ expect(BLE_CONNECT_TIMEOUT_MS).toBeLessThanOrEqual(12000);
162
+ });
163
+
164
+ test('a stalled connect is abandoned natively so the next attempt is not cancelled by it', async () => {
165
+ const { transport, bleManager } = createHarness(
166
+ () =>
167
+ new Promise(() => {
168
+ // never settles
169
+ })
170
+ );
171
+
172
+ let settled = false;
173
+ transport.acquire({ uuid: UUID }).catch(() => {
174
+ settled = true;
175
+ });
176
+ await flush();
177
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
178
+
179
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
180
+ });
181
+
182
+ test('repeated stalled connects recreate the BLE manager', async () => {
183
+ const { transport, bleManager } = createHarness(
184
+ () =>
185
+ new Promise(() => {
186
+ // never settles
187
+ })
188
+ );
189
+ const destroy = jest.fn();
190
+ (bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
191
+
192
+ for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
193
+ let settled = false;
194
+ transport.acquire({ uuid: UUID }).catch(() => {
195
+ settled = true;
196
+ });
197
+ // eslint-disable-next-line no-await-in-loop
198
+ await flush();
199
+ // eslint-disable-next-line no-await-in-loop
200
+ await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
201
+ }
202
+
203
+ expect(destroy).toHaveBeenCalledTimes(1);
204
+ expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
205
+ });
206
+
207
+ test('native connect timeouts contribute to the same manager reset budget', async () => {
208
+ const { transport, bleManager } = createHarness(() => Promise.resolve());
209
+ const destroy = jest.fn();
210
+ (bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
211
+ const nativeTimeout = Object.assign(new Error('Operation timed out'), {
212
+ errorCode: BleErrorCode.OperationTimedOut,
213
+ });
214
+
215
+ for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
216
+ // eslint-disable-next-line no-await-in-loop
217
+ await expect(
218
+ (transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
219
+ ).rejects.toBe(nativeTimeout);
220
+ }
221
+
222
+ expect(destroy).toHaveBeenCalledTimes(1);
223
+ });
224
+
225
+ test('GATT discovery is bounded and abandons the native connection', async () => {
226
+ const { transport, device, bleManager } = createHarness(() => Promise.resolve());
227
+ device.discoverAllServicesAndCharacteristics.mockImplementation(
228
+ () =>
229
+ new Promise(() => {
230
+ // never settles
231
+ })
232
+ );
233
+
234
+ const errors: Array<{ errorCode?: unknown }> = [];
235
+ let settled = false;
236
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch((error: unknown) => {
237
+ errors.push(error as { errorCode?: unknown });
238
+ settled = true;
239
+ });
240
+ await flush();
241
+ await advanceUntil(() => settled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
242
+
243
+ expect(errors).toHaveLength(1);
244
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
245
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
246
+ });
247
+
248
+ test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
249
+ const { transport, device, bleManager } = createHarness(() => Promise.resolve());
250
+ let resolveAbandonedDiscovery: (() => void) | undefined;
251
+ device.discoverAllServicesAndCharacteristics
252
+ .mockImplementationOnce(
253
+ () =>
254
+ new Promise<void>(resolve => {
255
+ resolveAbandonedDiscovery = resolve;
256
+ })
257
+ )
258
+ .mockResolvedValueOnce(undefined);
259
+
260
+ let firstSettled = false;
261
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch(() => {
262
+ firstSettled = true;
263
+ });
264
+ await flush();
265
+ await advanceUntil(() => firstSettled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
266
+
267
+ await expect(
268
+ (transport as any).resolveCharacteristicsWithTimeout(UUID, device)
269
+ ).resolves.toMatchObject({
270
+ writeCharacteristic: expect.any(Object),
271
+ notifyCharacteristic: expect.any(Object),
272
+ });
273
+
274
+ resolveAbandonedDiscovery?.();
275
+ await flush();
276
+
277
+ expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
278
+ expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
279
+ });
280
+ });
@@ -0,0 +1,132 @@
1
+ import ReactNativeBleTransport, { PROTOCOL_REPROBE_FALLBACK_ATTEMPTS } from '../index';
2
+ import protocolV1Schema from './protocolV1SchemaFixture';
3
+
4
+ jest.mock(
5
+ 'react-native',
6
+ () => ({
7
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
8
+ PermissionsAndroid: {
9
+ PERMISSIONS: {},
10
+ RESULTS: {},
11
+ request: jest.fn(),
12
+ requestMultiple: jest.fn(),
13
+ },
14
+ }),
15
+ { virtual: true }
16
+ );
17
+
18
+ jest.mock('react-native-ble-plx', () => ({
19
+ BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
20
+ BleError: Error,
21
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
22
+ BleManager: jest.fn(),
23
+ ScanMode: { LowLatency: 2 },
24
+ }));
25
+
26
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
27
+ __esModule: true,
28
+ default: {
29
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
30
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
31
+ pairDevice: jest.fn(() => Promise.resolve()),
32
+ },
33
+ }));
34
+
35
+ const UUID = 'reprobe-device';
36
+
37
+ /** Drive detectProtocol with stubbed probes so we can observe which are attempted. */
38
+ function createHarness({ v1, v2 }: { v1: boolean; v2: boolean }) {
39
+ const transport = new ReactNativeBleTransport({});
40
+ transport.configure(protocolV1Schema);
41
+ const probeV1 = jest.fn(() => {
42
+ if (v1) {
43
+ (transport as any).deviceProtocol.set(UUID, 'V1');
44
+ }
45
+ return Promise.resolve(v1);
46
+ });
47
+ const probeV2 = jest.fn(() => {
48
+ if (v2) {
49
+ (transport as any).deviceProtocol.set(UUID, 'V2');
50
+ }
51
+ return Promise.resolve(v2);
52
+ });
53
+ (transport as any).probeProtocolV1 = probeV1;
54
+ (transport as any).probeProtocolV2 = probeV2;
55
+ (transport as any).resetProbeStateAfterProtocolProbe = jest.fn(() => Promise.resolve());
56
+ return { transport, probeV1, probeV2 };
57
+ }
58
+
59
+ const detect = (transport: ReactNativeBleTransport) =>
60
+ (transport as any).detectProtocol(UUID, undefined, undefined, () =>
61
+ Promise.resolve()
62
+ ) as Promise<string>;
63
+
64
+ describe('protocol re-probe after a known device goes away', () => {
65
+ test('an unknown device still probes both protocols', async () => {
66
+ const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
67
+
68
+ await expect(detect(transport)).rejects.toBeDefined();
69
+
70
+ expect(probeV1).toHaveBeenCalledTimes(1);
71
+ expect(probeV2).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ test('a device already known to speak V1 does not pay the V2 probe while it is away', async () => {
75
+ const known = createHarness({ v1: true, v2: false });
76
+ // First detection confirms V1 (this is the session that just talked to the device).
77
+ await expect(detect(known.transport)).resolves.toBe('V1');
78
+
79
+ // The device now reboots after a firmware install: V1 stops answering.
80
+ (known.transport as any).deviceProtocol.delete(UUID);
81
+ const probeV1 = jest.fn(() => Promise.resolve(false));
82
+ const probeV2 = jest.fn(() => Promise.resolve(false));
83
+ (known.transport as any).probeProtocolV1 = probeV1;
84
+ (known.transport as any).probeProtocolV2 = probeV2;
85
+
86
+ await expect(detect(known.transport)).rejects.toBeDefined();
87
+
88
+ expect(probeV1).toHaveBeenCalledTimes(1);
89
+ // The 10s Protocol V2 ping is pure waste for a device we just spoke V1 to.
90
+ expect(probeV2).not.toHaveBeenCalled();
91
+ });
92
+
93
+ test('after repeated failures it re-probes every protocol again', async () => {
94
+ const known = createHarness({ v1: true, v2: false });
95
+ await expect(detect(known.transport)).resolves.toBe('V1');
96
+
97
+ (known.transport as any).deviceProtocol.delete(UUID);
98
+ const probeV1 = jest.fn(() => Promise.resolve(false));
99
+ const probeV2 = jest.fn(() => Promise.resolve(false));
100
+ (known.transport as any).probeProtocolV1 = probeV1;
101
+ (known.transport as any).probeProtocolV2 = probeV2;
102
+
103
+ for (let attempt = 0; attempt < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS; attempt += 1) {
104
+ // eslint-disable-next-line no-await-in-loop
105
+ await expect(detect(known.transport)).rejects.toBeDefined();
106
+ }
107
+ expect(probeV2).not.toHaveBeenCalled();
108
+
109
+ // The device may genuinely have changed protocol, so the shortcut must expire.
110
+ await expect(detect(known.transport)).rejects.toBeDefined();
111
+ expect(probeV2).toHaveBeenCalled();
112
+ });
113
+
114
+ test('a successful detection clears the failure streak', async () => {
115
+ const known = createHarness({ v1: true, v2: false });
116
+ await expect(detect(known.transport)).resolves.toBe('V1');
117
+ (known.transport as any).deviceProtocol.delete(UUID);
118
+
119
+ const failingV1 = jest.fn(() => Promise.resolve(false));
120
+ (known.transport as any).probeProtocolV1 = failingV1;
121
+ await expect(detect(known.transport)).rejects.toBeDefined();
122
+
123
+ // Device comes back.
124
+ (known.transport as any).probeProtocolV1 = jest.fn(() => {
125
+ (known.transport as any).deviceProtocol.set(UUID, 'V1');
126
+ return Promise.resolve(true);
127
+ });
128
+ await expect(detect(known.transport)).resolves.toBe('V1');
129
+
130
+ expect((known.transport as any).protocolReprobeFailures.get(UUID)).toBeUndefined();
131
+ });
132
+ });
@@ -0,0 +1,39 @@
1
+ const protocolV1Schema = {
2
+ nested: {
3
+ Initialize: {
4
+ fields: {
5
+ session_id: { type: 'bytes', id: 1 },
6
+ derive_cardano: { type: 'bool', id: 3 },
7
+ },
8
+ },
9
+ GetFeatures: { fields: {} },
10
+ Success: {
11
+ fields: {
12
+ message: { type: 'string', id: 1 },
13
+ },
14
+ },
15
+ Ping: {
16
+ fields: {
17
+ message: { type: 'string', id: 1 },
18
+ button_protection: { type: 'bool', id: 2 },
19
+ },
20
+ },
21
+ FirmwareUpload: {
22
+ fields: {
23
+ payload: { rule: 'required', type: 'bytes', id: 1 },
24
+ hash: { type: 'bytes', id: 2 },
25
+ },
26
+ },
27
+ MessageType: {
28
+ values: {
29
+ MessageType_Initialize: 0,
30
+ MessageType_Ping: 1,
31
+ MessageType_Success: 2,
32
+ MessageType_FirmwareUpload: 7,
33
+ MessageType_GetFeatures: 55,
34
+ },
35
+ },
36
+ },
37
+ };
38
+
39
+ export default protocolV1Schema;
@@ -7,6 +7,7 @@ import transportPackage, {
7
7
  import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
8
8
 
9
9
  import ReactNativeBleTransport, {
10
+ BLE_WRITE_PACKET_TIMEOUT_MS,
10
11
  configureProtocolV2BleTuning,
11
12
  getFirmwareUploadWriteRetryType,
12
13
  resetProtocolV2BleTuning,
@@ -755,6 +756,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
755
756
 
756
757
  await expect(
757
758
  transport.writeProtocolV2Packet(
759
+ 'test-device',
758
760
  {
759
761
  writeCharacteristic: {
760
762
  isWritableWithResponse: true,
@@ -789,9 +791,19 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
789
791
  const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
790
792
 
791
793
  try {
792
- await transport.writeProtocolV2Frame(bleTransport, new Uint8Array(10), context, jest.fn());
794
+ const call = transport.writeProtocolV2Frame(
795
+ 'test-device',
796
+ bleTransport,
797
+ new Uint8Array(10),
798
+ context,
799
+ jest.fn()
800
+ );
793
801
 
794
- expect(setTimeoutSpy).not.toHaveBeenCalled();
802
+ await call;
803
+ // The only scheduled timer is the per-packet BLE write watchdog: no pacing delay.
804
+ expect(setTimeoutSpy.mock.calls.map(([, timeout]) => timeout)).toEqual([
805
+ BLE_WRITE_PACKET_TIMEOUT_MS,
806
+ ]);
795
807
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
796
808
  } finally {
797
809
  setTimeoutSpy.mockRestore();
@@ -855,6 +867,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
855
867
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
856
868
 
857
869
  await transport.writeProtocolV2Frame(
870
+ 'device-uuid',
858
871
  bleTransport,
859
872
  new Uint8Array(30),
860
873
  context,
@@ -886,7 +899,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
886
899
  configureProtocolV2BleTuning({ iosPacketLength: 20 });
887
900
 
888
901
  await expect(
889
- transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
902
+ transport.writeProtocolV2Frame(
903
+ 'device-uuid',
904
+ bleTransport,
905
+ new Uint8Array(30),
906
+ context,
907
+ jest.fn()
908
+ )
890
909
  ).rejects.toMatchObject({ errorCode: 205 });
891
910
  expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
892
911
  });
@@ -908,10 +927,21 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
908
927
  const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
909
928
 
910
929
  try {
911
- await transport.writeProtocolV2Frame(bleTransport, new Uint8Array(600), context, jest.fn());
930
+ await transport.writeProtocolV2Frame(
931
+ 'test-device',
932
+ bleTransport,
933
+ new Uint8Array(600),
934
+ context,
935
+ jest.fn()
936
+ );
912
937
 
913
938
  expect(writeWithoutResponse).toHaveBeenCalledTimes(3);
914
- expect(setTimeoutSpy).not.toHaveBeenCalled();
939
+ // One BLE write watchdog per packet and nothing else: no burst or flush pauses.
940
+ expect(setTimeoutSpy.mock.calls.map(([, timeout]) => timeout)).toEqual([
941
+ BLE_WRITE_PACKET_TIMEOUT_MS,
942
+ BLE_WRITE_PACKET_TIMEOUT_MS,
943
+ BLE_WRITE_PACKET_TIMEOUT_MS,
944
+ ]);
915
945
  } finally {
916
946
  setTimeoutSpy.mockRestore();
917
947
  }