@onekeyfe/hd-transport-react-native 1.2.2-alpha.100 → 1.2.2-alpha.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bleStaleBond.d.ts +5 -0
- package/dist/bleStaleBond.d.ts.map +1 -0
- package/dist/bleStrategy.d.ts +1 -1
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +199 -73
- package/package.json +5 -5
- package/src/__tests__/bleStaleBond.test.ts +47 -0
- package/src/__tests__/bleStrategy.test.ts +2 -3
- package/src/__tests__/protocolV2Link.test.ts +286 -16
- package/src/bleStaleBond.ts +64 -0
- package/src/bleStrategy.ts +1 -3
- package/src/index.ts +231 -77
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import { isNativeBleStaleBondError, toBleStaleBondHardwareError } from '../bleStaleBond';
|
|
4
|
+
|
|
5
|
+
describe('native BLE stale bond mapping', () => {
|
|
6
|
+
test.each([
|
|
7
|
+
[
|
|
8
|
+
{ attErrorCode: 15, reason: 'Encryption is insufficient' },
|
|
9
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
{ attErrorCode: 5, reason: 'GATT_INSUF_AUTHENTICATION' },
|
|
13
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
14
|
+
],
|
|
15
|
+
[
|
|
16
|
+
{ androidErrorCode: 5, reason: 'Connection state changed with status 5' },
|
|
17
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
{ androidErrorCode: 15, reason: 'Connection state changed with status 15' },
|
|
21
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
{ iosErrorCode: 14, reason: 'Peer removed pairing information' },
|
|
25
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
{ reason: 'peer removed pairing information' },
|
|
29
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
{ message: 'PEER REMOVED PAIRING INFORMATION' },
|
|
33
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
34
|
+
],
|
|
35
|
+
])('maps %j to %s', (nativeError, errorCode) => {
|
|
36
|
+
expect(isNativeBleStaleBondError(nativeError)).toBe(true);
|
|
37
|
+
expect(toBleStaleBondHardwareError(nativeError)).toMatchObject({ errorCode });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('does not treat the generic ATT unlikely error as a stale bond', () => {
|
|
41
|
+
expect(isNativeBleStaleBondError({ attErrorCode: 14, reason: 'Unlikely error' })).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('does not treat a generic disconnect as a stale bond', () => {
|
|
45
|
+
expect(isNativeBleStaleBondError({ reason: 'Device disconnected' })).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -90,7 +90,7 @@ describe('React Native BLE strategy', () => {
|
|
|
90
90
|
).toBe(182);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test('uses withoutResponse
|
|
93
|
+
test('uses withoutResponse by default unless explicitly overridden', () => {
|
|
94
94
|
const characteristic = {
|
|
95
95
|
isWritableWithResponse: true,
|
|
96
96
|
isWritableWithoutResponse: true,
|
|
@@ -99,8 +99,7 @@ describe('React Native BLE strategy', () => {
|
|
|
99
99
|
expect(
|
|
100
100
|
shouldWriteProtocolV2WithResponse({
|
|
101
101
|
platform: 'ios',
|
|
102
|
-
highThroughput:
|
|
103
|
-
requestedWithResponse: false,
|
|
102
|
+
highThroughput: false,
|
|
104
103
|
characteristic,
|
|
105
104
|
})
|
|
106
105
|
).toBe(false);
|
|
@@ -109,9 +109,11 @@ const schemas = {
|
|
|
109
109
|
const createHarness = ({
|
|
110
110
|
deviceName = 'OneKey Pro 2',
|
|
111
111
|
isWritableWithResponse = true,
|
|
112
|
+
monitorError,
|
|
112
113
|
}: {
|
|
113
114
|
deviceName?: string;
|
|
114
115
|
isWritableWithResponse?: boolean;
|
|
116
|
+
monitorError?: (Error & { reason?: string; attErrorCode?: number; iosErrorCode?: number }) | null;
|
|
115
117
|
} = {}) => {
|
|
116
118
|
const uuid = 'rn-pro2-id';
|
|
117
119
|
const sentSeqs: number[] = [];
|
|
@@ -130,6 +132,9 @@ const createHarness = ({
|
|
|
130
132
|
isNotifiable: true,
|
|
131
133
|
monitor: jest.fn(callback => {
|
|
132
134
|
notifyCallback = callback;
|
|
135
|
+
if (monitorError) {
|
|
136
|
+
queueMicrotask(() => callback(monitorError, null));
|
|
137
|
+
}
|
|
133
138
|
return { remove: jest.fn() };
|
|
134
139
|
}),
|
|
135
140
|
};
|
|
@@ -355,6 +360,134 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
355
360
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
356
361
|
});
|
|
357
362
|
|
|
363
|
+
test('connects the Android GATT link before starting system bonding', async () => {
|
|
364
|
+
setPlatformOS('android');
|
|
365
|
+
const { transport, uuid, device } = createHarness();
|
|
366
|
+
const operationOrder: string[] = [];
|
|
367
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
368
|
+
|
|
369
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
370
|
+
device.connect = jest.fn(() => {
|
|
371
|
+
operationOrder.push('connect');
|
|
372
|
+
return Promise.resolve(device);
|
|
373
|
+
});
|
|
374
|
+
pairDeviceMock.mockImplementationOnce(() => {
|
|
375
|
+
operationOrder.push('bond');
|
|
376
|
+
return Promise.resolve({ bonded: true, bonding: false });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
380
|
+
uuid,
|
|
381
|
+
protocolType: 'V2',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(operationOrder).toEqual(['connect', 'bond']);
|
|
385
|
+
await transport.release(uuid, true);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('does not start Android bonding when both reconnect attempts fail', async () => {
|
|
389
|
+
setPlatformOS('android');
|
|
390
|
+
const { transport, uuid, device } = createHarness();
|
|
391
|
+
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
392
|
+
message: string
|
|
393
|
+
) => Error;
|
|
394
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
395
|
+
const firstError = Object.assign(new BleErrorMock('MTU change failed'), {
|
|
396
|
+
errorCode: 206,
|
|
397
|
+
reason: 'MTU change failed',
|
|
398
|
+
});
|
|
399
|
+
const fallbackError = Object.assign(new BleErrorMock('GATT connect failed'), {
|
|
400
|
+
errorCode: 205,
|
|
401
|
+
reason: 'GATT connect failed',
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
405
|
+
device.connect = jest
|
|
406
|
+
.fn()
|
|
407
|
+
.mockRejectedValueOnce(firstError)
|
|
408
|
+
.mockRejectedValueOnce(fallbackError);
|
|
409
|
+
pairDeviceMock.mockClear();
|
|
410
|
+
|
|
411
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
412
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
413
|
+
});
|
|
414
|
+
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
415
|
+
expect(pairDeviceMock).not.toHaveBeenCalled();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('checks that the Android GATT link is connected before bonding', async () => {
|
|
419
|
+
setPlatformOS('android');
|
|
420
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
421
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
422
|
+
|
|
423
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValueOnce(false);
|
|
424
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
425
|
+
pairDeviceMock.mockClear();
|
|
426
|
+
|
|
427
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
428
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
429
|
+
});
|
|
430
|
+
expect(pairDeviceMock).not.toHaveBeenCalled();
|
|
431
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
432
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test.each([
|
|
436
|
+
[
|
|
437
|
+
'ios',
|
|
438
|
+
'OneKey Neo',
|
|
439
|
+
{
|
|
440
|
+
iosErrorCode: 14,
|
|
441
|
+
reason: 'Peer removed pairing information',
|
|
442
|
+
},
|
|
443
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
444
|
+
],
|
|
445
|
+
[
|
|
446
|
+
'android',
|
|
447
|
+
'OneKey Pro 2',
|
|
448
|
+
{
|
|
449
|
+
androidErrorCode: 5,
|
|
450
|
+
reason: 'Connection state changed with status 5',
|
|
451
|
+
},
|
|
452
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
453
|
+
],
|
|
454
|
+
] as const)(
|
|
455
|
+
'maps a %s %s stale bond during connect before protocol detection',
|
|
456
|
+
async (platform, deviceName, nativeError, errorCode) => {
|
|
457
|
+
setPlatformOS(platform);
|
|
458
|
+
const { transport, uuid, device } = createHarness({ deviceName });
|
|
459
|
+
const BleErrorMock = jest.requireMock('react-native-ble-plx').BleError as new (
|
|
460
|
+
message: string
|
|
461
|
+
) => Error;
|
|
462
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
463
|
+
pairDeviceMock.mockClear();
|
|
464
|
+
|
|
465
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
466
|
+
device.connect = jest
|
|
467
|
+
.fn()
|
|
468
|
+
.mockRejectedValue(Object.assign(new BleErrorMock(nativeError.reason), nativeError));
|
|
469
|
+
|
|
470
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({ errorCode });
|
|
471
|
+
expect(pairDeviceMock).not.toHaveBeenCalled();
|
|
472
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
test('closes the Android GATT link when system bonding fails', async () => {
|
|
477
|
+
setPlatformOS('android');
|
|
478
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
479
|
+
const pairDeviceMock = jest.requireMock('../BleManager').pairDevice as jest.Mock;
|
|
480
|
+
|
|
481
|
+
pairDeviceMock.mockRejectedValueOnce(new Error('bonding canceled'));
|
|
482
|
+
|
|
483
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toThrow(
|
|
484
|
+
'bonding canceled'
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
488
|
+
expect(device.cancelConnection).toHaveBeenCalledTimes(1);
|
|
489
|
+
});
|
|
490
|
+
|
|
358
491
|
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
359
492
|
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
360
493
|
respondOnWriteCount: [1, 2],
|
|
@@ -405,6 +538,22 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
405
538
|
await transport.release(uuid, true);
|
|
406
539
|
});
|
|
407
540
|
|
|
541
|
+
test('keeps native stale-bond write mapping out of Protocol V1 calls', async () => {
|
|
542
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness();
|
|
543
|
+
const nativeError = Object.assign(new Error('Encryption is insufficient'), {
|
|
544
|
+
attErrorCode: 15,
|
|
545
|
+
reason: 'Encryption is insufficient',
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
549
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce(nativeError);
|
|
550
|
+
|
|
551
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
|
|
552
|
+
errorCode: HardwareErrorCode.BleWriteCharacteristicError,
|
|
553
|
+
});
|
|
554
|
+
await transport.release(uuid, true);
|
|
555
|
+
});
|
|
556
|
+
|
|
408
557
|
test('detects Protocol V2 on iOS without using the BLE name as a protocol hint', async () => {
|
|
409
558
|
const { transport, uuid, device, sentSeqs, writeCharacteristic } = createHarness({
|
|
410
559
|
deviceName: 'Pro2 6E9E',
|
|
@@ -415,7 +564,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
415
564
|
protocolType: 'V2',
|
|
416
565
|
});
|
|
417
566
|
expect(device.requestMTU).toHaveBeenCalledWith(247);
|
|
418
|
-
expect(writeCharacteristic.writeWithResponse
|
|
567
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
568
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
419
569
|
|
|
420
570
|
await expect(
|
|
421
571
|
transport.call(uuid, 'Ping', { message: 'first-core-command' })
|
|
@@ -424,6 +574,93 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
424
574
|
await transport.release(uuid, true);
|
|
425
575
|
});
|
|
426
576
|
|
|
577
|
+
test('physically refreshes an uncached Protocol V2 firmware install link without Ping', async () => {
|
|
578
|
+
const { transport, uuid, device, bleManager, writeCharacteristic } = createHarness();
|
|
579
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
580
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
581
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
582
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
583
|
+
|
|
584
|
+
await expect(
|
|
585
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
586
|
+
).resolves.toEqual({
|
|
587
|
+
uuid,
|
|
588
|
+
protocolType: 'V2',
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
592
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
593
|
+
expect(device.connect).toHaveBeenCalled();
|
|
594
|
+
expect(probeProtocolV2).not.toHaveBeenCalled();
|
|
595
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
596
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
597
|
+
expect(transport.getProtocolType(uuid)).toBe('V2');
|
|
598
|
+
await transport.release(uuid, true);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('refreshes a cached firmware install connection instead of trusting GATT state', async () => {
|
|
602
|
+
const { transport, uuid, device, bleManager, writeCharacteristic } = createHarness();
|
|
603
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
604
|
+
|
|
605
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
606
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
607
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
608
|
+
writeCharacteristic.writeWithResponse.mockClear();
|
|
609
|
+
writeCharacteristic.writeWithoutResponse.mockClear();
|
|
610
|
+
|
|
611
|
+
await expect(
|
|
612
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
613
|
+
).resolves.toEqual({
|
|
614
|
+
uuid,
|
|
615
|
+
protocolType: 'V2',
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
619
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
620
|
+
expect(device.connect).toHaveBeenCalled();
|
|
621
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
622
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
623
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
624
|
+
await transport.release(uuid, true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test('rejects no-probe acquire before the BLE endpoint has confirmed a protocol', async () => {
|
|
628
|
+
const { transport, uuid } = createHarness();
|
|
629
|
+
|
|
630
|
+
await expect(
|
|
631
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
632
|
+
).rejects.toThrow('previously confirmed protocol');
|
|
633
|
+
|
|
634
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test('keeps confirmed V2 authorization across a BLE manager reset', async () => {
|
|
638
|
+
const { transport, uuid, device, bleManager, writeCharacteristic } = createHarness();
|
|
639
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
640
|
+
|
|
641
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
642
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
643
|
+
|
|
644
|
+
(transport as any).resetPlxManager();
|
|
645
|
+
transport.blePlxManager = bleManager as any;
|
|
646
|
+
device.connect = jest.fn().mockResolvedValue(device);
|
|
647
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
648
|
+
writeCharacteristic.writeWithResponse.mockClear();
|
|
649
|
+
writeCharacteristic.writeWithoutResponse.mockClear();
|
|
650
|
+
|
|
651
|
+
await expect(
|
|
652
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
653
|
+
).resolves.toEqual({
|
|
654
|
+
uuid,
|
|
655
|
+
protocolType: 'V2',
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
659
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
660
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
661
|
+
await transport.release(uuid, true);
|
|
662
|
+
});
|
|
663
|
+
|
|
427
664
|
test.each(['ios', 'android'] as const)(
|
|
428
665
|
'keeps a first expected Protocol V2 probe miss retryable on %s',
|
|
429
666
|
async platform => {
|
|
@@ -459,8 +696,37 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
459
696
|
}
|
|
460
697
|
);
|
|
461
698
|
|
|
699
|
+
test.each([
|
|
700
|
+
[
|
|
701
|
+
'Encryption is insufficient',
|
|
702
|
+
{ reason: 'Encryption is insufficient', attErrorCode: 15 },
|
|
703
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
704
|
+
],
|
|
705
|
+
[
|
|
706
|
+
'Peer removed pairing information',
|
|
707
|
+
{ reason: 'Peer removed pairing information', iosErrorCode: 14 },
|
|
708
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
709
|
+
],
|
|
710
|
+
] as const)(
|
|
711
|
+
'fails Protocol V2 acquire immediately on %s instead of waiting for Ping',
|
|
712
|
+
async (_label, nativeError, errorCode) => {
|
|
713
|
+
const { transport, uuid, device } = createHarness({
|
|
714
|
+
monitorError: Object.assign(new Error(nativeError.reason), nativeError),
|
|
715
|
+
});
|
|
716
|
+
const probe = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
717
|
+
|
|
718
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
719
|
+
errorCode,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
expect(probe).not.toHaveBeenCalled();
|
|
723
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
724
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
|
|
462
728
|
test.each(['ios', 'android'] as const)(
|
|
463
|
-
'
|
|
729
|
+
'keeps a confirmed Protocol V2 probe miss retryable on %s without native bond evidence',
|
|
464
730
|
async platform => {
|
|
465
731
|
setPlatformOS(platform);
|
|
466
732
|
const { transport, uuid, device } = createHarness();
|
|
@@ -469,10 +735,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
469
735
|
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
470
736
|
|
|
471
737
|
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
472
|
-
errorCode: HardwareErrorCode.
|
|
738
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
473
739
|
});
|
|
474
740
|
|
|
475
|
-
expect(device.cancelConnection).toHaveBeenCalled();
|
|
741
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
476
742
|
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
477
743
|
}
|
|
478
744
|
);
|
|
@@ -658,7 +924,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
658
924
|
});
|
|
659
925
|
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
660
926
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBeUndefined();
|
|
661
|
-
expect(writeCharacteristic.
|
|
927
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
662
928
|
await transport.release(uuid, true);
|
|
663
929
|
});
|
|
664
930
|
|
|
@@ -667,6 +933,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
667
933
|
device.mtu = undefined;
|
|
668
934
|
|
|
669
935
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
936
|
+
const writesBeforeFileWrite = writeCharacteristic.writeWithoutResponse.mock.calls.length;
|
|
670
937
|
device.requestMTU.mockImplementationOnce(() => {
|
|
671
938
|
device.mtu = 247;
|
|
672
939
|
return Promise.resolve(device);
|
|
@@ -674,7 +941,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
674
941
|
|
|
675
942
|
await expect(transport.call(uuid, 'FileWrite', {})).resolves.toBeDefined();
|
|
676
943
|
expect(device.requestMTU).toHaveBeenCalledTimes(4);
|
|
677
|
-
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
944
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
945
|
+
writesBeforeFileWrite + 1
|
|
946
|
+
);
|
|
678
947
|
await transport.release(uuid, true);
|
|
679
948
|
});
|
|
680
949
|
|
|
@@ -683,12 +952,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
683
952
|
device.mtu = undefined;
|
|
684
953
|
|
|
685
954
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
955
|
+
const writesBeforeFileWrite = writeCharacteristic.writeWithoutResponse.mock.calls.length;
|
|
686
956
|
|
|
687
957
|
await expect(transport.call(uuid, 'FileWrite', {})).rejects.toMatchObject({
|
|
688
958
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
689
959
|
});
|
|
690
960
|
expect(device.requestMTU).toHaveBeenCalledTimes(4);
|
|
691
|
-
expect(writeCharacteristic.writeWithoutResponse).
|
|
961
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(writesBeforeFileWrite);
|
|
692
962
|
await transport.release(uuid, true);
|
|
693
963
|
});
|
|
694
964
|
|
|
@@ -808,19 +1078,19 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
808
1078
|
await transport.release(uuid, true);
|
|
809
1079
|
});
|
|
810
1080
|
|
|
811
|
-
test('uses
|
|
1081
|
+
test('uses withoutResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
|
|
812
1082
|
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
813
1083
|
|
|
814
1084
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
815
1085
|
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
816
|
-
expect(writeCharacteristic.writeWithoutResponse).
|
|
817
|
-
expect(writeCharacteristic.writeWithResponse).
|
|
1086
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
1087
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
818
1088
|
|
|
819
1089
|
await transport.call(uuid, 'DeviceInfoGet', {});
|
|
820
1090
|
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
821
1091
|
|
|
822
|
-
expect(writeCharacteristic.
|
|
823
|
-
expect(writeCharacteristic.
|
|
1092
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
|
|
1093
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
824
1094
|
expect(releaseNative).not.toHaveBeenCalled();
|
|
825
1095
|
|
|
826
1096
|
await transport.release(uuid, true);
|
|
@@ -833,8 +1103,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
833
1103
|
|
|
834
1104
|
await transport.call(uuid, 'FileWrite', {});
|
|
835
1105
|
await transport.call(uuid, 'FileWrite', {});
|
|
836
|
-
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
837
|
-
expect(writeCharacteristic.writeWithResponse).
|
|
1106
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
|
|
1107
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
838
1108
|
expect(
|
|
839
1109
|
logger.debug.mock.calls.filter(
|
|
840
1110
|
([message]) =>
|
|
@@ -867,8 +1137,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
867
1137
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
868
1138
|
|
|
869
1139
|
await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
|
|
870
|
-
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(
|
|
871
|
-
expect(writeCharacteristic.writeWithoutResponse).
|
|
1140
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
1141
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
872
1142
|
await transport.release(uuid, true);
|
|
873
1143
|
});
|
|
874
1144
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ERRORS,
|
|
3
|
+
HardwareErrorCode,
|
|
4
|
+
isBleStaleBondErrorText,
|
|
5
|
+
isBleStaleBondHardwareError,
|
|
6
|
+
} from '@onekeyfe/hd-shared';
|
|
7
|
+
|
|
8
|
+
export { isBleStaleBondHardwareError };
|
|
9
|
+
|
|
10
|
+
const ATT_INSUFFICIENT_AUTHENTICATION = 5;
|
|
11
|
+
const ATT_INSUFFICIENT_ENCRYPTION = 15;
|
|
12
|
+
const IOS_PEER_REMOVED_PAIRING_INFORMATION = 14;
|
|
13
|
+
|
|
14
|
+
type NativeBleErrorFields = {
|
|
15
|
+
attErrorCode?: unknown;
|
|
16
|
+
androidErrorCode?: unknown;
|
|
17
|
+
iosErrorCode?: unknown;
|
|
18
|
+
reason?: unknown;
|
|
19
|
+
message?: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const nativeErrorText = (error: NativeBleErrorFields) =>
|
|
23
|
+
[error.reason, error.message]
|
|
24
|
+
.filter((value): value is string => typeof value === 'string')
|
|
25
|
+
.join(' ');
|
|
26
|
+
|
|
27
|
+
export const isNativeBleStaleBondError = (error: unknown): boolean => {
|
|
28
|
+
if (!error || typeof error !== 'object') {
|
|
29
|
+
return typeof error === 'string' ? isBleStaleBondErrorText(error) : false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const nativeError = error as NativeBleErrorFields;
|
|
33
|
+
if (
|
|
34
|
+
nativeError.attErrorCode === ATT_INSUFFICIENT_AUTHENTICATION ||
|
|
35
|
+
nativeError.attErrorCode === ATT_INSUFFICIENT_ENCRYPTION ||
|
|
36
|
+
nativeError.androidErrorCode === ATT_INSUFFICIENT_AUTHENTICATION ||
|
|
37
|
+
nativeError.androidErrorCode === ATT_INSUFFICIENT_ENCRYPTION ||
|
|
38
|
+
nativeError.iosErrorCode === IOS_PEER_REMOVED_PAIRING_INFORMATION
|
|
39
|
+
) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return isBleStaleBondErrorText(nativeErrorText(nativeError));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const toBleStaleBondHardwareError = (error: unknown) => {
|
|
47
|
+
if (isBleStaleBondHardwareError(error)) {
|
|
48
|
+
return error as Error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const nativeError = (error ?? {}) as NativeBleErrorFields;
|
|
52
|
+
const text = nativeErrorText(nativeError);
|
|
53
|
+
const normalizedText = text.toLowerCase();
|
|
54
|
+
const peerRemoved =
|
|
55
|
+
nativeError.iosErrorCode === IOS_PEER_REMOVED_PAIRING_INFORMATION ||
|
|
56
|
+
normalizedText.includes('peer removed pairing information');
|
|
57
|
+
|
|
58
|
+
return ERRORS.TypedError(
|
|
59
|
+
peerRemoved
|
|
60
|
+
? HardwareErrorCode.BlePeerRemovedPairingInformation
|
|
61
|
+
: HardwareErrorCode.BleDeviceBondError,
|
|
62
|
+
text || undefined
|
|
63
|
+
);
|
|
64
|
+
};
|
package/src/bleStrategy.ts
CHANGED
|
@@ -34,8 +34,6 @@ export function shouldRefreshNegotiatedMtu(mtu?: number | null) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function shouldWriteProtocolV2WithResponse({
|
|
37
|
-
platform,
|
|
38
|
-
highThroughput,
|
|
39
37
|
requestedWithResponse,
|
|
40
38
|
characteristic,
|
|
41
39
|
}: {
|
|
@@ -46,5 +44,5 @@ export function shouldWriteProtocolV2WithResponse({
|
|
|
46
44
|
}) {
|
|
47
45
|
if (!characteristic.isWritableWithResponse) return false;
|
|
48
46
|
if (!characteristic.isWritableWithoutResponse) return true;
|
|
49
|
-
return requestedWithResponse === true
|
|
47
|
+
return requestedWithResponse === true;
|
|
50
48
|
}
|