@onekeyfe/hd-transport-react-native 1.2.0-alpha.16 → 1.2.0-alpha.160
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 +2 -4
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStaleBond.d.ts +5 -0
- package/dist/bleStaleBond.d.ts.map +1 -0
- package/dist/bleStrategy.d.ts +7 -0
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +136 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +993 -326
- package/dist/transportLog.d.ts +1 -12
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +6 -5
- package/src/BleTransport.ts +10 -42
- package/src/__tests__/BleTransport.test.ts +114 -0
- package/src/__tests__/bleStaleBond.test.ts +31 -0
- package/src/__tests__/bleStrategy.test.ts +93 -5
- package/src/__tests__/connectTimeout.test.ts +303 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +150 -0
- package/src/__tests__/protocolReprobe.test.ts +156 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +916 -32
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStaleBond.ts +63 -0
- package/src/bleStrategy.ts +26 -11
- package/src/constants.ts +9 -14
- package/src/index.ts +1242 -295
- package/src/transportLog.ts +1 -18
- package/src/types.ts +1 -0
|
@@ -2,11 +2,17 @@ import { EventEmitter } from 'events';
|
|
|
2
2
|
import transportPackage, {
|
|
3
3
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
4
4
|
ProtocolV2,
|
|
5
|
-
|
|
5
|
+
TRANSPORT_EVENT,
|
|
6
6
|
} from '@onekeyfe/hd-transport';
|
|
7
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
7
|
+
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
8
8
|
|
|
9
|
-
import ReactNativeBleTransport
|
|
9
|
+
import ReactNativeBleTransport, {
|
|
10
|
+
BLE_NATIVE_TEARDOWN_TIMEOUT_MS,
|
|
11
|
+
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
12
|
+
configureProtocolV2BleTuning,
|
|
13
|
+
getFirmwareUploadWriteRetryType,
|
|
14
|
+
resetProtocolV2BleTuning,
|
|
15
|
+
} from '../index';
|
|
10
16
|
|
|
11
17
|
jest.mock(
|
|
12
18
|
'react-native',
|
|
@@ -28,6 +34,7 @@ jest.mock('react-native-ble-plx', () => ({
|
|
|
28
34
|
CharacteristicNotFound: 404,
|
|
29
35
|
},
|
|
30
36
|
BleManager: jest.fn(),
|
|
37
|
+
ConnectionPriority: { Balanced: 0, High: 1, LowPower: 2 },
|
|
31
38
|
ScanMode: { LowLatency: 2 },
|
|
32
39
|
}));
|
|
33
40
|
|
|
@@ -41,11 +48,17 @@ jest.mock('../subscribeBleOn', () => ({
|
|
|
41
48
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
42
49
|
}));
|
|
43
50
|
|
|
51
|
+
const setPlatformOS = (os: 'ios' | 'android') => {
|
|
52
|
+
const reactNative: { Platform: { OS: string } } = jest.requireMock('react-native');
|
|
53
|
+
reactNative.Platform.OS = os;
|
|
54
|
+
};
|
|
55
|
+
|
|
44
56
|
const { parseConfigure } = transportPackage;
|
|
45
57
|
|
|
46
58
|
const protocolV1Schema = {
|
|
47
59
|
nested: {
|
|
48
60
|
Initialize: { fields: {} },
|
|
61
|
+
GetFeatures: { fields: {} },
|
|
49
62
|
Success: {
|
|
50
63
|
fields: {
|
|
51
64
|
message: { type: 'string', id: 1 },
|
|
@@ -55,6 +68,7 @@ const protocolV1Schema = {
|
|
|
55
68
|
values: {
|
|
56
69
|
MessageType_Initialize: 1,
|
|
57
70
|
MessageType_Success: 2,
|
|
71
|
+
MessageType_GetFeatures: 55,
|
|
58
72
|
},
|
|
59
73
|
},
|
|
60
74
|
},
|
|
@@ -62,11 +76,13 @@ const protocolV1Schema = {
|
|
|
62
76
|
|
|
63
77
|
const protocolV2Schema = {
|
|
64
78
|
nested: {
|
|
79
|
+
ProtocolInfoRequest: { fields: {} },
|
|
65
80
|
Ping: {
|
|
66
81
|
fields: {
|
|
67
82
|
message: { type: 'string', id: 1 },
|
|
68
83
|
},
|
|
69
84
|
},
|
|
85
|
+
DeviceInfoGet: { fields: {} },
|
|
70
86
|
FileWrite: { fields: {} },
|
|
71
87
|
Success: {
|
|
72
88
|
fields: {
|
|
@@ -75,8 +91,10 @@ const protocolV2Schema = {
|
|
|
75
91
|
},
|
|
76
92
|
MessageType: {
|
|
77
93
|
values: {
|
|
94
|
+
MessageType_ProtocolInfoRequest: 60200,
|
|
78
95
|
MessageType_Ping: 60206,
|
|
79
96
|
MessageType_Success: 60207,
|
|
97
|
+
MessageType_DeviceInfoGet: 60600,
|
|
80
98
|
MessageType_FileWrite: 60805,
|
|
81
99
|
},
|
|
82
100
|
},
|
|
@@ -88,9 +106,18 @@ const schemas = {
|
|
|
88
106
|
protocolV2: parseConfigure(protocolV2Schema),
|
|
89
107
|
};
|
|
90
108
|
|
|
91
|
-
const createHarness = (
|
|
109
|
+
const createHarness = ({
|
|
110
|
+
deviceName = 'OneKey Pro 2',
|
|
111
|
+
isWritableWithResponse = true,
|
|
112
|
+
monitorError,
|
|
113
|
+
}: {
|
|
114
|
+
deviceName?: string;
|
|
115
|
+
isWritableWithResponse?: boolean;
|
|
116
|
+
monitorError?: (Error & { reason?: string; attErrorCode?: number; iosErrorCode?: number }) | null;
|
|
117
|
+
} = {}) => {
|
|
92
118
|
const uuid = 'rn-pro2-id';
|
|
93
119
|
const sentSeqs: number[] = [];
|
|
120
|
+
let responseSeq = 0;
|
|
94
121
|
let shouldRespond = true;
|
|
95
122
|
let notifyCallback:
|
|
96
123
|
| ((
|
|
@@ -98,12 +125,16 @@ const createHarness = () => {
|
|
|
98
125
|
characteristic: { value: string } | null
|
|
99
126
|
) => void)
|
|
100
127
|
| undefined;
|
|
128
|
+
let disconnectCallback: (() => void) | undefined;
|
|
101
129
|
const notifyCharacteristic = {
|
|
102
130
|
uuid: '0003',
|
|
103
131
|
deviceID: uuid,
|
|
104
132
|
isNotifiable: true,
|
|
105
133
|
monitor: jest.fn(callback => {
|
|
106
134
|
notifyCallback = callback;
|
|
135
|
+
if (monitorError) {
|
|
136
|
+
queueMicrotask(() => callback(monitorError, null));
|
|
137
|
+
}
|
|
107
138
|
return { remove: jest.fn() };
|
|
108
139
|
}),
|
|
109
140
|
};
|
|
@@ -111,11 +142,12 @@ const createHarness = () => {
|
|
|
111
142
|
const frame = Buffer.from(base64, 'base64');
|
|
112
143
|
sentSeqs.push(frame[6]);
|
|
113
144
|
if (shouldRespond) {
|
|
145
|
+
responseSeq += 1;
|
|
114
146
|
const response = ProtocolV2.encodeFrame(
|
|
115
147
|
schemas,
|
|
116
148
|
'Success',
|
|
117
149
|
{ message: 'ok' },
|
|
118
|
-
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
150
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
119
151
|
);
|
|
120
152
|
notifyCallback?.(null, { value: Buffer.from(response).toString('base64') });
|
|
121
153
|
}
|
|
@@ -124,36 +156,51 @@ const createHarness = () => {
|
|
|
124
156
|
const writeCharacteristic = {
|
|
125
157
|
uuid: '0002',
|
|
126
158
|
deviceID: uuid,
|
|
127
|
-
isWritableWithResponse
|
|
159
|
+
isWritableWithResponse,
|
|
128
160
|
isWritableWithoutResponse: true,
|
|
129
161
|
writeWithResponse: jest.fn(handleWrite),
|
|
130
162
|
writeWithoutResponse: jest.fn(handleWrite),
|
|
131
163
|
};
|
|
132
164
|
const device = {
|
|
133
165
|
id: uuid,
|
|
134
|
-
name:
|
|
135
|
-
localName:
|
|
136
|
-
|
|
166
|
+
name: deviceName,
|
|
167
|
+
localName: deviceName,
|
|
168
|
+
mtu: 247,
|
|
169
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
137
170
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
172
|
+
onDisconnected: jest.fn(callback => {
|
|
173
|
+
disconnectCallback = callback;
|
|
174
|
+
return { remove: jest.fn() };
|
|
175
|
+
}),
|
|
176
|
+
} as any;
|
|
177
|
+
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
178
|
+
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
140
179
|
const bleManager = {
|
|
141
180
|
devices: jest.fn(() => Promise.resolve([device])),
|
|
142
181
|
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
143
182
|
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
183
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
184
|
+
destroy: jest.fn(),
|
|
144
185
|
};
|
|
145
186
|
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
187
|
+
const emitter = new EventEmitter();
|
|
188
|
+
const logger = { debug: jest.fn(), error: jest.fn() };
|
|
146
189
|
transport.blePlxManager = bleManager;
|
|
147
190
|
transport.resolveCharacteristics = jest.fn(() =>
|
|
148
191
|
Promise.resolve({ writeCharacteristic, notifyCharacteristic })
|
|
149
192
|
);
|
|
150
|
-
transport.init(
|
|
193
|
+
transport.init(logger, emitter);
|
|
151
194
|
transport.configure(protocolV1Schema);
|
|
152
195
|
transport.configureProtocolV2(protocolV2Schema);
|
|
153
196
|
|
|
154
197
|
return {
|
|
155
198
|
transport,
|
|
199
|
+
emitter,
|
|
200
|
+
logger,
|
|
156
201
|
uuid,
|
|
202
|
+
device,
|
|
203
|
+
bleManager,
|
|
157
204
|
sentSeqs,
|
|
158
205
|
writeCharacteristic,
|
|
159
206
|
setShouldRespond(value: boolean) {
|
|
@@ -162,30 +209,615 @@ const createHarness = () => {
|
|
|
162
209
|
emitMonitorError(error: Error & { reason?: string }) {
|
|
163
210
|
notifyCallback?.(error, null);
|
|
164
211
|
},
|
|
212
|
+
emitDisconnect() {
|
|
213
|
+
disconnectCallback?.();
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const createV1Harness = ({
|
|
219
|
+
respondOnWriteCount = 1,
|
|
220
|
+
isWritableWithResponse = true,
|
|
221
|
+
}: {
|
|
222
|
+
respondOnWriteCount?: number | number[];
|
|
223
|
+
isWritableWithResponse?: boolean;
|
|
224
|
+
} = {}) => {
|
|
225
|
+
const uuid = 'rn-classic-id';
|
|
226
|
+
const notifySubscriptionRemovers: jest.Mock[] = [];
|
|
227
|
+
const disconnectSubscriptionRemovers: jest.Mock[] = [];
|
|
228
|
+
let notifyCallback:
|
|
229
|
+
| ((error: Error | null, characteristic: { value: string } | null) => void)
|
|
230
|
+
| undefined;
|
|
231
|
+
const notifyCharacteristic = {
|
|
232
|
+
uuid: '0003',
|
|
233
|
+
deviceID: uuid,
|
|
234
|
+
isNotifiable: true,
|
|
235
|
+
monitor: jest.fn(callback => {
|
|
236
|
+
notifyCallback = callback;
|
|
237
|
+
const remove = jest.fn();
|
|
238
|
+
notifySubscriptionRemovers.push(remove);
|
|
239
|
+
return { remove };
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
let writeCount = 0;
|
|
243
|
+
const responseWriteCounts = new Set(
|
|
244
|
+
Array.isArray(respondOnWriteCount) ? respondOnWriteCount : [respondOnWriteCount]
|
|
245
|
+
);
|
|
246
|
+
const handleWrite = () => {
|
|
247
|
+
writeCount += 1;
|
|
248
|
+
if (responseWriteCounts.has(writeCount)) {
|
|
249
|
+
notifyCallback?.(null, {
|
|
250
|
+
value: Buffer.from('3f23230002000000040a026f6b', 'hex').toString('base64'),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return Promise.resolve();
|
|
254
|
+
};
|
|
255
|
+
const writeCharacteristic = {
|
|
256
|
+
uuid: '0002',
|
|
257
|
+
deviceID: uuid,
|
|
258
|
+
isWritableWithResponse,
|
|
259
|
+
isWritableWithoutResponse: true,
|
|
260
|
+
writeWithResponse: jest.fn(handleWrite),
|
|
261
|
+
writeWithoutResponse: jest.fn(handleWrite),
|
|
262
|
+
};
|
|
263
|
+
const device = {
|
|
264
|
+
id: uuid,
|
|
265
|
+
name: 'OneKey Classic',
|
|
266
|
+
localName: 'OneKey Classic',
|
|
267
|
+
mtu: 247,
|
|
268
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
269
|
+
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
270
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
271
|
+
onDisconnected: jest.fn(() => {
|
|
272
|
+
const remove = jest.fn();
|
|
273
|
+
disconnectSubscriptionRemovers.push(remove);
|
|
274
|
+
return { remove };
|
|
275
|
+
}),
|
|
276
|
+
} as any;
|
|
277
|
+
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
278
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
279
|
+
const bleManager = {
|
|
280
|
+
devices: jest.fn(() => Promise.resolve([device])),
|
|
281
|
+
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
282
|
+
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
283
|
+
};
|
|
284
|
+
transport.blePlxManager = bleManager as any;
|
|
285
|
+
transport.resolveCharacteristics = jest.fn(() =>
|
|
286
|
+
Promise.resolve({ writeCharacteristic, notifyCharacteristic })
|
|
287
|
+
);
|
|
288
|
+
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
289
|
+
transport.configure(protocolV1Schema);
|
|
290
|
+
transport.configureProtocolV2(protocolV2Schema);
|
|
291
|
+
return {
|
|
292
|
+
transport,
|
|
293
|
+
uuid,
|
|
294
|
+
device,
|
|
295
|
+
bleManager,
|
|
296
|
+
writeCharacteristic,
|
|
297
|
+
notifySubscriptionRemovers,
|
|
298
|
+
disconnectSubscriptionRemovers,
|
|
165
299
|
};
|
|
166
300
|
};
|
|
167
301
|
|
|
168
302
|
describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
169
|
-
test('
|
|
303
|
+
test('does not classify disconnects as retryable firmware writes', () => {
|
|
304
|
+
expect(
|
|
305
|
+
getFirmwareUploadWriteRetryType({
|
|
306
|
+
errorCode: 205,
|
|
307
|
+
message: 'Device disconnected after write',
|
|
308
|
+
})
|
|
309
|
+
).toBeNull();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test.each(['status 143', 'status:143', 'status = 143', 'GATT_CONGESTED'])(
|
|
313
|
+
'classifies %s as transient GATT congestion',
|
|
314
|
+
message => {
|
|
315
|
+
expect(getFirmwareUploadWriteRetryType({ message })).toBe('congested');
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
test('handles long uncontrolled status messages without a backtracking regular expression', () => {
|
|
320
|
+
const message = `status${' '.repeat(100_000)}142`;
|
|
321
|
+
|
|
322
|
+
expect(getFirmwareUploadWriteRetryType({ message })).toBeNull();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('keeps another device reader when releasing a device with an active V1 call', async () => {
|
|
326
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
327
|
+
const activeV1Call = createDeferred<string>();
|
|
328
|
+
const otherDeviceReader = createDeferred<Uint8Array>();
|
|
329
|
+
activeV1Call.promise.catch(() => undefined);
|
|
330
|
+
otherDeviceReader.promise.catch(() => undefined);
|
|
331
|
+
transport.runPromise = activeV1Call;
|
|
332
|
+
transport.runPromiseDeviceId = 'device-a';
|
|
333
|
+
transport.protocolV2FramePromises.set('device-b', otherDeviceReader);
|
|
334
|
+
|
|
335
|
+
await transport.releaseNative('device-a', true);
|
|
336
|
+
|
|
337
|
+
expect(transport.protocolV2FramePromises.get('device-b')).toBe(otherDeviceReader);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('rejects a pending reader when its device frame state resets', async () => {
|
|
341
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
342
|
+
const reader = createDeferred<Uint8Array>();
|
|
343
|
+
transport.protocolV2FramePromises.set('device-a', reader);
|
|
344
|
+
const result = Promise.race([
|
|
345
|
+
reader.promise.then(
|
|
346
|
+
() => 'resolved',
|
|
347
|
+
() => 'rejected'
|
|
348
|
+
),
|
|
349
|
+
new Promise(resolve => {
|
|
350
|
+
setTimeout(() => resolve('pending'), 20);
|
|
351
|
+
}),
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
transport.resetProtocolV2Frames('device-a');
|
|
355
|
+
|
|
356
|
+
await expect(result).resolves.toBe('rejected');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('keeps the legacy default BLE scan timeout', () => {
|
|
360
|
+
expect(new ReactNativeBleTransport({}).scanTimeout).toBe(3000);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('uses withResponse for consecutive iOS Protocol V1 control commands without releasing', async () => {
|
|
364
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
365
|
+
respondOnWriteCount: [1, 2],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V1' })).resolves.toEqual({
|
|
369
|
+
uuid,
|
|
370
|
+
protocolType: 'V1',
|
|
371
|
+
});
|
|
372
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
373
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
374
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
375
|
+
|
|
376
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
377
|
+
await expect(transport.call(uuid, 'GetFeatures', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
378
|
+
|
|
379
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
380
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
381
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
382
|
+
await transport.release(uuid, true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('falls back to withoutResponse for an iOS Protocol V1 control command when required', async () => {
|
|
386
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness({
|
|
387
|
+
isWritableWithResponse: false,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
391
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).resolves.toBeDefined();
|
|
392
|
+
|
|
393
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
394
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
395
|
+
await transport.release(uuid, true);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('does not resend a failed iOS Protocol V1 control write without response', async () => {
|
|
399
|
+
const { transport, uuid, writeCharacteristic } = createV1Harness();
|
|
400
|
+
const writeError = new Error('write with response failed');
|
|
401
|
+
|
|
402
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
403
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce(writeError);
|
|
404
|
+
|
|
405
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 50 })).rejects.toMatchObject({
|
|
406
|
+
errorCode: HardwareErrorCode.BleWriteCharacteristicError,
|
|
407
|
+
});
|
|
408
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
410
|
+
await transport.release(uuid, true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('detects Protocol V2 on iOS without using the BLE name as a protocol hint', async () => {
|
|
414
|
+
const { transport, uuid, device, sentSeqs, writeCharacteristic } = createHarness({
|
|
415
|
+
deviceName: 'Pro2 6E9E',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
419
|
+
uuid,
|
|
420
|
+
protocolType: 'V2',
|
|
421
|
+
});
|
|
422
|
+
expect(device.requestMTU).toHaveBeenCalledWith(247);
|
|
423
|
+
expect(writeCharacteristic.writeWithResponse.mock.calls.length).toBeGreaterThan(1);
|
|
424
|
+
|
|
425
|
+
await expect(
|
|
426
|
+
transport.call(uuid, 'Ping', { message: 'first-core-command' })
|
|
427
|
+
).resolves.toBeDefined();
|
|
428
|
+
expect(sentSeqs.filter(seq => seq > 0)).toEqual([1, 2]);
|
|
429
|
+
await transport.release(uuid, true);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test.each(['ios', 'android'] as const)(
|
|
433
|
+
'keeps a first expected Protocol V2 probe miss retryable on %s',
|
|
434
|
+
async platform => {
|
|
435
|
+
setPlatformOS(platform);
|
|
436
|
+
const { transport, uuid, device } = createHarness();
|
|
437
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
438
|
+
|
|
439
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
440
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
444
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
test.each(['ios', 'android'] as const)(
|
|
449
|
+
'keeps a second expected Protocol V2 probe miss retryable on %s',
|
|
450
|
+
async platform => {
|
|
451
|
+
setPlatformOS(platform);
|
|
452
|
+
const { transport, uuid, device } = createHarness();
|
|
453
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
454
|
+
|
|
455
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
456
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
457
|
+
});
|
|
458
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
459
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
463
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
test.each([
|
|
468
|
+
[
|
|
469
|
+
'Encryption is insufficient',
|
|
470
|
+
{ reason: 'Encryption is insufficient', attErrorCode: 15 },
|
|
471
|
+
HardwareErrorCode.BleDeviceBondError,
|
|
472
|
+
],
|
|
473
|
+
[
|
|
474
|
+
'Peer removed pairing information',
|
|
475
|
+
{ reason: 'Peer removed pairing information', iosErrorCode: 14 },
|
|
476
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
477
|
+
],
|
|
478
|
+
] as const)(
|
|
479
|
+
'fails Protocol V2 acquire immediately on %s instead of waiting for Ping',
|
|
480
|
+
async (_label, nativeError, errorCode) => {
|
|
481
|
+
const { transport, uuid, device } = createHarness({
|
|
482
|
+
monitorError: Object.assign(new Error(nativeError.reason), nativeError),
|
|
483
|
+
});
|
|
484
|
+
const probe = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
485
|
+
|
|
486
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
487
|
+
errorCode,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(probe).not.toHaveBeenCalled();
|
|
491
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
492
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
test.each(['ios', 'android'] as const)(
|
|
497
|
+
'reports a stale bond on %s when a previously confirmed Protocol V2 device stops responding',
|
|
498
|
+
async platform => {
|
|
499
|
+
setPlatformOS(platform);
|
|
500
|
+
const { transport, uuid, device } = createHarness();
|
|
501
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
502
|
+
await transport.release(uuid, true);
|
|
503
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
504
|
+
|
|
505
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
506
|
+
errorCode: HardwareErrorCode.BleDeviceBondError,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
510
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
test('waits for an in-flight release before reacquiring the same device', async () => {
|
|
515
|
+
const { transport, uuid } = createHarness();
|
|
516
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
517
|
+
const releaseGate = createDeferred<void>();
|
|
518
|
+
const releaseStarted = createDeferred<void>();
|
|
519
|
+
const { protocolV2Links } = transport as any;
|
|
520
|
+
const invalidateLink = protocolV2Links.invalidateLink.bind(protocolV2Links);
|
|
521
|
+
jest
|
|
522
|
+
.spyOn(protocolV2Links, 'invalidateLink')
|
|
523
|
+
.mockImplementationOnce(async (...args: unknown[]) => {
|
|
524
|
+
releaseStarted.resolve();
|
|
525
|
+
await releaseGate.promise;
|
|
526
|
+
return invalidateLink(...args);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const release = transport.release(uuid, true);
|
|
530
|
+
await releaseStarted.promise;
|
|
531
|
+
let reacquired = false;
|
|
532
|
+
const acquire = transport.acquire({ uuid, expectedProtocol: 'V2' }).then(result => {
|
|
533
|
+
reacquired = true;
|
|
534
|
+
return result;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await Promise.resolve();
|
|
538
|
+
expect(reacquired).toBe(false);
|
|
539
|
+
|
|
540
|
+
releaseGate.resolve();
|
|
541
|
+
await release;
|
|
542
|
+
await expect(acquire).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
543
|
+
await expect(transport.call(uuid, 'Ping', { message: 'after-release' })).resolves.toMatchObject(
|
|
544
|
+
{
|
|
545
|
+
type: 'Success',
|
|
546
|
+
message: { message: 'ok' },
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test(
|
|
552
|
+
'releases the lifecycle queue when native teardown never settles',
|
|
553
|
+
async () => {
|
|
554
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
555
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
556
|
+
const otherUuid = 'rn-pro2-other-id';
|
|
557
|
+
const otherDevice = {
|
|
558
|
+
...device,
|
|
559
|
+
id: otherUuid,
|
|
560
|
+
name: 'OneKey Pro 2 Other',
|
|
561
|
+
localName: 'OneKey Pro 2 Other',
|
|
562
|
+
};
|
|
563
|
+
await (transport as any).installTransportForAcquire(otherUuid, otherDevice);
|
|
564
|
+
(transport as any).deviceProtocol.set(otherUuid, 'V2');
|
|
565
|
+
const disconnectEvents: Array<{ connectId: string }> = [];
|
|
566
|
+
transport.emitter?.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, event => {
|
|
567
|
+
disconnectEvents.push(event);
|
|
568
|
+
});
|
|
569
|
+
const stalledNativeCleanup = createDeferred<void>();
|
|
570
|
+
bleManager.cancelTransaction
|
|
571
|
+
.mockImplementationOnce(() => stalledNativeCleanup.promise)
|
|
572
|
+
.mockResolvedValue(undefined);
|
|
573
|
+
const resetPlxManager = jest.spyOn(transport as any, 'resetPlxManager');
|
|
574
|
+
const nextLifecycleOperation = jest.fn().mockResolvedValue('next-operation');
|
|
575
|
+
|
|
576
|
+
const release = transport.release(uuid, true);
|
|
577
|
+
const nextOperation = (transport as any).runLifecycleOperation(uuid, nextLifecycleOperation);
|
|
578
|
+
|
|
579
|
+
await expect(release).resolves.toBe(true);
|
|
580
|
+
await expect(nextOperation).resolves.toBe('next-operation');
|
|
581
|
+
expect(resetPlxManager).toHaveBeenCalledTimes(1);
|
|
582
|
+
expect(nextLifecycleOperation).toHaveBeenCalledTimes(1);
|
|
583
|
+
expect(disconnectEvents).toContainEqual(expect.objectContaining({ connectId: otherUuid }));
|
|
584
|
+
expect(() => (transport as any).getCachedTransport(otherUuid)).toThrow();
|
|
585
|
+
|
|
586
|
+
await (transport as any).installTransportForAcquire(uuid, device);
|
|
587
|
+
(transport as any).deviceProtocol.set(uuid, 'V2');
|
|
588
|
+
|
|
589
|
+
stalledNativeCleanup.resolve();
|
|
590
|
+
await Promise.resolve();
|
|
591
|
+
await expect(
|
|
592
|
+
transport.call(uuid, 'Ping', { message: 'after-stale-cleanup' })
|
|
593
|
+
).resolves.toMatchObject({
|
|
594
|
+
type: 'Success',
|
|
595
|
+
message: { message: 'ok' },
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
602
|
+
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
603
|
+
const probeProtocolV1 = jest
|
|
604
|
+
.spyOn(transport as any, 'probeProtocolV1')
|
|
605
|
+
.mockResolvedValue(false);
|
|
606
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
|
|
607
|
+
|
|
608
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
609
|
+
uuid,
|
|
610
|
+
protocolType: 'V2',
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(probeProtocolV1).toHaveBeenCalledTimes(1);
|
|
614
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
615
|
+
expect(probeProtocolV1.mock.invocationCallOrder[0]).toBeLessThan(
|
|
616
|
+
probeProtocolV2.mock.invocationCallOrder[0]
|
|
617
|
+
);
|
|
618
|
+
await transport.release(uuid, true);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('continues with the current MTU when the connected snapshot refresh fails', async () => {
|
|
622
|
+
const { transport, uuid, device } = createHarness();
|
|
623
|
+
const mtuError = new Error('MTU refresh failed');
|
|
624
|
+
device.requestMTU.mockRejectedValueOnce(mtuError);
|
|
625
|
+
|
|
626
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
627
|
+
uuid,
|
|
628
|
+
protocolType: 'V2',
|
|
629
|
+
});
|
|
630
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
631
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
632
|
+
await transport.release(uuid, true);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('refreshes a transient bootloader MTU after notifications are ready', async () => {
|
|
636
|
+
const { transport, uuid, device } = createHarness();
|
|
637
|
+
device.mtu = 23;
|
|
638
|
+
device.requestMTU
|
|
639
|
+
.mockResolvedValueOnce(device)
|
|
640
|
+
.mockResolvedValueOnce(device)
|
|
641
|
+
.mockImplementationOnce(() => {
|
|
642
|
+
device.mtu = 247;
|
|
643
|
+
return Promise.resolve(device);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
647
|
+
uuid,
|
|
648
|
+
protocolType: 'V2',
|
|
649
|
+
});
|
|
650
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
651
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
652
|
+
await transport.release(uuid, true);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test('continues with a low bootloader MTU when the bounded retry fails', async () => {
|
|
656
|
+
const { transport, uuid, device } = createHarness();
|
|
657
|
+
device.mtu = 23;
|
|
658
|
+
device.requestMTU
|
|
659
|
+
.mockResolvedValueOnce(device)
|
|
660
|
+
.mockResolvedValueOnce(device)
|
|
661
|
+
.mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
|
|
662
|
+
|
|
663
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
664
|
+
uuid,
|
|
665
|
+
protocolType: 'V2',
|
|
666
|
+
});
|
|
667
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
668
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
669
|
+
await transport.release(uuid, true);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('accepts a stable low MTU without the delayed refresh loop', async () => {
|
|
673
|
+
const { transport, uuid, device } = createHarness();
|
|
674
|
+
device.mtu = 185;
|
|
675
|
+
|
|
676
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
677
|
+
uuid,
|
|
678
|
+
protocolType: 'V2',
|
|
679
|
+
});
|
|
680
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
681
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(185);
|
|
682
|
+
await transport.release(uuid, true);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('continues Protocol V2 probing with a conservative packet size when MTU is unavailable', async () => {
|
|
686
|
+
const { transport, uuid, device, writeCharacteristic } = createHarness();
|
|
687
|
+
device.mtu = undefined;
|
|
688
|
+
|
|
689
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
690
|
+
uuid,
|
|
691
|
+
protocolType: 'V2',
|
|
692
|
+
});
|
|
693
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
694
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBeUndefined();
|
|
695
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalled();
|
|
696
|
+
await transport.release(uuid, true);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test('refreshes an unavailable MTU before a Protocol V2 high-volume write', async () => {
|
|
700
|
+
const { transport, uuid, device, writeCharacteristic } = createHarness();
|
|
701
|
+
device.mtu = undefined;
|
|
702
|
+
|
|
703
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
704
|
+
device.requestMTU.mockImplementationOnce(() => {
|
|
705
|
+
device.mtu = 247;
|
|
706
|
+
return Promise.resolve(device);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
await expect(transport.call(uuid, 'FileWrite', {})).resolves.toBeDefined();
|
|
710
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(4);
|
|
711
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
712
|
+
await transport.release(uuid, true);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test('rejects a Protocol V2 high-volume write when MTU remains unavailable', async () => {
|
|
716
|
+
const { transport, uuid, device, writeCharacteristic } = createHarness();
|
|
717
|
+
device.mtu = undefined;
|
|
718
|
+
|
|
719
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
720
|
+
|
|
721
|
+
await expect(transport.call(uuid, 'FileWrite', {})).rejects.toMatchObject({
|
|
722
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
723
|
+
});
|
|
724
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(4);
|
|
725
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
726
|
+
await transport.release(uuid, true);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('reconnects before falling back to Protocol V1 after a fatal V2 probe failure', async () => {
|
|
730
|
+
setPlatformOS('android');
|
|
731
|
+
const { transport, uuid, device, notifySubscriptionRemovers, disconnectSubscriptionRemovers } =
|
|
732
|
+
createV1Harness();
|
|
733
|
+
const probeProtocolV2 = jest
|
|
734
|
+
.spyOn(transport as any, 'probeProtocolV2')
|
|
735
|
+
.mockImplementation(async () => {
|
|
736
|
+
await (transport as any).releaseNative(uuid, true);
|
|
737
|
+
return false;
|
|
738
|
+
});
|
|
739
|
+
const resolveCharacteristics = jest.spyOn(transport as any, 'resolveCharacteristics');
|
|
740
|
+
|
|
741
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).resolves.toEqual({
|
|
742
|
+
uuid,
|
|
743
|
+
protocolType: 'V1',
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
expect(probeProtocolV2).toHaveBeenCalledTimes(1);
|
|
747
|
+
expect(resolveCharacteristics).toHaveBeenCalledTimes(2);
|
|
748
|
+
expect(transport.getProtocolType(uuid)).toBe('V1');
|
|
749
|
+
expect(device.onDisconnected).toHaveBeenCalledTimes(1);
|
|
750
|
+
expect(notifySubscriptionRemovers).toHaveLength(2);
|
|
751
|
+
expect(notifySubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
|
|
752
|
+
|
|
753
|
+
await transport.release(uuid, true);
|
|
754
|
+
|
|
755
|
+
expect(notifySubscriptionRemovers[1]).toHaveBeenCalledTimes(1);
|
|
756
|
+
expect(disconnectSubscriptionRemovers).toHaveLength(1);
|
|
757
|
+
expect(disconnectSubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test('cleans the rebuilt transport when Protocol V1 fallback also fails', async () => {
|
|
761
|
+
setPlatformOS('android');
|
|
762
|
+
const { transport, uuid, device, bleManager, notifySubscriptionRemovers } = createV1Harness();
|
|
763
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockImplementation(async () => {
|
|
764
|
+
await (transport as any).releaseNative(uuid, true);
|
|
765
|
+
return false;
|
|
766
|
+
});
|
|
767
|
+
jest.spyOn(transport as any, 'probeProtocolV1').mockResolvedValue(false);
|
|
768
|
+
|
|
769
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toMatchObject({
|
|
770
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
expect(device.onDisconnected).not.toHaveBeenCalled();
|
|
774
|
+
expect(notifySubscriptionRemovers).toHaveLength(2);
|
|
775
|
+
expect(notifySubscriptionRemovers[0]).toHaveBeenCalledTimes(1);
|
|
776
|
+
expect(notifySubscriptionRemovers[1]).toHaveBeenCalledTimes(1);
|
|
777
|
+
expect(bleManager.cancelTransaction).toHaveBeenCalled();
|
|
778
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('disconnects and invalidates a Protocol V1 link after a response timeout', async () => {
|
|
782
|
+
const { transport, uuid, device } = createV1Harness({
|
|
783
|
+
respondOnWriteCount: Number.POSITIVE_INFINITY,
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
await transport.acquire({ uuid, expectedProtocol: 'V1' });
|
|
787
|
+
await expect(transport.call(uuid, 'Initialize', {}, { timeoutMs: 5 })).rejects.toMatchObject({
|
|
788
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
792
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
afterEach(() => {
|
|
796
|
+
setPlatformOS('ios');
|
|
797
|
+
resetProtocolV2BleTuning();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test('uses the first Protocol V2 sequence for the first Core call when protocol is known', async () => {
|
|
170
801
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
171
802
|
|
|
172
|
-
await transport.acquire({ uuid });
|
|
173
|
-
await transport.call(uuid, 'Ping', { message: '
|
|
803
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
804
|
+
await transport.call(uuid, 'Ping', { message: 'first-core-command' });
|
|
174
805
|
|
|
175
806
|
expect(sentSeqs).toEqual([1, 2]);
|
|
176
|
-
expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102');
|
|
177
807
|
await transport.release(uuid, true);
|
|
178
808
|
});
|
|
179
809
|
|
|
180
810
|
test('rejects the active Protocol V2 reader when the current monitor errors', async () => {
|
|
181
811
|
const harness = createHarness();
|
|
182
812
|
const { transport, uuid, sentSeqs } = harness;
|
|
183
|
-
await transport.acquire({ uuid });
|
|
813
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
184
814
|
harness.setShouldRespond(false);
|
|
185
815
|
|
|
186
816
|
const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 });
|
|
187
|
-
while (sentSeqs.length <
|
|
188
|
-
await Promise
|
|
817
|
+
while (sentSeqs.length < 1) {
|
|
818
|
+
await new Promise(resolve => {
|
|
819
|
+
setTimeout(resolve, 0);
|
|
820
|
+
});
|
|
189
821
|
}
|
|
190
822
|
await new Promise(resolve => {
|
|
191
823
|
setTimeout(resolve, 0);
|
|
@@ -200,44 +832,296 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
200
832
|
test('retains the sequence cursor when a new monitor generation is acquired', async () => {
|
|
201
833
|
const { transport, uuid, sentSeqs } = createHarness();
|
|
202
834
|
|
|
203
|
-
await transport.acquire({ uuid });
|
|
835
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
836
|
+
await transport.call(uuid, 'Ping', { message: 'first-generation' });
|
|
204
837
|
await transport.release(uuid, true);
|
|
205
|
-
await transport.acquire({ uuid });
|
|
838
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
839
|
+
await transport.call(uuid, 'Ping', { message: 'second-generation' });
|
|
206
840
|
|
|
207
|
-
expect(sentSeqs).toEqual([1, 2]);
|
|
841
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
208
842
|
await transport.release(uuid, true);
|
|
209
843
|
});
|
|
210
844
|
|
|
211
|
-
test('uses
|
|
845
|
+
test('uses withResponse for consecutive iOS Protocol V2 control calls without releasing', async () => {
|
|
212
846
|
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
213
847
|
|
|
214
|
-
await transport.acquire({ uuid });
|
|
215
|
-
|
|
216
|
-
expect(writeCharacteristic.
|
|
848
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
849
|
+
const releaseNative = jest.spyOn(transport as any, 'releaseNative');
|
|
850
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
851
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
852
|
+
|
|
853
|
+
await transport.call(uuid, 'DeviceInfoGet', {});
|
|
854
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
217
855
|
|
|
218
|
-
|
|
856
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(3);
|
|
857
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
858
|
+
expect(releaseNative).not.toHaveBeenCalled();
|
|
859
|
+
|
|
860
|
+
await transport.release(uuid, true);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test('keeps iOS Protocol V2 high-volume calls on withoutResponse', async () => {
|
|
864
|
+
const { transport, uuid, logger, writeCharacteristic } = createHarness();
|
|
865
|
+
|
|
866
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
867
|
+
|
|
868
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
869
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
219
870
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
|
|
220
|
-
expect(writeCharacteristic.writeWithResponse).
|
|
871
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
872
|
+
expect(
|
|
873
|
+
logger.debug.mock.calls.filter(
|
|
874
|
+
([message]) =>
|
|
875
|
+
message === '[ReactNativeBleTransport] Protocol V2 high-volume write configured'
|
|
876
|
+
)
|
|
877
|
+
).toHaveLength(1);
|
|
878
|
+
await transport.release(uuid, true);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
|
|
882
|
+
setPlatformOS('android');
|
|
883
|
+
const { transport, uuid, device } = createHarness();
|
|
221
884
|
|
|
885
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
886
|
+
expect(device.requestMTU).toHaveBeenCalledWith(517);
|
|
887
|
+
|
|
888
|
+
await transport.call(uuid, 'FileWrite', {});
|
|
222
889
|
await transport.call(uuid, 'FileWrite', {});
|
|
223
|
-
|
|
890
|
+
|
|
891
|
+
expect(device.requestConnectionPriority).toHaveBeenCalledTimes(1);
|
|
892
|
+
expect(device.requestConnectionPriority).toHaveBeenCalledWith(1);
|
|
893
|
+
|
|
894
|
+
await transport.release(uuid, true);
|
|
895
|
+
expect(device.requestConnectionPriority).toHaveBeenLastCalledWith(0);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test('uses withResponse for an iOS Protocol V2 firmware file write when requested', async () => {
|
|
899
|
+
const { transport, uuid, writeCharacteristic } = createHarness();
|
|
900
|
+
|
|
901
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
902
|
+
|
|
903
|
+
await transport.call(uuid, 'FileWrite', {}, { writeWithResponse: true });
|
|
904
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(2);
|
|
905
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
906
|
+
await transport.release(uuid, true);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
test('falls back to withoutResponse for an iOS Protocol V2 control call when required', async () => {
|
|
910
|
+
const { transport, uuid, writeCharacteristic } = createHarness({
|
|
911
|
+
isWritableWithResponse: false,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
915
|
+
await transport.call(uuid, 'ProtocolInfoRequest', {});
|
|
916
|
+
|
|
224
917
|
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
918
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2);
|
|
225
919
|
await transport.release(uuid, true);
|
|
226
920
|
});
|
|
227
921
|
|
|
922
|
+
test('does not resend a failed iOS Protocol V2 control write without response', async () => {
|
|
923
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
924
|
+
const writeError = new Error('write with response failed');
|
|
925
|
+
const writeWithResponse = jest.fn().mockRejectedValue(writeError);
|
|
926
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
927
|
+
const context = {
|
|
928
|
+
messageName: 'ProtocolInfoRequest',
|
|
929
|
+
timeoutMs: 1000,
|
|
930
|
+
highThroughput: false,
|
|
931
|
+
generation: 1,
|
|
932
|
+
signal: new AbortController().signal,
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
await expect(
|
|
936
|
+
transport.writeProtocolV2Packet(
|
|
937
|
+
'test-device',
|
|
938
|
+
{
|
|
939
|
+
writeCharacteristic: {
|
|
940
|
+
isWritableWithResponse: true,
|
|
941
|
+
writeWithResponse,
|
|
942
|
+
writeWithoutResponse,
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
Buffer.from('control').toString('base64'),
|
|
946
|
+
context,
|
|
947
|
+
jest.fn()
|
|
948
|
+
)
|
|
949
|
+
).rejects.toBe(writeError);
|
|
950
|
+
expect(writeWithResponse).toHaveBeenCalledTimes(1);
|
|
951
|
+
expect(writeWithoutResponse).not.toHaveBeenCalled();
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test('does not pace a one-packet Protocol V2 control write on iOS', async () => {
|
|
955
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
956
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
957
|
+
const bleTransport = {
|
|
958
|
+
mtuSize: 23,
|
|
959
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
960
|
+
};
|
|
961
|
+
const context = {
|
|
962
|
+
messageName: 'ProtocolInfoRequest',
|
|
963
|
+
timeoutMs: 1000,
|
|
964
|
+
highThroughput: false,
|
|
965
|
+
generation: 1,
|
|
966
|
+
signal: new AbortController().signal,
|
|
967
|
+
};
|
|
968
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
969
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
const call = transport.writeProtocolV2Frame(
|
|
973
|
+
'test-device',
|
|
974
|
+
bleTransport,
|
|
975
|
+
new Uint8Array(10),
|
|
976
|
+
context,
|
|
977
|
+
jest.fn()
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
await call;
|
|
981
|
+
// The only scheduled timer is the per-packet BLE write watchdog: no pacing delay.
|
|
982
|
+
expect(setTimeoutSpy.mock.calls.map(([, timeout]) => timeout)).toEqual([
|
|
983
|
+
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
984
|
+
]);
|
|
985
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
986
|
+
} finally {
|
|
987
|
+
setTimeoutSpy.mockRestore();
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
228
991
|
test('rejects an active Protocol V2 reader when disconnect resets the link', async () => {
|
|
229
992
|
const harness = createHarness();
|
|
230
993
|
const { transport, uuid, sentSeqs } = harness;
|
|
231
|
-
await transport.acquire({ uuid });
|
|
994
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
232
995
|
harness.setShouldRespond(false);
|
|
233
996
|
|
|
234
997
|
const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 });
|
|
235
|
-
while (sentSeqs.length <
|
|
236
|
-
await Promise
|
|
998
|
+
while (sentSeqs.length < 1) {
|
|
999
|
+
await new Promise(resolve => {
|
|
1000
|
+
setTimeout(resolve, 0);
|
|
1001
|
+
});
|
|
237
1002
|
}
|
|
238
1003
|
|
|
239
1004
|
const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected');
|
|
240
1005
|
await transport.disconnect(uuid);
|
|
241
1006
|
await rejection;
|
|
242
1007
|
});
|
|
1008
|
+
|
|
1009
|
+
test('emits one disconnect event when the physical callback races manual cleanup', async () => {
|
|
1010
|
+
const harness = createHarness();
|
|
1011
|
+
const disconnectListener = jest.fn();
|
|
1012
|
+
harness.emitter.on(TRANSPORT_EVENT.DEVICE_DISCONNECT, disconnectListener);
|
|
1013
|
+
|
|
1014
|
+
await harness.transport.acquire({ uuid: harness.uuid, expectedProtocol: 'V2' });
|
|
1015
|
+
harness.emitDisconnect();
|
|
1016
|
+
await harness.transport.disconnect(harness.uuid);
|
|
1017
|
+
|
|
1018
|
+
expect(disconnectListener).toHaveBeenCalledTimes(1);
|
|
1019
|
+
expect(disconnectListener).toHaveBeenCalledWith({
|
|
1020
|
+
name: 'OneKey Pro 2',
|
|
1021
|
+
id: harness.uuid,
|
|
1022
|
+
connectId: harness.uuid,
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test('chunks large frames and retries only transient GATT congestion', async () => {
|
|
1027
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
1028
|
+
const congested = { status: 143, message: 'GATT_CONGESTED' };
|
|
1029
|
+
const writeWithoutResponse = jest
|
|
1030
|
+
.fn()
|
|
1031
|
+
.mockRejectedValueOnce(congested)
|
|
1032
|
+
.mockResolvedValue(undefined);
|
|
1033
|
+
const bleTransport = {
|
|
1034
|
+
mtuSize: 23,
|
|
1035
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
1036
|
+
};
|
|
1037
|
+
const context = {
|
|
1038
|
+
messageName: 'Ping',
|
|
1039
|
+
timeoutMs: 1000,
|
|
1040
|
+
highThroughput: false,
|
|
1041
|
+
generation: 1,
|
|
1042
|
+
signal: new AbortController().signal,
|
|
1043
|
+
};
|
|
1044
|
+
const assertCurrentGeneration = jest.fn();
|
|
1045
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
1046
|
+
|
|
1047
|
+
await transport.writeProtocolV2Frame(
|
|
1048
|
+
'device-uuid',
|
|
1049
|
+
bleTransport,
|
|
1050
|
+
new Uint8Array(30),
|
|
1051
|
+
context,
|
|
1052
|
+
assertCurrentGeneration
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(3);
|
|
1056
|
+
expect(writeWithoutResponse.mock.calls[0][0]).toBe(writeWithoutResponse.mock.calls[1][0]);
|
|
1057
|
+
expect(Buffer.from(writeWithoutResponse.mock.calls[2][0], 'base64')).toHaveLength(10);
|
|
1058
|
+
expect(assertCurrentGeneration).toHaveBeenCalled();
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test('does not retry a disconnected Protocol V2 write inside a partial frame', async () => {
|
|
1062
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
1063
|
+
const writeWithoutResponse = jest
|
|
1064
|
+
.fn()
|
|
1065
|
+
.mockRejectedValue({ errorCode: 205, message: 'Device disconnected' });
|
|
1066
|
+
const bleTransport = {
|
|
1067
|
+
mtuSize: 23,
|
|
1068
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
1069
|
+
};
|
|
1070
|
+
const context = {
|
|
1071
|
+
messageName: 'FileWrite',
|
|
1072
|
+
timeoutMs: 1000,
|
|
1073
|
+
highThroughput: true,
|
|
1074
|
+
generation: 1,
|
|
1075
|
+
signal: new AbortController().signal,
|
|
1076
|
+
};
|
|
1077
|
+
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
1078
|
+
|
|
1079
|
+
await expect(
|
|
1080
|
+
transport.writeProtocolV2Frame(
|
|
1081
|
+
'device-uuid',
|
|
1082
|
+
bleTransport,
|
|
1083
|
+
new Uint8Array(30),
|
|
1084
|
+
context,
|
|
1085
|
+
jest.fn()
|
|
1086
|
+
)
|
|
1087
|
+
).rejects.toMatchObject({ errorCode: 205 });
|
|
1088
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
test('does not apply fixed burst or flush pauses to high-volume Protocol V2 writes', async () => {
|
|
1092
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 }) as any;
|
|
1093
|
+
const writeWithoutResponse = jest.fn().mockResolvedValue(undefined);
|
|
1094
|
+
const bleTransport = {
|
|
1095
|
+
mtuSize: 247,
|
|
1096
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
1097
|
+
};
|
|
1098
|
+
const context = {
|
|
1099
|
+
messageName: 'FileWrite',
|
|
1100
|
+
timeoutMs: 1000,
|
|
1101
|
+
highThroughput: true,
|
|
1102
|
+
generation: 1,
|
|
1103
|
+
signal: new AbortController().signal,
|
|
1104
|
+
};
|
|
1105
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
await transport.writeProtocolV2Frame(
|
|
1109
|
+
'test-device',
|
|
1110
|
+
bleTransport,
|
|
1111
|
+
new Uint8Array(600),
|
|
1112
|
+
context,
|
|
1113
|
+
jest.fn()
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
expect(writeWithoutResponse).toHaveBeenCalledTimes(3);
|
|
1117
|
+
// One BLE write watchdog per packet and nothing else: no burst or flush pauses.
|
|
1118
|
+
expect(setTimeoutSpy.mock.calls.map(([, timeout]) => timeout)).toEqual([
|
|
1119
|
+
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
1120
|
+
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
1121
|
+
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
1122
|
+
]);
|
|
1123
|
+
} finally {
|
|
1124
|
+
setTimeoutSpy.mockRestore();
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
243
1127
|
});
|