@onekeyfe/hd-transport-react-native 1.2.0-alpha.54 → 1.2.0-alpha.56
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/dist/index.d.ts +1 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -162
- package/jest.config.js +0 -5
- package/package.json +5 -5
- package/src/BleTransport.ts +1 -1
- package/src/__tests__/BleTransport.test.ts +2 -2
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +241 -41
- package/src/index.ts +74 -236
- package/src/__tests__/staleCallTimeout.test.ts +0 -211
- package/src/__tests__/writePacketTimeout.test.ts +0 -306
|
@@ -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
|
|
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:
|
|
145
|
-
localName:
|
|
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
|
|
241
|
+
isWritableWithResponse,
|
|
210
242
|
isWritableWithoutResponse: true,
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
.
|
|
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').
|
|
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('
|
|
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: '
|
|
474
|
+
await transport.call(uuid, 'Ping', { message: 'first-core-command' });
|
|
376
475
|
|
|
377
|
-
expect(sentSeqs).toEqual([1
|
|
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 <
|
|
390
|
-
await Promise
|
|
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,135 @@ 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
|
|
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
|
-
|
|
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, '
|
|
421
|
-
|
|
422
|
-
|
|
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(
|
|
539
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
540
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
541
|
+
await transport.release(uuid, true);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
|
|
545
|
+
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
546
|
+
|
|
547
|
+
await transport.acquire({ uuid });
|
|
548
|
+
|
|
549
|
+
await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
|
|
550
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
551
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
552
|
+
await transport.release(uuid, true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
|
|
556
|
+
const { transport, uuid, writeCharacteristic } = createHarness({
|
|
557
|
+
isWritableWithResponse: false,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
await transport.acquire({ uuid });
|
|
561
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
562
|
+
|
|
426
563
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
564
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
427
565
|
await transport.release(uuid, true);
|
|
428
566
|
});
|
|
429
567
|
|
|
568
|
+
test('does not resend a failed iOS Protocol V2 control write without response', async () => {
|
|
569
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
570
|
+
const writeError = new Error('write with response failed');
|
|
571
|
+
const writeWithResponse = jest.fn().mockRejectedValue(writeError);
|
|
572
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
573
|
+
const context = {
|
|
574
|
+
messageName: 'ProtocolInfoRequest',
|
|
575
|
+
timeoutMs: 1000,
|
|
576
|
+
highVolume: false,
|
|
577
|
+
generation: 1,
|
|
578
|
+
signal: new AbortController().signal,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
await expect(
|
|
582
|
+
transport.writeProtocolV2Packet(
|
|
583
|
+
{
|
|
584
|
+
writeCharacteristic: {
|
|
585
|
+
isWritableWithResponse: true,
|
|
586
|
+
writeWithResponse,
|
|
587
|
+
writeWithoutResponse,
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
Buffer.from('control').toString('base64'),
|
|
591
|
+
context,
|
|
592
|
+
jest.fn()
|
|
593
|
+
)
|
|
594
|
+
).rejects.toBe(writeError);
|
|
595
|
+
expect(writeWithResponse).toHaveBeenCalledTimes(1);
|
|
596
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('paces a one-packet Protocol V2 control write on iOS', async () => {
|
|
600
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
601
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
602
|
+
const bleTransport = {
|
|
603
|
+
mtuSize: 23,
|
|
604
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
605
|
+
};
|
|
606
|
+
const context = {
|
|
607
|
+
messageName: 'ProtocolInfoRequest',
|
|
608
|
+
timeoutMs: 1000,
|
|
609
|
+
highVolume: false,
|
|
610
|
+
generation: 1,
|
|
611
|
+
signal: new AbortController().signal,
|
|
612
|
+
};
|
|
613
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
614
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const call = transport.writeProtocolV2Frame(
|
|
618
|
+
bleTransport,
|
|
619
|
+
new Uint8Array(10),
|
|
620
|
+
context,
|
|
621
|
+
jest.fn()
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
await Promise.resolve();
|
|
625
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
|
|
626
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
627
|
+
|
|
628
|
+
await call;
|
|
629
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
630
|
+
} finally {
|
|
631
|
+
setTimeoutSpy.mockRestore();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
430
635
|
test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
|
|
431
636
|
const harness = createHarness();
|
|
432
637
|
const { transport, uuid, sentSeqs } = harness;
|
|
@@ -434,8 +639,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
434
639
|
harness.setShouldRespond(false);
|
|
435
640
|
|
|
436
641
|
const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
|
|
437
|
-
while (sentSeqs.length <
|
|
438
|
-
await Promise
|
|
642
|
+
while (sentSeqs.length < 1) {
|
|
643
|
+
await new Promise(resolve => {
|
|
644
|
+
setTimeout(resolve, 0);
|
|
645
|
+
});
|
|
439
646
|
}
|
|
440
647
|
|
|
441
648
|
const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
|
|
@@ -482,7 +689,6 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
482
689
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
483
690
|
|
|
484
691
|
await transport.writeProtocolV2Frame(
|
|
485
|
-
'device-uuid',
|
|
486
692
|
bleTransport,
|
|
487
693
|
new Uint8Array(30),
|
|
488
694
|
context,
|
|
@@ -514,13 +720,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
514
720
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
515
721
|
|
|
516
722
|
await expect(
|
|
517
|
-
transport.writeProtocolV2Frame(
|
|
518
|
-
'device-uuid',
|
|
519
|
-
bleTransport,
|
|
520
|
-
new Uint8Array(30),
|
|
521
|
-
context,
|
|
522
|
-
jest.fn()
|
|
523
|
-
)
|
|
723
|
+
transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
|
|
524
724
|
).rejects.toMatchObject({ errorCode: 205 });
|
|
525
725
|
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
526
726
|
});
|