@onekeyfe/hd-transport-web-device 1.2.0-alpha.3 → 1.2.0-alpha.30
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/__tests__/electron-ble-transport.test.ts +267 -18
- package/__tests__/webusb-protocol-v2-timeout.test.ts +288 -0
- package/dist/electron-ble-transport.d.ts +6 -5
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +106 -11
- package/dist/index.js +267 -294
- package/dist/transportLog.d.ts +7 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/webusb.d.ts +15 -7
- package/dist/webusb.d.ts.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/electron-ble-transport.ts +153 -175
- package/src/transportLog.ts +11 -0
- package/src/webusb.ts +171 -179
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
|
|
2
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
|
+
import EventEmitter from 'events';
|
|
2
4
|
|
|
3
5
|
import ElectronBleTransport from '../src/electron-ble-transport';
|
|
4
6
|
|
|
@@ -28,21 +30,25 @@ const protocolV1Schema = {
|
|
|
28
30
|
|
|
29
31
|
const protocolV2Schema = {
|
|
30
32
|
nested: {
|
|
31
|
-
|
|
33
|
+
ProtocolInfoRequest: {
|
|
32
34
|
fields: {},
|
|
33
35
|
},
|
|
34
|
-
|
|
36
|
+
ProtocolInfo: {
|
|
35
37
|
fields: {
|
|
36
|
-
|
|
38
|
+
version: {
|
|
37
39
|
type: 'uint32',
|
|
38
40
|
id: 1,
|
|
39
41
|
},
|
|
40
|
-
|
|
42
|
+
supported_messages: {
|
|
43
|
+
rule: 'repeated',
|
|
41
44
|
type: 'uint32',
|
|
42
45
|
id: 2,
|
|
46
|
+
options: {
|
|
47
|
+
packed: false,
|
|
48
|
+
},
|
|
43
49
|
},
|
|
44
|
-
|
|
45
|
-
type: '
|
|
50
|
+
protobuf_definition: {
|
|
51
|
+
type: 'string',
|
|
46
52
|
id: 3,
|
|
47
53
|
},
|
|
48
54
|
},
|
|
@@ -65,8 +71,8 @@ const protocolV2Schema = {
|
|
|
65
71
|
},
|
|
66
72
|
MessageType: {
|
|
67
73
|
values: {
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
MessageType_ProtocolInfoRequest: 60200,
|
|
75
|
+
MessageType_ProtocolInfo: 60201,
|
|
70
76
|
MessageType_Ping: 60206,
|
|
71
77
|
MessageType_Success: 60207,
|
|
72
78
|
},
|
|
@@ -106,7 +112,10 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
|
|
|
106
112
|
),
|
|
107
113
|
});
|
|
108
114
|
|
|
109
|
-
const configureTransport = (
|
|
115
|
+
const configureTransport = (
|
|
116
|
+
nobleBle: ReturnType<typeof createNobleBle>,
|
|
117
|
+
emitter?: EventEmitter
|
|
118
|
+
) => {
|
|
110
119
|
(global as any).window = {
|
|
111
120
|
desktopApi: {
|
|
112
121
|
nobleBle,
|
|
@@ -114,7 +123,7 @@ const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
|
|
|
114
123
|
};
|
|
115
124
|
|
|
116
125
|
const transport = new ElectronBleTransport();
|
|
117
|
-
transport.init(createLogger());
|
|
126
|
+
transport.init(createLogger(), emitter);
|
|
118
127
|
transport.configure(protocolV1Schema);
|
|
119
128
|
transport.configureProtocolV2(protocolV2Schema);
|
|
120
129
|
return transport;
|
|
@@ -126,6 +135,72 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
126
135
|
jest.clearAllMocks();
|
|
127
136
|
});
|
|
128
137
|
|
|
138
|
+
test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
|
|
139
|
+
const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
|
|
140
|
+
const nobleBle = createNobleBle(device);
|
|
141
|
+
const emitter = new EventEmitter();
|
|
142
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
143
|
+
let disconnectHandler: ((device: { id: string; name: string | null }) => void) | undefined;
|
|
144
|
+
let responseSeq = 0;
|
|
145
|
+
|
|
146
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
147
|
+
notificationHandler = handler;
|
|
148
|
+
return jest.fn();
|
|
149
|
+
});
|
|
150
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
151
|
+
disconnectHandler = handler;
|
|
152
|
+
return jest.fn();
|
|
153
|
+
});
|
|
154
|
+
nobleBle.write.mockImplementation(() => {
|
|
155
|
+
responseSeq += 1;
|
|
156
|
+
const response = ProtocolV2.encodeFrame(
|
|
157
|
+
schemas,
|
|
158
|
+
'Success',
|
|
159
|
+
{ message: 'ok' },
|
|
160
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
161
|
+
);
|
|
162
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
163
|
+
return Promise.resolve();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const publicConnect = jest.fn();
|
|
167
|
+
const publicDisconnect = jest.fn();
|
|
168
|
+
const transportDisconnect = jest.fn();
|
|
169
|
+
emitter.on('device-connect', publicConnect);
|
|
170
|
+
emitter.on('device-disconnect', publicDisconnect);
|
|
171
|
+
emitter.on('transport-device-disconnect', transportDisconnect);
|
|
172
|
+
const bleTransport = configureTransport(nobleBle, emitter);
|
|
173
|
+
|
|
174
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
175
|
+
disconnectHandler?.(device);
|
|
176
|
+
|
|
177
|
+
expect(publicConnect).not.toHaveBeenCalled();
|
|
178
|
+
expect(publicDisconnect).not.toHaveBeenCalled();
|
|
179
|
+
expect(transportDisconnect).toHaveBeenCalledWith({
|
|
180
|
+
id: device.id,
|
|
181
|
+
connectId: device.id,
|
|
182
|
+
name: device.name,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('uses the Protocol V2 BLE writer with the Electron packet size', async () => {
|
|
187
|
+
const device = { id: 'chunked-pro2-id', name: 'OneKey Pro 2' };
|
|
188
|
+
const nobleBle = createNobleBle(device);
|
|
189
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
190
|
+
const context = {
|
|
191
|
+
messageName: 'Ping',
|
|
192
|
+
timeoutMs: 1000,
|
|
193
|
+
highVolume: false,
|
|
194
|
+
generation: 1,
|
|
195
|
+
signal: new AbortController().signal,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(193), context, jest.fn());
|
|
199
|
+
|
|
200
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(2);
|
|
201
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([192, 1]);
|
|
202
|
+
});
|
|
203
|
+
|
|
129
204
|
test('detects Protocol V2 after Protocol V1 probe timeout', async () => {
|
|
130
205
|
const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' };
|
|
131
206
|
const nobleBle = createNobleBle(device);
|
|
@@ -136,7 +211,6 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
136
211
|
{ message: 'ok' },
|
|
137
212
|
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
138
213
|
);
|
|
139
|
-
|
|
140
214
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
141
215
|
notificationHandler = handler;
|
|
142
216
|
return jest.fn();
|
|
@@ -183,6 +257,7 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
183
257
|
return Promise.resolve();
|
|
184
258
|
});
|
|
185
259
|
const transport = configureTransport(nobleBle);
|
|
260
|
+
const protocolV2Writer = jest.spyOn(transport as any, 'writeProtocolV2Frame');
|
|
186
261
|
|
|
187
262
|
try {
|
|
188
263
|
await expect(transport.acquire({ uuid: device.id })).resolves.toEqual(
|
|
@@ -191,6 +266,7 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
191
266
|
})
|
|
192
267
|
);
|
|
193
268
|
expect(transport.getProtocolType(device.id)).toBe('V1');
|
|
269
|
+
expect(protocolV2Writer).not.toHaveBeenCalled();
|
|
194
270
|
} finally {
|
|
195
271
|
await transport.release(device.id);
|
|
196
272
|
}
|
|
@@ -215,19 +291,21 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
215
291
|
const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
|
|
216
292
|
const nobleBle = createNobleBle(device);
|
|
217
293
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
218
|
-
const probeResponse = ProtocolV2.encodeFrame(
|
|
219
|
-
schemas,
|
|
220
|
-
'Success',
|
|
221
|
-
{ message: 'ok' },
|
|
222
|
-
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
223
|
-
);
|
|
224
294
|
|
|
225
295
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
226
296
|
notificationHandler = handler;
|
|
227
297
|
return jest.fn();
|
|
228
298
|
});
|
|
299
|
+
let responseSeq = 0;
|
|
229
300
|
nobleBle.write.mockImplementation(() => {
|
|
230
|
-
|
|
301
|
+
responseSeq += 1;
|
|
302
|
+
const response = ProtocolV2.encodeFrame(
|
|
303
|
+
schemas,
|
|
304
|
+
'Success',
|
|
305
|
+
{ message: 'ok' },
|
|
306
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
307
|
+
);
|
|
308
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
231
309
|
return Promise.resolve();
|
|
232
310
|
});
|
|
233
311
|
const transport = configureTransport(nobleBle);
|
|
@@ -241,8 +319,179 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
241
319
|
);
|
|
242
320
|
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
243
321
|
expect(transport.getProtocolType(device.id)).toBe('V2');
|
|
322
|
+
await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
|
|
323
|
+
type: 'Success',
|
|
324
|
+
message: { message: 'ok' },
|
|
325
|
+
});
|
|
326
|
+
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
327
|
+
Number.parseInt(hex.slice(12, 14), 16)
|
|
328
|
+
);
|
|
329
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
244
330
|
} finally {
|
|
245
331
|
await transport.release(device.id);
|
|
246
332
|
}
|
|
247
333
|
});
|
|
334
|
+
|
|
335
|
+
test('rejects the active Protocol V2 reader when pairing is rejected', async () => {
|
|
336
|
+
const device = { id: 'pairing-rejected-pro2-id', name: 'OneKey Pro 2' };
|
|
337
|
+
const nobleBle = createNobleBle(device);
|
|
338
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
339
|
+
let pairingRejected = false;
|
|
340
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
341
|
+
schemas,
|
|
342
|
+
'Success',
|
|
343
|
+
{ message: 'ok' },
|
|
344
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
348
|
+
notificationHandler = handler;
|
|
349
|
+
return jest.fn();
|
|
350
|
+
});
|
|
351
|
+
nobleBle.write.mockImplementation(() => {
|
|
352
|
+
setTimeout(
|
|
353
|
+
() =>
|
|
354
|
+
notificationHandler?.(
|
|
355
|
+
device.id,
|
|
356
|
+
pairingRejected ? 'PAIRING_REJECTED' : bytesToHex(probeResponse)
|
|
357
|
+
),
|
|
358
|
+
0
|
|
359
|
+
);
|
|
360
|
+
return Promise.resolve();
|
|
361
|
+
});
|
|
362
|
+
const transport = configureTransport(nobleBle);
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
await transport.acquire({ uuid: device.id });
|
|
366
|
+
pairingRejected = true;
|
|
367
|
+
|
|
368
|
+
await expect(
|
|
369
|
+
transport.call(device.id, 'Ping', { message: 'pairing' }, { timeoutMs: 50 })
|
|
370
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleDeviceBondedCanceled });
|
|
371
|
+
} finally {
|
|
372
|
+
await transport.release(device.id);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('rebuilds the active link when Core acquires the same device again', async () => {
|
|
377
|
+
const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' };
|
|
378
|
+
const nobleBle = createNobleBle(device);
|
|
379
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
380
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
381
|
+
notificationHandler = handler;
|
|
382
|
+
return jest.fn();
|
|
383
|
+
});
|
|
384
|
+
let responseSeq = 0;
|
|
385
|
+
nobleBle.write.mockImplementation(() => {
|
|
386
|
+
responseSeq += 1;
|
|
387
|
+
const sequencedResponse = ProtocolV2.encodeFrame(
|
|
388
|
+
schemas,
|
|
389
|
+
'Success',
|
|
390
|
+
{ message: 'ok' },
|
|
391
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
392
|
+
);
|
|
393
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(sequencedResponse)), 0);
|
|
394
|
+
return Promise.resolve();
|
|
395
|
+
});
|
|
396
|
+
const transport = configureTransport(nobleBle);
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
await transport.acquire({ uuid: device.id });
|
|
400
|
+
await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
401
|
+
await expect(
|
|
402
|
+
transport.call(device.id, 'Ping', { message: 'after-reacquire' })
|
|
403
|
+
).resolves.toEqual({
|
|
404
|
+
type: 'Success',
|
|
405
|
+
message: { message: 'ok' },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
409
|
+
Number.parseInt(hex.slice(12, 14), 16)
|
|
410
|
+
);
|
|
411
|
+
expect(sentSeqs).toEqual([1, 2, 3]);
|
|
412
|
+
} finally {
|
|
413
|
+
await transport.release(device.id);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
|
|
418
|
+
const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
|
|
419
|
+
const nobleBle = createNobleBle(device);
|
|
420
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
421
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
422
|
+
notificationHandler = handler;
|
|
423
|
+
return jest.fn();
|
|
424
|
+
});
|
|
425
|
+
let responseSeq = 0;
|
|
426
|
+
nobleBle.write.mockImplementation(() => {
|
|
427
|
+
responseSeq += 1;
|
|
428
|
+
const response = ProtocolV2.encodeFrame(
|
|
429
|
+
schemas,
|
|
430
|
+
'Success',
|
|
431
|
+
{ message: 'ok' },
|
|
432
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
433
|
+
);
|
|
434
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
435
|
+
return Promise.resolve();
|
|
436
|
+
});
|
|
437
|
+
const bleTransport = configureTransport(nobleBle);
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
441
|
+
const invalidateAllLinks = jest.spyOn(
|
|
442
|
+
(bleTransport as any).protocolV2Links,
|
|
443
|
+
'invalidateAllLinks'
|
|
444
|
+
);
|
|
445
|
+
bleTransport.configureProtocolV2(protocolV2Schema);
|
|
446
|
+
await new Promise<void>(resolve => {
|
|
447
|
+
setTimeout(resolve, 0);
|
|
448
|
+
});
|
|
449
|
+
expect(invalidateAllLinks).not.toHaveBeenCalled();
|
|
450
|
+
await expect(
|
|
451
|
+
bleTransport.call(device.id, 'Ping', { message: 'same-schema' })
|
|
452
|
+
).resolves.toEqual({
|
|
453
|
+
type: 'Success',
|
|
454
|
+
message: { message: 'ok' },
|
|
455
|
+
});
|
|
456
|
+
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
457
|
+
Number.parseInt(hex.slice(12, 14), 16)
|
|
458
|
+
);
|
|
459
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
460
|
+
} finally {
|
|
461
|
+
await bleTransport.release(device.id);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('rejects oversized Protocol V2 requests before writing to Electron BLE', async () => {
|
|
466
|
+
const device = { id: 'oversized-frame-pro2-id', name: 'OneKey Pro 2' };
|
|
467
|
+
const nobleBle = createNobleBle(device);
|
|
468
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
469
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
470
|
+
schemas,
|
|
471
|
+
'Success',
|
|
472
|
+
{ message: 'ok' },
|
|
473
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
474
|
+
);
|
|
475
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
476
|
+
notificationHandler = handler;
|
|
477
|
+
return jest.fn();
|
|
478
|
+
});
|
|
479
|
+
nobleBle.write.mockImplementation(() => {
|
|
480
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
|
|
481
|
+
return Promise.resolve();
|
|
482
|
+
});
|
|
483
|
+
const bleTransport = configureTransport(nobleBle);
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
487
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
488
|
+
|
|
489
|
+
await expect(
|
|
490
|
+
bleTransport.call(device.id, 'Ping', { message: 'x'.repeat(2048) })
|
|
491
|
+
).rejects.toThrow(/Protocol V2 frame too large for transport/);
|
|
492
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
493
|
+
} finally {
|
|
494
|
+
await bleTransport.release(device.id);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
248
497
|
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import transport, {
|
|
2
|
+
PROTOCOL_V2_CHANNEL_USB,
|
|
3
|
+
ProtocolV2,
|
|
4
|
+
ProtocolV2LinkError,
|
|
5
|
+
} from '@onekeyfe/hd-transport';
|
|
6
|
+
|
|
7
|
+
import WebUsbTransport from '../src/webusb';
|
|
8
|
+
|
|
9
|
+
const schema = {
|
|
10
|
+
nested: {
|
|
11
|
+
Ping: { fields: { message: { type: 'string', id: 1 } } },
|
|
12
|
+
Success: { fields: { message: { type: 'string', id: 1 } } },
|
|
13
|
+
MessageType: {
|
|
14
|
+
values: {
|
|
15
|
+
MessageType_Ping: 60206,
|
|
16
|
+
MessageType_Success: 60207,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('WebUsbTransport Protocol V2 timeout recovery', () => {
|
|
23
|
+
test('keeps active links when the Protocol V2 schema is configured repeatedly', () => {
|
|
24
|
+
const webusb = new WebUsbTransport() as any;
|
|
25
|
+
webusb.invalidateAllProtocolV2UsbLinks = jest.fn().mockResolvedValue(undefined);
|
|
26
|
+
const schemaSource = JSON.stringify(schema);
|
|
27
|
+
|
|
28
|
+
webusb.configureProtocolV2(schemaSource);
|
|
29
|
+
webusb.configureProtocolV2(schemaSource);
|
|
30
|
+
|
|
31
|
+
expect(webusb.invalidateAllProtocolV2UsbLinks).not.toHaveBeenCalled();
|
|
32
|
+
|
|
33
|
+
webusb.configureProtocolV2(
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
...schema,
|
|
36
|
+
nested: {
|
|
37
|
+
...schema.nested,
|
|
38
|
+
Failure: { fields: { message: { type: 'string', id: 1 } } },
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
expect(webusb.invalidateAllProtocolV2UsbLinks).toHaveBeenCalledWith(
|
|
43
|
+
'Protocol V2 schema reconfigured'
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('resets the connection between a failed V1 probe and the V2 probe', async () => {
|
|
48
|
+
const webusb = new WebUsbTransport() as any;
|
|
49
|
+
const path = 'pro2-webusb';
|
|
50
|
+
const events: string[] = [];
|
|
51
|
+
webusb.probeProtocolV1 = jest.fn().mockImplementation(() => {
|
|
52
|
+
events.push('probe-v1');
|
|
53
|
+
return Promise.resolve(false);
|
|
54
|
+
});
|
|
55
|
+
webusb.resetConnectionAfterProbe = jest.fn().mockImplementation(() => {
|
|
56
|
+
events.push('reset');
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
});
|
|
59
|
+
webusb.probeProtocolV2 = jest.fn().mockImplementation(() => {
|
|
60
|
+
events.push('probe-v2');
|
|
61
|
+
return Promise.resolve(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expect(webusb.detectProtocol(path)).resolves.toBe('V2');
|
|
65
|
+
|
|
66
|
+
expect(events).toEqual(['probe-v1', 'reset', 'probe-v2']);
|
|
67
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V2');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('retries an expected Protocol V2 probe once after resetting the connection', async () => {
|
|
71
|
+
const webusb = new WebUsbTransport() as any;
|
|
72
|
+
const path = 'pro2-webusb';
|
|
73
|
+
webusb.probeProtocolV1 = jest.fn();
|
|
74
|
+
webusb.probeProtocolV2 = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
|
75
|
+
webusb.resetConnectionAfterProbe = jest.fn().mockResolvedValue(undefined);
|
|
76
|
+
|
|
77
|
+
await expect(webusb.detectProtocol(path, 'V2')).resolves.toBe('V2');
|
|
78
|
+
|
|
79
|
+
expect(webusb.probeProtocolV2).toHaveBeenCalledTimes(2);
|
|
80
|
+
expect(webusb.probeProtocolV1).not.toHaveBeenCalled();
|
|
81
|
+
expect(webusb.resetConnectionAfterProbe).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(webusb.deviceProtocol.get(path)).toBe('V2');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('reports a Protocol V2 probe timeout only after the bounded retry is exhausted', async () => {
|
|
86
|
+
const webusb = new WebUsbTransport() as any;
|
|
87
|
+
const path = 'pro2-webusb';
|
|
88
|
+
webusb.probeProtocolV1 = jest.fn();
|
|
89
|
+
webusb.probeProtocolV2 = jest.fn().mockResolvedValue(false);
|
|
90
|
+
webusb.resetConnectionAfterProbe = jest.fn().mockResolvedValue(undefined);
|
|
91
|
+
|
|
92
|
+
await expect(webusb.detectProtocol(path, 'V2')).rejects.toThrow(
|
|
93
|
+
'Protocol V2 probe timeout after 2 attempts'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(webusb.probeProtocolV2).toHaveBeenCalledTimes(2);
|
|
97
|
+
expect(webusb.probeProtocolV1).not.toHaveBeenCalled();
|
|
98
|
+
expect(webusb.resetConnectionAfterProbe).toHaveBeenCalledTimes(2);
|
|
99
|
+
expect(webusb.deviceProtocol.has(path)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('invalidates and resets the cached connection before another call can start', async () => {
|
|
103
|
+
const webusb = new WebUsbTransport() as any;
|
|
104
|
+
const path = 'pro2-webusb';
|
|
105
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
106
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
107
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
108
|
+
webusb.readProtocolV2UsbPacket = jest.fn(() => new Promise<void>(() => {}));
|
|
109
|
+
webusb.resetProtocolV2UsbNativeLink = jest.fn().mockResolvedValue(undefined);
|
|
110
|
+
webusb.resetConnectionAfterProbe = jest.fn();
|
|
111
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
112
|
+
|
|
113
|
+
await expect(
|
|
114
|
+
webusb.callProtocolV2(path, 'Ping', { message: 'timeout' }, { timeoutMs: 10 })
|
|
115
|
+
).rejects.toThrow('timeout');
|
|
116
|
+
|
|
117
|
+
expect(webusb.resetProtocolV2UsbNativeLink).toHaveBeenCalledWith(
|
|
118
|
+
path,
|
|
119
|
+
expect.stringContaining('timeout')
|
|
120
|
+
);
|
|
121
|
+
expect(webusb.resetConnectionAfterProbe).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('does not reconnect inside a Protocol V2 frame read after a USB I/O failure', async () => {
|
|
125
|
+
const webusb = new WebUsbTransport() as any;
|
|
126
|
+
const path = 'pro2-webusb';
|
|
127
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
128
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
129
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
130
|
+
webusb.readProtocolV2UsbPacket = jest
|
|
131
|
+
.fn()
|
|
132
|
+
.mockRejectedValue(new Error('NetworkError: transferIn device disconnected'));
|
|
133
|
+
webusb.resetProtocolV2UsbNativeLink = jest.fn().mockResolvedValue(undefined);
|
|
134
|
+
webusb.resetConnectionAfterProbe = jest.fn();
|
|
135
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
136
|
+
|
|
137
|
+
await expect(webusb.callProtocolV2(path, 'Ping', { message: 'read-error' })).rejects.toThrow(
|
|
138
|
+
'NetworkError'
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(webusb.readProtocolV2UsbPacket).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(webusb.resetProtocolV2UsbNativeLink).toHaveBeenCalledWith(
|
|
143
|
+
path,
|
|
144
|
+
expect.stringContaining('NetworkError')
|
|
145
|
+
);
|
|
146
|
+
expect(webusb.resetConnectionAfterProbe).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('rejects an active Protocol V2 read without reconnecting after release', async () => {
|
|
150
|
+
const webusb = new WebUsbTransport() as any;
|
|
151
|
+
const path = 'pro2-webusb';
|
|
152
|
+
let markReadStarted: () => void = () => undefined;
|
|
153
|
+
const readStarted = new Promise<void>(resolve => {
|
|
154
|
+
markReadStarted = resolve;
|
|
155
|
+
});
|
|
156
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
157
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
158
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
159
|
+
webusb.readProtocolV2UsbPacket = jest.fn().mockImplementation(() => {
|
|
160
|
+
markReadStarted();
|
|
161
|
+
return new Promise<void>(() => {});
|
|
162
|
+
});
|
|
163
|
+
webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
|
|
164
|
+
webusb.connect = jest.fn();
|
|
165
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
166
|
+
|
|
167
|
+
const call = webusb.callProtocolV2(path, 'Ping', { message: 'release' });
|
|
168
|
+
await readStarted;
|
|
169
|
+
await webusb.release(path);
|
|
170
|
+
|
|
171
|
+
await expect(call).rejects.toThrow('WebUSB transport released');
|
|
172
|
+
expect(webusb.connect).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test.each(['router', 'packet-source', 'ack-sequence', 'response-sequence', 'frame'] as const)(
|
|
176
|
+
'invalidates cached state for typed Protocol V2 %s errors',
|
|
177
|
+
async code => {
|
|
178
|
+
const webusb = new WebUsbTransport() as any;
|
|
179
|
+
const path = 'pro2-webusb';
|
|
180
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
181
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
182
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
183
|
+
const recoveredResponse = ProtocolV2.encodeFrame(
|
|
184
|
+
{ protocolV1: webusb.messages, protocolV2: webusb.messagesV2 },
|
|
185
|
+
'Success',
|
|
186
|
+
{ message: 'recovered' },
|
|
187
|
+
{ seq: 1 }
|
|
188
|
+
);
|
|
189
|
+
webusb.readProtocolV2UsbPacket = jest
|
|
190
|
+
.fn()
|
|
191
|
+
.mockRejectedValueOnce(
|
|
192
|
+
new ProtocolV2LinkError(code, `Protocol V2 ${code} validation failed`)
|
|
193
|
+
)
|
|
194
|
+
.mockResolvedValue(recoveredResponse);
|
|
195
|
+
webusb.resetProtocolV2UsbNativeLink = jest.fn().mockResolvedValue(undefined);
|
|
196
|
+
webusb.resetConnectionAfterProbe = jest.fn();
|
|
197
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
198
|
+
|
|
199
|
+
await expect(webusb.callProtocolV2(path, 'Ping', { message: 'mismatch' })).rejects.toThrow(
|
|
200
|
+
`${code} validation failed`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(webusb.resetProtocolV2UsbNativeLink).toHaveBeenCalledWith(
|
|
204
|
+
path,
|
|
205
|
+
expect.stringContaining(`${code} validation failed`)
|
|
206
|
+
);
|
|
207
|
+
expect(webusb.resetConnectionAfterProbe).not.toHaveBeenCalled();
|
|
208
|
+
|
|
209
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test reconnect');
|
|
210
|
+
await expect(
|
|
211
|
+
webusb.callProtocolV2(path, 'Ping', { message: 'after-reset' })
|
|
212
|
+
).resolves.toMatchObject({
|
|
213
|
+
type: 'Success',
|
|
214
|
+
message: { message: 'recovered' },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
test('does not discard buffered Protocol V2 frames before each call', async () => {
|
|
220
|
+
const webusb = new WebUsbTransport() as any;
|
|
221
|
+
const path = 'pro2-webusb';
|
|
222
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
223
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
224
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
225
|
+
const firstResponse = ProtocolV2.encodeFrame(
|
|
226
|
+
{ protocolV1: webusb.messages, protocolV2: webusb.messagesV2 },
|
|
227
|
+
'Success',
|
|
228
|
+
{ message: 'first' },
|
|
229
|
+
{ router: PROTOCOL_V2_CHANNEL_USB, seq: 1 }
|
|
230
|
+
);
|
|
231
|
+
const secondResponse = ProtocolV2.encodeFrame(
|
|
232
|
+
{ protocolV1: webusb.messages, protocolV2: webusb.messagesV2 },
|
|
233
|
+
'Success',
|
|
234
|
+
{ message: 'second' },
|
|
235
|
+
{ router: PROTOCOL_V2_CHANNEL_USB, seq: 2 }
|
|
236
|
+
);
|
|
237
|
+
const coalescedResponses = new Uint8Array(firstResponse.length + secondResponse.length);
|
|
238
|
+
coalescedResponses.set(firstResponse);
|
|
239
|
+
coalescedResponses.set(secondResponse, firstResponse.length);
|
|
240
|
+
webusb.readProtocolV2UsbPacket = jest.fn().mockResolvedValue(coalescedResponses);
|
|
241
|
+
webusb.resetProtocolV2UsbNativeLink = jest.fn().mockResolvedValue(undefined);
|
|
242
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
243
|
+
|
|
244
|
+
await expect(webusb.callProtocolV2(path, 'Ping', { message: 'first' })).resolves.toMatchObject({
|
|
245
|
+
type: 'Success',
|
|
246
|
+
message: { message: 'first' },
|
|
247
|
+
});
|
|
248
|
+
await expect(webusb.callProtocolV2(path, 'Ping', { message: 'second' })).resolves.toMatchObject(
|
|
249
|
+
{
|
|
250
|
+
type: 'Success',
|
|
251
|
+
message: { message: 'second' },
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(webusb.readProtocolV2UsbPacket).toHaveBeenCalledTimes(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('keeps queued Protocol V2 read timeouts scoped to each call', async () => {
|
|
259
|
+
const webusb = new WebUsbTransport() as any;
|
|
260
|
+
const path = 'pro2-webusb';
|
|
261
|
+
let responseSequence = 0;
|
|
262
|
+
const readTimeouts: number[] = [];
|
|
263
|
+
webusb.messages = transport.parseConfigure(schema);
|
|
264
|
+
webusb.messagesV2 = transport.parseConfigure(schema);
|
|
265
|
+
webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
|
|
266
|
+
webusb.readProtocolV2UsbPacket = jest.fn().mockImplementation((_path, context) => {
|
|
267
|
+
responseSequence += 1;
|
|
268
|
+
readTimeouts.push(context.timeoutMs);
|
|
269
|
+
return Promise.resolve(
|
|
270
|
+
ProtocolV2.encodeFrame(
|
|
271
|
+
{ protocolV1: webusb.messages, protocolV2: webusb.messagesV2 },
|
|
272
|
+
'Success',
|
|
273
|
+
{ message: 'ok' },
|
|
274
|
+
{ seq: responseSequence }
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
webusb.resetProtocolV2UsbNativeLink = jest.fn().mockResolvedValue(undefined);
|
|
279
|
+
await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
|
|
280
|
+
|
|
281
|
+
await Promise.all([
|
|
282
|
+
webusb.callProtocolV2(path, 'Ping', { message: 'long' }, { timeoutMs: 1_000 }),
|
|
283
|
+
webusb.callProtocolV2(path, 'Ping', { message: 'short' }, { timeoutMs: 25 }),
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
expect(readTimeouts).toEqual([1_000, 25]);
|
|
287
|
+
});
|
|
288
|
+
});
|