@onekeyfe/hd-transport-react-native 1.2.2-alpha.11 → 1.2.2-alpha.111
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 +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +339 -74
- 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 +343 -3
- package/src/__tests__/protocolV2Link.test.ts +475 -8
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/bleStaleBond.ts +3 -0
- package/src/index.ts +280 -70
|
@@ -4,7 +4,7 @@ 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,
|
|
@@ -28,7 +28,7 @@ jest.mock('react-native-ble-plx', () => ({
|
|
|
28
28
|
BleError: class BleError extends Error {},
|
|
29
29
|
BleErrorCode: {
|
|
30
30
|
DeviceAlreadyConnected: 203,
|
|
31
|
-
DeviceDisconnected:
|
|
31
|
+
DeviceDisconnected: 201,
|
|
32
32
|
DeviceMTUChangeFailed: 206,
|
|
33
33
|
OperationCancelled: 2,
|
|
34
34
|
CharacteristicNotFound: 404,
|
|
@@ -360,6 +360,186 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
360
360
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
+
test.each(['V1', 'V2'] as const)(
|
|
364
|
+
'waits for Android bonding before connecting the %s GATT link',
|
|
365
|
+
async protocol => {
|
|
366
|
+
setPlatformOS('android');
|
|
367
|
+
const { transport, uuid, device } = protocol === 'V1' ? createV1Harness() : createHarness();
|
|
368
|
+
const operationOrder: string[] = [];
|
|
369
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
370
|
+
const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
|
|
371
|
+
const bonding = createDeferred<void>();
|
|
372
|
+
|
|
373
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
374
|
+
device.connect = jest.fn(() => {
|
|
375
|
+
operationOrder.push('connect');
|
|
376
|
+
return Promise.resolve(device);
|
|
377
|
+
});
|
|
378
|
+
pairDeviceMock.mockImplementationOnce(() => {
|
|
379
|
+
operationOrder.push('bond');
|
|
380
|
+
return Promise.resolve({ bonded: false, bonding: true });
|
|
381
|
+
});
|
|
382
|
+
bondStateMock.mockImplementationOnce(() => bonding.promise);
|
|
383
|
+
|
|
384
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: protocol });
|
|
385
|
+
await new Promise(resolve => {
|
|
386
|
+
setImmediate(resolve);
|
|
387
|
+
});
|
|
388
|
+
expect(operationOrder).toEqual(['bond']);
|
|
389
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
390
|
+
|
|
391
|
+
bonding.resolve();
|
|
392
|
+
await expect(acquiring).resolves.toEqual({
|
|
393
|
+
uuid,
|
|
394
|
+
protocolType: protocol,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(operationOrder).toEqual(['bond', 'connect']);
|
|
398
|
+
await transport.release(uuid, true);
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
test('preserves Android connect errors after bonding when both reconnect attempts fail', async () => {
|
|
403
|
+
setPlatformOS('android');
|
|
404
|
+
const { transport, uuid, device } = createHarness();
|
|
405
|
+
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
406
|
+
message: string
|
|
407
|
+
) => Error;
|
|
408
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
409
|
+
const firstError = Object.assign(new BleErrorMock('MTU change failed'), {
|
|
410
|
+
errorCode: 206,
|
|
411
|
+
reason: 'MTU change failed',
|
|
412
|
+
});
|
|
413
|
+
const fallbackError = Object.assign(new BleErrorMock('GATT connect failed'), {
|
|
414
|
+
errorCode: 205,
|
|
415
|
+
reason: 'GATT connect failed',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
419
|
+
device.connect = jest
|
|
420
|
+
.fn()
|
|
421
|
+
.mockRejectedValueOnce(firstError)
|
|
422
|
+
.mockRejectedValueOnce(fallbackError);
|
|
423
|
+
pairDeviceMock.mockClear();
|
|
424
|
+
|
|
425
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
426
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
427
|
+
});
|
|
428
|
+
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
429
|
+
expect(pairDeviceMock).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('does not connect Android GATT when the system cannot start bonding', async () => {
|
|
433
|
+
setPlatformOS('android');
|
|
434
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
435
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
436
|
+
|
|
437
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
438
|
+
pairDeviceMock.mockClear();
|
|
439
|
+
pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: false });
|
|
440
|
+
|
|
441
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
442
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
443
|
+
});
|
|
444
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
445
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
446
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('checks that Android GATT is connected after bonding and reconnecting', async () => {
|
|
450
|
+
setPlatformOS('android');
|
|
451
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
452
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
453
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
454
|
+
|
|
455
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
456
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
457
|
+
});
|
|
458
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
459
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test.each([
|
|
463
|
+
[
|
|
464
|
+
'ios',
|
|
465
|
+
'OneKey Neo',
|
|
466
|
+
{
|
|
467
|
+
iosErrorCode: 14,
|
|
468
|
+
reason: 'Peer removed pairing information',
|
|
469
|
+
},
|
|
470
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
471
|
+
],
|
|
472
|
+
[
|
|
473
|
+
'android',
|
|
474
|
+
'OneKey Pro 2',
|
|
475
|
+
{
|
|
476
|
+
androidErrorCode: 5,
|
|
477
|
+
reason: 'Connection state changed with status 5',
|
|
478
|
+
},
|
|
479
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
480
|
+
],
|
|
481
|
+
] as const)(
|
|
482
|
+
'maps a %s %s stale bond during connect before protocol detection',
|
|
483
|
+
async (platform, deviceName, nativeError, errorCode) => {
|
|
484
|
+
setPlatformOS(platform);
|
|
485
|
+
const { transport, uuid, device } = createHarness({ deviceName });
|
|
486
|
+
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
487
|
+
message: string
|
|
488
|
+
) => Error;
|
|
489
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
490
|
+
pairDeviceMock.mockClear();
|
|
491
|
+
|
|
492
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
493
|
+
device.connect = jest
|
|
494
|
+
.fn()
|
|
495
|
+
.mockRejectedValue(Object.assign(new BleErrorMock(nativeError.reason), nativeError));
|
|
496
|
+
|
|
497
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({ errorCode });
|
|
498
|
+
expect(pairDeviceMock).toHaveBeenCalledTimes(platform === 'android' ? 1 : 0);
|
|
499
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
test('cleans up Android bonding failures without starting GATT', async () => {
|
|
504
|
+
setPlatformOS('android');
|
|
505
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
506
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
507
|
+
|
|
508
|
+
pairDeviceMock.mockRejectedValueOnce(new Error('bonding canceled'));
|
|
509
|
+
|
|
510
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toThrow(
|
|
511
|
+
'bonding canceled'
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
515
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
516
|
+
expect(device.isConnected).not.toHaveBeenCalled();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test('does not connect after stop while Android bonding is pending', async () => {
|
|
520
|
+
setPlatformOS('android');
|
|
521
|
+
const { transport, uuid, bleManager } = createHarness();
|
|
522
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
523
|
+
const bondStateMock = jest.requireMock('../BleManager').onDeviceBondState as jest.Mock;
|
|
524
|
+
const bonding = createDeferred<void>();
|
|
525
|
+
pairDeviceMock.mockResolvedValueOnce({ bonded: false, bonding: true });
|
|
526
|
+
bondStateMock.mockImplementationOnce(() => bonding.promise);
|
|
527
|
+
|
|
528
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
529
|
+
const rejection = expect(acquiring).rejects.toMatchObject({
|
|
530
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
531
|
+
});
|
|
532
|
+
await new Promise(resolve => {
|
|
533
|
+
setImmediate(resolve);
|
|
534
|
+
});
|
|
535
|
+
const stopping = transport.stop();
|
|
536
|
+
bonding.resolve();
|
|
537
|
+
|
|
538
|
+
await rejection;
|
|
539
|
+
await stopping;
|
|
540
|
+
expect(bleManager.devices).not.toHaveBeenCalled();
|
|
541
|
+
});
|
|
542
|
+
|
|
363
543
|
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
364
544
|
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
365
545
|
respondOnWriteCount: [1, 2],
|
|
@@ -534,7 +714,72 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
534
714
|
});
|
|
535
715
|
|
|
536
716
|
test.each(['ios', 'android'] as const)(
|
|
537
|
-
'
|
|
717
|
+
'waits for native disconnect after failed protocol detection before reconnecting on %s',
|
|
718
|
+
async platform => {
|
|
719
|
+
setPlatformOS(platform);
|
|
720
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
721
|
+
const probes = transport as unknown as {
|
|
722
|
+
probeProtocolV1(uuid: string): Promise<boolean>;
|
|
723
|
+
probeProtocolV2(uuid: string): Promise<boolean>;
|
|
724
|
+
releaseNative(uuid: string, onclose: boolean): Promise<void>;
|
|
725
|
+
};
|
|
726
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValueOnce(false);
|
|
727
|
+
const probesFinished = createDeferred<void>();
|
|
728
|
+
const probeProtocolV2 = jest
|
|
729
|
+
.spyOn(probes, 'probeProtocolV2')
|
|
730
|
+
.mockImplementationOnce(async () => {
|
|
731
|
+
// A link-fatal V2 timeout removes the cached transport before acquire fails.
|
|
732
|
+
await probes.releaseNative(uuid, true);
|
|
733
|
+
probesFinished.resolve();
|
|
734
|
+
return false;
|
|
735
|
+
});
|
|
736
|
+
let connected = true;
|
|
737
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
738
|
+
device.connect = jest.fn(() => {
|
|
739
|
+
connected = true;
|
|
740
|
+
return Promise.resolve(device);
|
|
741
|
+
});
|
|
742
|
+
const disconnectGate = createDeferred<void>();
|
|
743
|
+
bleManager.cancelDeviceConnection.mockImplementation(async () => {
|
|
744
|
+
await disconnectGate.promise;
|
|
745
|
+
connected = false;
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const acquiring = transport.acquire({ uuid });
|
|
749
|
+
const failure = expect(acquiring).rejects.toMatchObject({
|
|
750
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
751
|
+
});
|
|
752
|
+
const reconnecting = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
await probesFinished.promise;
|
|
756
|
+
await new Promise(resolve => {
|
|
757
|
+
setImmediate(resolve);
|
|
758
|
+
});
|
|
759
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
760
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
761
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
762
|
+
|
|
763
|
+
disconnectGate.resolve();
|
|
764
|
+
await failure;
|
|
765
|
+
await expect(reconnecting).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
766
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
767
|
+
await expect(
|
|
768
|
+
transport.call(uuid, 'Ping', { message: 'after-reconnect' })
|
|
769
|
+
).resolves.toMatchObject({
|
|
770
|
+
type: 'Success',
|
|
771
|
+
message: { message: 'ok' },
|
|
772
|
+
});
|
|
773
|
+
} finally {
|
|
774
|
+
disconnectGate.resolve();
|
|
775
|
+
await Promise.allSettled([failure, reconnecting]);
|
|
776
|
+
await transport.release(uuid, true);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
test.each(['ios', 'android'] as const)(
|
|
782
|
+
'disconnects after a first expected Protocol V2 probe miss while keeping it retryable on %s',
|
|
538
783
|
async platform => {
|
|
539
784
|
setPlatformOS(platform);
|
|
540
785
|
const { transport, uuid, device } = createHarness();
|
|
@@ -544,13 +789,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
544
789
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
545
790
|
});
|
|
546
791
|
|
|
547
|
-
expect(device.cancelConnection).
|
|
792
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
548
793
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
549
794
|
}
|
|
550
795
|
);
|
|
551
796
|
|
|
552
797
|
test.each(['ios', 'android'] as const)(
|
|
553
|
-
'
|
|
798
|
+
'disconnects after each expected Protocol V2 probe miss while keeping it retryable on %s',
|
|
554
799
|
async platform => {
|
|
555
800
|
setPlatformOS(platform);
|
|
556
801
|
const { transport, uuid, device } = createHarness();
|
|
@@ -563,7 +808,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
563
808
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
564
809
|
});
|
|
565
810
|
|
|
566
|
-
expect(device.cancelConnection).
|
|
811
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(2);
|
|
567
812
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
568
813
|
}
|
|
569
814
|
);
|
|
@@ -598,7 +843,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
598
843
|
);
|
|
599
844
|
|
|
600
845
|
test.each(['ios', 'android'] as const)(
|
|
601
|
-
'
|
|
846
|
+
'disconnects after a confirmed Protocol V2 probe miss on %s without reporting a bond error',
|
|
602
847
|
async platform => {
|
|
603
848
|
setPlatformOS(platform);
|
|
604
849
|
const { transport, uuid, device } = createHarness();
|
|
@@ -610,7 +855,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
610
855
|
errorCode: HardwareErrorCode.RuntimeError,
|
|
611
856
|
});
|
|
612
857
|
|
|
613
|
-
expect(device.cancelConnection).
|
|
858
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
614
859
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
615
860
|
}
|
|
616
861
|
);
|
|
@@ -702,6 +947,118 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
702
947
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
|
|
703
948
|
);
|
|
704
949
|
|
|
950
|
+
test('preserves the unpaired error when iOS disconnects during the first V1 probe', async () => {
|
|
951
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
952
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce({
|
|
953
|
+
errorCode: 201,
|
|
954
|
+
iosErrorCode: 7,
|
|
955
|
+
reason: 'The specified device has disconnected from us.',
|
|
956
|
+
});
|
|
957
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
958
|
+
|
|
959
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
960
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
expect(probeProtocolV2).not.toHaveBeenCalled();
|
|
964
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
965
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test('preserves a native iOS disconnect during an expected Protocol V2 probe', async () => {
|
|
969
|
+
const { transport, uuid, writeCharacteristic, bleManager } = createHarness({
|
|
970
|
+
deviceName: 'Neo Test',
|
|
971
|
+
});
|
|
972
|
+
const nativeDisconnect = {
|
|
973
|
+
errorCode: 201,
|
|
974
|
+
iosErrorCode: 7,
|
|
975
|
+
reason: 'The specified device has disconnected from us.',
|
|
976
|
+
};
|
|
977
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce(nativeDisconnect);
|
|
978
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
979
|
+
|
|
980
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
981
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
985
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
986
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
987
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test('preserves a native iOS disconnect during a V2-first probe without falling back to V1', async () => {
|
|
991
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
992
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce({
|
|
993
|
+
errorCode: 201,
|
|
994
|
+
iosErrorCode: 7,
|
|
995
|
+
reason: 'The specified device has disconnected from us.',
|
|
996
|
+
});
|
|
997
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
998
|
+
|
|
999
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toMatchObject({
|
|
1000
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
1004
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
1005
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test.each(['V1', 'V2'] as const)(
|
|
1009
|
+
'preserves terminal BLE failures from the %s probe without trying another protocol',
|
|
1010
|
+
async protocol => {
|
|
1011
|
+
for (const errorCode of [
|
|
1012
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
1013
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
1014
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
1015
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
1016
|
+
HardwareErrorCode.BleCharacteristicNotifyError,
|
|
1017
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
|
|
1018
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1019
|
+
]) {
|
|
1020
|
+
const { transport, uuid } = createHarness();
|
|
1021
|
+
const error = ERRORS.TypedError(errorCode);
|
|
1022
|
+
const call = jest
|
|
1023
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
1024
|
+
.mockRejectedValue(error);
|
|
1025
|
+
const otherProbe = jest.spyOn(
|
|
1026
|
+
transport as any,
|
|
1027
|
+
protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1'
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).rejects.toBe(error);
|
|
1031
|
+
|
|
1032
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
1033
|
+
expect(otherProbe).not.toHaveBeenCalled();
|
|
1034
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
test.each(['V1', 'V2'] as const)(
|
|
1040
|
+
'still falls back after a silent %s probe timeout',
|
|
1041
|
+
async protocol => {
|
|
1042
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1043
|
+
jest
|
|
1044
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
1045
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
|
|
1046
|
+
const otherProbe = jest
|
|
1047
|
+
.spyOn(transport as any, protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1')
|
|
1048
|
+
.mockResolvedValue(true);
|
|
1049
|
+
|
|
1050
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).resolves.toEqual({
|
|
1051
|
+
uuid,
|
|
1052
|
+
protocolType: protocol === 'V1' ? 'V2' : 'V1',
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
expect(otherProbe).toHaveBeenCalledTimes(1);
|
|
1056
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1057
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1058
|
+
await transport.release(uuid, true);
|
|
1059
|
+
}
|
|
1060
|
+
);
|
|
1061
|
+
|
|
705
1062
|
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
706
1063
|
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
707
1064
|
const probeProtocolV1 = jest
|
|
@@ -883,6 +1240,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
883
1240
|
expect(notifySubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
|
|
884
1241
|
expect(notifySubscriptionRemovers[1]).toHaveBeenCalledTimes(1);
|
|
885
1242
|
expect(bleManager.cancelTransaction).toHaveBeenCalled();
|
|
1243
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
886
1244
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
887
1245
|
});
|
|
888
1246
|
|
|
@@ -937,6 +1295,115 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
937
1295
|
});
|
|
938
1296
|
});
|
|
939
1297
|
|
|
1298
|
+
test.each(['ios', 'android'] as const)(
|
|
1299
|
+
'disconnects after an active V2 timeout and reconnects even if a listener fails on %s',
|
|
1300
|
+
async platform => {
|
|
1301
|
+
setPlatformOS(platform);
|
|
1302
|
+
const harness = createHarness();
|
|
1303
|
+
const { transport, uuid, device, bleManager, sentSeqs, emitter } = harness;
|
|
1304
|
+
const disconnected = jest.fn(() => {
|
|
1305
|
+
throw new Error('Disconnect listener failed');
|
|
1306
|
+
});
|
|
1307
|
+
emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnected);
|
|
1308
|
+
let connected = true;
|
|
1309
|
+
device.isConnected.mockImplementation(() => Promise.resolve(connected));
|
|
1310
|
+
device.connect = jest.fn(() => {
|
|
1311
|
+
connected = true;
|
|
1312
|
+
return Promise.resolve(device);
|
|
1313
|
+
});
|
|
1314
|
+
bleManager.cancelDeviceConnection.mockImplementation(() => {
|
|
1315
|
+
connected = false;
|
|
1316
|
+
return Promise.resolve();
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1320
|
+
harness.setShouldRespond(false);
|
|
1321
|
+
await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
|
|
1322
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1326
|
+
expect(connected).toBe(false);
|
|
1327
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
1328
|
+
expect(disconnected).toHaveBeenCalledTimes(1);
|
|
1329
|
+
expect(disconnected).toHaveBeenCalledWith({
|
|
1330
|
+
id: uuid,
|
|
1331
|
+
connectId: uuid,
|
|
1332
|
+
name: device.name,
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
harness.setShouldRespond(true);
|
|
1336
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1337
|
+
await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
|
|
1338
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1339
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
1340
|
+
await transport.release(uuid, true);
|
|
1341
|
+
}
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
test('does not disconnect an active Protocol V2 call when a queued call times out', async () => {
|
|
1345
|
+
const harness = createHarness();
|
|
1346
|
+
const { transport, uuid, bleManager } = harness;
|
|
1347
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1348
|
+
harness.setShouldRespond(false);
|
|
1349
|
+
const active = transport.call(uuid, 'Ping', {}, { timeoutMs: 1000 }).catch(error => error);
|
|
1350
|
+
await new Promise(resolve => {
|
|
1351
|
+
setImmediate(resolve);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
try {
|
|
1355
|
+
await expect(transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })).rejects.toMatchObject({
|
|
1356
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1357
|
+
});
|
|
1358
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1359
|
+
expect(transport.getProtocolType(uuid)).toBe('V2');
|
|
1360
|
+
} finally {
|
|
1361
|
+
await transport.disconnect(uuid);
|
|
1362
|
+
await active;
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
test('does not disconnect a newer acquire when V2 timeout cleanup waits for the lifecycle lock', async () => {
|
|
1367
|
+
const harness = createHarness();
|
|
1368
|
+
const { transport, uuid, bleManager } = harness;
|
|
1369
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1370
|
+
const lifecycle = transport as unknown as {
|
|
1371
|
+
runLifecycleOperation(id: string, operation: () => Promise<void>): Promise<void>;
|
|
1372
|
+
releaseNative(id: string, onclose: boolean): Promise<boolean>;
|
|
1373
|
+
};
|
|
1374
|
+
const gate = createDeferred<void>();
|
|
1375
|
+
const holding = lifecycle.runLifecycleOperation(uuid, () => gate.promise);
|
|
1376
|
+
const reacquiring = transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1377
|
+
const invalidated = createDeferred<void>();
|
|
1378
|
+
const releaseNative = lifecycle.releaseNative.bind(lifecycle);
|
|
1379
|
+
jest.spyOn(lifecycle, 'releaseNative').mockImplementationOnce(async (id, onclose) => {
|
|
1380
|
+
const result = await releaseNative(id, onclose);
|
|
1381
|
+
invalidated.resolve();
|
|
1382
|
+
return result;
|
|
1383
|
+
});
|
|
1384
|
+
harness.setShouldRespond(false);
|
|
1385
|
+
const failure = expect(
|
|
1386
|
+
transport.call(uuid, 'Ping', {}, { timeoutMs: 10 })
|
|
1387
|
+
).rejects.toMatchObject({
|
|
1388
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
try {
|
|
1392
|
+
await invalidated.promise;
|
|
1393
|
+
harness.setShouldRespond(true);
|
|
1394
|
+
gate.resolve();
|
|
1395
|
+
await holding;
|
|
1396
|
+
await reacquiring;
|
|
1397
|
+
await failure;
|
|
1398
|
+
expect(bleManager.cancelDeviceConnection).not.toHaveBeenCalled();
|
|
1399
|
+
await expect(transport.call(uuid, 'Ping', {})).resolves.toMatchObject({ type: 'Success' });
|
|
1400
|
+
} finally {
|
|
1401
|
+
gate.resolve();
|
|
1402
|
+
await Promise.allSettled([holding, reacquiring, failure]);
|
|
1403
|
+
await transport.release(uuid, true);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
|
|
940
1407
|
test('retains the sequence cursor when a new monitor generation is acquired', async () => {
|
|
941
1408
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
942
1409
|
|
|
@@ -56,6 +56,34 @@ function createHarness() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('Protocol V1 stale call timeout', () => {
|
|
59
|
+
test('settles a cancelled read and waits for native teardown before completing cancel', async () => {
|
|
60
|
+
const { t, disconnectSpy } = createHarness();
|
|
61
|
+
let finishDisconnect!: () => void;
|
|
62
|
+
disconnectSpy.mockImplementationOnce(
|
|
63
|
+
() =>
|
|
64
|
+
new Promise<void>(resolve => {
|
|
65
|
+
finishDisconnect = resolve;
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
69
|
+
const result = call.catch(error => error);
|
|
70
|
+
await flush();
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
const cleanup = t.cancel().then(() => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
});
|
|
75
|
+
await flush();
|
|
76
|
+
expect(await result).toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
|
|
77
|
+
expect(t.runPromise).toBeNull();
|
|
78
|
+
expect(disconnectSpy).toHaveBeenCalledWith(UUID);
|
|
79
|
+
expect(cancelled).toBe(false);
|
|
80
|
+
finishDisconnect();
|
|
81
|
+
await cleanup;
|
|
82
|
+
jest.advanceTimersByTime(25000);
|
|
83
|
+
await flush();
|
|
84
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
59
87
|
beforeAll(() => {
|
|
60
88
|
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
61
89
|
});
|
|
@@ -172,24 +200,25 @@ describe('Protocol V1 stale call timeout', () => {
|
|
|
172
200
|
expect(secondErrors).toHaveLength(1);
|
|
173
201
|
});
|
|
174
202
|
|
|
175
|
-
test('
|
|
203
|
+
test('a cancelled read cannot disconnect a later transport when its old deadline passes', async () => {
|
|
176
204
|
const { t, disconnectSpy } = createHarness();
|
|
177
205
|
|
|
178
|
-
// cancel() nulls the ownership slot without settling the deferred, so the
|
|
179
|
-
// call's response timer stays armed (reachable via DeviceCommands.dispose).
|
|
180
206
|
const errors: Array<{ errorCode?: unknown }> = [];
|
|
181
207
|
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
182
208
|
p.catch(e => errors.push(e));
|
|
183
209
|
await flush();
|
|
184
210
|
|
|
185
|
-
t.cancel();
|
|
211
|
+
await t.cancel();
|
|
212
|
+
await flush();
|
|
213
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
214
|
+
disconnectSpy.mockClear();
|
|
186
215
|
|
|
187
216
|
jest.advanceTimersByTime(5000);
|
|
188
217
|
await flush();
|
|
189
218
|
|
|
190
219
|
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
191
220
|
expect(errors).toHaveLength(1);
|
|
192
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.
|
|
221
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.CallQueueActionCancelled);
|
|
193
222
|
});
|
|
194
223
|
|
|
195
224
|
test('timeout on the active call still disconnects the transport', async () => {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
const BLE_PLX_DEVICE_DISCONNECTED = 201;
|
|
4
|
+
const IOS_PERIPHERAL_DISCONNECTED = 7;
|
|
5
|
+
|
|
6
|
+
type NativeBleErrorFields = {
|
|
7
|
+
errorCode?: unknown;
|
|
8
|
+
iosErrorCode?: unknown;
|
|
9
|
+
reason?: unknown;
|
|
10
|
+
message?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const nativeErrorText = (error: NativeBleErrorFields) =>
|
|
14
|
+
[error.reason, error.message]
|
|
15
|
+
.filter((value): value is string => typeof value === 'string')
|
|
16
|
+
.join(' ');
|
|
17
|
+
|
|
18
|
+
export const isNativeBleDisconnectError = (error: unknown): boolean => {
|
|
19
|
+
if (!error || typeof error !== 'object') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nativeError = error as NativeBleErrorFields;
|
|
24
|
+
return (
|
|
25
|
+
nativeError.errorCode === BLE_PLX_DEVICE_DISCONNECTED ||
|
|
26
|
+
nativeError.iosErrorCode === IOS_PERIPHERAL_DISCONNECTED
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const toBleDisconnectHardwareError = (error: unknown) => {
|
|
31
|
+
if ((error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceDisconnected) {
|
|
32
|
+
return error as Error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nativeError = (error ?? {}) as NativeBleErrorFields;
|
|
36
|
+
return ERRORS.TypedError(
|
|
37
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
38
|
+
nativeErrorText(nativeError) || undefined
|
|
39
|
+
);
|
|
40
|
+
};
|
package/src/bleStaleBond.ts
CHANGED
|
@@ -13,6 +13,7 @@ const IOS_PEER_REMOVED_PAIRING_INFORMATION = 14;
|
|
|
13
13
|
|
|
14
14
|
type NativeBleErrorFields = {
|
|
15
15
|
attErrorCode?: unknown;
|
|
16
|
+
androidErrorCode?: unknown;
|
|
16
17
|
iosErrorCode?: unknown;
|
|
17
18
|
reason?: unknown;
|
|
18
19
|
message?: unknown;
|
|
@@ -32,6 +33,8 @@ export const isNativeBleStaleBondError = (error: unknown): boolean => {
|
|
|
32
33
|
if (
|
|
33
34
|
nativeError.attErrorCode === ATT_INSUFFICIENT_AUTHENTICATION ||
|
|
34
35
|
nativeError.attErrorCode === ATT_INSUFFICIENT_ENCRYPTION ||
|
|
36
|
+
nativeError.androidErrorCode === ATT_INSUFFICIENT_AUTHENTICATION ||
|
|
37
|
+
nativeError.androidErrorCode === ATT_INSUFFICIENT_ENCRYPTION ||
|
|
35
38
|
nativeError.iosErrorCode === IOS_PEER_REMOVED_PAIRING_INFORMATION
|
|
36
39
|
) {
|
|
37
40
|
return true;
|