@onekeyfe/hd-transport-react-native 1.2.2-alpha.8 → 1.2.2
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/README.md +1 -1
- package/dist/BleManager.d.ts +1 -1
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/bleStaleBond.d.ts.map +1 -1
- package/dist/index.d.ts +35 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +598 -127
- package/package.json +6 -6
- package/src/BleManager.ts +73 -5
- package/src/__tests__/bleNativeDisconnect.test.ts +33 -0
- package/src/__tests__/bleStaleBond.test.ts +8 -0
- package/src/__tests__/connectTimeout.test.ts +427 -3
- package/src/__tests__/protocolV2Link.test.ts +1075 -33
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/bleStaleBond.ts +3 -0
- package/src/index.ts +643 -134
|
@@ -4,10 +4,11 @@ import transportPackage, {
|
|
|
4
4
|
ProtocolV2,
|
|
5
5
|
TRANSPORT_EVENT,
|
|
6
6
|
} from '@onekeyfe/hd-transport';
|
|
7
|
-
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
7
|
+
import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
8
8
|
|
|
9
9
|
import ReactNativeBleTransport, {
|
|
10
10
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS,
|
|
11
|
+
BLE_SETUP_WEDGED_MESSAGE,
|
|
11
12
|
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
12
13
|
configureProtocolV2BleTuning,
|
|
13
14
|
getFirmwareUploadWriteRetryType,
|
|
@@ -28,7 +29,7 @@ jest.mock('react-native-ble-plx', () => ({
|
|
|
28
29
|
BleError: class BleError extends Error {},
|
|
29
30
|
BleErrorCode: {
|
|
30
31
|
DeviceAlreadyConnected: 203,
|
|
31
|
-
DeviceDisconnected:
|
|
32
|
+
DeviceDisconnected: 201,
|
|
32
33
|
DeviceMTUChangeFailed: 206,
|
|
33
34
|
OperationCancelled: 2,
|
|
34
35
|
CharacteristicNotFound: 404,
|
|
@@ -169,11 +170,13 @@ const createHarness = ({
|
|
|
169
170
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
170
171
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
171
172
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
173
|
+
connect: jest.fn(),
|
|
172
174
|
onDisconnected: jest.fn(callback => {
|
|
173
175
|
disconnectCallback = callback;
|
|
174
176
|
return { remove: jest.fn() };
|
|
175
177
|
}),
|
|
176
178
|
} as any;
|
|
179
|
+
device.connect.mockResolvedValue(device);
|
|
177
180
|
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
178
181
|
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
179
182
|
const bleManager = {
|
|
@@ -360,6 +363,186 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
360
363
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
361
364
|
});
|
|
362
365
|
|
|
366
|
+
test.each(['V1', 'V2'] as const)(
|
|
367
|
+
'waits for Android bonding before connecting the %s GATT link',
|
|
368
|
+
async protocol => {
|
|
369
|
+
setPlatformOS('android');
|
|
370
|
+
const { transport, uuid, device } = protocol === 'V1' ? createV1Harness() : createHarness();
|
|
371
|
+
const operationOrder: string[] = [];
|
|
372
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
373
|
+
const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
|
|
374
|
+
const bonding = createDeferred<void>();
|
|
375
|
+
|
|
376
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
377
|
+
device.connect = jest.fn(() => {
|
|
378
|
+
operationOrder.push('connect');
|
|
379
|
+
return Promise.resolve(device);
|
|
380
|
+
});
|
|
381
|
+
pairDeviceMock.mockImplementationOnce(() => {
|
|
382
|
+
operationOrder.push('bond');
|
|
383
|
+
return Promise.resolve({ bonded: false, bonding: true });
|
|
384
|
+
});
|
|
385
|
+
bondStateMock.mockImplementationOnce(() => bonding.promise);
|
|
386
|
+
|
|
387
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: protocol });
|
|
388
|
+
await new Promise(resolve => {
|
|
389
|
+
setImmediate(resolve);
|
|
390
|
+
});
|
|
391
|
+
expect(operationOrder).toEqual(['bond']);
|
|
392
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
393
|
+
|
|
394
|
+
bonding.resolve();
|
|
395
|
+
await expect(acquiring).resolves.toEqual({
|
|
396
|
+
uuid,
|
|
397
|
+
protocolType: protocol,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(operationOrder).toEqual(['bond', 'connect']);
|
|
401
|
+
await transport.release(uuid, true);
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
test('preserves Android connect errors after bonding when both reconnect attempts fail', async () => {
|
|
406
|
+
setPlatformOS('android');
|
|
407
|
+
const { transport, uuid, device } = createHarness();
|
|
408
|
+
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
409
|
+
message: string
|
|
410
|
+
) => Error;
|
|
411
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
412
|
+
const firstError = Object.assign(new BleErrorMock('MTU change failed'), {
|
|
413
|
+
errorCode: 206,
|
|
414
|
+
reason: 'MTU change failed',
|
|
415
|
+
});
|
|
416
|
+
const fallbackError = Object.assign(new BleErrorMock('GATT connect failed'), {
|
|
417
|
+
errorCode: 205,
|
|
418
|
+
reason: 'GATT connect failed',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
422
|
+
device.connect = jest
|
|
423
|
+
.fn()
|
|
424
|
+
.mockRejectedValueOnce(firstError)
|
|
425
|
+
.mockRejectedValueOnce(fallbackError);
|
|
426
|
+
pairDeviceMock.mockClear();
|
|
427
|
+
|
|
428
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
429
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
430
|
+
});
|
|
431
|
+
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
432
|
+
expect(pairDeviceMock).toHaveBeenCalledTimes(1);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('does not connect Android GATT when the system cannot start bonding', async () => {
|
|
436
|
+
setPlatformOS('android');
|
|
437
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
438
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
439
|
+
|
|
440
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
441
|
+
pairDeviceMock.mockClear();
|
|
442
|
+
pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: false });
|
|
443
|
+
|
|
444
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
445
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
446
|
+
});
|
|
447
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
448
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
449
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('checks that Android GATT is connected after bonding and reconnecting', async () => {
|
|
453
|
+
setPlatformOS('android');
|
|
454
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
455
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
456
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
457
|
+
|
|
458
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
459
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
460
|
+
});
|
|
461
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
462
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test.each([
|
|
466
|
+
[
|
|
467
|
+
'ios',
|
|
468
|
+
'OneKey Neo',
|
|
469
|
+
{
|
|
470
|
+
iosErrorCode: 14,
|
|
471
|
+
reason: 'Peer removed pairing information',
|
|
472
|
+
},
|
|
473
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
474
|
+
],
|
|
475
|
+
[
|
|
476
|
+
'android',
|
|
477
|
+
'OneKey Pro 2',
|
|
478
|
+
{
|
|
479
|
+
androidErrorCode: 5,
|
|
480
|
+
reason: 'Connection state changed with status 5',
|
|
481
|
+
},
|
|
482
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
483
|
+
],
|
|
484
|
+
] as const)(
|
|
485
|
+
'maps a %s %s stale bond during connect before protocol detection',
|
|
486
|
+
async (platform, deviceName, nativeError, errorCode) => {
|
|
487
|
+
setPlatformOS(platform);
|
|
488
|
+
const { transport, uuid, device } = createHarness({ deviceName });
|
|
489
|
+
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
490
|
+
message: string
|
|
491
|
+
) => Error;
|
|
492
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
493
|
+
pairDeviceMock.mockClear();
|
|
494
|
+
|
|
495
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
496
|
+
device.connect = jest
|
|
497
|
+
.fn()
|
|
498
|
+
.mockRejectedValue(Object.assign(new BleErrorMock(nativeError.reason), nativeError));
|
|
499
|
+
|
|
500
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({ errorCode });
|
|
501
|
+
expect(pairDeviceMock).toHaveBeenCalledTimes(platform === 'android' ? 1 : 0);
|
|
502
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
test('cleans up Android bonding failures without starting GATT', async () => {
|
|
507
|
+
setPlatformOS('android');
|
|
508
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
509
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
510
|
+
|
|
511
|
+
pairDeviceMock.mockRejectedValueOnce(new Error('bonding canceled'));
|
|
512
|
+
|
|
513
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toThrow(
|
|
514
|
+
'bonding canceled'
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
518
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
519
|
+
expect(device.isConnected).not.toHaveBeenCalled();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('does not connect after stop while Android bonding is pending', async () => {
|
|
523
|
+
setPlatformOS('android');
|
|
524
|
+
const { transport, uuid, bleManager } = createHarness();
|
|
525
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
526
|
+
const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
|
|
527
|
+
const bonding = createDeferred<void>();
|
|
528
|
+
pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: true });
|
|
529
|
+
bondStateMock.mockImplementationOnce(() => bonding.promise);
|
|
530
|
+
|
|
531
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
532
|
+
const rejection = expect(acquiring).rejects.toMatchObject({
|
|
533
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
534
|
+
});
|
|
535
|
+
await new Promise(resolve => {
|
|
536
|
+
setImmediate(resolve);
|
|
537
|
+
});
|
|
538
|
+
const stopping = transport.stop();
|
|
539
|
+
bonding.resolve();
|
|
540
|
+
|
|
541
|
+
await rejection;
|
|
542
|
+
await stopping;
|
|
543
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
544
|
+
});
|
|
545
|
+
|
|
363
546
|
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
364
547
|
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
365
548
|
respondOnWriteCount: [1, 2],
|
|
@@ -435,7 +618,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
435
618
|
uuid,
|
|
436
619
|
protocolType: 'V2',
|
|
437
620
|
});
|
|
438
|
-
expect(device.requestMTU).
|
|
621
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
439
622
|
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
440
623
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
441
624
|
|
|
@@ -534,7 +717,72 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
534
717
|
});
|
|
535
718
|
|
|
536
719
|
test.each(['ios', 'android'] as const)(
|
|
537
|
-
'
|
|
720
|
+
'waits for native disconnect after failed protocol detection before reconnecting on %s',
|
|
721
|
+
async platform => {
|
|
722
|
+
setPlatformOS(platform);
|
|
723
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
724
|
+
const probes = transport as unknown as {
|
|
725
|
+
probeProtocolV1(uuid: string): Promise<boolean>;
|
|
726
|
+
probeProtocolV2(uuid: string): Promise<boolean>;
|
|
727
|
+
releaseNative(uuid: string, onclose: boolean): Promise<void>;
|
|
728
|
+
};
|
|
729
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValueOnce(false);
|
|
730
|
+
const probesFinished = createDeferred<void>();
|
|
731
|
+
const probeProtocolV2 = jest
|
|
732
|
+
.spyOn(probes, 'probeProtocolV2')
|
|
733
|
+
.mockImplementationOnce(async () => {
|
|
734
|
+
// A link-fatal V2 timeout removes the cached transport before acquire fails.
|
|
735
|
+
await probes.releaseNative(uuid, true);
|
|
736
|
+
probesFinished.resolve();
|
|
737
|
+
return false;
|
|
738
|
+
});
|
|
739
|
+
let connected = true;
|
|
740
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
741
|
+
device.connect = jest.fn(() => {
|
|
742
|
+
connected = true;
|
|
743
|
+
return Promise.resolve(device);
|
|
744
|
+
});
|
|
745
|
+
const disconnectGate = createDeferred<void>();
|
|
746
|
+
bleManager.cancelDeviceConnection.mockImplementation(async () => {
|
|
747
|
+
await disconnectGate.promise;
|
|
748
|
+
connected = false;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const acquiring = transport.acquire({ uuid });
|
|
752
|
+
const failure = expect(acquiring).rejects.toMatchObject({
|
|
753
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
754
|
+
});
|
|
755
|
+
const reconnecting = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
await probesFinished.promise;
|
|
759
|
+
await new Promise(resolve => {
|
|
760
|
+
setImmediate(resolve);
|
|
761
|
+
});
|
|
762
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
763
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
764
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
765
|
+
|
|
766
|
+
disconnectGate.resolve();
|
|
767
|
+
await failure;
|
|
768
|
+
await expect(reconnecting).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
769
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
770
|
+
await expect(
|
|
771
|
+
transport.call(uuid, 'Ping', { message: 'after-reconnect' })
|
|
772
|
+
).resolves.toMatchObject({
|
|
773
|
+
type: 'Success',
|
|
774
|
+
message: { message: 'ok' },
|
|
775
|
+
});
|
|
776
|
+
} finally {
|
|
777
|
+
disconnectGate.resolve();
|
|
778
|
+
await Promise.allSettled([failure, reconnecting]);
|
|
779
|
+
await transport.release(uuid, true);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
test.each(['ios', 'android'] as const)(
|
|
785
|
+
'disconnects after a first expected Protocol V2 probe miss while keeping it retryable on %s',
|
|
538
786
|
async platform => {
|
|
539
787
|
setPlatformOS(platform);
|
|
540
788
|
const { transport, uuid, device } = createHarness();
|
|
@@ -544,13 +792,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
544
792
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
545
793
|
});
|
|
546
794
|
|
|
547
|
-
expect(device.cancelConnection).
|
|
795
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
548
796
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
549
797
|
}
|
|
550
798
|
);
|
|
551
799
|
|
|
552
800
|
test.each(['ios', 'android'] as const)(
|
|
553
|
-
'
|
|
801
|
+
'disconnects after each expected Protocol V2 probe miss while keeping it retryable on %s',
|
|
554
802
|
async platform => {
|
|
555
803
|
setPlatformOS(platform);
|
|
556
804
|
const { transport, uuid, device } = createHarness();
|
|
@@ -563,7 +811,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
563
811
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
564
812
|
});
|
|
565
813
|
|
|
566
|
-
expect(device.cancelConnection).
|
|
814
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(2);
|
|
567
815
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
568
816
|
}
|
|
569
817
|
);
|
|
@@ -598,7 +846,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
598
846
|
);
|
|
599
847
|
|
|
600
848
|
test.each(['ios', 'android'] as const)(
|
|
601
|
-
'
|
|
849
|
+
'disconnects after a confirmed Protocol V2 probe miss on %s without reporting a bond error',
|
|
602
850
|
async platform => {
|
|
603
851
|
setPlatformOS(platform);
|
|
604
852
|
const { transport, uuid, device } = createHarness();
|
|
@@ -610,7 +858,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
610
858
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
611
859
|
});
|
|
612
860
|
|
|
613
|
-
expect(device.cancelConnection).
|
|
861
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
614
862
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
615
863
|
}
|
|
616
864
|
);
|
|
@@ -702,6 +950,118 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
702
950
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
|
|
703
951
|
);
|
|
704
952
|
|
|
953
|
+
test('preserves the unpaired error when iOS disconnects during the first V1 probe', async () => {
|
|
954
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
955
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce({
|
|
956
|
+
errorCode: 201,
|
|
957
|
+
iosErrorCode: 7,
|
|
958
|
+
reason: 'The specified device has disconnected from us.',
|
|
959
|
+
});
|
|
960
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
961
|
+
|
|
962
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
963
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
expect(probeProtocolV2).not.toHaveBeenCalled();
|
|
967
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
968
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test('preserves a native iOS disconnect during an expected Protocol V2 probe', async () => {
|
|
972
|
+
const { transport, uuid, writeCharacteristic, bleManager } = createHarness({
|
|
973
|
+
deviceName: 'Neo Test',
|
|
974
|
+
});
|
|
975
|
+
const nativeDisconnect = {
|
|
976
|
+
errorCode: 201,
|
|
977
|
+
iosErrorCode: 7,
|
|
978
|
+
reason: 'The specified device has disconnected from us.',
|
|
979
|
+
};
|
|
980
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce(nativeDisconnect);
|
|
981
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
982
|
+
|
|
983
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
984
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
988
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
989
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
990
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
test('preserves a native iOS disconnect during a V2-first probe without falling back to V1', async () => {
|
|
994
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
995
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce({
|
|
996
|
+
errorCode: 201,
|
|
997
|
+
iosErrorCode: 7,
|
|
998
|
+
reason: 'The specified device has disconnected from us.',
|
|
999
|
+
});
|
|
1000
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
1001
|
+
|
|
1002
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toMatchObject({
|
|
1003
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
1007
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
1008
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test.each(['V1', 'V2'] as const)(
|
|
1012
|
+
'preserves terminal BLE failures from the %s probe without trying another protocol',
|
|
1013
|
+
async protocol => {
|
|
1014
|
+
for (const errorCode of [
|
|
1015
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
1016
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
1017
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
1018
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
1019
|
+
HardwareErrorCode.BleCharacteristicNotifyError,
|
|
1020
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
|
|
1021
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1022
|
+
]) {
|
|
1023
|
+
const { transport, uuid } = createHarness();
|
|
1024
|
+
const error = ERRORS.TypedError(errorCode);
|
|
1025
|
+
const call = jest
|
|
1026
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
1027
|
+
.mockRejectedValue(error);
|
|
1028
|
+
const otherProbe = jest.spyOn(
|
|
1029
|
+
transport as any,
|
|
1030
|
+
protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1'
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).rejects.toBe(error);
|
|
1034
|
+
|
|
1035
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
1036
|
+
expect(otherProbe).not.toHaveBeenCalled();
|
|
1037
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
test.each(['V1', 'V2'] as const)(
|
|
1043
|
+
'still falls back after a silent %s probe timeout',
|
|
1044
|
+
async protocol => {
|
|
1045
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1046
|
+
jest
|
|
1047
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
1048
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
|
|
1049
|
+
const otherProbe = jest
|
|
1050
|
+
.spyOn(transport as any, protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1')
|
|
1051
|
+
.mockResolvedValue(true);
|
|
1052
|
+
|
|
1053
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).resolves.toEqual({
|
|
1054
|
+
uuid,
|
|
1055
|
+
protocolType: protocol === 'V1' ? 'V2' : 'V1',
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
expect(otherProbe).toHaveBeenCalledTimes(1);
|
|
1059
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1060
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1061
|
+
await transport.release(uuid, true);
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
|
|
705
1065
|
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
706
1066
|
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
707
1067
|
const probeProtocolV1 = jest
|
|
@@ -722,57 +1082,621 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
722
1082
|
await transport.release(uuid, true);
|
|
723
1083
|
});
|
|
724
1084
|
|
|
725
|
-
test('
|
|
1085
|
+
test('does not renegotiate an already usable MTU during acquire', async () => {
|
|
726
1086
|
const { transport, uuid, device } = createHarness();
|
|
727
|
-
const mtuError = new Error('MTU refresh failed');
|
|
728
|
-
device.requestMTU.mockRejectedValueOnce(mtuError);
|
|
729
1087
|
|
|
730
1088
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
731
1089
|
uuid,
|
|
732
1090
|
protocolType: 'V2',
|
|
733
1091
|
});
|
|
734
|
-
expect(device.requestMTU).
|
|
1092
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
735
1093
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
736
1094
|
await transport.release(uuid, true);
|
|
737
1095
|
});
|
|
738
1096
|
|
|
739
|
-
test('
|
|
1097
|
+
test('requests a low MTU once before protocol probing', async () => {
|
|
740
1098
|
const { transport, uuid, device } = createHarness();
|
|
741
1099
|
device.mtu = 23;
|
|
742
|
-
device.requestMTU
|
|
743
|
-
.
|
|
744
|
-
.
|
|
745
|
-
|
|
746
|
-
device.mtu = 247;
|
|
747
|
-
return Promise.resolve(device);
|
|
748
|
-
});
|
|
1100
|
+
device.requestMTU.mockImplementationOnce(() => {
|
|
1101
|
+
device.mtu = 247;
|
|
1102
|
+
return Promise.resolve(device);
|
|
1103
|
+
});
|
|
749
1104
|
|
|
750
1105
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
751
1106
|
uuid,
|
|
752
1107
|
protocolType: 'V2',
|
|
753
1108
|
});
|
|
754
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1109
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1110
|
+
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
1111
|
+
247,
|
|
1112
|
+
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
1113
|
+
);
|
|
755
1114
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
756
1115
|
await transport.release(uuid, true);
|
|
757
1116
|
});
|
|
758
1117
|
|
|
759
|
-
test('continues with a low
|
|
1118
|
+
test('continues with a low MTU when the single negotiation fails', async () => {
|
|
760
1119
|
const { transport, uuid, device } = createHarness();
|
|
761
1120
|
device.mtu = 23;
|
|
762
|
-
device.requestMTU
|
|
763
|
-
.mockResolvedValueOnce(device)
|
|
764
|
-
.mockResolvedValueOnce(device)
|
|
765
|
-
.mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
|
|
1121
|
+
device.requestMTU.mockRejectedValueOnce(new Error('MTU negotiation failed'));
|
|
766
1122
|
|
|
767
1123
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
768
1124
|
uuid,
|
|
769
1125
|
protocolType: 'V2',
|
|
770
1126
|
});
|
|
771
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1127
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
772
1128
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
773
1129
|
await transport.release(uuid, true);
|
|
774
1130
|
});
|
|
775
1131
|
|
|
1132
|
+
test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
|
|
1133
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1134
|
+
device.mtu = 23;
|
|
1135
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1136
|
+
|
|
1137
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1138
|
+
uuid,
|
|
1139
|
+
protocolType: 'V2',
|
|
1140
|
+
});
|
|
1141
|
+
const transactionId = device.requestMTU.mock.calls[0]?.[1];
|
|
1142
|
+
expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
|
|
1143
|
+
expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
|
|
1144
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1145
|
+
expect(device.connect).toHaveBeenCalledWith(
|
|
1146
|
+
expect.objectContaining({ timeout: expect.any(Number) })
|
|
1147
|
+
);
|
|
1148
|
+
expect(device.connect.mock.calls.at(-1)?.[0]).not.toHaveProperty('requestMTU');
|
|
1149
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1150
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1151
|
+
await transport.release(uuid, true);
|
|
1152
|
+
}, 10_000);
|
|
1153
|
+
|
|
1154
|
+
test('keeps the iOS link when the MTU-timeout reconnect reports it is still connected', async () => {
|
|
1155
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1156
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1157
|
+
device.mtu = 23;
|
|
1158
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1159
|
+
device.connect.mockRejectedValueOnce(
|
|
1160
|
+
Object.assign(new BleErrorMock('Device already connected'), {
|
|
1161
|
+
errorCode: BleErrorCode.DeviceAlreadyConnected,
|
|
1162
|
+
})
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1166
|
+
uuid,
|
|
1167
|
+
protocolType: 'V2',
|
|
1168
|
+
});
|
|
1169
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
1170
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1171
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1172
|
+
expect(bleManager.destroy).not.toHaveBeenCalled();
|
|
1173
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1174
|
+
await transport.release(uuid, true);
|
|
1175
|
+
}, 10_000);
|
|
1176
|
+
|
|
1177
|
+
test('reconnects after an iOS MTU timeout when cancelling the timed-out link fails', async () => {
|
|
1178
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1179
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1180
|
+
device.mtu = 23;
|
|
1181
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1182
|
+
device.cancelConnection.mockRejectedValueOnce(
|
|
1183
|
+
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1184
|
+
errorCode: BleErrorCode.OperationCancelled,
|
|
1185
|
+
})
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1189
|
+
uuid,
|
|
1190
|
+
protocolType: 'V2',
|
|
1191
|
+
});
|
|
1192
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1193
|
+
expect(device.connect.mock.calls[0]?.[0]).not.toHaveProperty('requestMTU');
|
|
1194
|
+
expect(bleManager.destroy).not.toHaveBeenCalled();
|
|
1195
|
+
await transport.release(uuid, true);
|
|
1196
|
+
}, 10_000);
|
|
1197
|
+
|
|
1198
|
+
test('maps a stale bond from the iOS MTU-timeout reconnect', async () => {
|
|
1199
|
+
const { BleError: BleErrorMock } = jest.requireMock('react-native-ble-plx');
|
|
1200
|
+
const { transport, uuid, device } = createHarness();
|
|
1201
|
+
device.mtu = 23;
|
|
1202
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1203
|
+
device.connect.mockRejectedValueOnce(
|
|
1204
|
+
Object.assign(new BleErrorMock('Peer removed pairing information'), {
|
|
1205
|
+
errorCode: 200,
|
|
1206
|
+
iosErrorCode: 14,
|
|
1207
|
+
})
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1211
|
+
errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
1212
|
+
});
|
|
1213
|
+
await transport.release(uuid, true).catch(() => undefined);
|
|
1214
|
+
}, 10_000);
|
|
1215
|
+
|
|
1216
|
+
test('bounds a hung iOS MTU-timeout teardown and resets the BLE manager without reconnecting', async () => {
|
|
1217
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1218
|
+
device.mtu = 23;
|
|
1219
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1220
|
+
device.cancelConnection.mockImplementationOnce(() => new Promise(() => {}));
|
|
1221
|
+
|
|
1222
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1223
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1224
|
+
message: expect.stringContaining('BLE MTU cleanup timed out'),
|
|
1225
|
+
});
|
|
1226
|
+
expect(bleManager.destroy).toHaveBeenCalledTimes(1);
|
|
1227
|
+
expect(transport.blePlxManager).toBeUndefined();
|
|
1228
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1229
|
+
expect((transport as any).lifecycleOperations.has(uuid)).toBe(false);
|
|
1230
|
+
}, 15_000);
|
|
1231
|
+
|
|
1232
|
+
test('does not reconnect after a hung iOS MTU-timeout teardown when the manager reset is already pending', async () => {
|
|
1233
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1234
|
+
device.mtu = 23;
|
|
1235
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1236
|
+
device.cancelConnection.mockImplementationOnce(() => new Promise(() => {}));
|
|
1237
|
+
// Another device's reset is still draining, so this teardown timeout cannot swap the manager.
|
|
1238
|
+
jest.spyOn(transport as any, 'resetPlxManager').mockImplementation(() => undefined);
|
|
1239
|
+
|
|
1240
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1241
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1242
|
+
});
|
|
1243
|
+
expect(transport.blePlxManager).toBe(bleManager);
|
|
1244
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1245
|
+
}, 15_000);
|
|
1246
|
+
|
|
1247
|
+
test('does not request MTU again after connect falls back without requestMTU', async () => {
|
|
1248
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1249
|
+
const { transport, uuid, device } = createHarness();
|
|
1250
|
+
device.mtu = 23;
|
|
1251
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
1252
|
+
device.connect
|
|
1253
|
+
.mockRejectedValueOnce(
|
|
1254
|
+
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1255
|
+
errorCode: BleErrorCode.OperationCancelled,
|
|
1256
|
+
})
|
|
1257
|
+
)
|
|
1258
|
+
.mockResolvedValue(device);
|
|
1259
|
+
|
|
1260
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1261
|
+
uuid,
|
|
1262
|
+
protocolType: 'V2',
|
|
1263
|
+
});
|
|
1264
|
+
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
1265
|
+
expect(device.connect.mock.calls[0][0]).toEqual(expect.objectContaining({ requestMTU: 247 }));
|
|
1266
|
+
expect(device.connect.mock.calls[1][0]).not.toHaveProperty('requestMTU');
|
|
1267
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1268
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1269
|
+
await transport.release(uuid, true);
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
test('keeps the iOS connect chain unchanged', async () => {
|
|
1273
|
+
const { transport, uuid, device } = createHarness();
|
|
1274
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
1275
|
+
|
|
1276
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1277
|
+
uuid,
|
|
1278
|
+
protocolType: 'V2',
|
|
1279
|
+
});
|
|
1280
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1281
|
+
requestMTU: 247,
|
|
1282
|
+
timeout: expect.any(Number),
|
|
1283
|
+
refreshGatt: 'OnConnected',
|
|
1284
|
+
});
|
|
1285
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1286
|
+
await transport.release(uuid, true);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
describe('Android MTU before service discovery', () => {
|
|
1290
|
+
beforeEach(() => {
|
|
1291
|
+
setPlatformOS('android');
|
|
1292
|
+
jest.useFakeTimers({
|
|
1293
|
+
doNotFake: ['setImmediate', 'queueMicrotask', 'nextTick', 'performance'],
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
afterEach(() => {
|
|
1298
|
+
jest.clearAllTimers();
|
|
1299
|
+
jest.useRealTimers();
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
const advanceUntil = async (condition: () => boolean, budgetMs = 60_000) => {
|
|
1303
|
+
for (let elapsed = 0; !condition(); elapsed += 50) {
|
|
1304
|
+
if (elapsed >= budgetMs) throw new Error(`fake time exhausted after ${budgetMs}ms`);
|
|
1305
|
+
jest.advanceTimersByTime(50);
|
|
1306
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1307
|
+
await new Promise(resolve => {
|
|
1308
|
+
setImmediate(resolve);
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const settle = async <T>(promise: Promise<T>, budgetMs?: number): Promise<T> => {
|
|
1314
|
+
let done = false;
|
|
1315
|
+
promise.then(
|
|
1316
|
+
() => {
|
|
1317
|
+
done = true;
|
|
1318
|
+
},
|
|
1319
|
+
() => {
|
|
1320
|
+
done = true;
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
await advanceUntil(() => done, budgetMs);
|
|
1324
|
+
return promise;
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* A device whose LE link is only up between connect() and cancelConnection(), so the
|
|
1329
|
+
* transport has to reconnect after every drop instead of the test flipping the link.
|
|
1330
|
+
*/
|
|
1331
|
+
const reconnectingDevice = (device: any) => {
|
|
1332
|
+
const link = { connected: false };
|
|
1333
|
+
device.isConnected.mockImplementation(() => Promise.resolve(link.connected));
|
|
1334
|
+
device.connect = jest.fn(() => {
|
|
1335
|
+
link.connected = true;
|
|
1336
|
+
return Promise.resolve(device);
|
|
1337
|
+
});
|
|
1338
|
+
device.cancelConnection.mockImplementation(() => {
|
|
1339
|
+
link.connected = false;
|
|
1340
|
+
return Promise.resolve();
|
|
1341
|
+
});
|
|
1342
|
+
return link;
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
/** requestMTU resolves a fresh Device snapshot, as react-native-ble-plx does. */
|
|
1346
|
+
const negotiatedSnapshot = (device: any, mtu: number) => {
|
|
1347
|
+
const negotiated = { ...device, mtu };
|
|
1348
|
+
device.requestMTU.mockImplementation(() => Promise.resolve(negotiated));
|
|
1349
|
+
return negotiated;
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
test('negotiates the MTU after a bare connect and before service discovery, adopting the returned device', async () => {
|
|
1353
|
+
const { transport, uuid, device } = createHarness();
|
|
1354
|
+
reconnectingDevice(device);
|
|
1355
|
+
device.mtu = 23;
|
|
1356
|
+
const negotiated = negotiatedSnapshot(device, 247);
|
|
1357
|
+
|
|
1358
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1359
|
+
uuid,
|
|
1360
|
+
protocolType: 'V2',
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1364
|
+
expect(device.connect).toHaveBeenCalledWith({ timeout: expect.any(Number) });
|
|
1365
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1366
|
+
const [connectOrder] = device.connect.mock.invocationCallOrder;
|
|
1367
|
+
const [mtuOrder] = device.requestMTU.mock.invocationCallOrder;
|
|
1368
|
+
const [discoveryOrder] = resolveCharacteristics.mock.invocationCallOrder;
|
|
1369
|
+
expect(connectOrder).toBeLessThan(mtuOrder);
|
|
1370
|
+
expect(mtuOrder).toBeLessThan(discoveryOrder);
|
|
1371
|
+
const cached = (transport as any).getCachedTransport(uuid);
|
|
1372
|
+
expect(cached.device).toBe(negotiated);
|
|
1373
|
+
expect(cached.mtuSize).toBe(247);
|
|
1374
|
+
await settle(transport.release(uuid, true));
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
test.each([
|
|
1378
|
+
{ expectedProtocol: 'V2' as const, label: 'an expected Protocol V2 device' },
|
|
1379
|
+
{ expectedProtocol: 'V1' as const, label: 'an expected Protocol V1 device' },
|
|
1380
|
+
{ expectedProtocol: undefined, label: 'a device of unknown protocol' },
|
|
1381
|
+
])('drops a link that stays at the default MTU for $label', async ({ expectedProtocol }) => {
|
|
1382
|
+
const { transport, uuid, device } = createHarness();
|
|
1383
|
+
device.mtu = 23;
|
|
1384
|
+
|
|
1385
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol }))).rejects.toMatchObject({
|
|
1386
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1387
|
+
});
|
|
1388
|
+
expect((transport as any).resolveCharacteristics).not.toHaveBeenCalled();
|
|
1389
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
test.each([
|
|
1393
|
+
['never completes', () => new Promise(() => {})],
|
|
1394
|
+
['completes at the default MTU', undefined],
|
|
1395
|
+
['is rejected', () => Promise.reject(new Error('MTU request failed'))],
|
|
1396
|
+
])(
|
|
1397
|
+
'a second consecutive MTU failure that %s trips the wedged-link guard',
|
|
1398
|
+
async (_label, requestMTU) => {
|
|
1399
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1400
|
+
device.mtu = 23;
|
|
1401
|
+
if (requestMTU) device.requestMTU.mockImplementation(requestMTU);
|
|
1402
|
+
|
|
1403
|
+
await expect(
|
|
1404
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1405
|
+
).rejects.toMatchObject({
|
|
1406
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1407
|
+
});
|
|
1408
|
+
await expect(
|
|
1409
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1410
|
+
).rejects.toMatchObject({
|
|
1411
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
1412
|
+
message: expect.stringContaining(BLE_SETUP_WEDGED_MESSAGE),
|
|
1413
|
+
});
|
|
1414
|
+
expect(bleManager.destroy).toHaveBeenCalled();
|
|
1415
|
+
}
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
test('stop() during the link-drop wait releases it promptly', async () => {
|
|
1419
|
+
const { transport, uuid, device } = createHarness();
|
|
1420
|
+
device.mtu = 23;
|
|
1421
|
+
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1422
|
+
|
|
1423
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error);
|
|
1424
|
+
// The link-drop teardown cancels the device connection before the wait starts.
|
|
1425
|
+
await advanceUntil(() => device.cancelConnection.mock.calls.length > 0);
|
|
1426
|
+
await advanceUntil(() => false, 1000).catch(() => undefined);
|
|
1427
|
+
await settle(transport.stop(), 1000);
|
|
1428
|
+
await expect(settle(acquiring, 1000)).resolves.toBeInstanceOf(Error);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
test.each([
|
|
1432
|
+
{
|
|
1433
|
+
label: 'a missing OneKey service',
|
|
1434
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound),
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
label: 'a missing characteristic',
|
|
1438
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound),
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
label: 'a mis-typed characteristic',
|
|
1442
|
+
error: () =>
|
|
1443
|
+
ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable'),
|
|
1444
|
+
},
|
|
1445
|
+
])(
|
|
1446
|
+
'marks the endpoint on $label and refreshes the GATT table on the next connect, before the MTU exchange',
|
|
1447
|
+
async ({ error }) => {
|
|
1448
|
+
const { transport, uuid, device } = createHarness();
|
|
1449
|
+
const link = reconnectingDevice(device);
|
|
1450
|
+
device.mtu = 23;
|
|
1451
|
+
negotiatedSnapshot(device, 247);
|
|
1452
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1453
|
+
resolveCharacteristics.mockImplementationOnce(() => Promise.reject(error()));
|
|
1454
|
+
|
|
1455
|
+
await expect(
|
|
1456
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1457
|
+
).rejects.toBeDefined();
|
|
1458
|
+
expect(link.connected).toBe(true);
|
|
1459
|
+
|
|
1460
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1461
|
+
uuid,
|
|
1462
|
+
protocolType: 'V2',
|
|
1463
|
+
});
|
|
1464
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1465
|
+
timeout: expect.any(Number),
|
|
1466
|
+
refreshGatt: 'OnConnected',
|
|
1467
|
+
});
|
|
1468
|
+
// The stale table is only refreshed through a connect, so the live link is dropped first.
|
|
1469
|
+
const connectOrders: number[] = device.connect.mock.invocationCallOrder;
|
|
1470
|
+
expect(device.cancelConnection.mock.invocationCallOrder[0]).toBeLessThan(
|
|
1471
|
+
connectOrders[connectOrders.length - 1]
|
|
1472
|
+
);
|
|
1473
|
+
const discoveryOrders = resolveCharacteristics.mock.invocationCallOrder;
|
|
1474
|
+
const mtuOrders: number[] = device.requestMTU.mock.invocationCallOrder;
|
|
1475
|
+
expect(discoveryOrders[discoveryOrders.length - 1]).toBeLessThan(
|
|
1476
|
+
mtuOrders[mtuOrders.length - 1]
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
// The refresh is spent once the table resolved through a refreshed connect.
|
|
1480
|
+
await settle(transport.release(uuid, true));
|
|
1481
|
+
link.connected = false;
|
|
1482
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1483
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1484
|
+
await settle(transport.release(uuid, true));
|
|
1485
|
+
}
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
test('keeps the refresh marker when the refresh connect fell back without refreshGatt', async () => {
|
|
1489
|
+
const { transport, uuid, device } = createHarness();
|
|
1490
|
+
const link = reconnectingDevice(device);
|
|
1491
|
+
device.mtu = 23;
|
|
1492
|
+
negotiatedSnapshot(device, 247);
|
|
1493
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1494
|
+
resolveCharacteristics.mockImplementationOnce(() =>
|
|
1495
|
+
Promise.reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound))
|
|
1496
|
+
);
|
|
1497
|
+
await expect(
|
|
1498
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1499
|
+
).rejects.toBeDefined();
|
|
1500
|
+
|
|
1501
|
+
// The fallback connect carries no refreshGatt.
|
|
1502
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1503
|
+
device.connect.mockImplementationOnce(() => Promise.reject(cancelled));
|
|
1504
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1505
|
+
uuid,
|
|
1506
|
+
protocolType: 'V2',
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
await settle(transport.release(uuid, true));
|
|
1510
|
+
link.connected = false;
|
|
1511
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1512
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1513
|
+
timeout: expect.any(Number),
|
|
1514
|
+
refreshGatt: 'OnConnected',
|
|
1515
|
+
});
|
|
1516
|
+
await settle(transport.release(uuid, true));
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test('does not drop a link it has just connected with refreshGatt', async () => {
|
|
1520
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1521
|
+
bleManager.devices.mockResolvedValue([]);
|
|
1522
|
+
const connectToDevice = jest.fn(() => Promise.resolve(device));
|
|
1523
|
+
Object.assign(bleManager, { connectToDevice });
|
|
1524
|
+
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1525
|
+
|
|
1526
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1527
|
+
uuid,
|
|
1528
|
+
protocolType: 'V2',
|
|
1529
|
+
});
|
|
1530
|
+
expect(connectToDevice).toHaveBeenCalledTimes(1);
|
|
1531
|
+
expect(connectToDevice).toHaveBeenCalledWith(uuid, {
|
|
1532
|
+
timeout: expect.any(Number),
|
|
1533
|
+
refreshGatt: 'OnConnected',
|
|
1534
|
+
});
|
|
1535
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1536
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1537
|
+
await settle(transport.release(uuid, true));
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
test('still refreshes after a connect-by-id refresh fell back without refreshGatt', async () => {
|
|
1541
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1542
|
+
const link = reconnectingDevice(device);
|
|
1543
|
+
bleManager.devices.mockResolvedValue([]);
|
|
1544
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1545
|
+
const connectToDevice = jest
|
|
1546
|
+
.fn()
|
|
1547
|
+
.mockImplementationOnce(() => Promise.reject(cancelled))
|
|
1548
|
+
.mockImplementation(() => {
|
|
1549
|
+
link.connected = true;
|
|
1550
|
+
return Promise.resolve(device);
|
|
1551
|
+
});
|
|
1552
|
+
Object.assign(bleManager, { connectToDevice });
|
|
1553
|
+
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1554
|
+
|
|
1555
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1556
|
+
uuid,
|
|
1557
|
+
protocolType: 'V2',
|
|
1558
|
+
});
|
|
1559
|
+
expect(connectToDevice).toHaveBeenLastCalledWith(uuid, { timeout: expect.any(Number) });
|
|
1560
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1561
|
+
timeout: expect.any(Number),
|
|
1562
|
+
refreshGatt: 'OnConnected',
|
|
1563
|
+
});
|
|
1564
|
+
expect((transport as any).androidGattCacheRefreshes.has(uuid)).toBe(false);
|
|
1565
|
+
await settle(transport.release(uuid, true));
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
test.each([
|
|
1569
|
+
'Cannot write client characteristic config descriptor',
|
|
1570
|
+
'Cannot find client characteristic config descriptor',
|
|
1571
|
+
'The handle is invalid',
|
|
1572
|
+
'Writing is not permitted',
|
|
1573
|
+
])('arms a GATT refresh from the notify failure "%s"', async reason => {
|
|
1574
|
+
const harness = createHarness();
|
|
1575
|
+
const { transport, uuid, device } = harness;
|
|
1576
|
+
reconnectingDevice(device);
|
|
1577
|
+
device.mtu = 23;
|
|
1578
|
+
negotiatedSnapshot(device, 247);
|
|
1579
|
+
|
|
1580
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1581
|
+
harness.emitMonitorError(Object.assign(new Error('notify failed'), { reason }));
|
|
1582
|
+
|
|
1583
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1584
|
+
uuid,
|
|
1585
|
+
protocolType: 'V2',
|
|
1586
|
+
});
|
|
1587
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1588
|
+
timeout: expect.any(Number),
|
|
1589
|
+
refreshGatt: 'OnConnected',
|
|
1590
|
+
});
|
|
1591
|
+
await settle(transport.release(uuid, true));
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
test('arms a GATT refresh from a notify failure while notifications are being enabled', async () => {
|
|
1595
|
+
const { transport, uuid, device } = createHarness({
|
|
1596
|
+
monitorError: Object.assign(new Error('notify failed'), {
|
|
1597
|
+
reason: 'Cannot write client characteristic config descriptor',
|
|
1598
|
+
}),
|
|
1599
|
+
});
|
|
1600
|
+
const link = reconnectingDevice(device);
|
|
1601
|
+
device.mtu = 23;
|
|
1602
|
+
negotiatedSnapshot(device, 247);
|
|
1603
|
+
|
|
1604
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1605
|
+
await settle(transport.release(uuid, true));
|
|
1606
|
+
link.connected = false;
|
|
1607
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1608
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1609
|
+
timeout: expect.any(Number),
|
|
1610
|
+
refreshGatt: 'OnConnected',
|
|
1611
|
+
});
|
|
1612
|
+
await settle(transport.release(uuid, true));
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
test('keeps the GATT refresh for a firmware-install reconnect', async () => {
|
|
1616
|
+
const { transport, uuid, device } = createHarness();
|
|
1617
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
1618
|
+
reconnectingDevice(device);
|
|
1619
|
+
|
|
1620
|
+
await expect(
|
|
1621
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true }))
|
|
1622
|
+
).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
1623
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1624
|
+
timeout: expect.any(Number),
|
|
1625
|
+
refreshGatt: 'OnConnected',
|
|
1626
|
+
});
|
|
1627
|
+
await settle(transport.release(uuid, true));
|
|
1628
|
+
});
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
test('spends one Initialize wake on the detection after a fully silent one', async () => {
|
|
1632
|
+
setPlatformOS('android');
|
|
1633
|
+
const { transport, uuid } = createHarness();
|
|
1634
|
+
const probes = transport as any;
|
|
1635
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1636
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1637
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1638
|
+
|
|
1639
|
+
// Nothing has gone silent yet, so the device keeps whatever session it has.
|
|
1640
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1641
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1642
|
+
|
|
1643
|
+
// The previous detection answered on no protocol, which is what a sleeping Classic
|
|
1644
|
+
// looks like, so this one wakes on the fresh link before probing.
|
|
1645
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1646
|
+
expect(callProtocolV1).toHaveBeenCalledTimes(1);
|
|
1647
|
+
expect(callProtocolV1.mock.calls[0]?.[1]).toBe('Initialize');
|
|
1648
|
+
|
|
1649
|
+
// Still silent: the wake is not repeated, so a device that is simply away is not
|
|
1650
|
+
// pushed into a fresh wallet session on every poll.
|
|
1651
|
+
callProtocolV1.mockClear();
|
|
1652
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1653
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
test('does not send the Initialize wake before a V2-first probe', async () => {
|
|
1657
|
+
setPlatformOS('android');
|
|
1658
|
+
const { transport, uuid } = createHarness();
|
|
1659
|
+
const probes = transport as any;
|
|
1660
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1661
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1662
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1663
|
+
|
|
1664
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toBeDefined();
|
|
1665
|
+
// Silent now, but the next detection probes V2 first: a late V1 reply would land on
|
|
1666
|
+
// the V2 probe, so no Initialize is sent.
|
|
1667
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toBeDefined();
|
|
1668
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
test('does not send the Initialize wake on iOS', async () => {
|
|
1672
|
+
const { transport, uuid } = createHarness();
|
|
1673
|
+
const probes = transport as any;
|
|
1674
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1675
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1676
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1677
|
+
|
|
1678
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1679
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1680
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
test('an unanswered Initialize wake settles the acquire and frees the lifecycle lock', async () => {
|
|
1684
|
+
setPlatformOS('android');
|
|
1685
|
+
const harness = createHarness();
|
|
1686
|
+
const { transport, uuid } = harness;
|
|
1687
|
+
const probes = transport as any;
|
|
1688
|
+
harness.setShouldRespond(false);
|
|
1689
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1690
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1691
|
+
|
|
1692
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1693
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1694
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1695
|
+
});
|
|
1696
|
+
await transport.disconnect(uuid);
|
|
1697
|
+
await transport.stop();
|
|
1698
|
+
}, 10_000);
|
|
1699
|
+
|
|
776
1700
|
test('accepts a stable low MTU without the delayed refresh loop', async () => {
|
|
777
1701
|
const { transport, uuid, device } = createHarness();
|
|
778
1702
|
device.mtu = 185;
|
|
@@ -781,7 +1705,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
781
1705
|
uuid,
|
|
782
1706
|
protocolType: 'V2',
|
|
783
1707
|
});
|
|
784
|
-
expect(device.requestMTU).
|
|
1708
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
785
1709
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(185);
|
|
786
1710
|
await transport.release(uuid, true);
|
|
787
1711
|
});
|
|
@@ -794,7 +1718,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
794
1718
|
uuid,
|
|
795
1719
|
protocolType: 'V2',
|
|
796
1720
|
});
|
|
797
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1721
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
798
1722
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBeUndefined();
|
|
799
1723
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
800
1724
|
await transport.release(uuid, true);
|
|
@@ -812,7 +1736,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
812
1736
|
});
|
|
813
1737
|
|
|
814
1738
|
await expect(transport.call(uuid, 'FileWrite', {})).resolves.toBeDefined();
|
|
815
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1739
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(2);
|
|
816
1740
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
817
1741
|
writesBeforeFileWrite + 1
|
|
818
1742
|
);
|
|
@@ -829,7 +1753,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
829
1753
|
await expect(transport.call(uuid, 'FileWrite', {})).rejects.toMatchObject({
|
|
830
1754
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
831
1755
|
});
|
|
832
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1756
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(2);
|
|
833
1757
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(writesBeforeFileWrite);
|
|
834
1758
|
await transport.release(uuid, true);
|
|
835
1759
|
});
|
|
@@ -883,6 +1807,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
883
1807
|
expect(notifySubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
|
|
884
1808
|
expect(notifySubscriptionRemovers[1]).toHaveBeenCalledTimes(1);
|
|
885
1809
|
expect(bleManager.cancelTransaction).toHaveBeenCalled();
|
|
1810
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
886
1811
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
887
1812
|
});
|
|
888
1813
|
|
|
@@ -937,6 +1862,115 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
937
1862
|
});
|
|
938
1863
|
});
|
|
939
1864
|
|
|
1865
|
+
test.each(['ios', 'android'] as const)(
|
|
1866
|
+
'disconnects after an active V2 timeout and reconnects even if a listener fails on %s',
|
|
1867
|
+
async platform => {
|
|
1868
|
+
setPlatformOS(platform);
|
|
1869
|
+
const harness = createHarness();
|
|
1870
|
+
const { transport, uuid, device, bleManager, sentSeqs, emitter } = harness;
|
|
1871
|
+
const disconnected = jest.fn(() => {
|
|
1872
|
+
throw new Error('Disconnect listener failed');
|
|
1873
|
+
});
|
|
1874
|
+
emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnected);
|
|
1875
|
+
let connected = true;
|
|
1876
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
1877
|
+
device.connect = jest.fn(() => {
|
|
1878
|
+
connected = true;
|
|
1879
|
+
return Promise.resolve(device);
|
|
1880
|
+
});
|
|
1881
|
+
bleManager.cancelDeviceConnection.mockImplementation(() => {
|
|
1882
|
+
connected = false;
|
|
1883
|
+
return Promise.resolve();
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1887
|
+
harness.setShouldRespond(false);
|
|
1888
|
+
await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
|
|
1889
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1893
|
+
expect(connected).toBe(false);
|
|
1894
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1895
|
+
expect(disconnected).toHaveBeenCalledTimes(1);
|
|
1896
|
+
expect(disconnected).toHaveBeenCalledWith({
|
|
1897
|
+
id: uuid,
|
|
1898
|
+
connectId: uuid,
|
|
1899
|
+
name: device.name,
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
harness.setShouldRespond(true);
|
|
1903
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1904
|
+
await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
|
|
1905
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1906
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
1907
|
+
await transport.release(uuid, true);
|
|
1908
|
+
}
|
|
1909
|
+
);
|
|
1910
|
+
|
|
1911
|
+
test('does not disconnect an active Protocol V2 call when a queued call times out', async () => {
|
|
1912
|
+
const harness = createHarness();
|
|
1913
|
+
const { transport, uuid, bleManager } = harness;
|
|
1914
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1915
|
+
harness.setShouldRespond(false);
|
|
1916
|
+
const active = transport.call(uuid, 'Ping', {}, { timeoutMs: 1000 }).catch(error => error);
|
|
1917
|
+
await new Promise(resolve => {
|
|
1918
|
+
setImmediate(resolve);
|
|
1919
|
+
});
|
|
1920
|
+
|
|
1921
|
+
try {
|
|
1922
|
+
await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
|
|
1923
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1924
|
+
});
|
|
1925
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1926
|
+
expect(transport.getProtocolType(uuid)).toBe('V2');
|
|
1927
|
+
} finally {
|
|
1928
|
+
await transport.disconnect(uuid);
|
|
1929
|
+
await active;
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
test('does not disconnect a newer acquire when V2 timeout cleanup waits for the lifecycle lock', async () => {
|
|
1934
|
+
const harness = createHarness();
|
|
1935
|
+
const { transport, uuid, bleManager } = harness;
|
|
1936
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1937
|
+
const lifecycle = transport as unknown as {
|
|
1938
|
+
runLifecycleOperation(id: string, operation: () => Promise<void>): Promise<void>;
|
|
1939
|
+
releaseNative(id: string, onclose: boolean): Promise<boolean>;
|
|
1940
|
+
};
|
|
1941
|
+
const gate = createDeferred<void>();
|
|
1942
|
+
const holding = lifecycle.runLifecycleOperation(uuid, () => gate.promise);
|
|
1943
|
+
const reacquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1944
|
+
const invalidated = createDeferred<void>();
|
|
1945
|
+
const releaseNative = lifecycle.releaseNative.bind(lifecycle);
|
|
1946
|
+
jest.spyOn(lifecycle, 'releaseNative').mockImplementationOnce(async (id, onclose) => {
|
|
1947
|
+
const result = await releaseNative(id, onclose);
|
|
1948
|
+
invalidated.resolve();
|
|
1949
|
+
return result;
|
|
1950
|
+
});
|
|
1951
|
+
harness.setShouldRespond(false);
|
|
1952
|
+
const failure = expect(
|
|
1953
|
+
transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })
|
|
1954
|
+
).rejects.toMatchObject({
|
|
1955
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
try {
|
|
1959
|
+
await invalidated.promise;
|
|
1960
|
+
harness.setShouldRespond(true);
|
|
1961
|
+
gate.resolve();
|
|
1962
|
+
await holding;
|
|
1963
|
+
await reacquiring;
|
|
1964
|
+
await failure;
|
|
1965
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1966
|
+
await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
|
|
1967
|
+
} finally {
|
|
1968
|
+
gate.resolve();
|
|
1969
|
+
await Promise.allSettled([holding, reacquiring, failure]);
|
|
1970
|
+
await transport.release(uuid, true);
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
|
|
940
1974
|
test('retains the sequence cursor when a new monitor generation is acquired', async () => {
|
|
941
1975
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
942
1976
|
|
|
@@ -989,9 +2023,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
989
2023
|
test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
|
|
990
2024
|
setPlatformOS('android');
|
|
991
2025
|
const { transport, uuid, device } = createHarness();
|
|
2026
|
+
device.mtu = 23;
|
|
2027
|
+
device.requestMTU.mockImplementationOnce(() => {
|
|
2028
|
+
device.mtu = 517;
|
|
2029
|
+
return Promise.resolve(device);
|
|
2030
|
+
});
|
|
992
2031
|
|
|
993
2032
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
994
|
-
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
2033
|
+
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
2034
|
+
517,
|
|
2035
|
+
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
2036
|
+
);
|
|
995
2037
|
|
|
996
2038
|
await transport.call(uuid, 'FileWrite', {});
|
|
997
2039
|
await transport.call(uuid, 'FileWrite', {});
|