@onekeyfe/hd-transport-react-native 1.2.0-alpha.56 → 1.2.0-alpha.62
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 +59 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +245 -84
- package/jest.config.js +5 -0
- package/package.json +5 -5
- package/src/BleTransport.ts +1 -1
- package/src/__tests__/BleTransport.test.ts +2 -2
- package/src/__tests__/connectTimeout.test.ts +192 -0
- package/src/__tests__/protocolV2Link.test.ts +41 -241
- package/src/__tests__/staleCallTimeout.test.ts +211 -0
- package/src/__tests__/writePacketTimeout.test.ts +306 -0
- package/src/index.ts +357 -83
- package/src/__tests__/enumerate.test.ts +0 -132
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.62",
|
|
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.62",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.62",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.62",
|
|
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": "f38f4fc8bf05aea42a4da5b2bd633544597ad958"
|
|
30
30
|
}
|
package/src/BleTransport.ts
CHANGED
|
@@ -24,7 +24,7 @@ describe('BleTransport side-effecting writes', () => {
|
|
|
24
24
|
errorCode: BleErrorCode.DeviceDisconnected,
|
|
25
25
|
});
|
|
26
26
|
const writeCharacteristic = {
|
|
27
|
-
|
|
27
|
+
writeWithoutResponse: jest.fn(() => Promise.reject(error)),
|
|
28
28
|
};
|
|
29
29
|
const device = {
|
|
30
30
|
id: 'classic-id',
|
|
@@ -35,7 +35,7 @@ describe('BleTransport side-effecting writes', () => {
|
|
|
35
35
|
|
|
36
36
|
await expect(transport.writeWithRetry('payload')).rejects.toBe(error);
|
|
37
37
|
|
|
38
|
-
expect(writeCharacteristic.
|
|
38
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
39
39
|
expect(device.connect).not.toHaveBeenCalled();
|
|
40
40
|
});
|
|
41
41
|
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
+
|
|
5
|
+
import ReactNativeBleTransport, {
|
|
6
|
+
BLE_CONNECT_TIMEOUT_MS,
|
|
7
|
+
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
8
|
+
} from '../index';
|
|
9
|
+
|
|
10
|
+
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
11
|
+
|
|
12
|
+
jest.mock(
|
|
13
|
+
'react-native',
|
|
14
|
+
() => ({
|
|
15
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
16
|
+
PermissionsAndroid: {
|
|
17
|
+
PERMISSIONS: {},
|
|
18
|
+
RESULTS: {},
|
|
19
|
+
request: jest.fn(),
|
|
20
|
+
requestMultiple: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
{ virtual: true }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
27
|
+
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
28
|
+
BleError: Error,
|
|
29
|
+
BleErrorCode: {
|
|
30
|
+
DeviceDisconnected: 201,
|
|
31
|
+
OperationStartFailed: 601,
|
|
32
|
+
DeviceMTUChangeFailed: 401,
|
|
33
|
+
OperationCancelled: 2,
|
|
34
|
+
DeviceAlreadyConnected: 203,
|
|
35
|
+
},
|
|
36
|
+
BleManager: jest.fn(),
|
|
37
|
+
ScanMode: { LowLatency: 2 },
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: {
|
|
43
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
44
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
45
|
+
pairDevice: jest.fn(() => Promise.resolve()),
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const UUID = 'stalled-connect-device';
|
|
50
|
+
|
|
51
|
+
const flush = () =>
|
|
52
|
+
new Promise(resolve => {
|
|
53
|
+
setImmediate(resolve);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 250) {
|
|
57
|
+
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
58
|
+
jest.advanceTimersByTime(stepMs);
|
|
59
|
+
// eslint-disable-next-line no-await-in-loop
|
|
60
|
+
await flush();
|
|
61
|
+
if (settled()) return;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`fake timers exhausted after ${totalMs}ms before the connect settled`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A device whose native connect() never settles — the observed iOS failure mode. */
|
|
67
|
+
function createHarness(connectImpl: () => Promise<unknown>) {
|
|
68
|
+
const connect = jest.fn(connectImpl);
|
|
69
|
+
const device = {
|
|
70
|
+
id: UUID,
|
|
71
|
+
name: 'OneKey Classic',
|
|
72
|
+
localName: 'OneKey Classic',
|
|
73
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
74
|
+
isConnected: jest.fn(() => Promise.resolve(false)),
|
|
75
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
76
|
+
connect,
|
|
77
|
+
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
78
|
+
};
|
|
79
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
80
|
+
const bleManager = {
|
|
81
|
+
devices: jest.fn(() => Promise.resolve([device])),
|
|
82
|
+
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
83
|
+
connectToDevice: jest.fn(connectImpl),
|
|
84
|
+
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
85
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
86
|
+
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
87
|
+
// Dispatch asynchronously: subscribeBleOn wires its own cleanup after
|
|
88
|
+
// registering, so a synchronous callback would run before it is ready.
|
|
89
|
+
setImmediate(() => listener('PoweredOn'));
|
|
90
|
+
return { remove: jest.fn() };
|
|
91
|
+
}),
|
|
92
|
+
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
93
|
+
startDeviceScan: jest.fn(),
|
|
94
|
+
stopDeviceScan: jest.fn(),
|
|
95
|
+
};
|
|
96
|
+
(transport as any).blePlxManager = bleManager;
|
|
97
|
+
transport.init(
|
|
98
|
+
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
99
|
+
new EventEmitter()
|
|
100
|
+
);
|
|
101
|
+
transport.configure(messages);
|
|
102
|
+
return { transport, device, bleManager, connect };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('BLE connect timeout', () => {
|
|
106
|
+
beforeAll(() => {
|
|
107
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterAll(() => {
|
|
111
|
+
jest.useRealTimers();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
jest.clearAllTimers();
|
|
116
|
+
jest.restoreAllMocks();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('a native connect that never settles is bounded instead of blocking forever', async () => {
|
|
120
|
+
// iOS applies its own connect timeout on a serial queue; when that queue is busy
|
|
121
|
+
// the timeout never fires and acquire() blocks until the app-level 60s timeout.
|
|
122
|
+
const { transport } = createHarness(
|
|
123
|
+
() =>
|
|
124
|
+
new Promise(() => {
|
|
125
|
+
// never settles
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
130
|
+
let settled = false;
|
|
131
|
+
transport.acquire({ uuid: UUID }).catch(e => {
|
|
132
|
+
errors.push(e);
|
|
133
|
+
settled = true;
|
|
134
|
+
});
|
|
135
|
+
await flush();
|
|
136
|
+
|
|
137
|
+
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
138
|
+
|
|
139
|
+
expect(errors).toHaveLength(1);
|
|
140
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('the connect budget leaves generous headroom over a healthy connect', () => {
|
|
144
|
+
// Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
|
|
145
|
+
// fires when the native timeout itself fails to.
|
|
146
|
+
expect(BLE_CONNECT_TIMEOUT_MS).toBeGreaterThanOrEqual(6000);
|
|
147
|
+
expect(BLE_CONNECT_TIMEOUT_MS).toBeLessThanOrEqual(12000);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('a stalled connect is abandoned natively so the next attempt is not cancelled by it', async () => {
|
|
151
|
+
const { transport, bleManager } = createHarness(
|
|
152
|
+
() =>
|
|
153
|
+
new Promise(() => {
|
|
154
|
+
// never settles
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
let settled = false;
|
|
159
|
+
transport.acquire({ uuid: UUID }).catch(() => {
|
|
160
|
+
settled = true;
|
|
161
|
+
});
|
|
162
|
+
await flush();
|
|
163
|
+
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
164
|
+
|
|
165
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('repeated stalled connects recreate the BLE manager', async () => {
|
|
169
|
+
const { transport, bleManager } = createHarness(
|
|
170
|
+
() =>
|
|
171
|
+
new Promise(() => {
|
|
172
|
+
// never settles
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
const destroy = jest.fn();
|
|
176
|
+
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
177
|
+
|
|
178
|
+
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
179
|
+
let settled = false;
|
|
180
|
+
transport.acquire({ uuid: UUID }).catch(() => {
|
|
181
|
+
settled = true;
|
|
182
|
+
});
|
|
183
|
+
// eslint-disable-next-line no-await-in-loop
|
|
184
|
+
await flush();
|
|
185
|
+
// eslint-disable-next-line no-await-in-loop
|
|
186
|
+
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -3,6 +3,7 @@ import transportPackage, {
|
|
|
3
3
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
4
4
|
ProtocolV2,
|
|
5
5
|
TRANSPORT_EVENT,
|
|
6
|
+
bytesToHex,
|
|
6
7
|
} from '@onekeyfe/hd-transport';
|
|
7
8
|
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
8
9
|
|
|
@@ -45,11 +46,6 @@ jest.mock('../subscribeBleOn', () => ({
|
|
|
45
46
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
46
47
|
}));
|
|
47
48
|
|
|
48
|
-
const setPlatformOS = (os: 'ios' | 'android') => {
|
|
49
|
-
const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
|
|
50
|
-
reactNative.Platform.OS = os;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
49
|
const { parseConfigure } = transportPackage;
|
|
54
50
|
|
|
55
51
|
const protocolV1Schema = {
|
|
@@ -73,13 +69,11 @@ const protocolV1Schema = {
|
|
|
73
69
|
|
|
74
70
|
const protocolV2Schema = {
|
|
75
71
|
nested: {
|
|
76
|
-
ProtocolInfoRequest: { fields: {} },
|
|
77
72
|
Ping: {
|
|
78
73
|
fields: {
|
|
79
74
|
message: { type: 'string', id: 1 },
|
|
80
75
|
},
|
|
81
76
|
},
|
|
82
|
-
DeviceInfoGet: { fields: {} },
|
|
83
77
|
FileWrite: { fields: {} },
|
|
84
78
|
Success: {
|
|
85
79
|
fields: {
|
|
@@ -88,10 +82,8 @@ const protocolV2Schema = {
|
|
|
88
82
|
},
|
|
89
83
|
MessageType: {
|
|
90
84
|
values: {
|
|
91
|
-
MessageType_ProtocolInfoRequest: 60200,
|
|
92
85
|
MessageType_Ping: 60206,
|
|
93
86
|
MessageType_Success: 60207,
|
|
94
|
-
MessageType_DeviceInfoGet: 60600,
|
|
95
87
|
MessageType_FileWrite: 60805,
|
|
96
88
|
},
|
|
97
89
|
},
|
|
@@ -103,13 +95,7 @@ const schemas = {
|
|
|
103
95
|
protocolV2: parseConfigure(protocolV2Schema),
|
|
104
96
|
};
|
|
105
97
|
|
|
106
|
-
const createHarness = ({
|
|
107
|
-
deviceName = 'OneKey Pro 2',
|
|
108
|
-
isWritableWithResponse = true,
|
|
109
|
-
}: {
|
|
110
|
-
deviceName?: string;
|
|
111
|
-
isWritableWithResponse?: boolean;
|
|
112
|
-
} = {}) => {
|
|
98
|
+
const createHarness = () => {
|
|
113
99
|
const uuid = 'rn-pro2-id';
|
|
114
100
|
const sentSeqs: number[] = [];
|
|
115
101
|
let responseSeq = 0;
|
|
@@ -148,15 +134,15 @@ const createHarness = ({
|
|
|
148
134
|
const writeCharacteristic = {
|
|
149
135
|
uuid: '0002',
|
|
150
136
|
deviceID: uuid,
|
|
151
|
-
isWritableWithResponse,
|
|
137
|
+
isWritableWithResponse: true,
|
|
152
138
|
isWritableWithoutResponse: true,
|
|
153
139
|
writeWithResponse: jest.fn(handleWrite),
|
|
154
140
|
writeWithoutResponse: jest.fn(handleWrite),
|
|
155
141
|
};
|
|
156
142
|
const device = {
|
|
157
143
|
id: uuid,
|
|
158
|
-
name:
|
|
159
|
-
localName:
|
|
144
|
+
name: 'OneKey Pro 2',
|
|
145
|
+
localName: 'OneKey Pro 2',
|
|
160
146
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
161
147
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
162
148
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
@@ -198,13 +184,7 @@ const createHarness = ({
|
|
|
198
184
|
};
|
|
199
185
|
};
|
|
200
186
|
|
|
201
|
-
const createV1Harness = ({
|
|
202
|
-
respondOnWriteCount = 1,
|
|
203
|
-
isWritableWithResponse = true,
|
|
204
|
-
}: {
|
|
205
|
-
respondOnWriteCount?: number | number[];
|
|
206
|
-
isWritableWithResponse?: boolean;
|
|
207
|
-
} = {}) => {
|
|
187
|
+
const createV1Harness = () => {
|
|
208
188
|
const uuid = 'rn-classic-id';
|
|
209
189
|
const notifySubscriptionRemovers: jest.Mock[] = [];
|
|
210
190
|
const disconnectSubscriptionRemovers: jest.Mock[] = [];
|
|
@@ -223,25 +203,20 @@ const createV1Harness = ({
|
|
|
223
203
|
}),
|
|
224
204
|
};
|
|
225
205
|
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
|
-
};
|
|
238
206
|
const writeCharacteristic = {
|
|
239
207
|
uuid: '0002',
|
|
240
208
|
deviceID: uuid,
|
|
241
|
-
isWritableWithResponse,
|
|
209
|
+
isWritableWithResponse: true,
|
|
242
210
|
isWritableWithoutResponse: true,
|
|
243
|
-
|
|
244
|
-
|
|
211
|
+
writeWithoutResponse: jest.fn(() => {
|
|
212
|
+
writeCount += 1;
|
|
213
|
+
if (writeCount === 1) {
|
|
214
|
+
notifyCallback?.(null, {
|
|
215
|
+
value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
}),
|
|
245
220
|
};
|
|
246
221
|
const device = {
|
|
247
222
|
id: uuid,
|
|
@@ -274,7 +249,6 @@ const createV1Harness = ({
|
|
|
274
249
|
uuid,
|
|
275
250
|
device,
|
|
276
251
|
bleManager,
|
|
277
|
-
writeCharacteristic,
|
|
278
252
|
notifySubscriptionRemovers,
|
|
279
253
|
disconnectSubscriptionRemovers,
|
|
280
254
|
};
|
|
@@ -328,81 +302,12 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
328
302
|
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
329
303
|
});
|
|
330
304
|
|
|
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
|
-
|
|
399
305
|
test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
|
|
400
|
-
setPlatformOS('android');
|
|
401
306
|
const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
|
|
402
307
|
createV1Harness();
|
|
403
308
|
const probeProtocolV2 = jest
|
|
404
309
|
.spyOn(transport as any, 'probeProtocolV2')
|
|
405
|
-
.
|
|
310
|
+
.mockImplementationOnce(async () => {
|
|
406
311
|
await (transport as any).releaseNative(uuid, true);
|
|
407
312
|
return false;
|
|
408
313
|
});
|
|
@@ -428,9 +333,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
428
333
|
});
|
|
429
334
|
|
|
430
335
|
test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
|
|
431
|
-
setPlatformOS('android');
|
|
432
336
|
const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
|
|
433
|
-
jest.spyOn(transport as any, 'probeProtocolV2').
|
|
337
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockImplementationOnce(async () => {
|
|
434
338
|
await (transport as any).releaseNative(uuid, true);
|
|
435
339
|
return false;
|
|
436
340
|
});
|
|
@@ -449,9 +353,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
449
353
|
});
|
|
450
354
|
|
|
451
355
|
test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
|
|
452
|
-
const { transport, uuid, device } = createV1Harness(
|
|
453
|
-
respondOnWriteCount: Number.POSITIVE_INFINITY,
|
|
454
|
-
});
|
|
356
|
+
const { transport, uuid, device } = createV1Harness();
|
|
455
357
|
|
|
456
358
|
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
457
359
|
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
|
|
@@ -463,17 +365,17 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
463
365
|
});
|
|
464
366
|
|
|
465
367
|
afterEach(() => {
|
|
466
|
-
setPlatformOS('ios');
|
|
467
368
|
resetProtocolV2BleTuning();
|
|
468
369
|
});
|
|
469
370
|
|
|
470
|
-
test('
|
|
371
|
+
test('keeps the Protocol V2 sequence across probe and the next call', async () => {
|
|
471
372
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
472
373
|
|
|
473
374
|
await transport.acquire({ uuid });
|
|
474
|
-
await transport.call(uuid, 'Ping', { message: '
|
|
375
|
+
await transport.call(uuid, 'Ping', { message: 'after-probe' });
|
|
475
376
|
|
|
476
|
-
expect(sentSeqs).toEqual([1]);
|
|
377
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
378
|
+
expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
|
|
477
379
|
await transport.release(uuid, true);
|
|
478
380
|
});
|
|
479
381
|
|
|
@@ -484,10 +386,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
484
386
|
harness.setShouldRespond(false);
|
|
485
387
|
|
|
486
388
|
const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
|
|
487
|
-
while (sentSeqs.length <
|
|
488
|
-
await
|
|
489
|
-
setTimeout(resolve, 0);
|
|
490
|
-
});
|
|
389
|
+
while (sentSeqs.length < 2) {
|
|
390
|
+
await Promise.resolve();
|
|
491
391
|
}
|
|
492
392
|
await new Promise(resolve => {
|
|
493
393
|
setTimeout(resolve, 0);
|
|
@@ -503,135 +403,30 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
503
403
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
504
404
|
|
|
505
405
|
await transport.acquire({ uuid });
|
|
506
|
-
await transport.call(uuid, 'Ping', { message: 'first-generation' });
|
|
507
406
|
await transport.release(uuid, true);
|
|
508
407
|
await transport.acquire({ uuid });
|
|
509
|
-
await transport.call(uuid, 'Ping', { message: 'second-generation' });
|
|
510
408
|
|
|
511
409
|
expect(sentSeqs).toEqual([1, 2]);
|
|
512
410
|
await transport.release(uuid, true);
|
|
513
411
|
});
|
|
514
412
|
|
|
515
|
-
test('uses
|
|
516
|
-
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
517
|
-
|
|
518
|
-
await transport.acquire({ uuid });
|
|
519
|
-
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
520
|
-
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
521
|
-
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
522
|
-
|
|
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 () => {
|
|
413
|
+
test('uses withoutResponse for normal and high-volume calls', async () => {
|
|
534
414
|
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
535
415
|
|
|
536
416
|
await transport.acquire({ uuid });
|
|
537
|
-
|
|
538
|
-
await transport.call(uuid, 'FileWrite', {});
|
|
539
417
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
540
418
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
541
|
-
await transport.release(uuid, true);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
|
|
545
|
-
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
546
|
-
|
|
547
|
-
await transport.acquire({ uuid });
|
|
548
419
|
|
|
549
|
-
await transport.call(uuid, '
|
|
550
|
-
expect(writeCharacteristic.
|
|
551
|
-
expect(writeCharacteristic.
|
|
552
|
-
await transport.release(uuid, true);
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
|
|
556
|
-
const { transport, uuid, writeCharacteristic } = createHarness({
|
|
557
|
-
isWritableWithResponse: false,
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
await transport.acquire({ uuid });
|
|
561
|
-
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
420
|
+
await transport.call(uuid, 'Ping', { message: 'normal' });
|
|
421
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
|
|
422
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
562
423
|
|
|
424
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
425
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3);
|
|
563
426
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
564
|
-
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
565
427
|
await transport.release(uuid, true);
|
|
566
428
|
});
|
|
567
429
|
|
|
568
|
-
test('does not resend a failed iOS Protocol V2 control write without response', async () => {
|
|
569
|
-
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
570
|
-
const writeError = new Error('write with response failed');
|
|
571
|
-
const writeWithResponse = jest.fn().mockRejectedValue(writeError);
|
|
572
|
-
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
573
|
-
const context = {
|
|
574
|
-
messageName: 'ProtocolInfoRequest',
|
|
575
|
-
timeoutMs: 1000,
|
|
576
|
-
highVolume: false,
|
|
577
|
-
generation: 1,
|
|
578
|
-
signal: new AbortController().signal,
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
await expect(
|
|
582
|
-
transport.writeProtocolV2Packet(
|
|
583
|
-
{
|
|
584
|
-
writeCharacteristic: {
|
|
585
|
-
isWritableWithResponse: true,
|
|
586
|
-
writeWithResponse,
|
|
587
|
-
writeWithoutResponse,
|
|
588
|
-
},
|
|
589
|
-
},
|
|
590
|
-
Buffer.from('control').toString('base64'),
|
|
591
|
-
context,
|
|
592
|
-
jest.fn()
|
|
593
|
-
)
|
|
594
|
-
).rejects.toBe(writeError);
|
|
595
|
-
expect(writeWithResponse).toHaveBeenCalledTimes(1);
|
|
596
|
-
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
test('paces a one-packet Protocol V2 control write on iOS', async () => {
|
|
600
|
-
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
601
|
-
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
602
|
-
const bleTransport = {
|
|
603
|
-
mtuSize: 23,
|
|
604
|
-
writeCharacteristic: { writeWithoutResponse },
|
|
605
|
-
};
|
|
606
|
-
const context = {
|
|
607
|
-
messageName: 'ProtocolInfoRequest',
|
|
608
|
-
timeoutMs: 1000,
|
|
609
|
-
highVolume: false,
|
|
610
|
-
generation: 1,
|
|
611
|
-
signal: new AbortController().signal,
|
|
612
|
-
};
|
|
613
|
-
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
614
|
-
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
const call = transport.writeProtocolV2Frame(
|
|
618
|
-
bleTransport,
|
|
619
|
-
new Uint8Array(10),
|
|
620
|
-
context,
|
|
621
|
-
jest.fn()
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
await Promise.resolve();
|
|
625
|
-
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 5);
|
|
626
|
-
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
627
|
-
|
|
628
|
-
await call;
|
|
629
|
-
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
630
|
-
} finally {
|
|
631
|
-
setTimeoutSpy.mockRestore();
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
|
|
635
430
|
test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
|
|
636
431
|
const harness = createHarness();
|
|
637
432
|
const { transport, uuid, sentSeqs } = harness;
|
|
@@ -639,10 +434,8 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
639
434
|
harness.setShouldRespond(false);
|
|
640
435
|
|
|
641
436
|
const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
|
|
642
|
-
while (sentSeqs.length <
|
|
643
|
-
await
|
|
644
|
-
setTimeout(resolve, 0);
|
|
645
|
-
});
|
|
437
|
+
while (sentSeqs.length < 2) {
|
|
438
|
+
await Promise.resolve();
|
|
646
439
|
}
|
|
647
440
|
|
|
648
441
|
const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
|
|
@@ -689,6 +482,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
689
482
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
690
483
|
|
|
691
484
|
await transport.writeProtocolV2Frame(
|
|
485
|
+
'device-uuid',
|
|
692
486
|
bleTransport,
|
|
693
487
|
new Uint8Array(30),
|
|
694
488
|
context,
|
|
@@ -720,7 +514,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
720
514
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
721
515
|
|
|
722
516
|
await expect(
|
|
723
|
-
transport.writeProtocolV2Frame(
|
|
517
|
+
transport.writeProtocolV2Frame(
|
|
518
|
+
'device-uuid',
|
|
519
|
+
bleTransport,
|
|
520
|
+
new Uint8Array(30),
|
|
521
|
+
context,
|
|
522
|
+
jest.fn()
|
|
523
|
+
)
|
|
724
524
|
).rejects.toMatchObject({ errorCode: 205 });
|
|
725
525
|
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
726
526
|
});
|