@onekeyfe/hd-transport-react-native 1.2.0-alpha.67 → 1.2.0-alpha.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -17
- package/package.json +5 -5
- package/src/BleTransport.ts +6 -0
- package/src/__tests__/BleTransport.test.ts +54 -2
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +282 -40
- package/src/index.ts +82 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BleTransport.d.ts","sourceRoot":"","sources":["../src/BleTransport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BleTransport.d.ts","sourceRoot":"","sources":["../src/BleTransport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEjF,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,EAAE,EAAE,MAAM,CAAC;IAEX,IAAI,SAA6B;IAEjC,MAAM,EAAE,MAAM,CAAC;IAEf,OAAO,SAAM;IAEb,mBAAmB,EAAE,cAAc,CAAC;IAEpC,oBAAoB,EAAE,cAAc,CAAC;IAErC,kBAAkB,CAAC,EAAE,YAAY,CAAC;IAElC,sBAAsB,CAAC,EAAE,YAAY,CAAC;IAEtC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,YAAY,CAAC,EAAE,MAAM,CAAC;gBAGpB,MAAM,EAAE,MAAM,EACd,mBAAmB,EAAE,cAAc,EACnC,oBAAoB,EAAE,cAAc;IAQhC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAOlD"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAIL,UAAU,IAAI,aAAa,EAE5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,SAAS,EAAE,EAEhB,KAAK,oBAAoB,EAIzB,KAAK,YAAY,EAKjB,KAAK,oBAAoB,EAG1B,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -210,6 +210,10 @@ class BleTransport {
|
|
|
210
210
|
}
|
|
211
211
|
writeWithRetry(data) {
|
|
212
212
|
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
+
if (reactNative.Platform.OS === 'ios' && this.writeCharacteristic.isWritableWithResponse) {
|
|
214
|
+
yield this.writeCharacteristic.writeWithResponse(data);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
213
217
|
yield this.writeCharacteristic.writeWithoutResponse(data);
|
|
214
218
|
});
|
|
215
219
|
}
|
|
@@ -222,9 +226,36 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = reactNative.Platform.OS === 'ios' ? 4 :
|
|
|
222
226
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = reactNative.Platform.OS === 'ios' ? 8 : 10;
|
|
223
227
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = reactNative.Platform.OS === 'ios' ? 24 : 30;
|
|
224
228
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
229
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
225
230
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
226
231
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY = reactNative.Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
227
232
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
233
|
+
const isAsciiWhitespace = (code) => code === 0x09 ||
|
|
234
|
+
code === 0x0a ||
|
|
235
|
+
code === 0x0b ||
|
|
236
|
+
code === 0x0c ||
|
|
237
|
+
code === 0x0d ||
|
|
238
|
+
code === 0x20;
|
|
239
|
+
const hasGattCongestedStatus = (text) => {
|
|
240
|
+
let searchFrom = 0;
|
|
241
|
+
while (searchFrom < text.length) {
|
|
242
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
243
|
+
if (statusIndex < 0)
|
|
244
|
+
return false;
|
|
245
|
+
let cursor = statusIndex + 'status'.length;
|
|
246
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor)))
|
|
247
|
+
cursor += 1;
|
|
248
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
249
|
+
cursor += 1;
|
|
250
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor)))
|
|
251
|
+
cursor += 1;
|
|
252
|
+
}
|
|
253
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor))
|
|
254
|
+
return true;
|
|
255
|
+
searchFrom = statusIndex + 'status'.length;
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
};
|
|
228
259
|
const delay = (ms) => new Promise(resolve => {
|
|
229
260
|
setTimeout(resolve, ms);
|
|
230
261
|
});
|
|
@@ -239,7 +270,7 @@ const getFirmwareUploadWriteRetryType = (error) => {
|
|
|
239
270
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
240
271
|
.filter(value => typeof value === 'string')
|
|
241
272
|
.join(' ');
|
|
242
|
-
return
|
|
273
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
243
274
|
};
|
|
244
275
|
const resolveFirmwareUploadRetryDelay = (attempt, baseDelayMs = 200, maxDelayMs = 1200) => Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
245
276
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
@@ -638,12 +669,17 @@ class ReactNativeBleTransport {
|
|
|
638
669
|
return;
|
|
639
670
|
}
|
|
640
671
|
const displayName = getDeviceDisplayName(device);
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
672
|
+
const isUnnamedIOSPeripheral = reactNative.Platform.OS === 'ios' && !(displayName === null || displayName === void 0 ? void 0 : displayName.trim());
|
|
673
|
+
const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.name) ||
|
|
674
|
+
hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.localName);
|
|
675
|
+
const isOneKey = !isUnnamedIOSPeripheral &&
|
|
676
|
+
!isFindMyPeripheral &&
|
|
677
|
+
hdShared.isOnekeyBluetoothDevice({
|
|
678
|
+
id: device === null || device === void 0 ? void 0 : device.id,
|
|
679
|
+
name: device === null || device === void 0 ? void 0 : device.name,
|
|
680
|
+
localName: device === null || device === void 0 ? void 0 : device.localName,
|
|
681
|
+
serviceUuids: device === null || device === void 0 ? void 0 : device.serviceUUIDs,
|
|
682
|
+
});
|
|
647
683
|
if (isOneKey) {
|
|
648
684
|
addDevice(device);
|
|
649
685
|
}
|
|
@@ -661,12 +697,15 @@ class ReactNativeBleTransport {
|
|
|
661
697
|
const localName = 'localName' in device && typeof device.localName === 'string'
|
|
662
698
|
? device.localName
|
|
663
699
|
: null;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
700
|
+
const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device.name) ||
|
|
701
|
+
hdShared.isPro2FindMyAdvertisementName(localName);
|
|
702
|
+
if (!isFindMyPeripheral &&
|
|
703
|
+
hdShared.isOnekeyBluetoothDevice({
|
|
704
|
+
id: device.id,
|
|
705
|
+
name: device.name,
|
|
706
|
+
localName,
|
|
707
|
+
serviceUuids: device.serviceUUIDs,
|
|
708
|
+
})) {
|
|
670
709
|
Log === null || Log === void 0 ? void 0 : Log.debug('search connected peripheral: ', device.id);
|
|
671
710
|
addDevice(device);
|
|
672
711
|
}
|
|
@@ -1160,7 +1199,7 @@ class ReactNativeBleTransport {
|
|
|
1160
1199
|
let attempt = 0;
|
|
1161
1200
|
while (true) {
|
|
1162
1201
|
try {
|
|
1163
|
-
yield this.writeBlePacket(uuid, data, payload => transport.
|
|
1202
|
+
yield this.writeBlePacket(uuid, data, payload => transport.writeWithRetry(payload), isCurrentOwner);
|
|
1164
1203
|
return;
|
|
1165
1204
|
}
|
|
1166
1205
|
catch (error) {
|
|
@@ -1187,7 +1226,10 @@ class ReactNativeBleTransport {
|
|
|
1187
1226
|
for (const o of buffers) {
|
|
1188
1227
|
const outData = o.toString('base64');
|
|
1189
1228
|
try {
|
|
1190
|
-
|
|
1229
|
+
const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1230
|
+
yield this.writeBlePacket(uuid, outData, payload => shouldUseWriteWithResponse
|
|
1231
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1232
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
|
|
1191
1233
|
}
|
|
1192
1234
|
catch (e) {
|
|
1193
1235
|
Log === null || Log === void 0 ? void 0 : Log.debug('writeCharacteristic write error: ', e);
|
|
@@ -1520,6 +1562,15 @@ class ReactNativeBleTransport {
|
|
|
1520
1562
|
detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
|
|
1521
1563
|
var _a;
|
|
1522
1564
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1565
|
+
if (reactNative.Platform.OS === 'ios' && expectedProtocol) {
|
|
1566
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1567
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1568
|
+
deviceId: uuid,
|
|
1569
|
+
protocol: expectedProtocol,
|
|
1570
|
+
source: 'expected',
|
|
1571
|
+
});
|
|
1572
|
+
return expectedProtocol;
|
|
1573
|
+
}
|
|
1523
1574
|
if (expectedProtocol === 'V1') {
|
|
1524
1575
|
if (yield this.probeProtocolV1(uuid)) {
|
|
1525
1576
|
this.deviceProtocol.set(uuid, 'V1');
|
|
@@ -1749,6 +1800,8 @@ class ReactNativeBleTransport {
|
|
|
1749
1800
|
}
|
|
1750
1801
|
writeProtocolV2Packet(uuid, transport, base64, context, assertCurrentGeneration) {
|
|
1751
1802
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1803
|
+
const shouldUseWriteWithResponse = transport.writeCharacteristic.isWritableWithResponse &&
|
|
1804
|
+
(context.writeWithResponse === true || (reactNative.Platform.OS === 'ios' && !context.highVolume));
|
|
1752
1805
|
let attempt = 0;
|
|
1753
1806
|
for (;;) {
|
|
1754
1807
|
assertCurrentGeneration();
|
|
@@ -1756,7 +1809,9 @@ class ReactNativeBleTransport {
|
|
|
1756
1809
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1757
1810
|
}
|
|
1758
1811
|
try {
|
|
1759
|
-
yield this.writeBlePacket(uuid, base64, payload =>
|
|
1812
|
+
yield this.writeBlePacket(uuid, base64, payload => shouldUseWriteWithResponse
|
|
1813
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1814
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload), () => {
|
|
1760
1815
|
try {
|
|
1761
1816
|
assertCurrentGeneration();
|
|
1762
1817
|
return !context.signal.aborted;
|
|
@@ -1794,12 +1849,16 @@ class ReactNativeBleTransport {
|
|
|
1794
1849
|
androidPacketLength: tuning.androidPacketLength,
|
|
1795
1850
|
mtu: reactNative.Platform.OS === 'android' ? transport$1.mtuSize : undefined,
|
|
1796
1851
|
});
|
|
1852
|
+
const initialDelayMs = reactNative.Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
1853
|
+
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
1854
|
+
: 0;
|
|
1797
1855
|
yield transport.writeProtocolV2BleFrame({
|
|
1798
1856
|
frame,
|
|
1799
1857
|
packetCapacity,
|
|
1800
1858
|
assertActive: assertCurrentGeneration,
|
|
1801
1859
|
signal: context.signal,
|
|
1802
1860
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1861
|
+
initialDelayMs,
|
|
1803
1862
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1804
1863
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1805
1864
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
@@ -1819,7 +1878,7 @@ class ReactNativeBleTransport {
|
|
|
1819
1878
|
const tuning = getProtocolV2BleTuning();
|
|
1820
1879
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
1821
1880
|
name,
|
|
1822
|
-
writeMode: 'withoutResponse',
|
|
1881
|
+
writeMode: (options === null || options === void 0 ? void 0 : options.writeWithResponse) ? 'withResponse' : 'withoutResponse',
|
|
1823
1882
|
packetCapacity: reactNative.Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
1824
1883
|
});
|
|
1825
1884
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.68",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.68",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.68",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.68",
|
|
26
26
|
"@onekeyfe/react-native-ble-utils": "^0.1.6",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "6e8a931d08768980cfe7fa918bba447ed73e2724"
|
|
30
30
|
}
|
package/src/BleTransport.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
1
3
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
2
4
|
|
|
3
5
|
export default class BleTransport {
|
|
@@ -33,6 +35,10 @@ export default class BleTransport {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
async writeWithRetry(data: string): Promise<void> {
|
|
38
|
+
if (Platform.OS === 'ios' && this.writeCharacteristic.isWritableWithResponse) {
|
|
39
|
+
await this.writeCharacteristic.writeWithResponse(data);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
36
42
|
await this.writeCharacteristic.writeWithoutResponse(data);
|
|
37
43
|
}
|
|
38
44
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { BleErrorCode } from 'react-native-ble-plx';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import BleTransport from '../BleTransport';
|
|
4
5
|
|
|
6
|
+
jest.mock(
|
|
7
|
+
'react-native',
|
|
8
|
+
() => ({
|
|
9
|
+
Platform: { OS: 'ios' },
|
|
10
|
+
}),
|
|
11
|
+
{ virtual: true }
|
|
12
|
+
);
|
|
13
|
+
|
|
5
14
|
jest.mock(
|
|
6
15
|
'react-native-ble-plx',
|
|
7
16
|
() => ({
|
|
@@ -19,12 +28,18 @@ jest.mock('@onekeyfe/hd-shared', () => ({
|
|
|
19
28
|
}));
|
|
20
29
|
|
|
21
30
|
describe('BleTransport side-effecting writes', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
Platform.OS = 'ios';
|
|
33
|
+
});
|
|
34
|
+
|
|
22
35
|
test('does not reconnect or replay after the device disconnects', async () => {
|
|
23
36
|
const error = Object.assign(new Error('device disconnected'), {
|
|
24
37
|
errorCode: BleErrorCode.DeviceDisconnected,
|
|
25
38
|
});
|
|
26
39
|
const writeCharacteristic = {
|
|
27
|
-
|
|
40
|
+
isWritableWithResponse: true,
|
|
41
|
+
writeWithResponse: jest.fn(() => Promise.reject(error)),
|
|
42
|
+
writeWithoutResponse: jest.fn(() => Promise.resolve()),
|
|
28
43
|
};
|
|
29
44
|
const device = {
|
|
30
45
|
id: 'classic-id',
|
|
@@ -35,7 +50,44 @@ describe('BleTransport side-effecting writes', () => {
|
|
|
35
50
|
|
|
36
51
|
await expect(transport.writeWithRetry('payload')).rejects.toBe(error);
|
|
37
52
|
|
|
38
|
-
expect(writeCharacteristic.
|
|
53
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
39
54
|
expect(device.connect).not.toHaveBeenCalled();
|
|
40
55
|
});
|
|
56
|
+
|
|
57
|
+
test('keeps Android Protocol V1 writes on writeWithoutResponse', async () => {
|
|
58
|
+
Platform.OS = 'android';
|
|
59
|
+
const writeCharacteristic = {
|
|
60
|
+
isWritableWithResponse: true,
|
|
61
|
+
writeWithResponse: jest.fn(() => Promise.resolve()),
|
|
62
|
+
writeWithoutResponse: jest.fn(() => Promise.resolve()),
|
|
63
|
+
};
|
|
64
|
+
const transport = new BleTransport(
|
|
65
|
+
{ id: 'classic-id' } as any,
|
|
66
|
+
writeCharacteristic as any,
|
|
67
|
+
{} as any
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await transport.writeWithRetry('payload');
|
|
71
|
+
|
|
72
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
73
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledWith('payload');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('uses writeWithoutResponse on iOS when the characteristic lacks response support', async () => {
|
|
77
|
+
const writeCharacteristic = {
|
|
78
|
+
isWritableWithResponse: false,
|
|
79
|
+
writeWithResponse: jest.fn(() => Promise.resolve()),
|
|
80
|
+
writeWithoutResponse: jest.fn(() => Promise.resolve()),
|
|
81
|
+
};
|
|
82
|
+
const transport = new BleTransport(
|
|
83
|
+
{ id: 'classic-id' } as any,
|
|
84
|
+
writeCharacteristic as any,
|
|
85
|
+
{} as any
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await transport.writeWithRetry('payload');
|
|
89
|
+
|
|
90
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
91
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledWith('payload');
|
|
92
|
+
});
|
|
41
93
|
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
import { getConnectedDeviceIds } from '../BleManager';
|
|
4
|
+
import ReactNativeBleTransport from '../index';
|
|
5
|
+
|
|
6
|
+
jest.mock(
|
|
7
|
+
'react-native',
|
|
8
|
+
() => ({
|
|
9
|
+
PermissionsAndroid: {},
|
|
10
|
+
Platform: { OS: 'ios' },
|
|
11
|
+
}),
|
|
12
|
+
{ virtual: true }
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
16
|
+
BleError: class BleError extends Error {},
|
|
17
|
+
BleErrorCode: {},
|
|
18
|
+
BleManager: jest.fn(),
|
|
19
|
+
ScanMode: { LowLatency: 2 },
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('../BleManager', () => ({
|
|
23
|
+
getConnectedDeviceIds: jest.fn(),
|
|
24
|
+
onDeviceBondState: jest.fn(),
|
|
25
|
+
pairDevice: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('../subscribeBleOn', () => ({
|
|
29
|
+
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const ONEKEY_SERVICE_UUID = '00000001-0000-1000-8000-00805f9b34fb';
|
|
33
|
+
|
|
34
|
+
describe('ReactNativeBleTransport iOS discovery', () => {
|
|
35
|
+
test('filters a bonded Pro2 Find My peripheral while keeping the wallet peripheral', async () => {
|
|
36
|
+
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([
|
|
37
|
+
{
|
|
38
|
+
id: 'find-my-peripheral',
|
|
39
|
+
name: 'Pro2 6E9E - Find My',
|
|
40
|
+
localName: null,
|
|
41
|
+
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'wallet-peripheral',
|
|
45
|
+
name: 'Pro2 6E9E',
|
|
46
|
+
localName: 'Pro2 6E9E',
|
|
47
|
+
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
48
|
+
},
|
|
49
|
+
] as never);
|
|
50
|
+
const blePlxManager = {
|
|
51
|
+
startDeviceScan: jest.fn(),
|
|
52
|
+
stopDeviceScan: jest.fn(),
|
|
53
|
+
};
|
|
54
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
55
|
+
transport.blePlxManager = blePlxManager as never;
|
|
56
|
+
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
57
|
+
|
|
58
|
+
const devices = await transport.enumerate();
|
|
59
|
+
|
|
60
|
+
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('filters a scanned Pro2 Find My peripheral by name when localName is null', async () => {
|
|
64
|
+
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
65
|
+
const blePlxManager = {
|
|
66
|
+
startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
|
|
67
|
+
queueMicrotask(() => {
|
|
68
|
+
listener(null, {
|
|
69
|
+
id: 'find-my-peripheral',
|
|
70
|
+
name: 'Pro2 6E9E - Find My',
|
|
71
|
+
localName: null,
|
|
72
|
+
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
73
|
+
});
|
|
74
|
+
listener(null, {
|
|
75
|
+
id: 'wallet-peripheral',
|
|
76
|
+
name: 'Pro2 6E9E',
|
|
77
|
+
localName: 'Pro2 6E9E',
|
|
78
|
+
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}),
|
|
82
|
+
stopDeviceScan: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
85
|
+
transport.blePlxManager = blePlxManager as never;
|
|
86
|
+
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
87
|
+
|
|
88
|
+
const devices = await transport.enumerate();
|
|
89
|
+
|
|
90
|
+
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('ignores an unnamed scanned advertisement while keeping the named wallet peripheral', async () => {
|
|
94
|
+
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
95
|
+
const blePlxManager = {
|
|
96
|
+
startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
|
|
97
|
+
queueMicrotask(() => {
|
|
98
|
+
listener(null, {
|
|
99
|
+
id: 'unnamed-peripheral',
|
|
100
|
+
name: null,
|
|
101
|
+
localName: null,
|
|
102
|
+
serviceUUIDs: [
|
|
103
|
+
'0000180a-0000-1000-8000-00805f9b34fb',
|
|
104
|
+
'0000180f-0000-1000-8000-00805f9b34fb',
|
|
105
|
+
'0000fffd-0000-1000-8000-00805f9b34fb',
|
|
106
|
+
ONEKEY_SERVICE_UUID,
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
listener(null, {
|
|
110
|
+
id: 'wallet-peripheral',
|
|
111
|
+
name: 'Pro2 769D',
|
|
112
|
+
localName: 'Pro2 769D',
|
|
113
|
+
serviceUUIDs: [
|
|
114
|
+
'0000180a-0000-1000-8000-00805f9b34fb',
|
|
115
|
+
'0000180f-0000-1000-8000-00805f9b34fb',
|
|
116
|
+
'0000fffd-0000-1000-8000-00805f9b34fb',
|
|
117
|
+
ONEKEY_SERVICE_UUID,
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}),
|
|
122
|
+
stopDeviceScan: jest.fn(),
|
|
123
|
+
};
|
|
124
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
125
|
+
transport.blePlxManager = blePlxManager as never;
|
|
126
|
+
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
127
|
+
|
|
128
|
+
const devices = await transport.enumerate();
|
|
129
|
+
|
|
130
|
+
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -3,7 +3,6 @@ import transportPackage, {
|
|
|
3
3
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
4
4
|
ProtocolV2,
|
|
5
5
|
TRANSPORT_EVENT,
|
|
6
|
-
bytesToHex,
|
|
7
6
|
} from '@onekeyfe/hd-transport';
|
|
8
7
|
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
9
8
|
|
|
@@ -46,6 +45,11 @@ jest.mock('../subscribeBleOn', () => ({
|
|
|
46
45
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
47
46
|
}));
|
|
48
47
|
|
|
48
|
+
const setPlatformOS = (os: 'ios' | 'android') => {
|
|
49
|
+
const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
|
|
50
|
+
reactNative.Platform.OS = os;
|
|
51
|
+
};
|
|
52
|
+
|
|
49
53
|
const { parseConfigure } = transportPackage;
|
|
50
54
|
|
|
51
55
|
const protocolV1Schema = {
|
|
@@ -69,11 +73,13 @@ const protocolV1Schema = {
|
|
|
69
73
|
|
|
70
74
|
const protocolV2Schema = {
|
|
71
75
|
nested: {
|
|
76
|
+
ProtocolInfoRequest: { fields: {} },
|
|
72
77
|
Ping: {
|
|
73
78
|
fields: {
|
|
74
79
|
message: { type: 'string', id: 1 },
|
|
75
80
|
},
|
|
76
81
|
},
|
|
82
|
+
DeviceInfoGet: { fields: {} },
|
|
77
83
|
FileWrite: { fields: {} },
|
|
78
84
|
Success: {
|
|
79
85
|
fields: {
|
|
@@ -82,8 +88,10 @@ const protocolV2Schema = {
|
|
|
82
88
|
},
|
|
83
89
|
MessageType: {
|
|
84
90
|
values: {
|
|
91
|
+
MessageType_ProtocolInfoRequest: 60200,
|
|
85
92
|
MessageType_Ping: 60206,
|
|
86
93
|
MessageType_Success: 60207,
|
|
94
|
+
MessageType_DeviceInfoGet: 60600,
|
|
87
95
|
MessageType_FileWrite: 60805,
|
|
88
96
|
},
|
|
89
97
|
},
|
|
@@ -95,7 +103,13 @@ const schemas = {
|
|
|
95
103
|
protocolV2: parseConfigure(protocolV2Schema),
|
|
96
104
|
};
|
|
97
105
|
|
|
98
|
-
const createHarness = (
|
|
106
|
+
const createHarness = ({
|
|
107
|
+
deviceName = 'OneKey Pro 2',
|
|
108
|
+
isWritableWithResponse = true,
|
|
109
|
+
}: {
|
|
110
|
+
deviceName?: string;
|
|
111
|
+
isWritableWithResponse?: boolean;
|
|
112
|
+
} = {}) => {
|
|
99
113
|
const uuid = 'rn-pro2-id';
|
|
100
114
|
const sentSeqs: number[] = [];
|
|
101
115
|
let responseSeq = 0;
|
|
@@ -134,15 +148,15 @@ const createHarness = () => {
|
|
|
134
148
|
const writeCharacteristic = {
|
|
135
149
|
uuid: '0002',
|
|
136
150
|
deviceID: uuid,
|
|
137
|
-
isWritableWithResponse
|
|
151
|
+
isWritableWithResponse,
|
|
138
152
|
isWritableWithoutResponse: true,
|
|
139
153
|
writeWithResponse: jest.fn(handleWrite),
|
|
140
154
|
writeWithoutResponse: jest.fn(handleWrite),
|
|
141
155
|
};
|
|
142
156
|
const device = {
|
|
143
157
|
id: uuid,
|
|
144
|
-
name:
|
|
145
|
-
localName:
|
|
158
|
+
name: deviceName,
|
|
159
|
+
localName: deviceName,
|
|
146
160
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
147
161
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
148
162
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
@@ -184,7 +198,13 @@ const createHarness = () => {
|
|
|
184
198
|
};
|
|
185
199
|
};
|
|
186
200
|
|
|
187
|
-
const createV1Harness = (
|
|
201
|
+
const createV1Harness = ({
|
|
202
|
+
respondOnWriteCount = 1,
|
|
203
|
+
isWritableWithResponse = true,
|
|
204
|
+
}: {
|
|
205
|
+
respondOnWriteCount?: number | number[];
|
|
206
|
+
isWritableWithResponse?: boolean;
|
|
207
|
+
} = {}) => {
|
|
188
208
|
const uuid = 'rn-classic-id';
|
|
189
209
|
const notifySubscriptionRemovers: jest.Mock[] = [];
|
|
190
210
|
const disconnectSubscriptionRemovers: jest.Mock[] = [];
|
|
@@ -203,20 +223,25 @@ const createV1Harness = () => {
|
|
|
203
223
|
}),
|
|
204
224
|
};
|
|
205
225
|
let writeCount = 0;
|
|
226
|
+
const responseWriteCounts = new Set(
|
|
227
|
+
Array.isArray(respondOnWriteCount) ? respondOnWriteCount : [respondOnWriteCount]
|
|
228
|
+
);
|
|
229
|
+
const handleWrite = () => {
|
|
230
|
+
writeCount += 1;
|
|
231
|
+
if (responseWriteCounts.has(writeCount)) {
|
|
232
|
+
notifyCallback?.(null, {
|
|
233
|
+
value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return Promise.resolve();
|
|
237
|
+
};
|
|
206
238
|
const writeCharacteristic = {
|
|
207
239
|
uuid: '0002',
|
|
208
240
|
deviceID: uuid,
|
|
209
|
-
isWritableWithResponse
|
|
241
|
+
isWritableWithResponse,
|
|
210
242
|
isWritableWithoutResponse: true,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (writeCount === 1) {
|
|
214
|
-
notifyCallback?.(null, {
|
|
215
|
-
value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return Promise.resolve();
|
|
219
|
-
}),
|
|
243
|
+
writeWithResponse: jest.fn(handleWrite),
|
|
244
|
+
writeWithoutResponse: jest.fn(handleWrite),
|
|
220
245
|
};
|
|
221
246
|
const device = {
|
|
222
247
|
id: uuid,
|
|
@@ -249,6 +274,7 @@ const createV1Harness = () => {
|
|
|
249
274
|
uuid,
|
|
250
275
|
device,
|
|
251
276
|
bleManager,
|
|
277
|
+
writeCharacteristic,
|
|
252
278
|
notifySubscriptionRemovers,
|
|
253
279
|
disconnectSubscriptionRemovers,
|
|
254
280
|
};
|
|
@@ -264,6 +290,19 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
264
290
|
).toBeNull();
|
|
265
291
|
});
|
|
266
292
|
|
|
293
|
+
test.each(['status 143', 'status:143', 'status = 143', 'GATT_CONGESTED'])(
|
|
294
|
+
'classifies %s as transient GATT congestion',
|
|
295
|
+
message => {
|
|
296
|
+
expect(getFirmwareUploadWriteRetryType({ message })).toBe('congested');
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
test('handles long uncontrolled status messages without a backtracking regular expression', () => {
|
|
301
|
+
const message = `status${' '.repeat(100_000)}142`;
|
|
302
|
+
|
|
303
|
+
expect(getFirmwareUploadWriteRetryType({ message })).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
267
306
|
test('keeps another device reader when releasing a device with an active V1 call', async () => {
|
|
268
307
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
269
308
|
const activeV1Call = createDeferred<string>();
|
|
@@ -302,12 +341,101 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
302
341
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
303
342
|
});
|
|
304
343
|
|
|
344
|
+
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
345
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
346
|
+
respondOnWriteCount: [1, 2],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V1' })).resolves.toEqual({
|
|
350
|
+
uuid,
|
|
351
|
+
protocolType: 'V1',
|
|
352
|
+
});
|
|
353
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
354
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
355
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
356
|
+
|
|
357
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
358
|
+
await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
359
|
+
|
|
360
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
361
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
362
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
363
|
+
await transport.release(uuid, true);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
|
|
367
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
368
|
+
isWritableWithResponse: false,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
372
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
373
|
+
|
|
374
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
375
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
376
|
+
await transport.release(uuid, true);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('does not resend a failed iOS Protocol V1 control write without response', async () => {
|
|
380
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness();
|
|
381
|
+
const writeError = new Error('write with response failed');
|
|
382
|
+
|
|
383
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
384
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
|
|
385
|
+
|
|
386
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
|
|
387
|
+
errorCode: HardwareErrorCode.BleWriteCharacteristicError,
|
|
388
|
+
});
|
|
389
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
390
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
391
|
+
await transport.release(uuid, true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('actively probes Protocol V2 on iOS when only a name-derived hint is available', async () => {
|
|
395
|
+
const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
|
|
396
|
+
deviceName: 'Pro2 6E9E',
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
400
|
+
uuid,
|
|
401
|
+
protocolType: 'V2',
|
|
402
|
+
});
|
|
403
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
404
|
+
|
|
405
|
+
await expect(
|
|
406
|
+
transport.call(uuid, 'Ping', { message: 'first-core-command' })
|
|
407
|
+
).resolves.toBeDefined();
|
|
408
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
409
|
+
await transport.release(uuid, true);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
413
|
+
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
414
|
+
const probeProtocolV1 = jest
|
|
415
|
+
.spyOn(transport as any, 'probeProtocolV1')
|
|
416
|
+
.mockResolvedValue(false);
|
|
417
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
|
|
418
|
+
|
|
419
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
420
|
+
uuid,
|
|
421
|
+
protocolType: 'V2',
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(probeProtocolV1).toHaveBeenCalledTimes(1);
|
|
425
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
426
|
+
expect(probeProtocolV1.mock.invocationCallOrder[0]).toBeLessThan(
|
|
427
|
+
probeProtocolV2.mock.invocationCallOrder[0]
|
|
428
|
+
);
|
|
429
|
+
await transport.release(uuid, true);
|
|
430
|
+
});
|
|
431
|
+
|
|
305
432
|
test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
|
|
433
|
+
setPlatformOS('android');
|
|
306
434
|
const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
|
|
307
435
|
createV1Harness();
|
|
308
436
|
const probeProtocolV2 = jest
|
|
309
437
|
.spyOn(transport as any, 'probeProtocolV2')
|
|
310
|
-
.
|
|
438
|
+
.mockImplementation(async () => {
|
|
311
439
|
await (transport as any).releaseNative(uuid, true);
|
|
312
440
|
return false;
|
|
313
441
|
});
|
|
@@ -333,8 +461,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
333
461
|
});
|
|
334
462
|
|
|
335
463
|
test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
|
|
464
|
+
setPlatformOS('android');
|
|
336
465
|
const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
|
|
337
|
-
jest.spyOn(transport as any, 'probeProtocolV2').
|
|
466
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
|
|
338
467
|
await (transport as any).releaseNative(uuid, true);
|
|
339
468
|
return false;
|
|
340
469
|
});
|
|
@@ -353,7 +482,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
353
482
|
});
|
|
354
483
|
|
|
355
484
|
test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
|
|
356
|
-
const { transport, uuid, device } = createV1Harness(
|
|
485
|
+
const { transport, uuid, device } = createV1Harness({
|
|
486
|
+
respondOnWriteCount: Number.POSITIVE_INFINITY,
|
|
487
|
+
});
|
|
357
488
|
|
|
358
489
|
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
359
490
|
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
|
|
@@ -365,29 +496,31 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
365
496
|
});
|
|
366
497
|
|
|
367
498
|
afterEach(() => {
|
|
499
|
+
setPlatformOS('ios');
|
|
368
500
|
resetProtocolV2BleTuning();
|
|
369
501
|
});
|
|
370
502
|
|
|
371
|
-
test('
|
|
503
|
+
test('uses the first Protocol V2 sequence for the first Core call when protocol is known', async () => {
|
|
372
504
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
373
505
|
|
|
374
|
-
await transport.acquire({ uuid });
|
|
375
|
-
await transport.call(uuid, 'Ping', { message: '
|
|
506
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
507
|
+
await transport.call(uuid, 'Ping', { message: 'first-core-command' });
|
|
376
508
|
|
|
377
|
-
expect(sentSeqs).toEqual([1
|
|
378
|
-
expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
|
|
509
|
+
expect(sentSeqs).toEqual([1]);
|
|
379
510
|
await transport.release(uuid, true);
|
|
380
511
|
});
|
|
381
512
|
|
|
382
513
|
test('rejects the active Protocol V2 reader when the current monitor errors', async () => {
|
|
383
514
|
const harness = createHarness();
|
|
384
515
|
const { transport, uuid, sentSeqs } = harness;
|
|
385
|
-
await transport.acquire({ uuid });
|
|
516
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
386
517
|
harness.setShouldRespond(false);
|
|
387
518
|
|
|
388
519
|
const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
|
|
389
|
-
while (sentSeqs.length <
|
|
390
|
-
await Promise
|
|
520
|
+
while (sentSeqs.length < 1) {
|
|
521
|
+
await new Promise(resolve => {
|
|
522
|
+
setTimeout(resolve, 0);
|
|
523
|
+
});
|
|
391
524
|
}
|
|
392
525
|
await new Promise(resolve => {
|
|
393
526
|
setTimeout(resolve, 0);
|
|
@@ -402,40 +535,149 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
402
535
|
test('retains the sequence cursor when a new monitor generation is acquired', async () => {
|
|
403
536
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
404
537
|
|
|
405
|
-
await transport.acquire({ uuid });
|
|
538
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
539
|
+
await transport.call(uuid, 'Ping', { message: 'first-generation' });
|
|
406
540
|
await transport.release(uuid, true);
|
|
407
|
-
await transport.acquire({ uuid });
|
|
541
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
542
|
+
await transport.call(uuid, 'Ping', { message: 'second-generation' });
|
|
408
543
|
|
|
409
544
|
expect(sentSeqs).toEqual([1, 2]);
|
|
410
545
|
await transport.release(uuid, true);
|
|
411
546
|
});
|
|
412
547
|
|
|
413
|
-
test('uses
|
|
548
|
+
test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
|
|
414
549
|
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
415
550
|
|
|
416
|
-
await transport.acquire({ uuid });
|
|
417
|
-
|
|
551
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
552
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
553
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
418
554
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
419
555
|
|
|
420
|
-
await transport.call(uuid, '
|
|
421
|
-
|
|
422
|
-
|
|
556
|
+
await transport.call(uuid, 'DeviceInfoGet', {});
|
|
557
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
558
|
+
|
|
559
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
560
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
561
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
562
|
+
|
|
563
|
+
await transport.release(uuid, true);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
|
|
567
|
+
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
568
|
+
|
|
569
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
423
570
|
|
|
424
571
|
await transport.call(uuid, 'FileWrite', {});
|
|
425
|
-
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
572
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
426
573
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
427
574
|
await transport.release(uuid, true);
|
|
428
575
|
});
|
|
429
576
|
|
|
577
|
+
test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
|
|
578
|
+
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
579
|
+
|
|
580
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
581
|
+
|
|
582
|
+
await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
|
|
583
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
584
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
585
|
+
await transport.release(uuid, true);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
|
|
589
|
+
const { transport, uuid, writeCharacteristic } = createHarness({
|
|
590
|
+
isWritableWithResponse: false,
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
594
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
595
|
+
|
|
596
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
597
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
598
|
+
await transport.release(uuid, true);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test('does not resend a failed iOS Protocol V2 control write without response', async () => {
|
|
602
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
603
|
+
const writeError = new Error('write with response failed');
|
|
604
|
+
const writeWithResponse = jest.fn().mockRejectedValue(writeError);
|
|
605
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
606
|
+
const context = {
|
|
607
|
+
messageName: 'ProtocolInfoRequest',
|
|
608
|
+
timeoutMs: 1000,
|
|
609
|
+
highVolume: false,
|
|
610
|
+
generation: 1,
|
|
611
|
+
signal: new AbortController().signal,
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
await expect(
|
|
615
|
+
transport.writeProtocolV2Packet(
|
|
616
|
+
'test-device',
|
|
617
|
+
{
|
|
618
|
+
writeCharacteristic: {
|
|
619
|
+
isWritableWithResponse: true,
|
|
620
|
+
writeWithResponse,
|
|
621
|
+
writeWithoutResponse,
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
Buffer.from('control').toString('base64'),
|
|
625
|
+
context,
|
|
626
|
+
jest.fn()
|
|
627
|
+
)
|
|
628
|
+
).rejects.toBe(writeError);
|
|
629
|
+
expect(writeWithResponse).toHaveBeenCalledTimes(1);
|
|
630
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('paces a one-packet Protocol V2 control write on iOS', async () => {
|
|
634
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
635
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
636
|
+
const bleTransport = {
|
|
637
|
+
mtuSize: 23,
|
|
638
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
639
|
+
};
|
|
640
|
+
const context = {
|
|
641
|
+
messageName: 'ProtocolInfoRequest',
|
|
642
|
+
timeoutMs: 1000,
|
|
643
|
+
highVolume: false,
|
|
644
|
+
generation: 1,
|
|
645
|
+
signal: new AbortController().signal,
|
|
646
|
+
};
|
|
647
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
648
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
649
|
+
|
|
650
|
+
try {
|
|
651
|
+
const call = transport.writeProtocolV2Frame(
|
|
652
|
+
'test-device',
|
|
653
|
+
bleTransport,
|
|
654
|
+
new Uint8Array(10),
|
|
655
|
+
context,
|
|
656
|
+
jest.fn()
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
await Promise.resolve();
|
|
660
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
|
|
661
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
662
|
+
|
|
663
|
+
await call;
|
|
664
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
665
|
+
} finally {
|
|
666
|
+
setTimeoutSpy.mockRestore();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
430
670
|
test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
|
|
431
671
|
const harness = createHarness();
|
|
432
672
|
const { transport, uuid, sentSeqs } = harness;
|
|
433
|
-
await transport.acquire({ uuid });
|
|
673
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
434
674
|
harness.setShouldRespond(false);
|
|
435
675
|
|
|
436
676
|
const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
|
|
437
|
-
while (sentSeqs.length <
|
|
438
|
-
await Promise
|
|
677
|
+
while (sentSeqs.length < 1) {
|
|
678
|
+
await new Promise(resolve => {
|
|
679
|
+
setTimeout(resolve, 0);
|
|
680
|
+
});
|
|
439
681
|
}
|
|
440
682
|
|
|
441
683
|
const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
|
|
@@ -448,7 +690,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
448
690
|
const disconnectListener = jest.fn();
|
|
449
691
|
harness.emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnectListener);
|
|
450
692
|
|
|
451
|
-
await harness.transport.acquire({ uuid: harness.uuid });
|
|
693
|
+
await harness.transport.acquire({ uuid: harness.uuid, expectedProtocol: 'V2' });
|
|
452
694
|
harness.emitDisconnect();
|
|
453
695
|
await harness.transport.disconnect(harness.uuid);
|
|
454
696
|
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
HardwareErrorCode,
|
|
29
29
|
createDeferred,
|
|
30
30
|
isOnekeyBluetoothDevice,
|
|
31
|
+
isPro2FindMyAdvertisementName,
|
|
31
32
|
} from '@onekeyfe/hd-shared';
|
|
32
33
|
|
|
33
34
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
@@ -60,6 +61,7 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
60
61
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
61
62
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
62
63
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
64
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
63
65
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
64
66
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
65
67
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
@@ -71,6 +73,33 @@ type ResolvedBleCharacteristics = {
|
|
|
71
73
|
notifyCharacteristic: Characteristic;
|
|
72
74
|
};
|
|
73
75
|
|
|
76
|
+
const isAsciiWhitespace = (code: number) =>
|
|
77
|
+
code === 0x09 ||
|
|
78
|
+
code === 0x0a ||
|
|
79
|
+
code === 0x0b ||
|
|
80
|
+
code === 0x0c ||
|
|
81
|
+
code === 0x0d ||
|
|
82
|
+
code === 0x20;
|
|
83
|
+
|
|
84
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
85
|
+
let searchFrom = 0;
|
|
86
|
+
while (searchFrom < text.length) {
|
|
87
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
88
|
+
if (statusIndex < 0) return false;
|
|
89
|
+
|
|
90
|
+
let cursor = statusIndex + 'status'.length;
|
|
91
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
92
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
93
|
+
cursor += 1;
|
|
94
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
95
|
+
}
|
|
96
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
97
|
+
|
|
98
|
+
searchFrom = statusIndex + 'status'.length;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
74
103
|
const delay = (ms: number) =>
|
|
75
104
|
new Promise<void>(resolve => {
|
|
76
105
|
setTimeout(resolve, ms);
|
|
@@ -99,7 +128,7 @@ export const getFirmwareUploadWriteRetryType = (
|
|
|
99
128
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
100
129
|
.filter(value => typeof value === 'string')
|
|
101
130
|
.join(' ');
|
|
102
|
-
return
|
|
131
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
103
132
|
};
|
|
104
133
|
|
|
105
134
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
@@ -655,12 +684,21 @@ export default class ReactNativeBleTransport {
|
|
|
655
684
|
}
|
|
656
685
|
|
|
657
686
|
const displayName = getDeviceDisplayName(device);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
687
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
688
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
689
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
690
|
+
const isFindMyPeripheral =
|
|
691
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
692
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
693
|
+
const isOneKey =
|
|
694
|
+
!isUnnamedIOSPeripheral &&
|
|
695
|
+
!isFindMyPeripheral &&
|
|
696
|
+
isOnekeyBluetoothDevice({
|
|
697
|
+
id: device?.id,
|
|
698
|
+
name: device?.name,
|
|
699
|
+
localName: device?.localName,
|
|
700
|
+
serviceUuids: device?.serviceUUIDs,
|
|
701
|
+
});
|
|
664
702
|
if (isOneKey) {
|
|
665
703
|
addDevice(device as unknown as Device);
|
|
666
704
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -681,7 +719,12 @@ export default class ReactNativeBleTransport {
|
|
|
681
719
|
'localName' in device && typeof device.localName === 'string'
|
|
682
720
|
? device.localName
|
|
683
721
|
: null;
|
|
722
|
+
const isFindMyPeripheral =
|
|
723
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
724
|
+
isPro2FindMyAdvertisementName(localName);
|
|
725
|
+
|
|
684
726
|
if (
|
|
727
|
+
!isFindMyPeripheral &&
|
|
685
728
|
isOnekeyBluetoothDevice({
|
|
686
729
|
id: device.id,
|
|
687
730
|
name: device.name,
|
|
@@ -1331,7 +1374,7 @@ export default class ReactNativeBleTransport {
|
|
|
1331
1374
|
await this.writeBlePacket(
|
|
1332
1375
|
uuid,
|
|
1333
1376
|
data,
|
|
1334
|
-
payload => transport.
|
|
1377
|
+
payload => transport.writeWithRetry(payload),
|
|
1335
1378
|
isCurrentOwner
|
|
1336
1379
|
);
|
|
1337
1380
|
return;
|
|
@@ -1361,10 +1404,15 @@ export default class ReactNativeBleTransport {
|
|
|
1361
1404
|
const outData = o.toString('base64');
|
|
1362
1405
|
// Upload resources on low-end phones may OOM
|
|
1363
1406
|
try {
|
|
1407
|
+
const shouldUseWriteWithResponse =
|
|
1408
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1364
1409
|
await this.writeBlePacket(
|
|
1365
1410
|
uuid,
|
|
1366
1411
|
outData,
|
|
1367
|
-
payload =>
|
|
1412
|
+
payload =>
|
|
1413
|
+
shouldUseWriteWithResponse
|
|
1414
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1415
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1368
1416
|
isCurrentOwner
|
|
1369
1417
|
);
|
|
1370
1418
|
} catch (e) {
|
|
@@ -1780,6 +1828,16 @@ export default class ReactNativeBleTransport {
|
|
|
1780
1828
|
protocolHint?: ProtocolType,
|
|
1781
1829
|
rebuildTransport?: () => Promise<void>
|
|
1782
1830
|
): Promise<ProtocolType> {
|
|
1831
|
+
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1832
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1833
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1834
|
+
deviceId: uuid,
|
|
1835
|
+
protocol: expectedProtocol,
|
|
1836
|
+
source: 'expected',
|
|
1837
|
+
});
|
|
1838
|
+
return expectedProtocol;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1783
1841
|
if (expectedProtocol === 'V1') {
|
|
1784
1842
|
if (await this.probeProtocolV1(uuid)) {
|
|
1785
1843
|
this.deviceProtocol.set(uuid, 'V1');
|
|
@@ -2048,6 +2106,9 @@ export default class ReactNativeBleTransport {
|
|
|
2048
2106
|
context: ProtocolV2CallContext,
|
|
2049
2107
|
assertCurrentGeneration: () => void
|
|
2050
2108
|
) {
|
|
2109
|
+
const shouldUseWriteWithResponse =
|
|
2110
|
+
transport.writeCharacteristic.isWritableWithResponse &&
|
|
2111
|
+
(context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
|
|
2051
2112
|
let attempt = 0;
|
|
2052
2113
|
for (;;) {
|
|
2053
2114
|
assertCurrentGeneration();
|
|
@@ -2058,7 +2119,10 @@ export default class ReactNativeBleTransport {
|
|
|
2058
2119
|
await this.writeBlePacket(
|
|
2059
2120
|
uuid,
|
|
2060
2121
|
base64,
|
|
2061
|
-
payload =>
|
|
2122
|
+
payload =>
|
|
2123
|
+
shouldUseWriteWithResponse
|
|
2124
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2125
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2062
2126
|
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2063
2127
|
// tear down the link that the current generation is using.
|
|
2064
2128
|
() => {
|
|
@@ -2105,12 +2169,19 @@ export default class ReactNativeBleTransport {
|
|
|
2105
2169
|
androidPacketLength: tuning.androidPacketLength,
|
|
2106
2170
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
2107
2171
|
});
|
|
2172
|
+
// Match Desktop BLE pacing so Pro2 firmware can finish the previous response
|
|
2173
|
+
// before the next single-packet control command is written.
|
|
2174
|
+
const initialDelayMs =
|
|
2175
|
+
Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
2176
|
+
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
2177
|
+
: 0;
|
|
2108
2178
|
await writeProtocolV2BleFrame({
|
|
2109
2179
|
frame,
|
|
2110
2180
|
packetCapacity,
|
|
2111
2181
|
assertActive: assertCurrentGeneration,
|
|
2112
2182
|
signal: context.signal,
|
|
2113
2183
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2184
|
+
initialDelayMs,
|
|
2114
2185
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
2115
2186
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
2116
2187
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
@@ -2143,7 +2214,7 @@ export default class ReactNativeBleTransport {
|
|
|
2143
2214
|
const tuning = getProtocolV2BleTuning();
|
|
2144
2215
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2145
2216
|
name,
|
|
2146
|
-
writeMode: 'withoutResponse',
|
|
2217
|
+
writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
|
|
2147
2218
|
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
2148
2219
|
});
|
|
2149
2220
|
}
|