@onekeyfe/hd-transport-web-device 1.2.0-alpha.13 → 1.2.0-alpha.131
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 +377 -22
- package/__tests__/webusb-protocol-v2-timeout.test.ts +357 -11
- package/dist/ble-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/electron-ble-transport.d.ts +16 -3
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +110 -12
- package/dist/index.js +345 -255
- package/dist/transportLog.d.ts +1 -6
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/webusb.d.ts +15 -10
- package/dist/webusb.d.ts.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/ble-packet-capacity.ts +13 -0
- package/src/electron-ble-transport.ts +241 -104
- package/src/transportLog.ts +1 -11
- package/src/webusb.ts +173 -189
package/src/webusb.ts
CHANGED
|
@@ -6,8 +6,8 @@ import transport, {
|
|
|
6
6
|
PROTOCOL_V1_USB_PACKET_SIZE,
|
|
7
7
|
PROTOCOL_V2_CHANNEL_USB,
|
|
8
8
|
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
ProtocolV2LinkError,
|
|
10
|
+
ProtocolV2UsbTransportBase,
|
|
11
11
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
12
12
|
} from '@onekeyfe/hd-transport';
|
|
13
13
|
import {
|
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
} from '@onekeyfe/hd-shared';
|
|
20
20
|
import ByteBuffer from 'bytebuffer';
|
|
21
21
|
|
|
22
|
-
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
23
|
-
|
|
24
22
|
import type {
|
|
25
23
|
AcquireInput,
|
|
26
24
|
OneKeyDeviceInfoBase,
|
|
27
25
|
ProtocolType,
|
|
26
|
+
ProtocolV2CallContext,
|
|
27
|
+
ProtocolV2Schemas,
|
|
28
28
|
TransportCallOptions,
|
|
29
29
|
} from '@onekeyfe/hd-transport';
|
|
30
30
|
|
|
@@ -39,7 +39,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
|
|
|
39
39
|
const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
40
40
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
41
41
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
42
|
-
|
|
42
|
+
// Legacy devices can take longer to answer Initialize after a WebUSB reset.
|
|
43
|
+
// Keep this aligned with Node USB so a slow Protocol V1 response is not
|
|
44
|
+
// misreported as a protocol mismatch during acquire.
|
|
45
|
+
const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
|
|
46
|
+
const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
|
|
47
|
+
const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
43
48
|
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
44
49
|
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
45
50
|
}
|
|
@@ -64,38 +69,22 @@ interface TransferCancelToken {
|
|
|
64
69
|
cancelled: boolean;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
export default class WebUsbTransport {
|
|
72
|
+
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
68
73
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
69
74
|
|
|
70
75
|
/** Protobuf schema for Protocol V2 transports. */
|
|
71
76
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
72
77
|
|
|
78
|
+
private protocolV2SchemaSource: string | undefined;
|
|
79
|
+
|
|
73
80
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
74
81
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
75
82
|
|
|
76
83
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
77
84
|
|
|
78
|
-
/** 按设备缓存 Protocol V2 frame assembler,保留同一次读取里多出来的后续 frame。 */
|
|
79
|
-
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
80
|
-
|
|
81
|
-
/** 按设备缓存 Protocol V2 session,保持 seq 与设备会话一致递增。 */
|
|
82
|
-
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
83
|
-
|
|
84
|
-
/** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */
|
|
85
|
-
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
86
|
-
|
|
87
85
|
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
88
86
|
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
89
87
|
|
|
90
|
-
/**
|
|
91
|
-
* 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。
|
|
92
|
-
* 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path),
|
|
93
|
-
* 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
|
|
94
|
-
*/
|
|
95
|
-
private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
|
|
96
|
-
|
|
97
|
-
private mockSerialCounter = 0;
|
|
98
|
-
|
|
99
88
|
name = 'WebUsbTransport';
|
|
100
89
|
|
|
101
90
|
stopped = false;
|
|
@@ -118,6 +107,14 @@ export default class WebUsbTransport {
|
|
|
118
107
|
|
|
119
108
|
interfaceId = INTERFACE_ID;
|
|
120
109
|
|
|
110
|
+
constructor() {
|
|
111
|
+
super({
|
|
112
|
+
router: PROTOCOL_V2_CHANNEL_USB,
|
|
113
|
+
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
114
|
+
logPrefix: 'ProtocolV2 WebUSB',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
121
118
|
/**
|
|
122
119
|
* Initialize WebUSB transport
|
|
123
120
|
*/
|
|
@@ -147,9 +144,20 @@ export default class WebUsbTransport {
|
|
|
147
144
|
* Cache the Protocol V2 protobuf schema.
|
|
148
145
|
*/
|
|
149
146
|
configureProtocolV2(signedData: any) {
|
|
147
|
+
const schemaSource =
|
|
148
|
+
typeof signedData === 'string'
|
|
149
|
+
? signedData
|
|
150
|
+
: JSON.stringify(signedData) ?? String(signedData);
|
|
151
|
+
if (schemaSource === this.protocolV2SchemaSource) return;
|
|
152
|
+
|
|
153
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
150
154
|
this.messagesV2 = parseConfigure(signedData);
|
|
151
|
-
this.
|
|
152
|
-
|
|
155
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
156
|
+
if (!hadProtocolV2Schema) return;
|
|
157
|
+
|
|
158
|
+
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
159
|
+
this.Log?.debug('[WebUsbTransport] schema link cleanup failed:', error)
|
|
160
|
+
);
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
/**
|
|
@@ -181,26 +189,6 @@ export default class WebUsbTransport {
|
|
|
181
189
|
return this.deviceList;
|
|
182
190
|
}
|
|
183
191
|
|
|
184
|
-
/**
|
|
185
|
-
* 设备 path:正常设备直接用 USB serial number;
|
|
186
|
-
* 空 serial(早期工程板)回退到会话内稳定的 mock path。
|
|
187
|
-
*/
|
|
188
|
-
private getDevicePath(device: USBDevice): string {
|
|
189
|
-
if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
|
|
190
|
-
return device.serialNumber;
|
|
191
|
-
}
|
|
192
|
-
let path = this.mockSerialPaths.get(device);
|
|
193
|
-
if (!path) {
|
|
194
|
-
this.mockSerialCounter += 1;
|
|
195
|
-
path = `mock-serial:${device.vendorId.toString(16)}:${device.productId.toString(16)}:${
|
|
196
|
-
this.mockSerialCounter
|
|
197
|
-
}`;
|
|
198
|
-
this.mockSerialPaths.set(device, path);
|
|
199
|
-
this.Log?.debug(`[WebUSB] device has no serial number, using mock path: ${path}`);
|
|
200
|
-
}
|
|
201
|
-
return path;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
192
|
/**
|
|
205
193
|
* Get list of connected devices
|
|
206
194
|
*/
|
|
@@ -213,11 +201,12 @@ export default class WebUsbTransport {
|
|
|
213
201
|
(desc: { vendorId: number; productId: number }) =>
|
|
214
202
|
dev.vendorId === desc.vendorId && dev.productId === desc.productId
|
|
215
203
|
);
|
|
216
|
-
|
|
204
|
+
const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0;
|
|
205
|
+
return isOneKey && hasSerialNumber && !isKnownTrezorWebUsbDevice(dev);
|
|
217
206
|
});
|
|
218
207
|
|
|
219
208
|
this.deviceList = onekeyDevices.map(device => {
|
|
220
|
-
const path =
|
|
209
|
+
const path = device.serialNumber as string;
|
|
221
210
|
const protocolHint = inferProtocolHintFromDeviceName(device.productName);
|
|
222
211
|
if (protocolHint) {
|
|
223
212
|
this.deviceProtocolHints.set(path, protocolHint);
|
|
@@ -239,13 +228,16 @@ export default class WebUsbTransport {
|
|
|
239
228
|
async acquire(input: AcquireInput) {
|
|
240
229
|
if (!input.path) return;
|
|
241
230
|
try {
|
|
231
|
+
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
242
232
|
await this.closeOpenDevice(input.path);
|
|
243
233
|
await this.connect(input.path ?? '', true);
|
|
244
234
|
const deviceName = this.deviceList.find(device => device.path === input.path)?.device
|
|
245
235
|
.productName;
|
|
246
236
|
const protocolHint = input.expectedProtocol
|
|
247
237
|
? undefined
|
|
248
|
-
:
|
|
238
|
+
: input.protocolHint ??
|
|
239
|
+
this.deviceProtocolHints.get(input.path) ??
|
|
240
|
+
inferProtocolHintFromDeviceName(deviceName);
|
|
249
241
|
if (protocolHint) {
|
|
250
242
|
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
251
243
|
}
|
|
@@ -253,6 +245,7 @@ export default class WebUsbTransport {
|
|
|
253
245
|
return await Promise.resolve(input.path);
|
|
254
246
|
} catch (e) {
|
|
255
247
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
248
|
+
await this.closeOpenDevice(input.path);
|
|
256
249
|
throw e;
|
|
257
250
|
}
|
|
258
251
|
}
|
|
@@ -269,13 +262,17 @@ export default class WebUsbTransport {
|
|
|
269
262
|
);
|
|
270
263
|
}
|
|
271
264
|
|
|
272
|
-
private
|
|
265
|
+
private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
|
|
273
266
|
return ERRORS.TypedError(
|
|
274
|
-
HardwareErrorCode.
|
|
275
|
-
|
|
267
|
+
HardwareErrorCode.DeviceInitializeFailed,
|
|
268
|
+
`Protocol ${expected} probe timeout after ${attempts} attempts`
|
|
276
269
|
);
|
|
277
270
|
}
|
|
278
271
|
|
|
272
|
+
private createProtocolDetectionError() {
|
|
273
|
+
return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
274
|
+
}
|
|
275
|
+
|
|
279
276
|
private async detectProtocol(
|
|
280
277
|
path: string,
|
|
281
278
|
expectedProtocol?: ProtocolType,
|
|
@@ -286,29 +283,54 @@ export default class WebUsbTransport {
|
|
|
286
283
|
this.deviceProtocol.set(path, 'V1');
|
|
287
284
|
return 'V1';
|
|
288
285
|
}
|
|
286
|
+
await this.closeConnectionAfterProbe(path);
|
|
289
287
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
290
288
|
}
|
|
291
289
|
|
|
292
290
|
if (expectedProtocol === 'V2') {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
291
|
+
for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
|
|
292
|
+
if (await this.probeProtocolV2(path)) {
|
|
293
|
+
this.deviceProtocol.set(path, 'V2');
|
|
294
|
+
return 'V2';
|
|
295
|
+
}
|
|
296
|
+
if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
|
|
297
|
+
await this.resetConnectionAfterProbe(path);
|
|
298
|
+
this.Log?.debug(
|
|
299
|
+
`[WebUsbTransport] Protocol V2 probe timed out, retrying ${
|
|
300
|
+
attempt + 1
|
|
301
|
+
}/${EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS}`
|
|
302
|
+
);
|
|
303
|
+
} else {
|
|
304
|
+
await this.closeConnectionAfterProbe(path);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this.deviceProtocol.delete(path);
|
|
308
|
+
throw this.createProtocolProbeTimeoutError(
|
|
309
|
+
expectedProtocol,
|
|
310
|
+
EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS
|
|
311
|
+
);
|
|
297
312
|
}
|
|
298
313
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// 不能作为最终结论。
|
|
314
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
315
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
302
316
|
const probeOrder: ProtocolType[] =
|
|
303
317
|
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
304
318
|
|
|
305
|
-
for (const protocol of probeOrder) {
|
|
319
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
306
320
|
const detected =
|
|
307
321
|
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
308
322
|
if (detected) {
|
|
309
323
|
this.deviceProtocol.set(path, protocol);
|
|
310
324
|
return protocol;
|
|
311
325
|
}
|
|
326
|
+
if (index < probeOrder.length - 1) {
|
|
327
|
+
// A timed-out WebUSB transferIn cannot be cancelled in place. Closing and
|
|
328
|
+
// reopening the device guarantees the next protocol probe cannot consume a
|
|
329
|
+
// late response from the previous protocol generation.
|
|
330
|
+
await this.resetConnectionAfterProbe(path);
|
|
331
|
+
} else {
|
|
332
|
+
await this.closeConnectionAfterProbe(path);
|
|
333
|
+
}
|
|
312
334
|
}
|
|
313
335
|
|
|
314
336
|
this.deviceProtocol.delete(path);
|
|
@@ -420,16 +442,12 @@ export default class WebUsbTransport {
|
|
|
420
442
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
421
443
|
const endpoints = this.discoverEndpoints(device);
|
|
422
444
|
this.deviceEndpoints.set(path, endpoints);
|
|
423
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
424
|
-
this.protocolV2Assemblers.set(path, new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES));
|
|
425
|
-
|
|
426
445
|
await device.claimInterface(endpoints.interfaceNumber);
|
|
427
446
|
await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
|
|
428
447
|
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
429
448
|
}
|
|
430
449
|
|
|
431
450
|
private async closeOpenDevice(path: string) {
|
|
432
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
433
451
|
const current = this.deviceList.find(device => device.path === path)?.device;
|
|
434
452
|
if (!current?.opened) return;
|
|
435
453
|
|
|
@@ -463,6 +481,10 @@ export default class WebUsbTransport {
|
|
|
463
481
|
}
|
|
464
482
|
|
|
465
483
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
484
|
+
if (this.deviceProtocol.get(session) === 'V2') {
|
|
485
|
+
await this.sendProtocolV2UsbFlowControl(session, name, data);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
466
488
|
await this.call(session, name, data);
|
|
467
489
|
}
|
|
468
490
|
|
|
@@ -624,33 +646,30 @@ export default class WebUsbTransport {
|
|
|
624
646
|
throw lastError;
|
|
625
647
|
}
|
|
626
648
|
|
|
627
|
-
private async
|
|
628
|
-
this.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
try {
|
|
633
|
-
const device = await this.findDevice(path);
|
|
634
|
-
if (device.opened) {
|
|
635
|
-
const endpoints = this.deviceEndpoints.get(path);
|
|
636
|
-
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
637
|
-
try {
|
|
638
|
-
await device.releaseInterface(ifaceNum);
|
|
639
|
-
} catch (error) {
|
|
640
|
-
this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error);
|
|
641
|
-
}
|
|
642
|
-
await device.close();
|
|
643
|
-
}
|
|
644
|
-
} catch (error) {
|
|
645
|
-
this.Log.debug('[WebUsbTransport] close after protocol probe error:', error);
|
|
649
|
+
private async transferInOnce(path: string, length: number): Promise<DataView> {
|
|
650
|
+
const device = await this.findDevice(path);
|
|
651
|
+
if (!device.opened) {
|
|
652
|
+
throw new Error('USBDevice is not open for transferIn');
|
|
646
653
|
}
|
|
654
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
655
|
+
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
656
|
+
const result = await device.transferIn(endpointIn, length);
|
|
657
|
+
return this.getTransferInData(result);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private async closeConnectionAfterProbe(path: string) {
|
|
661
|
+
await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
|
|
662
|
+
await this.closeOpenDevice(path);
|
|
663
|
+
}
|
|
647
664
|
|
|
665
|
+
private async resetConnectionAfterProbe(path: string) {
|
|
666
|
+
await this.closeConnectionAfterProbe(path);
|
|
648
667
|
await this.getConnectedDevices();
|
|
649
668
|
await this.connect(path, false);
|
|
650
669
|
}
|
|
651
670
|
|
|
652
671
|
private async withProtocolReadTimeout<T>(
|
|
653
|
-
|
|
672
|
+
path: string,
|
|
654
673
|
promise: Promise<T>,
|
|
655
674
|
timeoutMs: number,
|
|
656
675
|
protocol: ProtocolType,
|
|
@@ -672,9 +691,16 @@ export default class WebUsbTransport {
|
|
|
672
691
|
return await Promise.race([
|
|
673
692
|
guardedPromise,
|
|
674
693
|
new Promise<never>((_, reject) => {
|
|
675
|
-
timer = setTimeout(() => {
|
|
694
|
+
timer = setTimeout(async () => {
|
|
676
695
|
timedOut = true;
|
|
677
696
|
onTimeout?.();
|
|
697
|
+
if (protocol === 'V1') {
|
|
698
|
+
try {
|
|
699
|
+
await this.resetConnectionAfterProbe(path);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
678
704
|
reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
|
|
679
705
|
}, timeoutMs);
|
|
680
706
|
}),
|
|
@@ -690,7 +716,14 @@ export default class WebUsbTransport {
|
|
|
690
716
|
}
|
|
691
717
|
|
|
692
718
|
try {
|
|
693
|
-
await this.callProtocolV1(
|
|
719
|
+
await this.callProtocolV1(
|
|
720
|
+
path,
|
|
721
|
+
'Initialize',
|
|
722
|
+
{},
|
|
723
|
+
{
|
|
724
|
+
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
725
|
+
}
|
|
726
|
+
);
|
|
694
727
|
return true;
|
|
695
728
|
} catch (_error) {
|
|
696
729
|
return false;
|
|
@@ -704,10 +737,9 @@ export default class WebUsbTransport {
|
|
|
704
737
|
|
|
705
738
|
return probeProtocolV2Helper({
|
|
706
739
|
call: (name, data, options) => this.callProtocolV2(path, name, data, options),
|
|
707
|
-
timeoutMs:
|
|
740
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
|
|
708
741
|
logger: this.Log,
|
|
709
742
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
710
|
-
onProbeFailed: () => this.resetConnectionAfterProbe(path),
|
|
711
743
|
});
|
|
712
744
|
}
|
|
713
745
|
|
|
@@ -737,10 +769,6 @@ export default class WebUsbTransport {
|
|
|
737
769
|
);
|
|
738
770
|
}
|
|
739
771
|
|
|
740
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
741
|
-
this.Log.debug('transport call', createTransportCallLog(name, protocol));
|
|
742
|
-
}
|
|
743
|
-
|
|
744
772
|
if (protocol === 'V2') {
|
|
745
773
|
return this.callProtocolV2(path, name, data, options);
|
|
746
774
|
}
|
|
@@ -788,94 +816,7 @@ export default class WebUsbTransport {
|
|
|
788
816
|
data: Record<string, unknown>,
|
|
789
817
|
options?: TransportCallOptions
|
|
790
818
|
) {
|
|
791
|
-
|
|
792
|
-
if (!this.messagesV2) {
|
|
793
|
-
throw ERRORS.TypedError(
|
|
794
|
-
HardwareErrorCode.TransportNotConfigured,
|
|
795
|
-
'Protocol V2 schema not configured'
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
if (!protocolV1Messages) {
|
|
799
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
let session = this.protocolV2Sessions.get(path);
|
|
803
|
-
if (!session) {
|
|
804
|
-
session = new ProtocolV2Session({
|
|
805
|
-
schemas: {
|
|
806
|
-
protocolV1: protocolV1Messages,
|
|
807
|
-
protocolV2: this.messagesV2,
|
|
808
|
-
},
|
|
809
|
-
router: PROTOCOL_V2_CHANNEL_USB,
|
|
810
|
-
writeFrame: (frame: Uint8Array) => this.transferOutOnce(path, frame),
|
|
811
|
-
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
812
|
-
logger: this.Log,
|
|
813
|
-
logPrefix: 'ProtocolV2 WebUSB',
|
|
814
|
-
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
815
|
-
new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
816
|
-
});
|
|
817
|
-
this.protocolV2Sessions.set(path, session);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
|
|
821
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
822
|
-
try {
|
|
823
|
-
return await session.call(name, data, options);
|
|
824
|
-
} catch (error) {
|
|
825
|
-
const message =
|
|
826
|
-
error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
827
|
-
if (message.includes('protocol v2 read timeout') || message.includes('response timeout')) {
|
|
828
|
-
try {
|
|
829
|
-
await this.resetConnectionAfterProbe(path);
|
|
830
|
-
} catch (resetError) {
|
|
831
|
-
this.Log.debug('[WebUsbTransport] Protocol V2 timeout reset failed:', resetError);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
throw error;
|
|
835
|
-
} finally {
|
|
836
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
|
|
841
|
-
let assembler = this.protocolV2Assemblers.get(path);
|
|
842
|
-
if (!assembler) {
|
|
843
|
-
assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
844
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
|
|
848
|
-
const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
849
|
-
|
|
850
|
-
while (!frame) {
|
|
851
|
-
const cancelToken = { cancelled: false };
|
|
852
|
-
const transferIn = this.transferInWithRetry(path, PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken);
|
|
853
|
-
const dataView = deadline
|
|
854
|
-
? await this.withProtocolReadTimeout(
|
|
855
|
-
path,
|
|
856
|
-
transferIn,
|
|
857
|
-
Math.max(deadline - Date.now(), 1),
|
|
858
|
-
'V2',
|
|
859
|
-
() => {
|
|
860
|
-
cancelToken.cancelled = true;
|
|
861
|
-
}
|
|
862
|
-
)
|
|
863
|
-
: await transferIn;
|
|
864
|
-
const bytes = new Uint8Array(
|
|
865
|
-
this.toArrayBuffer(
|
|
866
|
-
dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
|
|
867
|
-
)
|
|
868
|
-
);
|
|
869
|
-
try {
|
|
870
|
-
frame = assembler.push(bytes);
|
|
871
|
-
} catch (error) {
|
|
872
|
-
throw ERRORS.TypedError(
|
|
873
|
-
HardwareErrorCode.NetworkError,
|
|
874
|
-
error instanceof Error ? error.message : String(error)
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
return frame;
|
|
819
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
879
820
|
}
|
|
880
821
|
|
|
881
822
|
/**
|
|
@@ -930,20 +871,63 @@ export default class WebUsbTransport {
|
|
|
930
871
|
* Release device
|
|
931
872
|
*/
|
|
932
873
|
async release(path: string) {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
936
|
-
await device.releaseInterface(ifaceNum);
|
|
937
|
-
await device.close();
|
|
874
|
+
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
875
|
+
await this.closeOpenDevice(path);
|
|
938
876
|
this.deviceProtocol.delete(path);
|
|
939
877
|
this.deviceProtocolHints.delete(path);
|
|
940
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
941
|
-
this.protocolV2Assemblers.delete(path);
|
|
942
|
-
this.protocolV2Sessions.delete(path);
|
|
943
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
944
878
|
this.deviceEndpoints.delete(path);
|
|
945
879
|
}
|
|
946
880
|
|
|
881
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
882
|
+
if (!this.messages || !this.messagesV2) {
|
|
883
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
protocolV1: this.messages,
|
|
887
|
+
protocolV2: this.messagesV2,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
protected getProtocolV2UsbLogger() {
|
|
892
|
+
return this.Log;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
protected async writeProtocolV2UsbPacket(
|
|
896
|
+
path: string,
|
|
897
|
+
frame: Uint8Array,
|
|
898
|
+
_context: ProtocolV2CallContext
|
|
899
|
+
): Promise<void> {
|
|
900
|
+
await this.transferOutOnce(path, frame);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
protected async readProtocolV2UsbPacket(
|
|
904
|
+
path: string,
|
|
905
|
+
_context: ProtocolV2CallContext
|
|
906
|
+
): Promise<Uint8Array> {
|
|
907
|
+
const dataView = await this.transferInOnce(path, PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
908
|
+
return new Uint8Array(
|
|
909
|
+
this.toArrayBuffer(
|
|
910
|
+
dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
|
|
911
|
+
)
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
916
|
+
await this.closeOpenDevice(path);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
920
|
+
this.deviceProtocol.delete(path);
|
|
921
|
+
this.Log?.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
925
|
+
return new ProtocolV2LinkError(
|
|
926
|
+
'response-timeout',
|
|
927
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
947
931
|
/**
|
|
948
932
|
* Expose the detected protocol type for a given device path.
|
|
949
933
|
* Used by upper layers (e.g. TransportManager) to select the correct schema.
|