@onekeyfe/hd-transport-react-native 1.2.3-alpha.1 → 1.2.3-alpha.10
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 +6 -5
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/bleEncryption.d.ts +17 -0
- package/dist/bleEncryption.d.ts.map +1 -0
- package/dist/bleKeyMissing.d.ts +7 -0
- package/dist/bleKeyMissing.d.ts.map +1 -0
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/index.d.ts +69 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +951 -139
- package/package.json +6 -6
- package/src/BleManager.ts +113 -5
- package/src/__tests__/androidEncryption.test.ts +482 -0
- package/src/__tests__/androidKeyMissing.test.ts +307 -0
- package/src/__tests__/bleNativeDisconnect.test.ts +33 -0
- package/src/__tests__/connectTimeout.test.ts +427 -3
- package/src/__tests__/enumerate.test.ts +15 -0
- package/src/__tests__/protocolV2Link.test.ts +991 -62
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleEncryption.ts +267 -0
- package/src/bleKeyMissing.ts +106 -0
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/index.ts +785 -156
|
@@ -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,
|
|
@@ -44,6 +45,21 @@ jest.mock('../BleManager', () => ({
|
|
|
44
45
|
pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
|
|
45
46
|
}));
|
|
46
47
|
|
|
48
|
+
// The native facade is replaced wholesale above; key missing and link encryption are covered
|
|
49
|
+
// by their own suites.
|
|
50
|
+
jest.mock('../bleKeyMissing', () => ({
|
|
51
|
+
isBleKeyMissingSupported: jest.fn(() => false),
|
|
52
|
+
startBleKeyMissingTracking: jest.fn(() => false),
|
|
53
|
+
stopBleKeyMissingTracking: jest.fn(),
|
|
54
|
+
waitForBleKeyMissing: jest.fn(() => Promise.resolve(false)),
|
|
55
|
+
}));
|
|
56
|
+
jest.mock('../bleEncryption', () => ({
|
|
57
|
+
markBleLinkEncrypted: jest.fn(),
|
|
58
|
+
startBleEncryptionTracking: jest.fn(() => false),
|
|
59
|
+
stopBleEncryptionTracking: jest.fn(),
|
|
60
|
+
waitForAndroidLinkEncryption: jest.fn(() => Promise.resolve('unresolved')),
|
|
61
|
+
}));
|
|
62
|
+
|
|
47
63
|
jest.mock('../subscribeBleOn', () => ({
|
|
48
64
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
49
65
|
}));
|
|
@@ -169,11 +185,13 @@ const createHarness = ({
|
|
|
169
185
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
170
186
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
171
187
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
188
|
+
connect: jest.fn(),
|
|
172
189
|
onDisconnected: jest.fn(callback => {
|
|
173
190
|
disconnectCallback = callback;
|
|
174
191
|
return { remove: jest.fn() };
|
|
175
192
|
}),
|
|
176
193
|
} as any;
|
|
194
|
+
device.connect.mockResolvedValue(device);
|
|
177
195
|
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
178
196
|
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
179
197
|
const bleManager = {
|
|
@@ -360,32 +378,46 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
360
378
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
361
379
|
});
|
|
362
380
|
|
|
363
|
-
test('
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
381
|
+
test.each(['V1', 'V2'] as const)(
|
|
382
|
+
'waits for Android bonding before connecting the %s GATT link',
|
|
383
|
+
async protocol => {
|
|
384
|
+
setPlatformOS('android');
|
|
385
|
+
const { transport, uuid, device } = protocol === 'V1' ? createV1Harness() : createHarness();
|
|
386
|
+
const operationOrder: string[] = [];
|
|
387
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
388
|
+
const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
|
|
389
|
+
const bonding = createDeferred<void>();
|
|
368
390
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
391
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
392
|
+
device.connect = jest.fn(() => {
|
|
393
|
+
operationOrder.push('connect');
|
|
394
|
+
return Promise.resolve(device);
|
|
395
|
+
});
|
|
396
|
+
pairDeviceMock.mockImplementationOnce(() => {
|
|
397
|
+
operationOrder.push('bond');
|
|
398
|
+
return Promise.resolve({ bonded: false, bonding: true });
|
|
399
|
+
});
|
|
400
|
+
bondStateMock.mockImplementationOnce(() => bonding.promise);
|
|
378
401
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
402
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: protocol });
|
|
403
|
+
await new Promise(resolve => {
|
|
404
|
+
setImmediate(resolve);
|
|
405
|
+
});
|
|
406
|
+
expect(operationOrder).toEqual(['bond']);
|
|
407
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
383
408
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
409
|
+
bonding.resolve();
|
|
410
|
+
await expect(acquiring).resolves.toEqual({
|
|
411
|
+
uuid,
|
|
412
|
+
protocolType: protocol,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(operationOrder).toEqual(['bond', 'connect']);
|
|
416
|
+
await transport.release(uuid, true);
|
|
417
|
+
}
|
|
418
|
+
);
|
|
387
419
|
|
|
388
|
-
test('
|
|
420
|
+
test('preserves Android connect errors after bonding when both reconnect attempts fail', async () => {
|
|
389
421
|
setPlatformOS('android');
|
|
390
422
|
const { transport, uuid, device } = createHarness();
|
|
391
423
|
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
@@ -412,22 +444,35 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
412
444
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
413
445
|
});
|
|
414
446
|
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
415
|
-
expect(pairDeviceMock).
|
|
447
|
+
expect(pairDeviceMock).toHaveBeenCalledTimes(1);
|
|
416
448
|
});
|
|
417
449
|
|
|
418
|
-
test('
|
|
450
|
+
test('does not connect Android GATT when the system cannot start bonding', async () => {
|
|
419
451
|
setPlatformOS('android');
|
|
420
452
|
const { transport, uuid, device, bleManager } = createHarness();
|
|
421
453
|
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
422
454
|
|
|
423
|
-
device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
424
455
|
device.connect = jest.fn().mockResolvedValue(device);
|
|
425
456
|
pairDeviceMock.mockClear();
|
|
457
|
+
pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: false });
|
|
458
|
+
|
|
459
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
460
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
461
|
+
});
|
|
462
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
463
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
464
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('checks that Android GATT is connected after bonding and reconnecting', async () => {
|
|
468
|
+
setPlatformOS('android');
|
|
469
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
470
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
471
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
426
472
|
|
|
427
473
|
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
428
474
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
429
475
|
});
|
|
430
|
-
expect(pairDeviceMock).not.toHaveBeenCalled();
|
|
431
476
|
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
432
477
|
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
433
478
|
});
|
|
@@ -468,12 +513,12 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
468
513
|
.mockRejectedValue(Object.assign(new BleErrorMock(nativeError.reason), nativeError));
|
|
469
514
|
|
|
470
515
|
await expect(transport.acquire({ uuid })).rejects.toMatchObject({ errorCode });
|
|
471
|
-
expect(pairDeviceMock).
|
|
516
|
+
expect(pairDeviceMock).toHaveBeenCalledTimes(platform === 'android' ? 1 : 0);
|
|
472
517
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
473
518
|
}
|
|
474
519
|
);
|
|
475
520
|
|
|
476
|
-
test('
|
|
521
|
+
test('cleans up Android bonding failures without starting GATT', async () => {
|
|
477
522
|
setPlatformOS('android');
|
|
478
523
|
const { transport, uuid, device, bleManager } = createHarness();
|
|
479
524
|
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
@@ -485,7 +530,32 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
485
530
|
);
|
|
486
531
|
|
|
487
532
|
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
488
|
-
expect(
|
|
533
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
534
|
+
expect(device.isConnected).not.toHaveBeenCalled();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('does not connect after stop while Android bonding is pending', async () => {
|
|
538
|
+
setPlatformOS('android');
|
|
539
|
+
const { transport, uuid, bleManager } = createHarness();
|
|
540
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
541
|
+
const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
|
|
542
|
+
const bonding = createDeferred<void>();
|
|
543
|
+
pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: true });
|
|
544
|
+
bondStateMock.mockImplementationOnce(() => bonding.promise);
|
|
545
|
+
|
|
546
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
547
|
+
const rejection = expect(acquiring).rejects.toMatchObject({
|
|
548
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
549
|
+
});
|
|
550
|
+
await new Promise(resolve => {
|
|
551
|
+
setImmediate(resolve);
|
|
552
|
+
});
|
|
553
|
+
const stopping = transport.stop();
|
|
554
|
+
bonding.resolve();
|
|
555
|
+
|
|
556
|
+
await rejection;
|
|
557
|
+
await stopping;
|
|
558
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
489
559
|
});
|
|
490
560
|
|
|
491
561
|
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
@@ -563,7 +633,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
563
633
|
uuid,
|
|
564
634
|
protocolType: 'V2',
|
|
565
635
|
});
|
|
566
|
-
expect(device.requestMTU).
|
|
636
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
567
637
|
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
568
638
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
569
639
|
|
|
@@ -662,7 +732,72 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
662
732
|
});
|
|
663
733
|
|
|
664
734
|
test.each(['ios', 'android'] as const)(
|
|
665
|
-
'
|
|
735
|
+
'waits for native disconnect after failed protocol detection before reconnecting on %s',
|
|
736
|
+
async platform => {
|
|
737
|
+
setPlatformOS(platform);
|
|
738
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
739
|
+
const probes = transport as unknown as {
|
|
740
|
+
probeProtocolV1(uuid: string): Promise<boolean>;
|
|
741
|
+
probeProtocolV2(uuid: string): Promise<boolean>;
|
|
742
|
+
releaseNative(uuid: string, onclose: boolean): Promise<void>;
|
|
743
|
+
};
|
|
744
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValueOnce(false);
|
|
745
|
+
const probesFinished = createDeferred<void>();
|
|
746
|
+
const probeProtocolV2 = jest
|
|
747
|
+
.spyOn(probes, 'probeProtocolV2')
|
|
748
|
+
.mockImplementationOnce(async () => {
|
|
749
|
+
// A link-fatal V2 timeout removes the cached transport before acquire fails.
|
|
750
|
+
await probes.releaseNative(uuid, true);
|
|
751
|
+
probesFinished.resolve();
|
|
752
|
+
return false;
|
|
753
|
+
});
|
|
754
|
+
let connected = true;
|
|
755
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
756
|
+
device.connect = jest.fn(() => {
|
|
757
|
+
connected = true;
|
|
758
|
+
return Promise.resolve(device);
|
|
759
|
+
});
|
|
760
|
+
const disconnectGate = createDeferred<void>();
|
|
761
|
+
bleManager.cancelDeviceConnection.mockImplementation(async () => {
|
|
762
|
+
await disconnectGate.promise;
|
|
763
|
+
connected = false;
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
const acquiring = transport.acquire({ uuid });
|
|
767
|
+
const failure = expect(acquiring).rejects.toMatchObject({
|
|
768
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
769
|
+
});
|
|
770
|
+
const reconnecting = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
await probesFinished.promise;
|
|
774
|
+
await new Promise(resolve => {
|
|
775
|
+
setImmediate(resolve);
|
|
776
|
+
});
|
|
777
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
778
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
779
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
780
|
+
|
|
781
|
+
disconnectGate.resolve();
|
|
782
|
+
await failure;
|
|
783
|
+
await expect(reconnecting).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
784
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
785
|
+
await expect(
|
|
786
|
+
transport.call(uuid, 'Ping', { message: 'after-reconnect' })
|
|
787
|
+
).resolves.toMatchObject({
|
|
788
|
+
type: 'Success',
|
|
789
|
+
message: { message: 'ok' },
|
|
790
|
+
});
|
|
791
|
+
} finally {
|
|
792
|
+
disconnectGate.resolve();
|
|
793
|
+
await Promise.allSettled([failure, reconnecting]);
|
|
794
|
+
await transport.release(uuid, true);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
test.each(['ios', 'android'] as const)(
|
|
800
|
+
'disconnects after a first expected Protocol V2 probe miss while keeping it retryable on %s',
|
|
666
801
|
async platform => {
|
|
667
802
|
setPlatformOS(platform);
|
|
668
803
|
const { transport, uuid, device } = createHarness();
|
|
@@ -672,13 +807,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
672
807
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
673
808
|
});
|
|
674
809
|
|
|
675
|
-
expect(device.cancelConnection).
|
|
810
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
676
811
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
677
812
|
}
|
|
678
813
|
);
|
|
679
814
|
|
|
680
815
|
test.each(['ios', 'android'] as const)(
|
|
681
|
-
'
|
|
816
|
+
'disconnects after each expected Protocol V2 probe miss while keeping it retryable on %s',
|
|
682
817
|
async platform => {
|
|
683
818
|
setPlatformOS(platform);
|
|
684
819
|
const { transport, uuid, device } = createHarness();
|
|
@@ -691,7 +826,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
691
826
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
692
827
|
});
|
|
693
828
|
|
|
694
|
-
expect(device.cancelConnection).
|
|
829
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(2);
|
|
695
830
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
696
831
|
}
|
|
697
832
|
);
|
|
@@ -726,7 +861,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
726
861
|
);
|
|
727
862
|
|
|
728
863
|
test.each(['ios', 'android'] as const)(
|
|
729
|
-
'
|
|
864
|
+
'disconnects after a confirmed Protocol V2 probe miss on %s without reporting a bond error',
|
|
730
865
|
async platform => {
|
|
731
866
|
setPlatformOS(platform);
|
|
732
867
|
const { transport, uuid, device } = createHarness();
|
|
@@ -738,7 +873,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
738
873
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
739
874
|
});
|
|
740
875
|
|
|
741
|
-
expect(device.cancelConnection).
|
|
876
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
742
877
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
743
878
|
}
|
|
744
879
|
);
|
|
@@ -830,6 +965,118 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
830
965
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
|
|
831
966
|
);
|
|
832
967
|
|
|
968
|
+
test('preserves the unpaired error when iOS disconnects during the first V1 probe', async () => {
|
|
969
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
970
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce({
|
|
971
|
+
errorCode: 201,
|
|
972
|
+
iosErrorCode: 7,
|
|
973
|
+
reason: 'The specified device has disconnected from us.',
|
|
974
|
+
});
|
|
975
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
976
|
+
|
|
977
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
978
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
expect(probeProtocolV2).not.toHaveBeenCalled();
|
|
982
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
983
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test('preserves a native iOS disconnect during an expected Protocol V2 probe', async () => {
|
|
987
|
+
const { transport, uuid, writeCharacteristic, bleManager } = createHarness({
|
|
988
|
+
deviceName: 'Neo Test',
|
|
989
|
+
});
|
|
990
|
+
const nativeDisconnect = {
|
|
991
|
+
errorCode: 201,
|
|
992
|
+
iosErrorCode: 7,
|
|
993
|
+
reason: 'The specified device has disconnected from us.',
|
|
994
|
+
};
|
|
995
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce(nativeDisconnect);
|
|
996
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
997
|
+
|
|
998
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
999
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
1003
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
1004
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1005
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test('preserves a native iOS disconnect during a V2-first probe without falling back to V1', async () => {
|
|
1009
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
1010
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce({
|
|
1011
|
+
errorCode: 201,
|
|
1012
|
+
iosErrorCode: 7,
|
|
1013
|
+
reason: 'The specified device has disconnected from us.',
|
|
1014
|
+
});
|
|
1015
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
1016
|
+
|
|
1017
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toMatchObject({
|
|
1018
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
1022
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
1023
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test.each(['V1', 'V2'] as const)(
|
|
1027
|
+
'preserves terminal BLE failures from the %s probe without trying another protocol',
|
|
1028
|
+
async protocol => {
|
|
1029
|
+
for (const errorCode of [
|
|
1030
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
1031
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
1032
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
1033
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
1034
|
+
HardwareErrorCode.BleCharacteristicNotifyError,
|
|
1035
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
|
|
1036
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1037
|
+
]) {
|
|
1038
|
+
const { transport, uuid } = createHarness();
|
|
1039
|
+
const error = ERRORS.TypedError(errorCode);
|
|
1040
|
+
const call = jest
|
|
1041
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
1042
|
+
.mockRejectedValue(error);
|
|
1043
|
+
const otherProbe = jest.spyOn(
|
|
1044
|
+
transport as any,
|
|
1045
|
+
protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1'
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).rejects.toBe(error);
|
|
1049
|
+
|
|
1050
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
1051
|
+
expect(otherProbe).not.toHaveBeenCalled();
|
|
1052
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
test.each(['V1', 'V2'] as const)(
|
|
1058
|
+
'still falls back after a silent %s probe timeout',
|
|
1059
|
+
async protocol => {
|
|
1060
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1061
|
+
jest
|
|
1062
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
1063
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
|
|
1064
|
+
const otherProbe = jest
|
|
1065
|
+
.spyOn(transport as any, protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1')
|
|
1066
|
+
.mockResolvedValue(true);
|
|
1067
|
+
|
|
1068
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).resolves.toEqual({
|
|
1069
|
+
uuid,
|
|
1070
|
+
protocolType: protocol === 'V1' ? 'V2' : 'V1',
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
expect(otherProbe).toHaveBeenCalledTimes(1);
|
|
1074
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1075
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1076
|
+
await transport.release(uuid, true);
|
|
1077
|
+
}
|
|
1078
|
+
);
|
|
1079
|
+
|
|
833
1080
|
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
834
1081
|
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
835
1082
|
const probeProtocolV1 = jest
|
|
@@ -850,57 +1097,621 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
850
1097
|
await transport.release(uuid, true);
|
|
851
1098
|
});
|
|
852
1099
|
|
|
853
|
-
test('
|
|
1100
|
+
test('does not renegotiate an already usable MTU during acquire', async () => {
|
|
854
1101
|
const { transport, uuid, device } = createHarness();
|
|
855
|
-
const mtuError = new Error('MTU refresh failed');
|
|
856
|
-
device.requestMTU.mockRejectedValueOnce(mtuError);
|
|
857
1102
|
|
|
858
1103
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
859
1104
|
uuid,
|
|
860
1105
|
protocolType: 'V2',
|
|
861
1106
|
});
|
|
862
|
-
expect(device.requestMTU).
|
|
1107
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
863
1108
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
864
1109
|
await transport.release(uuid, true);
|
|
865
1110
|
});
|
|
866
1111
|
|
|
867
|
-
test('
|
|
1112
|
+
test('requests a low MTU once before protocol probing', async () => {
|
|
868
1113
|
const { transport, uuid, device } = createHarness();
|
|
869
1114
|
device.mtu = 23;
|
|
870
|
-
device.requestMTU
|
|
871
|
-
.
|
|
872
|
-
.
|
|
873
|
-
|
|
874
|
-
device.mtu = 247;
|
|
875
|
-
return Promise.resolve(device);
|
|
876
|
-
});
|
|
1115
|
+
device.requestMTU.mockImplementationOnce(() => {
|
|
1116
|
+
device.mtu = 247;
|
|
1117
|
+
return Promise.resolve(device);
|
|
1118
|
+
});
|
|
877
1119
|
|
|
878
1120
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
879
1121
|
uuid,
|
|
880
1122
|
protocolType: 'V2',
|
|
881
1123
|
});
|
|
882
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1124
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1125
|
+
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
1126
|
+
247,
|
|
1127
|
+
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
1128
|
+
);
|
|
883
1129
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
884
1130
|
await transport.release(uuid, true);
|
|
885
1131
|
});
|
|
886
1132
|
|
|
887
|
-
test('continues with a low
|
|
1133
|
+
test('continues with a low MTU when the single negotiation fails', async () => {
|
|
888
1134
|
const { transport, uuid, device } = createHarness();
|
|
889
1135
|
device.mtu = 23;
|
|
890
|
-
device.requestMTU
|
|
891
|
-
.mockResolvedValueOnce(device)
|
|
892
|
-
.mockResolvedValueOnce(device)
|
|
893
|
-
.mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
|
|
1136
|
+
device.requestMTU.mockRejectedValueOnce(new Error('MTU negotiation failed'));
|
|
894
1137
|
|
|
895
1138
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
896
1139
|
uuid,
|
|
897
1140
|
protocolType: 'V2',
|
|
898
1141
|
});
|
|
899
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1142
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
900
1143
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
901
1144
|
await transport.release(uuid, true);
|
|
902
1145
|
});
|
|
903
1146
|
|
|
1147
|
+
test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
|
|
1148
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1149
|
+
device.mtu = 23;
|
|
1150
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1151
|
+
|
|
1152
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1153
|
+
uuid,
|
|
1154
|
+
protocolType: 'V2',
|
|
1155
|
+
});
|
|
1156
|
+
const transactionId = device.requestMTU.mock.calls[0]?.[1];
|
|
1157
|
+
expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
|
|
1158
|
+
expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
|
|
1159
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1160
|
+
expect(device.connect).toHaveBeenCalledWith(
|
|
1161
|
+
expect.objectContaining({ timeout: expect.any(Number) })
|
|
1162
|
+
);
|
|
1163
|
+
expect(device.connect.mock.calls.at(-1)?.[0]).not.toHaveProperty('requestMTU');
|
|
1164
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1165
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1166
|
+
await transport.release(uuid, true);
|
|
1167
|
+
}, 10_000);
|
|
1168
|
+
|
|
1169
|
+
test('keeps the iOS link when the MTU-timeout reconnect reports it is still connected', async () => {
|
|
1170
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1171
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1172
|
+
device.mtu = 23;
|
|
1173
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1174
|
+
device.connect.mockRejectedValueOnce(
|
|
1175
|
+
Object.assign(new BleErrorMock('Device already connected'), {
|
|
1176
|
+
errorCode: BleErrorCode.DeviceAlreadyConnected,
|
|
1177
|
+
})
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1181
|
+
uuid,
|
|
1182
|
+
protocolType: 'V2',
|
|
1183
|
+
});
|
|
1184
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
1185
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1186
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1187
|
+
expect(bleManager.destroy).not.toHaveBeenCalled();
|
|
1188
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1189
|
+
await transport.release(uuid, true);
|
|
1190
|
+
}, 10_000);
|
|
1191
|
+
|
|
1192
|
+
test('reconnects after an iOS MTU timeout when cancelling the timed-out link fails', async () => {
|
|
1193
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1194
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1195
|
+
device.mtu = 23;
|
|
1196
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1197
|
+
device.cancelConnection.mockRejectedValueOnce(
|
|
1198
|
+
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1199
|
+
errorCode: BleErrorCode.OperationCancelled,
|
|
1200
|
+
})
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1204
|
+
uuid,
|
|
1205
|
+
protocolType: 'V2',
|
|
1206
|
+
});
|
|
1207
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1208
|
+
expect(device.connect.mock.calls[0]?.[0]).not.toHaveProperty('requestMTU');
|
|
1209
|
+
expect(bleManager.destroy).not.toHaveBeenCalled();
|
|
1210
|
+
await transport.release(uuid, true);
|
|
1211
|
+
}, 10_000);
|
|
1212
|
+
|
|
1213
|
+
test('maps a stale bond from the iOS MTU-timeout reconnect', async () => {
|
|
1214
|
+
const { BleError: BleErrorMock } = jest.requireMock('react-native-ble-plx');
|
|
1215
|
+
const { transport, uuid, device } = createHarness();
|
|
1216
|
+
device.mtu = 23;
|
|
1217
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1218
|
+
device.connect.mockRejectedValueOnce(
|
|
1219
|
+
Object.assign(new BleErrorMock('Peer removed pairing information'), {
|
|
1220
|
+
errorCode: 200,
|
|
1221
|
+
iosErrorCode: 14,
|
|
1222
|
+
})
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1226
|
+
errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
1227
|
+
});
|
|
1228
|
+
await transport.release(uuid, true).catch(() => undefined);
|
|
1229
|
+
}, 10_000);
|
|
1230
|
+
|
|
1231
|
+
test('bounds a hung iOS MTU-timeout teardown and resets the BLE manager without reconnecting', async () => {
|
|
1232
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1233
|
+
device.mtu = 23;
|
|
1234
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1235
|
+
device.cancelConnection.mockImplementationOnce(() => new Promise(() => {}));
|
|
1236
|
+
|
|
1237
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1238
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1239
|
+
message: expect.stringContaining('BLE MTU cleanup timed out'),
|
|
1240
|
+
});
|
|
1241
|
+
expect(bleManager.destroy).toHaveBeenCalledTimes(1);
|
|
1242
|
+
expect(transport.blePlxManager).toBeUndefined();
|
|
1243
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1244
|
+
expect((transport as any).lifecycleOperations.has(uuid)).toBe(false);
|
|
1245
|
+
}, 15_000);
|
|
1246
|
+
|
|
1247
|
+
test('does not reconnect after a hung iOS MTU-timeout teardown when the manager reset is already pending', async () => {
|
|
1248
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1249
|
+
device.mtu = 23;
|
|
1250
|
+
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1251
|
+
device.cancelConnection.mockImplementationOnce(() => new Promise(() => {}));
|
|
1252
|
+
// Another device's reset is still draining, so this teardown timeout cannot swap the manager.
|
|
1253
|
+
jest.spyOn(transport as any, 'resetPlxManager').mockImplementation(() => undefined);
|
|
1254
|
+
|
|
1255
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1256
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1257
|
+
});
|
|
1258
|
+
expect(transport.blePlxManager).toBe(bleManager);
|
|
1259
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1260
|
+
}, 15_000);
|
|
1261
|
+
|
|
1262
|
+
test('does not request MTU again after connect falls back without requestMTU', async () => {
|
|
1263
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1264
|
+
const { transport, uuid, device } = createHarness();
|
|
1265
|
+
device.mtu = 23;
|
|
1266
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
1267
|
+
device.connect
|
|
1268
|
+
.mockRejectedValueOnce(
|
|
1269
|
+
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1270
|
+
errorCode: BleErrorCode.OperationCancelled,
|
|
1271
|
+
})
|
|
1272
|
+
)
|
|
1273
|
+
.mockResolvedValue(device);
|
|
1274
|
+
|
|
1275
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1276
|
+
uuid,
|
|
1277
|
+
protocolType: 'V2',
|
|
1278
|
+
});
|
|
1279
|
+
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
1280
|
+
expect(device.connect.mock.calls[0][0]).toEqual(expect.objectContaining({ requestMTU: 247 }));
|
|
1281
|
+
expect(device.connect.mock.calls[1][0]).not.toHaveProperty('requestMTU');
|
|
1282
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1283
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1284
|
+
await transport.release(uuid, true);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test('keeps the iOS connect chain unchanged', async () => {
|
|
1288
|
+
const { transport, uuid, device } = createHarness();
|
|
1289
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
1290
|
+
|
|
1291
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1292
|
+
uuid,
|
|
1293
|
+
protocolType: 'V2',
|
|
1294
|
+
});
|
|
1295
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1296
|
+
requestMTU: 247,
|
|
1297
|
+
timeout: expect.any(Number),
|
|
1298
|
+
refreshGatt: 'OnConnected',
|
|
1299
|
+
});
|
|
1300
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1301
|
+
await transport.release(uuid, true);
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
describe('Android MTU before service discovery', () => {
|
|
1305
|
+
beforeEach(() => {
|
|
1306
|
+
setPlatformOS('android');
|
|
1307
|
+
jest.useFakeTimers({
|
|
1308
|
+
doNotFake: ['setImmediate', 'queueMicrotask', 'nextTick', 'performance'],
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
afterEach(() => {
|
|
1313
|
+
jest.clearAllTimers();
|
|
1314
|
+
jest.useRealTimers();
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
const advanceUntil = async (condition: () => boolean, budgetMs = 60_000) => {
|
|
1318
|
+
for (let elapsed = 0; !condition(); elapsed += 50) {
|
|
1319
|
+
if (elapsed >= budgetMs) throw new Error(`fake time exhausted after ${budgetMs}ms`);
|
|
1320
|
+
jest.advanceTimersByTime(50);
|
|
1321
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1322
|
+
await new Promise(resolve => {
|
|
1323
|
+
setImmediate(resolve);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
const settle = async <T>(promise: Promise<T>, budgetMs?: number): Promise<T> => {
|
|
1329
|
+
let done = false;
|
|
1330
|
+
promise.then(
|
|
1331
|
+
() => {
|
|
1332
|
+
done = true;
|
|
1333
|
+
},
|
|
1334
|
+
() => {
|
|
1335
|
+
done = true;
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
await advanceUntil(() => done, budgetMs);
|
|
1339
|
+
return promise;
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* A device whose LE link is only up between connect() and cancelConnection(), so the
|
|
1344
|
+
* transport has to reconnect after every drop instead of the test flipping the link.
|
|
1345
|
+
*/
|
|
1346
|
+
const reconnectingDevice = (device: any) => {
|
|
1347
|
+
const link = { connected: false };
|
|
1348
|
+
device.isConnected.mockImplementation(() => Promise.resolve(link.connected));
|
|
1349
|
+
device.connect = jest.fn(() => {
|
|
1350
|
+
link.connected = true;
|
|
1351
|
+
return Promise.resolve(device);
|
|
1352
|
+
});
|
|
1353
|
+
device.cancelConnection.mockImplementation(() => {
|
|
1354
|
+
link.connected = false;
|
|
1355
|
+
return Promise.resolve();
|
|
1356
|
+
});
|
|
1357
|
+
return link;
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
/** requestMTU resolves a fresh Device snapshot, as react-native-ble-plx does. */
|
|
1361
|
+
const negotiatedSnapshot = (device: any, mtu: number) => {
|
|
1362
|
+
const negotiated = { ...device, mtu };
|
|
1363
|
+
device.requestMTU.mockImplementation(() => Promise.resolve(negotiated));
|
|
1364
|
+
return negotiated;
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
test('negotiates the MTU after a bare connect and before service discovery, adopting the returned device', async () => {
|
|
1368
|
+
const { transport, uuid, device } = createHarness();
|
|
1369
|
+
reconnectingDevice(device);
|
|
1370
|
+
device.mtu = 23;
|
|
1371
|
+
const negotiated = negotiatedSnapshot(device, 247);
|
|
1372
|
+
|
|
1373
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1374
|
+
uuid,
|
|
1375
|
+
protocolType: 'V2',
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1379
|
+
expect(device.connect).toHaveBeenCalledWith({ timeout: expect.any(Number) });
|
|
1380
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1381
|
+
const [connectOrder] = device.connect.mock.invocationCallOrder;
|
|
1382
|
+
const [mtuOrder] = device.requestMTU.mock.invocationCallOrder;
|
|
1383
|
+
const [discoveryOrder] = resolveCharacteristics.mock.invocationCallOrder;
|
|
1384
|
+
expect(connectOrder).toBeLessThan(mtuOrder);
|
|
1385
|
+
expect(mtuOrder).toBeLessThan(discoveryOrder);
|
|
1386
|
+
const cached = (transport as any).getCachedTransport(uuid);
|
|
1387
|
+
expect(cached.device).toBe(negotiated);
|
|
1388
|
+
expect(cached.mtuSize).toBe(247);
|
|
1389
|
+
await settle(transport.release(uuid, true));
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
test.each([
|
|
1393
|
+
{ expectedProtocol: 'V2' as const, label: 'an expected Protocol V2 device' },
|
|
1394
|
+
{ expectedProtocol: 'V1' as const, label: 'an expected Protocol V1 device' },
|
|
1395
|
+
{ expectedProtocol: undefined, label: 'a device of unknown protocol' },
|
|
1396
|
+
])('drops a link that stays at the default MTU for $label', async ({ expectedProtocol }) => {
|
|
1397
|
+
const { transport, uuid, device } = createHarness();
|
|
1398
|
+
device.mtu = 23;
|
|
1399
|
+
|
|
1400
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol }))).rejects.toMatchObject({
|
|
1401
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1402
|
+
});
|
|
1403
|
+
expect((transport as any).resolveCharacteristics).not.toHaveBeenCalled();
|
|
1404
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
test.each([
|
|
1408
|
+
['never completes', () => new Promise(() => {})],
|
|
1409
|
+
['completes at the default MTU', undefined],
|
|
1410
|
+
['is rejected', () => Promise.reject(new Error('MTU request failed'))],
|
|
1411
|
+
])(
|
|
1412
|
+
'a second consecutive MTU failure that %s trips the wedged-link guard',
|
|
1413
|
+
async (_label, requestMTU) => {
|
|
1414
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1415
|
+
device.mtu = 23;
|
|
1416
|
+
if (requestMTU) device.requestMTU.mockImplementation(requestMTU);
|
|
1417
|
+
|
|
1418
|
+
await expect(
|
|
1419
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1420
|
+
).rejects.toMatchObject({
|
|
1421
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1422
|
+
});
|
|
1423
|
+
await expect(
|
|
1424
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1425
|
+
).rejects.toMatchObject({
|
|
1426
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
1427
|
+
message: expect.stringContaining(BLE_SETUP_WEDGED_MESSAGE),
|
|
1428
|
+
});
|
|
1429
|
+
expect(bleManager.destroy).toHaveBeenCalled();
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
test('stop() during the link-drop wait releases it promptly', async () => {
|
|
1434
|
+
const { transport, uuid, device } = createHarness();
|
|
1435
|
+
device.mtu = 23;
|
|
1436
|
+
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1437
|
+
|
|
1438
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error);
|
|
1439
|
+
// The link-drop teardown cancels the device connection before the wait starts.
|
|
1440
|
+
await advanceUntil(() => device.cancelConnection.mock.calls.length > 0);
|
|
1441
|
+
await advanceUntil(() => false, 1000).catch(() => undefined);
|
|
1442
|
+
await settle(transport.stop(), 1000);
|
|
1443
|
+
await expect(settle(acquiring, 1000)).resolves.toBeInstanceOf(Error);
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
test.each([
|
|
1447
|
+
{
|
|
1448
|
+
label: 'a missing OneKey service',
|
|
1449
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound),
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
label: 'a missing characteristic',
|
|
1453
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound),
|
|
1454
|
+
},
|
|
1455
|
+
{
|
|
1456
|
+
label: 'a mis-typed characteristic',
|
|
1457
|
+
error: () =>
|
|
1458
|
+
ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable'),
|
|
1459
|
+
},
|
|
1460
|
+
])(
|
|
1461
|
+
'marks the endpoint on $label and refreshes the GATT table on the next connect, before the MTU exchange',
|
|
1462
|
+
async ({ error }) => {
|
|
1463
|
+
const { transport, uuid, device } = createHarness();
|
|
1464
|
+
const link = reconnectingDevice(device);
|
|
1465
|
+
device.mtu = 23;
|
|
1466
|
+
negotiatedSnapshot(device, 247);
|
|
1467
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1468
|
+
resolveCharacteristics.mockImplementationOnce(() => Promise.reject(error()));
|
|
1469
|
+
|
|
1470
|
+
await expect(
|
|
1471
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1472
|
+
).rejects.toBeDefined();
|
|
1473
|
+
expect(link.connected).toBe(true);
|
|
1474
|
+
|
|
1475
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1476
|
+
uuid,
|
|
1477
|
+
protocolType: 'V2',
|
|
1478
|
+
});
|
|
1479
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1480
|
+
timeout: expect.any(Number),
|
|
1481
|
+
refreshGatt: 'OnConnected',
|
|
1482
|
+
});
|
|
1483
|
+
// The stale table is only refreshed through a connect, so the live link is dropped first.
|
|
1484
|
+
const connectOrders: number[] = device.connect.mock.invocationCallOrder;
|
|
1485
|
+
expect(device.cancelConnection.mock.invocationCallOrder[0]).toBeLessThan(
|
|
1486
|
+
connectOrders[connectOrders.length - 1]
|
|
1487
|
+
);
|
|
1488
|
+
const discoveryOrders = resolveCharacteristics.mock.invocationCallOrder;
|
|
1489
|
+
const mtuOrders: number[] = device.requestMTU.mock.invocationCallOrder;
|
|
1490
|
+
expect(discoveryOrders[discoveryOrders.length - 1]).toBeLessThan(
|
|
1491
|
+
mtuOrders[mtuOrders.length - 1]
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
// The refresh is spent once the table resolved through a refreshed connect.
|
|
1495
|
+
await settle(transport.release(uuid, true));
|
|
1496
|
+
link.connected = false;
|
|
1497
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1498
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1499
|
+
await settle(transport.release(uuid, true));
|
|
1500
|
+
}
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
test('keeps the refresh marker when the refresh connect fell back without refreshGatt', async () => {
|
|
1504
|
+
const { transport, uuid, device } = createHarness();
|
|
1505
|
+
const link = reconnectingDevice(device);
|
|
1506
|
+
device.mtu = 23;
|
|
1507
|
+
negotiatedSnapshot(device, 247);
|
|
1508
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1509
|
+
resolveCharacteristics.mockImplementationOnce(() =>
|
|
1510
|
+
Promise.reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound))
|
|
1511
|
+
);
|
|
1512
|
+
await expect(
|
|
1513
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1514
|
+
).rejects.toBeDefined();
|
|
1515
|
+
|
|
1516
|
+
// The fallback connect carries no refreshGatt.
|
|
1517
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1518
|
+
device.connect.mockImplementationOnce(() => Promise.reject(cancelled));
|
|
1519
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1520
|
+
uuid,
|
|
1521
|
+
protocolType: 'V2',
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
await settle(transport.release(uuid, true));
|
|
1525
|
+
link.connected = false;
|
|
1526
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1527
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1528
|
+
timeout: expect.any(Number),
|
|
1529
|
+
refreshGatt: 'OnConnected',
|
|
1530
|
+
});
|
|
1531
|
+
await settle(transport.release(uuid, true));
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
test('does not drop a link it has just connected with refreshGatt', async () => {
|
|
1535
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1536
|
+
bleManager.devices.mockResolvedValue([]);
|
|
1537
|
+
const connectToDevice = jest.fn(() => Promise.resolve(device));
|
|
1538
|
+
Object.assign(bleManager, { connectToDevice });
|
|
1539
|
+
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1540
|
+
|
|
1541
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1542
|
+
uuid,
|
|
1543
|
+
protocolType: 'V2',
|
|
1544
|
+
});
|
|
1545
|
+
expect(connectToDevice).toHaveBeenCalledTimes(1);
|
|
1546
|
+
expect(connectToDevice).toHaveBeenCalledWith(uuid, {
|
|
1547
|
+
timeout: expect.any(Number),
|
|
1548
|
+
refreshGatt: 'OnConnected',
|
|
1549
|
+
});
|
|
1550
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1551
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1552
|
+
await settle(transport.release(uuid, true));
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
test('still refreshes after a connect-by-id refresh fell back without refreshGatt', async () => {
|
|
1556
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1557
|
+
const link = reconnectingDevice(device);
|
|
1558
|
+
bleManager.devices.mockResolvedValue([]);
|
|
1559
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1560
|
+
const connectToDevice = jest
|
|
1561
|
+
.fn()
|
|
1562
|
+
.mockImplementationOnce(() => Promise.reject(cancelled))
|
|
1563
|
+
.mockImplementation(() => {
|
|
1564
|
+
link.connected = true;
|
|
1565
|
+
return Promise.resolve(device);
|
|
1566
|
+
});
|
|
1567
|
+
Object.assign(bleManager, { connectToDevice });
|
|
1568
|
+
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1569
|
+
|
|
1570
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1571
|
+
uuid,
|
|
1572
|
+
protocolType: 'V2',
|
|
1573
|
+
});
|
|
1574
|
+
expect(connectToDevice).toHaveBeenLastCalledWith(uuid, { timeout: expect.any(Number) });
|
|
1575
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1576
|
+
timeout: expect.any(Number),
|
|
1577
|
+
refreshGatt: 'OnConnected',
|
|
1578
|
+
});
|
|
1579
|
+
expect((transport as any).androidGattCacheRefreshes.has(uuid)).toBe(false);
|
|
1580
|
+
await settle(transport.release(uuid, true));
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
test.each([
|
|
1584
|
+
'Cannot write client characteristic config descriptor',
|
|
1585
|
+
'Cannot find client characteristic config descriptor',
|
|
1586
|
+
'The handle is invalid',
|
|
1587
|
+
'Writing is not permitted',
|
|
1588
|
+
])('arms a GATT refresh from the notify failure "%s"', async reason => {
|
|
1589
|
+
const harness = createHarness();
|
|
1590
|
+
const { transport, uuid, device } = harness;
|
|
1591
|
+
reconnectingDevice(device);
|
|
1592
|
+
device.mtu = 23;
|
|
1593
|
+
negotiatedSnapshot(device, 247);
|
|
1594
|
+
|
|
1595
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1596
|
+
harness.emitMonitorError(Object.assign(new Error('notify failed'), { reason }));
|
|
1597
|
+
|
|
1598
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1599
|
+
uuid,
|
|
1600
|
+
protocolType: 'V2',
|
|
1601
|
+
});
|
|
1602
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1603
|
+
timeout: expect.any(Number),
|
|
1604
|
+
refreshGatt: 'OnConnected',
|
|
1605
|
+
});
|
|
1606
|
+
await settle(transport.release(uuid, true));
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
test('arms a GATT refresh from a notify failure while notifications are being enabled', async () => {
|
|
1610
|
+
const { transport, uuid, device } = createHarness({
|
|
1611
|
+
monitorError: Object.assign(new Error('notify failed'), {
|
|
1612
|
+
reason: 'Cannot write client characteristic config descriptor',
|
|
1613
|
+
}),
|
|
1614
|
+
});
|
|
1615
|
+
const link = reconnectingDevice(device);
|
|
1616
|
+
device.mtu = 23;
|
|
1617
|
+
negotiatedSnapshot(device, 247);
|
|
1618
|
+
|
|
1619
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1620
|
+
await settle(transport.release(uuid, true));
|
|
1621
|
+
link.connected = false;
|
|
1622
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1623
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1624
|
+
timeout: expect.any(Number),
|
|
1625
|
+
refreshGatt: 'OnConnected',
|
|
1626
|
+
});
|
|
1627
|
+
await settle(transport.release(uuid, true));
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
test('keeps the GATT refresh for a firmware-install reconnect', async () => {
|
|
1631
|
+
const { transport, uuid, device } = createHarness();
|
|
1632
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
1633
|
+
reconnectingDevice(device);
|
|
1634
|
+
|
|
1635
|
+
await expect(
|
|
1636
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true }))
|
|
1637
|
+
).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
1638
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1639
|
+
timeout: expect.any(Number),
|
|
1640
|
+
refreshGatt: 'OnConnected',
|
|
1641
|
+
});
|
|
1642
|
+
await settle(transport.release(uuid, true));
|
|
1643
|
+
});
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
test('spends one Initialize wake on the detection after a fully silent one', async () => {
|
|
1647
|
+
setPlatformOS('android');
|
|
1648
|
+
const { transport, uuid } = createHarness();
|
|
1649
|
+
const probes = transport as any;
|
|
1650
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1651
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1652
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1653
|
+
|
|
1654
|
+
// Nothing has gone silent yet, so the device keeps whatever session it has.
|
|
1655
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1656
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1657
|
+
|
|
1658
|
+
// The previous detection answered on no protocol, which is what a sleeping Classic
|
|
1659
|
+
// looks like, so this one wakes on the fresh link before probing.
|
|
1660
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1661
|
+
expect(callProtocolV1).toHaveBeenCalledTimes(1);
|
|
1662
|
+
expect(callProtocolV1.mock.calls[0]?.[1]).toBe('Initialize');
|
|
1663
|
+
|
|
1664
|
+
// Still silent: the wake is not repeated, so a device that is simply away is not
|
|
1665
|
+
// pushed into a fresh wallet session on every poll.
|
|
1666
|
+
callProtocolV1.mockClear();
|
|
1667
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1668
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
test('does not send the Initialize wake before a V2-first probe', async () => {
|
|
1672
|
+
setPlatformOS('android');
|
|
1673
|
+
const { transport, uuid } = createHarness();
|
|
1674
|
+
const probes = transport as any;
|
|
1675
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1676
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1677
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1678
|
+
|
|
1679
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toBeDefined();
|
|
1680
|
+
// Silent now, but the next detection probes V2 first: a late V1 reply would land on
|
|
1681
|
+
// the V2 probe, so no Initialize is sent.
|
|
1682
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toBeDefined();
|
|
1683
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
test('does not send the Initialize wake on iOS', async () => {
|
|
1687
|
+
const { transport, uuid } = createHarness();
|
|
1688
|
+
const probes = transport as any;
|
|
1689
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1690
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1691
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1692
|
+
|
|
1693
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1694
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1695
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
test('an unanswered Initialize wake settles the acquire and frees the lifecycle lock', async () => {
|
|
1699
|
+
setPlatformOS('android');
|
|
1700
|
+
const harness = createHarness();
|
|
1701
|
+
const { transport, uuid } = harness;
|
|
1702
|
+
const probes = transport as any;
|
|
1703
|
+
harness.setShouldRespond(false);
|
|
1704
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1705
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1706
|
+
|
|
1707
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1708
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1709
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1710
|
+
});
|
|
1711
|
+
await transport.disconnect(uuid);
|
|
1712
|
+
await transport.stop();
|
|
1713
|
+
}, 10_000);
|
|
1714
|
+
|
|
904
1715
|
test('accepts a stable low MTU without the delayed refresh loop', async () => {
|
|
905
1716
|
const { transport, uuid, device } = createHarness();
|
|
906
1717
|
device.mtu = 185;
|
|
@@ -909,7 +1720,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
909
1720
|
uuid,
|
|
910
1721
|
protocolType: 'V2',
|
|
911
1722
|
});
|
|
912
|
-
expect(device.requestMTU).
|
|
1723
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
913
1724
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(185);
|
|
914
1725
|
await transport.release(uuid, true);
|
|
915
1726
|
});
|
|
@@ -922,7 +1733,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
922
1733
|
uuid,
|
|
923
1734
|
protocolType: 'V2',
|
|
924
1735
|
});
|
|
925
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1736
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
926
1737
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBeUndefined();
|
|
927
1738
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
928
1739
|
await transport.release(uuid, true);
|
|
@@ -940,7 +1751,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
940
1751
|
});
|
|
941
1752
|
|
|
942
1753
|
await expect(transport.call(uuid, 'FileWrite', {})).resolves.toBeDefined();
|
|
943
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1754
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(2);
|
|
944
1755
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
945
1756
|
writesBeforeFileWrite + 1
|
|
946
1757
|
);
|
|
@@ -957,7 +1768,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
957
1768
|
await expect(transport.call(uuid, 'FileWrite', {})).rejects.toMatchObject({
|
|
958
1769
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
959
1770
|
});
|
|
960
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1771
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(2);
|
|
961
1772
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(writesBeforeFileWrite);
|
|
962
1773
|
await transport.release(uuid, true);
|
|
963
1774
|
});
|
|
@@ -1011,6 +1822,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1011
1822
|
expect(notifySubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
|
|
1012
1823
|
expect(notifySubscriptionRemovers[1]).toHaveBeenCalledTimes(1);
|
|
1013
1824
|
expect(bleManager.cancelTransaction).toHaveBeenCalled();
|
|
1825
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
1014
1826
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1015
1827
|
});
|
|
1016
1828
|
|
|
@@ -1065,6 +1877,115 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1065
1877
|
});
|
|
1066
1878
|
});
|
|
1067
1879
|
|
|
1880
|
+
test.each(['ios', 'android'] as const)(
|
|
1881
|
+
'disconnects after an active V2 timeout and reconnects even if a listener fails on %s',
|
|
1882
|
+
async platform => {
|
|
1883
|
+
setPlatformOS(platform);
|
|
1884
|
+
const harness = createHarness();
|
|
1885
|
+
const { transport, uuid, device, bleManager, sentSeqs, emitter } = harness;
|
|
1886
|
+
const disconnected = jest.fn(() => {
|
|
1887
|
+
throw new Error('Disconnect listener failed');
|
|
1888
|
+
});
|
|
1889
|
+
emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnected);
|
|
1890
|
+
let connected = true;
|
|
1891
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
1892
|
+
device.connect = jest.fn(() => {
|
|
1893
|
+
connected = true;
|
|
1894
|
+
return Promise.resolve(device);
|
|
1895
|
+
});
|
|
1896
|
+
bleManager.cancelDeviceConnection.mockImplementation(() => {
|
|
1897
|
+
connected = false;
|
|
1898
|
+
return Promise.resolve();
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1902
|
+
harness.setShouldRespond(false);
|
|
1903
|
+
await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
|
|
1904
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1908
|
+
expect(connected).toBe(false);
|
|
1909
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1910
|
+
expect(disconnected).toHaveBeenCalledTimes(1);
|
|
1911
|
+
expect(disconnected).toHaveBeenCalledWith({
|
|
1912
|
+
id: uuid,
|
|
1913
|
+
connectId: uuid,
|
|
1914
|
+
name: device.name,
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
harness.setShouldRespond(true);
|
|
1918
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1919
|
+
await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
|
|
1920
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1921
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
1922
|
+
await transport.release(uuid, true);
|
|
1923
|
+
}
|
|
1924
|
+
);
|
|
1925
|
+
|
|
1926
|
+
test('does not disconnect an active Protocol V2 call when a queued call times out', async () => {
|
|
1927
|
+
const harness = createHarness();
|
|
1928
|
+
const { transport, uuid, bleManager } = harness;
|
|
1929
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1930
|
+
harness.setShouldRespond(false);
|
|
1931
|
+
const active = transport.call(uuid, 'Ping', {}, { timeoutMs: 1000 }).catch(error => error);
|
|
1932
|
+
await new Promise(resolve => {
|
|
1933
|
+
setImmediate(resolve);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
try {
|
|
1937
|
+
await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
|
|
1938
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1939
|
+
});
|
|
1940
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1941
|
+
expect(transport.getProtocolType(uuid)).toBe('V2');
|
|
1942
|
+
} finally {
|
|
1943
|
+
await transport.disconnect(uuid);
|
|
1944
|
+
await active;
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
test('does not disconnect a newer acquire when V2 timeout cleanup waits for the lifecycle lock', async () => {
|
|
1949
|
+
const harness = createHarness();
|
|
1950
|
+
const { transport, uuid, bleManager } = harness;
|
|
1951
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1952
|
+
const lifecycle = transport as unknown as {
|
|
1953
|
+
runLifecycleOperation(id: string, operation: () => Promise<void>): Promise<void>;
|
|
1954
|
+
releaseNative(id: string, onclose: boolean): Promise<boolean>;
|
|
1955
|
+
};
|
|
1956
|
+
const gate = createDeferred<void>();
|
|
1957
|
+
const holding = lifecycle.runLifecycleOperation(uuid, () => gate.promise);
|
|
1958
|
+
const reacquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1959
|
+
const invalidated = createDeferred<void>();
|
|
1960
|
+
const releaseNative = lifecycle.releaseNative.bind(lifecycle);
|
|
1961
|
+
jest.spyOn(lifecycle, 'releaseNative').mockImplementationOnce(async (id, onclose) => {
|
|
1962
|
+
const result = await releaseNative(id, onclose);
|
|
1963
|
+
invalidated.resolve();
|
|
1964
|
+
return result;
|
|
1965
|
+
});
|
|
1966
|
+
harness.setShouldRespond(false);
|
|
1967
|
+
const failure = expect(
|
|
1968
|
+
transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })
|
|
1969
|
+
).rejects.toMatchObject({
|
|
1970
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
try {
|
|
1974
|
+
await invalidated.promise;
|
|
1975
|
+
harness.setShouldRespond(true);
|
|
1976
|
+
gate.resolve();
|
|
1977
|
+
await holding;
|
|
1978
|
+
await reacquiring;
|
|
1979
|
+
await failure;
|
|
1980
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1981
|
+
await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
|
|
1982
|
+
} finally {
|
|
1983
|
+
gate.resolve();
|
|
1984
|
+
await Promise.allSettled([holding, reacquiring, failure]);
|
|
1985
|
+
await transport.release(uuid, true);
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1068
1989
|
test('retains the sequence cursor when a new monitor generation is acquired', async () => {
|
|
1069
1990
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
1070
1991
|
|
|
@@ -1117,9 +2038,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1117
2038
|
test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
|
|
1118
2039
|
setPlatformOS('android');
|
|
1119
2040
|
const { transport, uuid, device } = createHarness();
|
|
2041
|
+
device.mtu = 23;
|
|
2042
|
+
device.requestMTU.mockImplementationOnce(() => {
|
|
2043
|
+
device.mtu = 517;
|
|
2044
|
+
return Promise.resolve(device);
|
|
2045
|
+
});
|
|
1120
2046
|
|
|
1121
2047
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1122
|
-
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
2048
|
+
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
2049
|
+
517,
|
|
2050
|
+
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
2051
|
+
);
|
|
1123
2052
|
|
|
1124
2053
|
await transport.call(uuid, 'FileWrite', {});
|
|
1125
2054
|
await transport.call(uuid, 'FileWrite', {});
|