@onekeyfe/hd-transport-react-native 1.2.0-alpha.49 → 1.2.0-alpha.50
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/index.d.ts.map +1 -1
- package/dist/index.js +59 -14
- package/package.json +5 -5
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +229 -33
- package/src/index.ts +62 -8
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
|
@@ -222,6 +222,7 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = reactNative.Platform.OS === 'ios' ? 4 :
|
|
|
222
222
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = reactNative.Platform.OS === 'ios' ? 8 : 10;
|
|
223
223
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = reactNative.Platform.OS === 'ios' ? 24 : 30;
|
|
224
224
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
225
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
225
226
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
226
227
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY = reactNative.Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
227
228
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
@@ -614,12 +615,17 @@ class ReactNativeBleTransport {
|
|
|
614
615
|
return;
|
|
615
616
|
}
|
|
616
617
|
const displayName = getDeviceDisplayName(device);
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
618
|
+
const isUnnamedIOSPeripheral = reactNative.Platform.OS === 'ios' && !(displayName === null || displayName === void 0 ? void 0 : displayName.trim());
|
|
619
|
+
const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.name) ||
|
|
620
|
+
hdShared.isPro2FindMyAdvertisementName(device === null || device === void 0 ? void 0 : device.localName);
|
|
621
|
+
const isOneKey = !isUnnamedIOSPeripheral &&
|
|
622
|
+
!isFindMyPeripheral &&
|
|
623
|
+
hdShared.isOnekeyBluetoothDevice({
|
|
624
|
+
id: device === null || device === void 0 ? void 0 : device.id,
|
|
625
|
+
name: device === null || device === void 0 ? void 0 : device.name,
|
|
626
|
+
localName: device === null || device === void 0 ? void 0 : device.localName,
|
|
627
|
+
serviceUuids: device === null || device === void 0 ? void 0 : device.serviceUUIDs,
|
|
628
|
+
});
|
|
623
629
|
if (isOneKey) {
|
|
624
630
|
addDevice(device);
|
|
625
631
|
}
|
|
@@ -637,12 +643,15 @@ class ReactNativeBleTransport {
|
|
|
637
643
|
const localName = 'localName' in device && typeof device.localName === 'string'
|
|
638
644
|
? device.localName
|
|
639
645
|
: null;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
+
const isFindMyPeripheral = hdShared.isPro2FindMyAdvertisementName(device.name) ||
|
|
647
|
+
hdShared.isPro2FindMyAdvertisementName(localName);
|
|
648
|
+
if (!isFindMyPeripheral &&
|
|
649
|
+
hdShared.isOnekeyBluetoothDevice({
|
|
650
|
+
id: device.id,
|
|
651
|
+
name: device.name,
|
|
652
|
+
localName,
|
|
653
|
+
serviceUuids: device.serviceUUIDs,
|
|
654
|
+
})) {
|
|
646
655
|
Log === null || Log === void 0 ? void 0 : Log.debug('search connected peripheral: ', device.id);
|
|
647
656
|
addDevice(device);
|
|
648
657
|
}
|
|
@@ -1138,7 +1147,13 @@ class ReactNativeBleTransport {
|
|
|
1138
1147
|
for (const o of buffers) {
|
|
1139
1148
|
const outData = o.toString('base64');
|
|
1140
1149
|
try {
|
|
1141
|
-
|
|
1150
|
+
const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1151
|
+
if (shouldUseWriteWithResponse) {
|
|
1152
|
+
yield transport.writeCharacteristic.writeWithResponse(outData);
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
yield transport.writeCharacteristic.writeWithoutResponse(outData);
|
|
1156
|
+
}
|
|
1142
1157
|
}
|
|
1143
1158
|
catch (e) {
|
|
1144
1159
|
Log === null || Log === void 0 ? void 0 : Log.debug('writeCharacteristic write error: ', e);
|
|
@@ -1293,7 +1308,25 @@ class ReactNativeBleTransport {
|
|
|
1293
1308
|
}
|
|
1294
1309
|
}
|
|
1295
1310
|
detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
|
|
1311
|
+
var _a;
|
|
1296
1312
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1313
|
+
if (reactNative.Platform.OS === 'ios') {
|
|
1314
|
+
const protocol = (_a = expectedProtocol !== null && expectedProtocol !== void 0 ? expectedProtocol : protocolHint) !== null && _a !== void 0 ? _a : 'V1';
|
|
1315
|
+
let source = 'ios-legacy-default';
|
|
1316
|
+
if (expectedProtocol) {
|
|
1317
|
+
source = 'expected';
|
|
1318
|
+
}
|
|
1319
|
+
else if (protocolHint) {
|
|
1320
|
+
source = 'hint';
|
|
1321
|
+
}
|
|
1322
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
1323
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1324
|
+
deviceId: uuid,
|
|
1325
|
+
protocol,
|
|
1326
|
+
source,
|
|
1327
|
+
});
|
|
1328
|
+
return protocol;
|
|
1329
|
+
}
|
|
1297
1330
|
if (expectedProtocol === 'V1') {
|
|
1298
1331
|
if (yield this.probeProtocolV1(uuid)) {
|
|
1299
1332
|
this.deviceProtocol.set(uuid, 'V1');
|
|
@@ -1499,6 +1532,9 @@ class ReactNativeBleTransport {
|
|
|
1499
1532
|
}
|
|
1500
1533
|
writeProtocolV2Packet(transport, base64, context, assertCurrentGeneration) {
|
|
1501
1534
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1535
|
+
const shouldUseWriteWithResponse = reactNative.Platform.OS === 'ios' &&
|
|
1536
|
+
!context.highVolume &&
|
|
1537
|
+
transport.writeCharacteristic.isWritableWithResponse;
|
|
1502
1538
|
let attempt = 0;
|
|
1503
1539
|
for (;;) {
|
|
1504
1540
|
assertCurrentGeneration();
|
|
@@ -1506,7 +1542,12 @@ class ReactNativeBleTransport {
|
|
|
1506
1542
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1507
1543
|
}
|
|
1508
1544
|
try {
|
|
1509
|
-
|
|
1545
|
+
if (shouldUseWriteWithResponse) {
|
|
1546
|
+
yield transport.writeCharacteristic.writeWithResponse(base64);
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
yield transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1550
|
+
}
|
|
1510
1551
|
assertCurrentGeneration();
|
|
1511
1552
|
return;
|
|
1512
1553
|
}
|
|
@@ -1536,12 +1577,16 @@ class ReactNativeBleTransport {
|
|
|
1536
1577
|
androidPacketLength: tuning.androidPacketLength,
|
|
1537
1578
|
mtu: reactNative.Platform.OS === 'android' ? transport$1.mtuSize : undefined,
|
|
1538
1579
|
});
|
|
1580
|
+
const initialDelayMs = reactNative.Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
1581
|
+
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
1582
|
+
: 0;
|
|
1539
1583
|
yield transport.writeProtocolV2BleFrame({
|
|
1540
1584
|
frame,
|
|
1541
1585
|
packetCapacity,
|
|
1542
1586
|
assertActive: assertCurrentGeneration,
|
|
1543
1587
|
signal: context.signal,
|
|
1544
1588
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1589
|
+
initialDelayMs,
|
|
1545
1590
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1546
1591
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1547
1592
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
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.50",
|
|
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.50",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.50",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.50",
|
|
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": "0f0565477474a3f29119e0f977eba9328e59f7dd"
|
|
30
30
|
}
|
|
@@ -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
|
};
|
|
@@ -302,12 +328,81 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
302
328
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
303
329
|
});
|
|
304
330
|
|
|
331
|
+
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
332
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
333
|
+
respondOnWriteCount: [1, 2],
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
337
|
+
uuid,
|
|
338
|
+
protocolType: 'V1',
|
|
339
|
+
});
|
|
340
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
341
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
342
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
343
|
+
|
|
344
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
345
|
+
await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
346
|
+
|
|
347
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
348
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
349
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
350
|
+
await transport.release(uuid, true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
|
|
354
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
355
|
+
isWritableWithResponse: false,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await transport.acquire({ uuid });
|
|
359
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
360
|
+
|
|
361
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
362
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
363
|
+
await transport.release(uuid, true);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('does not resend a failed iOS Protocol V1 control write without response', async () => {
|
|
367
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness();
|
|
368
|
+
const writeError = new Error('write with response failed');
|
|
369
|
+
|
|
370
|
+
await transport.acquire({ uuid });
|
|
371
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
|
|
372
|
+
|
|
373
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
|
|
374
|
+
errorCode: HardwareErrorCode.BleWriteCharacteristicError,
|
|
375
|
+
});
|
|
376
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
377
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
378
|
+
await transport.release(uuid, true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('keeps the first Core command as the first iOS BLE request for a Protocol V2 device', async () => {
|
|
382
|
+
const { transport, uuid, sentSeqs, writeCharacteristic } = createHarness({
|
|
383
|
+
deviceName: 'Pro2 6E9E',
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
387
|
+
uuid,
|
|
388
|
+
protocolType: 'V2',
|
|
389
|
+
});
|
|
390
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
391
|
+
|
|
392
|
+
await expect(
|
|
393
|
+
transport.call(uuid, 'Ping', { message: 'first-core-command' })
|
|
394
|
+
).resolves.toBeDefined();
|
|
395
|
+
expect(sentSeqs).toEqual([1]);
|
|
396
|
+
await transport.release(uuid, true);
|
|
397
|
+
});
|
|
398
|
+
|
|
305
399
|
test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
|
|
400
|
+
setPlatformOS('android');
|
|
306
401
|
const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
|
|
307
402
|
createV1Harness();
|
|
308
403
|
const probeProtocolV2 = jest
|
|
309
404
|
.spyOn(transport as any, 'probeProtocolV2')
|
|
310
|
-
.
|
|
405
|
+
.mockImplementation(async () => {
|
|
311
406
|
await (transport as any).releaseNative(uuid, true);
|
|
312
407
|
return false;
|
|
313
408
|
});
|
|
@@ -333,8 +428,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
333
428
|
});
|
|
334
429
|
|
|
335
430
|
test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
|
|
431
|
+
setPlatformOS('android');
|
|
336
432
|
const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
|
|
337
|
-
jest.spyOn(transport as any, 'probeProtocolV2').
|
|
433
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
|
|
338
434
|
await (transport as any).releaseNative(uuid, true);
|
|
339
435
|
return false;
|
|
340
436
|
});
|
|
@@ -353,7 +449,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
353
449
|
});
|
|
354
450
|
|
|
355
451
|
test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
|
|
356
|
-
const { transport, uuid, device } = createV1Harness(
|
|
452
|
+
const { transport, uuid, device } = createV1Harness({
|
|
453
|
+
respondOnWriteCount: Number.POSITIVE_INFINITY,
|
|
454
|
+
});
|
|
357
455
|
|
|
358
456
|
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
359
457
|
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
|
|
@@ -365,17 +463,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
365
463
|
});
|
|
366
464
|
|
|
367
465
|
afterEach(() => {
|
|
466
|
+
setPlatformOS('ios');
|
|
368
467
|
resetProtocolV2BleTuning();
|
|
369
468
|
});
|
|
370
469
|
|
|
371
|
-
test('
|
|
470
|
+
test('starts the Protocol V2 sequence with the first Core call on iOS', async () => {
|
|
372
471
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
373
472
|
|
|
374
473
|
await transport.acquire({ uuid });
|
|
375
|
-
await transport.call(uuid, 'Ping', { message: '
|
|
474
|
+
await transport.call(uuid, 'Ping', { message: 'first-core-command' });
|
|
376
475
|
|
|
377
|
-
expect(sentSeqs).toEqual([1
|
|
378
|
-
expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
|
|
476
|
+
expect(sentSeqs).toEqual([1]);
|
|
379
477
|
await transport.release(uuid, true);
|
|
380
478
|
});
|
|
381
479
|
|
|
@@ -386,8 +484,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
386
484
|
harness.setShouldRespond(false);
|
|
387
485
|
|
|
388
486
|
const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
|
|
389
|
-
while (sentSeqs.length <
|
|
390
|
-
await Promise
|
|
487
|
+
while (sentSeqs.length < 1) {
|
|
488
|
+
await new Promise(resolve => {
|
|
489
|
+
setTimeout(resolve, 0);
|
|
490
|
+
});
|
|
391
491
|
}
|
|
392
492
|
await new Promise(resolve => {
|
|
393
493
|
setTimeout(resolve, 0);
|
|
@@ -403,30 +503,124 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
403
503
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
404
504
|
|
|
405
505
|
await transport.acquire({ uuid });
|
|
506
|
+
await transport.call(uuid, 'Ping', { message: 'first-generation' });
|
|
406
507
|
await transport.release(uuid, true);
|
|
407
508
|
await transport.acquire({ uuid });
|
|
509
|
+
await transport.call(uuid, 'Ping', { message: 'second-generation' });
|
|
408
510
|
|
|
409
511
|
expect(sentSeqs).toEqual([1, 2]);
|
|
410
512
|
await transport.release(uuid, true);
|
|
411
513
|
});
|
|
412
514
|
|
|
413
|
-
test('uses
|
|
515
|
+
test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
|
|
414
516
|
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
415
517
|
|
|
416
518
|
await transport.acquire({ uuid });
|
|
417
|
-
|
|
519
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
520
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
418
521
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
419
522
|
|
|
420
|
-
await transport.call(uuid, '
|
|
421
|
-
|
|
422
|
-
|
|
523
|
+
await transport.call(uuid, 'DeviceInfoGet', {});
|
|
524
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
525
|
+
|
|
526
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
527
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
528
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
529
|
+
|
|
530
|
+
await transport.release(uuid, true);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
|
|
534
|
+
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
535
|
+
|
|
536
|
+
await transport.acquire({ uuid });
|
|
423
537
|
|
|
424
538
|
await transport.call(uuid, 'FileWrite', {});
|
|
425
|
-
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
539
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
540
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
541
|
+
await transport.release(uuid, true);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
|
|
545
|
+
const { transport, uuid, writeCharacteristic } = createHarness({
|
|
546
|
+
isWritableWithResponse: false,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await transport.acquire({ uuid });
|
|
550
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
551
|
+
|
|
426
552
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
553
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
427
554
|
await transport.release(uuid, true);
|
|
428
555
|
});
|
|
429
556
|
|
|
557
|
+
test('does not resend a failed iOS Protocol V2 control write without response', async () => {
|
|
558
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
559
|
+
const writeError = new Error('write with response failed');
|
|
560
|
+
const writeWithResponse = jest.fn().mockRejectedValue(writeError);
|
|
561
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
562
|
+
const context = {
|
|
563
|
+
messageName: 'ProtocolInfoRequest',
|
|
564
|
+
timeoutMs: 1000,
|
|
565
|
+
highVolume: false,
|
|
566
|
+
generation: 1,
|
|
567
|
+
signal: new AbortController().signal,
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
await expect(
|
|
571
|
+
transport.writeProtocolV2Packet(
|
|
572
|
+
{
|
|
573
|
+
writeCharacteristic: {
|
|
574
|
+
isWritableWithResponse: true,
|
|
575
|
+
writeWithResponse,
|
|
576
|
+
writeWithoutResponse,
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
Buffer.from('control').toString('base64'),
|
|
580
|
+
context,
|
|
581
|
+
jest.fn()
|
|
582
|
+
)
|
|
583
|
+
).rejects.toBe(writeError);
|
|
584
|
+
expect(writeWithResponse).toHaveBeenCalledTimes(1);
|
|
585
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test('paces a one-packet Protocol V2 control write on iOS', async () => {
|
|
589
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
590
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
591
|
+
const bleTransport = {
|
|
592
|
+
mtuSize: 23,
|
|
593
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
594
|
+
};
|
|
595
|
+
const context = {
|
|
596
|
+
messageName: 'ProtocolInfoRequest',
|
|
597
|
+
timeoutMs: 1000,
|
|
598
|
+
highVolume: false,
|
|
599
|
+
generation: 1,
|
|
600
|
+
signal: new AbortController().signal,
|
|
601
|
+
};
|
|
602
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
603
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
const call = transport.writeProtocolV2Frame(
|
|
607
|
+
bleTransport,
|
|
608
|
+
new Uint8Array(10),
|
|
609
|
+
context,
|
|
610
|
+
jest.fn()
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
await Promise.resolve();
|
|
614
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
|
|
615
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
616
|
+
|
|
617
|
+
await call;
|
|
618
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
619
|
+
} finally {
|
|
620
|
+
setTimeoutSpy.mockRestore();
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
430
624
|
test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
|
|
431
625
|
const harness = createHarness();
|
|
432
626
|
const { transport, uuid, sentSeqs } = harness;
|
|
@@ -434,8 +628,10 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
434
628
|
harness.setShouldRespond(false);
|
|
435
629
|
|
|
436
630
|
const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
|
|
437
|
-
while (sentSeqs.length <
|
|
438
|
-
await Promise
|
|
631
|
+
while (sentSeqs.length < 1) {
|
|
632
|
+
await new Promise(resolve => {
|
|
633
|
+
setTimeout(resolve, 0);
|
|
634
|
+
});
|
|
439
635
|
}
|
|
440
636
|
|
|
441
637
|
const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
|
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;
|
|
@@ -580,12 +582,21 @@ export default class ReactNativeBleTransport {
|
|
|
580
582
|
}
|
|
581
583
|
|
|
582
584
|
const displayName = getDeviceDisplayName(device);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
585
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
586
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
587
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
588
|
+
const isFindMyPeripheral =
|
|
589
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
590
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
591
|
+
const isOneKey =
|
|
592
|
+
!isUnnamedIOSPeripheral &&
|
|
593
|
+
!isFindMyPeripheral &&
|
|
594
|
+
isOnekeyBluetoothDevice({
|
|
595
|
+
id: device?.id,
|
|
596
|
+
name: device?.name,
|
|
597
|
+
localName: device?.localName,
|
|
598
|
+
serviceUuids: device?.serviceUUIDs,
|
|
599
|
+
});
|
|
589
600
|
if (isOneKey) {
|
|
590
601
|
addDevice(device as unknown as Device);
|
|
591
602
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -606,7 +617,12 @@ export default class ReactNativeBleTransport {
|
|
|
606
617
|
'localName' in device && typeof device.localName === 'string'
|
|
607
618
|
? device.localName
|
|
608
619
|
: null;
|
|
620
|
+
const isFindMyPeripheral =
|
|
621
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
622
|
+
isPro2FindMyAdvertisementName(localName);
|
|
623
|
+
|
|
609
624
|
if (
|
|
625
|
+
!isFindMyPeripheral &&
|
|
610
626
|
isOnekeyBluetoothDevice({
|
|
611
627
|
id: device.id,
|
|
612
628
|
name: device.name,
|
|
@@ -1235,7 +1251,13 @@ export default class ReactNativeBleTransport {
|
|
|
1235
1251
|
const outData = o.toString('base64');
|
|
1236
1252
|
// Upload resources on low-end phones may OOM
|
|
1237
1253
|
try {
|
|
1238
|
-
|
|
1254
|
+
const shouldUseWriteWithResponse =
|
|
1255
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1256
|
+
if (shouldUseWriteWithResponse) {
|
|
1257
|
+
await transport.writeCharacteristic.writeWithResponse(outData);
|
|
1258
|
+
} else {
|
|
1259
|
+
await transport.writeCharacteristic.writeWithoutResponse(outData);
|
|
1260
|
+
}
|
|
1239
1261
|
} catch (e) {
|
|
1240
1262
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1241
1263
|
this.runPromise = null;
|
|
@@ -1422,6 +1444,23 @@ export default class ReactNativeBleTransport {
|
|
|
1422
1444
|
protocolHint?: ProtocolType,
|
|
1423
1445
|
rebuildTransport?: () => Promise<void>
|
|
1424
1446
|
): Promise<ProtocolType> {
|
|
1447
|
+
if (Platform.OS === 'ios') {
|
|
1448
|
+
const protocol = expectedProtocol ?? protocolHint ?? 'V1';
|
|
1449
|
+
let source = 'ios-legacy-default';
|
|
1450
|
+
if (expectedProtocol) {
|
|
1451
|
+
source = 'expected';
|
|
1452
|
+
} else if (protocolHint) {
|
|
1453
|
+
source = 'hint';
|
|
1454
|
+
}
|
|
1455
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
1456
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1457
|
+
deviceId: uuid,
|
|
1458
|
+
protocol,
|
|
1459
|
+
source,
|
|
1460
|
+
});
|
|
1461
|
+
return protocol;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1425
1464
|
if (expectedProtocol === 'V1') {
|
|
1426
1465
|
if (await this.probeProtocolV1(uuid)) {
|
|
1427
1466
|
this.deviceProtocol.set(uuid, 'V1');
|
|
@@ -1658,6 +1697,10 @@ export default class ReactNativeBleTransport {
|
|
|
1658
1697
|
context: ProtocolV2CallContext,
|
|
1659
1698
|
assertCurrentGeneration: () => void
|
|
1660
1699
|
) {
|
|
1700
|
+
const shouldUseWriteWithResponse =
|
|
1701
|
+
Platform.OS === 'ios' &&
|
|
1702
|
+
!context.highVolume &&
|
|
1703
|
+
transport.writeCharacteristic.isWritableWithResponse;
|
|
1661
1704
|
let attempt = 0;
|
|
1662
1705
|
for (;;) {
|
|
1663
1706
|
assertCurrentGeneration();
|
|
@@ -1665,7 +1708,11 @@ export default class ReactNativeBleTransport {
|
|
|
1665
1708
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1666
1709
|
}
|
|
1667
1710
|
try {
|
|
1668
|
-
|
|
1711
|
+
if (shouldUseWriteWithResponse) {
|
|
1712
|
+
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1713
|
+
} else {
|
|
1714
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1715
|
+
}
|
|
1669
1716
|
assertCurrentGeneration();
|
|
1670
1717
|
return;
|
|
1671
1718
|
} catch (error) {
|
|
@@ -1700,12 +1747,19 @@ export default class ReactNativeBleTransport {
|
|
|
1700
1747
|
androidPacketLength: tuning.androidPacketLength,
|
|
1701
1748
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1702
1749
|
});
|
|
1750
|
+
// Match Desktop BLE pacing so Pro2 firmware can finish the previous response
|
|
1751
|
+
// before the next single-packet control command is written.
|
|
1752
|
+
const initialDelayMs =
|
|
1753
|
+
Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
1754
|
+
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
1755
|
+
: 0;
|
|
1703
1756
|
await writeProtocolV2BleFrame({
|
|
1704
1757
|
frame,
|
|
1705
1758
|
packetCapacity,
|
|
1706
1759
|
assertActive: assertCurrentGeneration,
|
|
1707
1760
|
signal: context.signal,
|
|
1708
1761
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1762
|
+
initialDelayMs,
|
|
1709
1763
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1710
1764
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1711
1765
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|