@onekeyfe/hd-transport-react-native 1.2.0-alpha.67 → 1.2.0-alpha.69
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/BleTransport.d.ts +1 -1
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +6 -0
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +9 -80
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +271 -343
- package/jest.config.js +0 -5
- package/package.json +5 -5
- package/src/BleTransport.ts +7 -1
- package/src/__tests__/BleTransport.test.ts +54 -2
- package/src/__tests__/bleStrategy.test.ts +77 -7
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +384 -49
- package/src/bleStrategy.ts +23 -10
- package/src/constants.ts +2 -1
- package/src/index.ts +290 -476
- package/src/__tests__/connectTimeout.test.ts +0 -281
- package/src/__tests__/protocolReprobe.test.ts +0 -132
- package/src/__tests__/protocolV1SchemaFixture.ts +0 -39
- package/src/__tests__/staleCallTimeout.test.ts +0 -210
- package/src/__tests__/writePacketTimeout.test.ts +0 -305
|
@@ -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
|
|
|
@@ -33,6 +32,7 @@ jest.mock('react-native-ble-plx', () => ({
|
|
|
33
32
|
CharacteristicNotFound: 404,
|
|
34
33
|
},
|
|
35
34
|
BleManager: jest.fn(),
|
|
35
|
+
ConnectionPriority: { Balanced: 0, High: 1, LowPower: 2 },
|
|
36
36
|
ScanMode: { LowLatency: 2 },
|
|
37
37
|
}));
|
|
38
38
|
|
|
@@ -46,6 +46,11 @@ jest.mock('../subscribeBleOn', () => ({
|
|
|
46
46
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
|
+
const setPlatformOS = (os: 'ios' | 'android') => {
|
|
50
|
+
const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
|
|
51
|
+
reactNative.Platform.OS = os;
|
|
52
|
+
};
|
|
53
|
+
|
|
49
54
|
const { parseConfigure } = transportPackage;
|
|
50
55
|
|
|
51
56
|
const protocolV1Schema = {
|
|
@@ -69,11 +74,13 @@ const protocolV1Schema = {
|
|
|
69
74
|
|
|
70
75
|
const protocolV2Schema = {
|
|
71
76
|
nested: {
|
|
77
|
+
ProtocolInfoRequest: { fields: {} },
|
|
72
78
|
Ping: {
|
|
73
79
|
fields: {
|
|
74
80
|
message: { type: 'string', id: 1 },
|
|
75
81
|
},
|
|
76
82
|
},
|
|
83
|
+
DeviceInfoGet: { fields: {} },
|
|
77
84
|
FileWrite: { fields: {} },
|
|
78
85
|
Success: {
|
|
79
86
|
fields: {
|
|
@@ -82,8 +89,10 @@ const protocolV2Schema = {
|
|
|
82
89
|
},
|
|
83
90
|
MessageType: {
|
|
84
91
|
values: {
|
|
92
|
+
MessageType_ProtocolInfoRequest: 60200,
|
|
85
93
|
MessageType_Ping: 60206,
|
|
86
94
|
MessageType_Success: 60207,
|
|
95
|
+
MessageType_DeviceInfoGet: 60600,
|
|
87
96
|
MessageType_FileWrite: 60805,
|
|
88
97
|
},
|
|
89
98
|
},
|
|
@@ -95,7 +104,13 @@ const schemas = {
|
|
|
95
104
|
protocolV2: parseConfigure(protocolV2Schema),
|
|
96
105
|
};
|
|
97
106
|
|
|
98
|
-
const createHarness = (
|
|
107
|
+
const createHarness = ({
|
|
108
|
+
deviceName = 'OneKey Pro 2',
|
|
109
|
+
isWritableWithResponse = true,
|
|
110
|
+
}: {
|
|
111
|
+
deviceName?: string;
|
|
112
|
+
isWritableWithResponse?: boolean;
|
|
113
|
+
} = {}) => {
|
|
99
114
|
const uuid = 'rn-pro2-id';
|
|
100
115
|
const sentSeqs: number[] = [];
|
|
101
116
|
let responseSeq = 0;
|
|
@@ -134,15 +149,16 @@ const createHarness = () => {
|
|
|
134
149
|
const writeCharacteristic = {
|
|
135
150
|
uuid: '0002',
|
|
136
151
|
deviceID: uuid,
|
|
137
|
-
isWritableWithResponse
|
|
152
|
+
isWritableWithResponse,
|
|
138
153
|
isWritableWithoutResponse: true,
|
|
139
154
|
writeWithResponse: jest.fn(handleWrite),
|
|
140
155
|
writeWithoutResponse: jest.fn(handleWrite),
|
|
141
156
|
};
|
|
142
157
|
const device = {
|
|
143
158
|
id: uuid,
|
|
144
|
-
name:
|
|
145
|
-
localName:
|
|
159
|
+
name: deviceName,
|
|
160
|
+
localName: deviceName,
|
|
161
|
+
mtu: 247,
|
|
146
162
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
147
163
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
148
164
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
@@ -150,7 +166,9 @@ const createHarness = () => {
|
|
|
150
166
|
disconnectCallback = callback;
|
|
151
167
|
return { remove: jest.fn() };
|
|
152
168
|
}),
|
|
153
|
-
};
|
|
169
|
+
} as any;
|
|
170
|
+
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
171
|
+
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
154
172
|
const bleManager = {
|
|
155
173
|
devices: jest.fn(() => Promise.resolve([device])),
|
|
156
174
|
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
@@ -158,18 +176,21 @@ const createHarness = () => {
|
|
|
158
176
|
};
|
|
159
177
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
160
178
|
const emitter = new EventEmitter();
|
|
179
|
+
const logger = { debug: jest.fn(), error: jest.fn() };
|
|
161
180
|
transport.blePlxManager = bleManager;
|
|
162
181
|
transport.resolveCharacteristics = jest.fn(() =>
|
|
163
182
|
Promise.resolve({ writeCharacteristic, notifyCharacteristic })
|
|
164
183
|
);
|
|
165
|
-
transport.init(
|
|
184
|
+
transport.init(logger, emitter);
|
|
166
185
|
transport.configure(protocolV1Schema);
|
|
167
186
|
transport.configureProtocolV2(protocolV2Schema);
|
|
168
187
|
|
|
169
188
|
return {
|
|
170
189
|
transport,
|
|
171
190
|
emitter,
|
|
191
|
+
logger,
|
|
172
192
|
uuid,
|
|
193
|
+
device,
|
|
173
194
|
sentSeqs,
|
|
174
195
|
writeCharacteristic,
|
|
175
196
|
setShouldRespond(value: boolean) {
|
|
@@ -184,7 +205,13 @@ const createHarness = () => {
|
|
|
184
205
|
};
|
|
185
206
|
};
|
|
186
207
|
|
|
187
|
-
const createV1Harness = (
|
|
208
|
+
const createV1Harness = ({
|
|
209
|
+
respondOnWriteCount = 1,
|
|
210
|
+
isWritableWithResponse = true,
|
|
211
|
+
}: {
|
|
212
|
+
respondOnWriteCount?: number | number[];
|
|
213
|
+
isWritableWithResponse?: boolean;
|
|
214
|
+
} = {}) => {
|
|
188
215
|
const uuid = 'rn-classic-id';
|
|
189
216
|
const notifySubscriptionRemovers: jest.Mock[] = [];
|
|
190
217
|
const disconnectSubscriptionRemovers: jest.Mock[] = [];
|
|
@@ -203,25 +230,31 @@ const createV1Harness = () => {
|
|
|
203
230
|
}),
|
|
204
231
|
};
|
|
205
232
|
let writeCount = 0;
|
|
233
|
+
const responseWriteCounts = new Set(
|
|
234
|
+
Array.isArray(respondOnWriteCount) ? respondOnWriteCount : [respondOnWriteCount]
|
|
235
|
+
);
|
|
236
|
+
const handleWrite = () => {
|
|
237
|
+
writeCount += 1;
|
|
238
|
+
if (responseWriteCounts.has(writeCount)) {
|
|
239
|
+
notifyCallback?.(null, {
|
|
240
|
+
value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return Promise.resolve();
|
|
244
|
+
};
|
|
206
245
|
const writeCharacteristic = {
|
|
207
246
|
uuid: '0002',
|
|
208
247
|
deviceID: uuid,
|
|
209
|
-
isWritableWithResponse
|
|
248
|
+
isWritableWithResponse,
|
|
210
249
|
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
|
-
}),
|
|
250
|
+
writeWithResponse: jest.fn(handleWrite),
|
|
251
|
+
writeWithoutResponse: jest.fn(handleWrite),
|
|
220
252
|
};
|
|
221
253
|
const device = {
|
|
222
254
|
id: uuid,
|
|
223
255
|
name: 'OneKey Classic',
|
|
224
256
|
localName: 'OneKey Classic',
|
|
257
|
+
mtu: 247,
|
|
225
258
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
226
259
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
227
260
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
@@ -230,7 +263,8 @@ const createV1Harness = () => {
|
|
|
230
263
|
disconnectSubscriptionRemovers.push(remove);
|
|
231
264
|
return { remove };
|
|
232
265
|
}),
|
|
233
|
-
};
|
|
266
|
+
} as any;
|
|
267
|
+
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
234
268
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
235
269
|
const bleManager = {
|
|
236
270
|
devices: jest.fn(() => Promise.resolve([device])),
|
|
@@ -249,6 +283,7 @@ const createV1Harness = () => {
|
|
|
249
283
|
uuid,
|
|
250
284
|
device,
|
|
251
285
|
bleManager,
|
|
286
|
+
writeCharacteristic,
|
|
252
287
|
notifySubscriptionRemovers,
|
|
253
288
|
disconnectSubscriptionRemovers,
|
|
254
289
|
};
|
|
@@ -264,6 +299,19 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
264
299
|
).toBeNull();
|
|
265
300
|
});
|
|
266
301
|
|
|
302
|
+
test.each(['status 143', 'status:143', 'status = 143', 'GATT_CONGESTED'])(
|
|
303
|
+
'classifies %s as transient GATT congestion',
|
|
304
|
+
message => {
|
|
305
|
+
expect(getFirmwareUploadWriteRetryType({ message })).toBe('congested');
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
test('handles long uncontrolled status messages without a backtracking regular expression', () => {
|
|
310
|
+
const message = `status${' '.repeat(100_000)}142`;
|
|
311
|
+
|
|
312
|
+
expect(getFirmwareUploadWriteRetryType({ message })).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
267
315
|
test('keeps another device reader when releasing a device with an active V1 call', async () => {
|
|
268
316
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
269
317
|
const activeV1Call = createDeferred<string>();
|
|
@@ -302,12 +350,153 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
302
350
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
303
351
|
});
|
|
304
352
|
|
|
353
|
+
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
354
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
355
|
+
respondOnWriteCount: [1, 2],
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V1' })).resolves.toEqual({
|
|
359
|
+
uuid,
|
|
360
|
+
protocolType: 'V1',
|
|
361
|
+
});
|
|
362
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
363
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
364
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
365
|
+
|
|
366
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
367
|
+
await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
368
|
+
|
|
369
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
370
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
371
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
372
|
+
await transport.release(uuid, true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
|
|
376
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
377
|
+
isWritableWithResponse: false,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
381
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
382
|
+
|
|
383
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
384
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
385
|
+
await transport.release(uuid, true);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('does not resend a failed iOS Protocol V1 control write without response', async () => {
|
|
389
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness();
|
|
390
|
+
const writeError = new Error('write with response failed');
|
|
391
|
+
|
|
392
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
393
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
|
|
394
|
+
|
|
395
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
|
|
396
|
+
errorCode: HardwareErrorCode.BleWriteCharacteristicError,
|
|
397
|
+
});
|
|
398
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
399
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
400
|
+
await transport.release(uuid, true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('actively probes Protocol V2 on iOS when only a name-derived hint is available', async () => {
|
|
404
|
+
const { transport, uuid, device, sentSeqs, writeCharacteristic } = createHarness({
|
|
405
|
+
deviceName: 'Pro2 6E9E',
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
409
|
+
uuid,
|
|
410
|
+
protocolType: 'V2',
|
|
411
|
+
});
|
|
412
|
+
expect(device.requestMTU).toHaveBeenCalledWith(247);
|
|
413
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
414
|
+
|
|
415
|
+
await expect(
|
|
416
|
+
transport.call(uuid, 'Ping', { message: 'first-core-command' })
|
|
417
|
+
).resolves.toBeDefined();
|
|
418
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
419
|
+
await transport.release(uuid, true);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
423
|
+
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
424
|
+
const probeProtocolV1 = jest
|
|
425
|
+
.spyOn(transport as any, 'probeProtocolV1')
|
|
426
|
+
.mockResolvedValue(false);
|
|
427
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
|
|
428
|
+
|
|
429
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
430
|
+
uuid,
|
|
431
|
+
protocolType: 'V2',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(probeProtocolV1).toHaveBeenCalledTimes(1);
|
|
435
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
436
|
+
expect(probeProtocolV1.mock.invocationCallOrder[0]).toBeLessThan(
|
|
437
|
+
probeProtocolV2.mock.invocationCallOrder[0]
|
|
438
|
+
);
|
|
439
|
+
await transport.release(uuid, true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('continues with the current MTU when the connected snapshot refresh fails', async () => {
|
|
443
|
+
const { transport, uuid, device } = createHarness();
|
|
444
|
+
const mtuError = new Error('MTU refresh failed');
|
|
445
|
+
device.requestMTU.mockRejectedValueOnce(mtuError);
|
|
446
|
+
|
|
447
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
448
|
+
uuid,
|
|
449
|
+
protocolType: 'V2',
|
|
450
|
+
});
|
|
451
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
452
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
453
|
+
await transport.release(uuid, true);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('refreshes a transient bootloader MTU after notifications are ready', async () => {
|
|
457
|
+
const { transport, uuid, device } = createHarness();
|
|
458
|
+
device.mtu = 23;
|
|
459
|
+
device.requestMTU
|
|
460
|
+
.mockResolvedValueOnce(device)
|
|
461
|
+
.mockResolvedValueOnce(device)
|
|
462
|
+
.mockImplementationOnce(() => {
|
|
463
|
+
device.mtu = 247;
|
|
464
|
+
return Promise.resolve(device);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
468
|
+
uuid,
|
|
469
|
+
protocolType: 'V2',
|
|
470
|
+
});
|
|
471
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
472
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
473
|
+
await transport.release(uuid, true);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('continues with a low bootloader MTU when the bounded retry fails', async () => {
|
|
477
|
+
const { transport, uuid, device } = createHarness();
|
|
478
|
+
device.mtu = 23;
|
|
479
|
+
device.requestMTU
|
|
480
|
+
.mockResolvedValueOnce(device)
|
|
481
|
+
.mockResolvedValueOnce(device)
|
|
482
|
+
.mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
|
|
483
|
+
|
|
484
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
485
|
+
uuid,
|
|
486
|
+
protocolType: 'V2',
|
|
487
|
+
});
|
|
488
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
489
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
490
|
+
await transport.release(uuid, true);
|
|
491
|
+
});
|
|
492
|
+
|
|
305
493
|
test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
|
|
494
|
+
setPlatformOS('android');
|
|
306
495
|
const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
|
|
307
496
|
createV1Harness();
|
|
308
497
|
const probeProtocolV2 = jest
|
|
309
498
|
.spyOn(transport as any, 'probeProtocolV2')
|
|
310
|
-
.
|
|
499
|
+
.mockImplementation(async () => {
|
|
311
500
|
await (transport as any).releaseNative(uuid, true);
|
|
312
501
|
return false;
|
|
313
502
|
});
|
|
@@ -333,8 +522,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
333
522
|
});
|
|
334
523
|
|
|
335
524
|
test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
|
|
525
|
+
setPlatformOS('android');
|
|
336
526
|
const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
|
|
337
|
-
jest.spyOn(transport as any, 'probeProtocolV2').
|
|
527
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
|
|
338
528
|
await (transport as any).releaseNative(uuid, true);
|
|
339
529
|
return false;
|
|
340
530
|
});
|
|
@@ -353,7 +543,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
353
543
|
});
|
|
354
544
|
|
|
355
545
|
test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
|
|
356
|
-
const { transport, uuid, device } = createV1Harness(
|
|
546
|
+
const { transport, uuid, device } = createV1Harness({
|
|
547
|
+
respondOnWriteCount: Number.POSITIVE_INFINITY,
|
|
548
|
+
});
|
|
357
549
|
|
|
358
550
|
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
359
551
|
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
|
|
@@ -365,29 +557,31 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
365
557
|
});
|
|
366
558
|
|
|
367
559
|
afterEach(() => {
|
|
560
|
+
setPlatformOS('ios');
|
|
368
561
|
resetProtocolV2BleTuning();
|
|
369
562
|
});
|
|
370
563
|
|
|
371
|
-
test('
|
|
564
|
+
test('uses the first Protocol V2 sequence for the first Core call when protocol is known', async () => {
|
|
372
565
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
373
566
|
|
|
374
|
-
await transport.acquire({ uuid });
|
|
375
|
-
await transport.call(uuid, 'Ping', { message: '
|
|
567
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
568
|
+
await transport.call(uuid, 'Ping', { message: 'first-core-command' });
|
|
376
569
|
|
|
377
|
-
expect(sentSeqs).toEqual([1
|
|
378
|
-
expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
|
|
570
|
+
expect(sentSeqs).toEqual([1]);
|
|
379
571
|
await transport.release(uuid, true);
|
|
380
572
|
});
|
|
381
573
|
|
|
382
574
|
test('rejects the active Protocol V2 reader when the current monitor errors', async () => {
|
|
383
575
|
const harness = createHarness();
|
|
384
576
|
const { transport, uuid, sentSeqs } = harness;
|
|
385
|
-
await transport.acquire({ uuid });
|
|
577
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
386
578
|
harness.setShouldRespond(false);
|
|
387
579
|
|
|
388
580
|
const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
|
|
389
|
-
while (sentSeqs.length <
|
|
390
|
-
await Promise
|
|
581
|
+
while (sentSeqs.length < 1) {
|
|
582
|
+
await new Promise(resolve => {
|
|
583
|
+
setTimeout(resolve, 0);
|
|
584
|
+
});
|
|
391
585
|
}
|
|
392
586
|
await new Promise(resolve => {
|
|
393
587
|
setTimeout(resolve, 0);
|
|
@@ -402,40 +596,162 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
402
596
|
test('retains the sequence cursor when a new monitor generation is acquired', async () => {
|
|
403
597
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
404
598
|
|
|
405
|
-
await transport.acquire({ uuid });
|
|
599
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
600
|
+
await transport.call(uuid, 'Ping', { message: 'first-generation' });
|
|
406
601
|
await transport.release(uuid, true);
|
|
407
|
-
await transport.acquire({ uuid });
|
|
602
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
603
|
+
await transport.call(uuid, 'Ping', { message: 'second-generation' });
|
|
408
604
|
|
|
409
605
|
expect(sentSeqs).toEqual([1, 2]);
|
|
410
606
|
await transport.release(uuid, true);
|
|
411
607
|
});
|
|
412
608
|
|
|
413
|
-
test('uses
|
|
609
|
+
test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
|
|
414
610
|
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
415
611
|
|
|
416
|
-
await transport.acquire({ uuid });
|
|
417
|
-
|
|
612
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
613
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
614
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
418
615
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
419
616
|
|
|
420
|
-
await transport.call(uuid, '
|
|
617
|
+
await transport.call(uuid, 'DeviceInfoGet', {});
|
|
618
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
619
|
+
|
|
620
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
621
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
622
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
623
|
+
|
|
624
|
+
await transport.release(uuid, true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
|
|
628
|
+
const { transport, uuid, logger, writeCharacteristic } = createHarness();
|
|
629
|
+
|
|
630
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
631
|
+
|
|
632
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
633
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
421
634
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
|
|
422
635
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
636
|
+
expect(
|
|
637
|
+
logger.debug.mock.calls.filter(
|
|
638
|
+
([message]) =>
|
|
639
|
+
message === '[ReactNativeBleTransport] Protocol V2 high-volume write configured'
|
|
640
|
+
)
|
|
641
|
+
).toHaveLength(1);
|
|
642
|
+
await transport.release(uuid, true);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
|
|
646
|
+
setPlatformOS('android');
|
|
647
|
+
const { transport, uuid, device } = createHarness();
|
|
423
648
|
|
|
649
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
650
|
+
expect(device.requestMTU).toHaveBeenCalledWith(517);
|
|
651
|
+
|
|
652
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
424
653
|
await transport.call(uuid, 'FileWrite', {});
|
|
425
|
-
|
|
654
|
+
|
|
655
|
+
expect(device.requestConnectionPriority).toHaveBeenCalledTimes(1);
|
|
656
|
+
expect(device.requestConnectionPriority).toHaveBeenCalledWith(1);
|
|
657
|
+
|
|
658
|
+
await transport.release(uuid, true);
|
|
659
|
+
expect(device.requestConnectionPriority).toHaveBeenLastCalledWith(0);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
|
|
663
|
+
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
664
|
+
|
|
665
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
666
|
+
|
|
667
|
+
await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
|
|
668
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
669
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
670
|
+
await transport.release(uuid, true);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
|
|
674
|
+
const { transport, uuid, writeCharacteristic } = createHarness({
|
|
675
|
+
isWritableWithResponse: false,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
679
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
680
|
+
|
|
426
681
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
682
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
427
683
|
await transport.release(uuid, true);
|
|
428
684
|
});
|
|
429
685
|
|
|
686
|
+
test('does not resend a failed iOS Protocol V2 control write without response', async () => {
|
|
687
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
688
|
+
const writeError = new Error('write with response failed');
|
|
689
|
+
const writeWithResponse = jest.fn().mockRejectedValue(writeError);
|
|
690
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
691
|
+
const context = {
|
|
692
|
+
messageName: 'ProtocolInfoRequest',
|
|
693
|
+
timeoutMs: 1000,
|
|
694
|
+
highVolume: false,
|
|
695
|
+
generation: 1,
|
|
696
|
+
signal: new AbortController().signal,
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
await expect(
|
|
700
|
+
transport.writeProtocolV2Packet(
|
|
701
|
+
{
|
|
702
|
+
writeCharacteristic: {
|
|
703
|
+
isWritableWithResponse: true,
|
|
704
|
+
writeWithResponse,
|
|
705
|
+
writeWithoutResponse,
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
Buffer.from('control').toString('base64'),
|
|
709
|
+
context,
|
|
710
|
+
jest.fn()
|
|
711
|
+
)
|
|
712
|
+
).rejects.toBe(writeError);
|
|
713
|
+
expect(writeWithResponse).toHaveBeenCalledTimes(1);
|
|
714
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test('does not pace a one-packet Protocol V2 control write on iOS', async () => {
|
|
718
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
719
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
720
|
+
const bleTransport = {
|
|
721
|
+
mtuSize: 23,
|
|
722
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
723
|
+
};
|
|
724
|
+
const context = {
|
|
725
|
+
messageName: 'ProtocolInfoRequest',
|
|
726
|
+
timeoutMs: 1000,
|
|
727
|
+
highVolume: false,
|
|
728
|
+
generation: 1,
|
|
729
|
+
signal: new AbortController().signal,
|
|
730
|
+
};
|
|
731
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
732
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
await transport.writeProtocolV2Frame(bleTransport, new Uint8Array(10), context, jest.fn());
|
|
736
|
+
|
|
737
|
+
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
738
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
739
|
+
} finally {
|
|
740
|
+
setTimeoutSpy.mockRestore();
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
430
744
|
test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
|
|
431
745
|
const harness = createHarness();
|
|
432
746
|
const { transport, uuid, sentSeqs } = harness;
|
|
433
|
-
await transport.acquire({ uuid });
|
|
747
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
434
748
|
harness.setShouldRespond(false);
|
|
435
749
|
|
|
436
750
|
const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
|
|
437
|
-
while (sentSeqs.length <
|
|
438
|
-
await Promise
|
|
751
|
+
while (sentSeqs.length < 1) {
|
|
752
|
+
await new Promise(resolve => {
|
|
753
|
+
setTimeout(resolve, 0);
|
|
754
|
+
});
|
|
439
755
|
}
|
|
440
756
|
|
|
441
757
|
const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
|
|
@@ -448,7 +764,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
448
764
|
const disconnectListener = jest.fn();
|
|
449
765
|
harness.emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnectListener);
|
|
450
766
|
|
|
451
|
-
await harness.transport.acquire({ uuid: harness.uuid });
|
|
767
|
+
await harness.transport.acquire({ uuid: harness.uuid, expectedProtocol: 'V2' });
|
|
452
768
|
harness.emitDisconnect();
|
|
453
769
|
await harness.transport.disconnect(harness.uuid);
|
|
454
770
|
|
|
@@ -482,7 +798,6 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
482
798
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
483
799
|
|
|
484
800
|
await transport.writeProtocolV2Frame(
|
|
485
|
-
'device-uuid',
|
|
486
801
|
bleTransport,
|
|
487
802
|
new Uint8Array(30),
|
|
488
803
|
context,
|
|
@@ -514,14 +829,34 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
514
829
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
515
830
|
|
|
516
831
|
await expect(
|
|
517
|
-
transport.writeProtocolV2Frame(
|
|
518
|
-
'device-uuid',
|
|
519
|
-
bleTransport,
|
|
520
|
-
new Uint8Array(30),
|
|
521
|
-
context,
|
|
522
|
-
jest.fn()
|
|
523
|
-
)
|
|
832
|
+
transport.writeProtocolV2Frame(bleTransport, new Uint8Array(30), context, jest.fn())
|
|
524
833
|
).rejects.toMatchObject({ errorCode: 205 });
|
|
525
834
|
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
526
835
|
});
|
|
836
|
+
|
|
837
|
+
test('does not apply fixed burst or flush pauses to high-volume Protocol V2 writes', async () => {
|
|
838
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
839
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
840
|
+
const bleTransport = {
|
|
841
|
+
mtuSize: 247,
|
|
842
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
843
|
+
};
|
|
844
|
+
const context = {
|
|
845
|
+
messageName: 'FileWrite',
|
|
846
|
+
timeoutMs: 1000,
|
|
847
|
+
highVolume: true,
|
|
848
|
+
generation: 1,
|
|
849
|
+
signal: new AbortController().signal,
|
|
850
|
+
};
|
|
851
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
await transport.writeProtocolV2Frame(bleTransport, new Uint8Array(600), context, jest.fn());
|
|
855
|
+
|
|
856
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(3);
|
|
857
|
+
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
858
|
+
} finally {
|
|
859
|
+
setTimeoutSpy.mockRestore();
|
|
860
|
+
}
|
|
861
|
+
});
|
|
527
862
|
});
|