@onekeyfe/hd-transport-web-device 1.2.0-alpha.12 → 1.2.0-alpha.121
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 +373 -22
- package/__tests__/webusb-protocol-v2-timeout.test.ts +386 -0
- 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 +15 -3
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +109 -12
- package/dist/index.js +344 -223
- 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 +231 -85
- package/src/transportLog.ts +1 -11
- package/src/webusb.ts +182 -181
package/src/webusb.ts
CHANGED
|
@@ -6,19 +6,25 @@ 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
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ERRORS,
|
|
15
|
+
HardwareErrorCode,
|
|
16
|
+
ONEKEY_WEBUSB_FILTER,
|
|
17
|
+
isKnownTrezorWebUsbDevice,
|
|
18
|
+
wait,
|
|
19
|
+
} from '@onekeyfe/hd-shared';
|
|
14
20
|
import ByteBuffer from 'bytebuffer';
|
|
15
21
|
|
|
16
|
-
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
17
|
-
|
|
18
22
|
import type {
|
|
19
23
|
AcquireInput,
|
|
20
24
|
OneKeyDeviceInfoBase,
|
|
21
25
|
ProtocolType,
|
|
26
|
+
ProtocolV2CallContext,
|
|
27
|
+
ProtocolV2Schemas,
|
|
22
28
|
TransportCallOptions,
|
|
23
29
|
} from '@onekeyfe/hd-transport';
|
|
24
30
|
|
|
@@ -33,7 +39,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
|
|
|
33
39
|
const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
34
40
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
35
41
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
36
|
-
|
|
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;
|
|
37
48
|
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
38
49
|
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
39
50
|
}
|
|
@@ -58,38 +69,22 @@ interface TransferCancelToken {
|
|
|
58
69
|
cancelled: boolean;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
|
-
export default class WebUsbTransport {
|
|
72
|
+
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
62
73
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
63
74
|
|
|
64
75
|
/** Protobuf schema for Protocol V2 transports. */
|
|
65
76
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
66
77
|
|
|
78
|
+
private protocolV2SchemaSource: string | undefined;
|
|
79
|
+
|
|
67
80
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
68
81
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
69
82
|
|
|
70
83
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
71
84
|
|
|
72
|
-
/** 按设备缓存 Protocol V2 frame assembler,保留同一次读取里多出来的后续 frame。 */
|
|
73
|
-
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
74
|
-
|
|
75
|
-
/** 按设备缓存 Protocol V2 session,保持 seq 与设备会话一致递增。 */
|
|
76
|
-
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
77
|
-
|
|
78
|
-
/** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */
|
|
79
|
-
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
80
|
-
|
|
81
85
|
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
82
86
|
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
83
87
|
|
|
84
|
-
/**
|
|
85
|
-
* 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。
|
|
86
|
-
* 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path),
|
|
87
|
-
* 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
|
|
88
|
-
*/
|
|
89
|
-
private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
|
|
90
|
-
|
|
91
|
-
private mockSerialCounter = 0;
|
|
92
|
-
|
|
93
88
|
name = 'WebUsbTransport';
|
|
94
89
|
|
|
95
90
|
stopped = false;
|
|
@@ -112,6 +107,14 @@ export default class WebUsbTransport {
|
|
|
112
107
|
|
|
113
108
|
interfaceId = INTERFACE_ID;
|
|
114
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
|
+
|
|
115
118
|
/**
|
|
116
119
|
* Initialize WebUSB transport
|
|
117
120
|
*/
|
|
@@ -141,9 +144,20 @@ export default class WebUsbTransport {
|
|
|
141
144
|
* Cache the Protocol V2 protobuf schema.
|
|
142
145
|
*/
|
|
143
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;
|
|
144
154
|
this.messagesV2 = parseConfigure(signedData);
|
|
145
|
-
this.
|
|
146
|
-
|
|
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
|
+
);
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
/**
|
|
@@ -175,26 +189,6 @@ export default class WebUsbTransport {
|
|
|
175
189
|
return this.deviceList;
|
|
176
190
|
}
|
|
177
191
|
|
|
178
|
-
/**
|
|
179
|
-
* 设备 path:正常设备直接用 USB serial number;
|
|
180
|
-
* 空 serial(早期工程板)回退到会话内稳定的 mock path。
|
|
181
|
-
*/
|
|
182
|
-
private getDevicePath(device: USBDevice): string {
|
|
183
|
-
if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
|
|
184
|
-
return device.serialNumber;
|
|
185
|
-
}
|
|
186
|
-
let path = this.mockSerialPaths.get(device);
|
|
187
|
-
if (!path) {
|
|
188
|
-
this.mockSerialCounter += 1;
|
|
189
|
-
path = `mock-serial:${device.vendorId.toString(16)}:${device.productId.toString(16)}:${
|
|
190
|
-
this.mockSerialCounter
|
|
191
|
-
}`;
|
|
192
|
-
this.mockSerialPaths.set(device, path);
|
|
193
|
-
this.Log?.debug(`[WebUSB] device has no serial number, using mock path: ${path}`);
|
|
194
|
-
}
|
|
195
|
-
return path;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
192
|
/**
|
|
199
193
|
* Get list of connected devices
|
|
200
194
|
*/
|
|
@@ -202,14 +196,17 @@ export default class WebUsbTransport {
|
|
|
202
196
|
if (!this.usb) return [];
|
|
203
197
|
|
|
204
198
|
const devices = await this.usb.getDevices();
|
|
205
|
-
const onekeyDevices = devices.filter(dev =>
|
|
206
|
-
ONEKEY_WEBUSB_FILTER.some(
|
|
207
|
-
desc
|
|
208
|
-
|
|
209
|
-
|
|
199
|
+
const onekeyDevices = devices.filter(dev => {
|
|
200
|
+
const isOneKey = ONEKEY_WEBUSB_FILTER.some(
|
|
201
|
+
(desc: { vendorId: number; productId: number }) =>
|
|
202
|
+
dev.vendorId === desc.vendorId && dev.productId === desc.productId
|
|
203
|
+
);
|
|
204
|
+
const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0;
|
|
205
|
+
return isOneKey && hasSerialNumber && !isKnownTrezorWebUsbDevice(dev);
|
|
206
|
+
});
|
|
210
207
|
|
|
211
208
|
this.deviceList = onekeyDevices.map(device => {
|
|
212
|
-
const path =
|
|
209
|
+
const path = device.serialNumber as string;
|
|
213
210
|
const protocolHint = inferProtocolHintFromDeviceName(device.productName);
|
|
214
211
|
if (protocolHint) {
|
|
215
212
|
this.deviceProtocolHints.set(path, protocolHint);
|
|
@@ -231,13 +228,16 @@ export default class WebUsbTransport {
|
|
|
231
228
|
async acquire(input: AcquireInput) {
|
|
232
229
|
if (!input.path) return;
|
|
233
230
|
try {
|
|
231
|
+
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
234
232
|
await this.closeOpenDevice(input.path);
|
|
235
233
|
await this.connect(input.path ?? '', true);
|
|
236
234
|
const deviceName = this.deviceList.find(device => device.path === input.path)?.device
|
|
237
235
|
.productName;
|
|
238
236
|
const protocolHint = input.expectedProtocol
|
|
239
237
|
? undefined
|
|
240
|
-
:
|
|
238
|
+
: input.protocolHint ??
|
|
239
|
+
this.deviceProtocolHints.get(input.path) ??
|
|
240
|
+
inferProtocolHintFromDeviceName(deviceName);
|
|
241
241
|
if (protocolHint) {
|
|
242
242
|
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
243
243
|
}
|
|
@@ -245,6 +245,7 @@ export default class WebUsbTransport {
|
|
|
245
245
|
return await Promise.resolve(input.path);
|
|
246
246
|
} catch (e) {
|
|
247
247
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
248
|
+
await this.closeOpenDevice(input.path);
|
|
248
249
|
throw e;
|
|
249
250
|
}
|
|
250
251
|
}
|
|
@@ -261,13 +262,17 @@ export default class WebUsbTransport {
|
|
|
261
262
|
);
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
private
|
|
265
|
+
private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
|
|
265
266
|
return ERRORS.TypedError(
|
|
266
|
-
HardwareErrorCode.
|
|
267
|
-
|
|
267
|
+
HardwareErrorCode.DeviceInitializeFailed,
|
|
268
|
+
`Protocol ${expected} probe timeout after ${attempts} attempts`
|
|
268
269
|
);
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
private createProtocolDetectionError() {
|
|
273
|
+
return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
274
|
+
}
|
|
275
|
+
|
|
271
276
|
private async detectProtocol(
|
|
272
277
|
path: string,
|
|
273
278
|
expectedProtocol?: ProtocolType,
|
|
@@ -278,29 +283,54 @@ export default class WebUsbTransport {
|
|
|
278
283
|
this.deviceProtocol.set(path, 'V1');
|
|
279
284
|
return 'V1';
|
|
280
285
|
}
|
|
286
|
+
await this.closeConnectionAfterProbe(path);
|
|
281
287
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
if (expectedProtocol === 'V2') {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
+
);
|
|
289
312
|
}
|
|
290
313
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// 不能作为最终结论。
|
|
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.
|
|
294
316
|
const probeOrder: ProtocolType[] =
|
|
295
317
|
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
296
318
|
|
|
297
|
-
for (const protocol of probeOrder) {
|
|
319
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
298
320
|
const detected =
|
|
299
321
|
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
300
322
|
if (detected) {
|
|
301
323
|
this.deviceProtocol.set(path, protocol);
|
|
302
324
|
return protocol;
|
|
303
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
|
+
}
|
|
304
334
|
}
|
|
305
335
|
|
|
306
336
|
this.deviceProtocol.delete(path);
|
|
@@ -412,16 +442,12 @@ export default class WebUsbTransport {
|
|
|
412
442
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
413
443
|
const endpoints = this.discoverEndpoints(device);
|
|
414
444
|
this.deviceEndpoints.set(path, endpoints);
|
|
415
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
416
|
-
this.protocolV2Assemblers.set(path, new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES));
|
|
417
|
-
|
|
418
445
|
await device.claimInterface(endpoints.interfaceNumber);
|
|
419
446
|
await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
|
|
420
447
|
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
421
448
|
}
|
|
422
449
|
|
|
423
450
|
private async closeOpenDevice(path: string) {
|
|
424
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
425
451
|
const current = this.deviceList.find(device => device.path === path)?.device;
|
|
426
452
|
if (!current?.opened) return;
|
|
427
453
|
|
|
@@ -616,33 +642,30 @@ export default class WebUsbTransport {
|
|
|
616
642
|
throw lastError;
|
|
617
643
|
}
|
|
618
644
|
|
|
619
|
-
private async
|
|
620
|
-
this.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
try {
|
|
625
|
-
const device = await this.findDevice(path);
|
|
626
|
-
if (device.opened) {
|
|
627
|
-
const endpoints = this.deviceEndpoints.get(path);
|
|
628
|
-
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
629
|
-
try {
|
|
630
|
-
await device.releaseInterface(ifaceNum);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error);
|
|
633
|
-
}
|
|
634
|
-
await device.close();
|
|
635
|
-
}
|
|
636
|
-
} catch (error) {
|
|
637
|
-
this.Log.debug('[WebUsbTransport] close after protocol probe error:', error);
|
|
645
|
+
private async transferInOnce(path: string, length: number): Promise<DataView> {
|
|
646
|
+
const device = await this.findDevice(path);
|
|
647
|
+
if (!device.opened) {
|
|
648
|
+
throw new Error('USBDevice is not open for transferIn');
|
|
638
649
|
}
|
|
650
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
651
|
+
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
652
|
+
const result = await device.transferIn(endpointIn, length);
|
|
653
|
+
return this.getTransferInData(result);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private async closeConnectionAfterProbe(path: string) {
|
|
657
|
+
await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
|
|
658
|
+
await this.closeOpenDevice(path);
|
|
659
|
+
}
|
|
639
660
|
|
|
661
|
+
private async resetConnectionAfterProbe(path: string) {
|
|
662
|
+
await this.closeConnectionAfterProbe(path);
|
|
640
663
|
await this.getConnectedDevices();
|
|
641
664
|
await this.connect(path, false);
|
|
642
665
|
}
|
|
643
666
|
|
|
644
667
|
private async withProtocolReadTimeout<T>(
|
|
645
|
-
|
|
668
|
+
path: string,
|
|
646
669
|
promise: Promise<T>,
|
|
647
670
|
timeoutMs: number,
|
|
648
671
|
protocol: ProtocolType,
|
|
@@ -664,9 +687,16 @@ export default class WebUsbTransport {
|
|
|
664
687
|
return await Promise.race([
|
|
665
688
|
guardedPromise,
|
|
666
689
|
new Promise<never>((_, reject) => {
|
|
667
|
-
timer = setTimeout(() => {
|
|
690
|
+
timer = setTimeout(async () => {
|
|
668
691
|
timedOut = true;
|
|
669
692
|
onTimeout?.();
|
|
693
|
+
if (protocol === 'V1') {
|
|
694
|
+
try {
|
|
695
|
+
await this.resetConnectionAfterProbe(path);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
670
700
|
reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
|
|
671
701
|
}, timeoutMs);
|
|
672
702
|
}),
|
|
@@ -682,7 +712,14 @@ export default class WebUsbTransport {
|
|
|
682
712
|
}
|
|
683
713
|
|
|
684
714
|
try {
|
|
685
|
-
await this.callProtocolV1(
|
|
715
|
+
await this.callProtocolV1(
|
|
716
|
+
path,
|
|
717
|
+
'Initialize',
|
|
718
|
+
{},
|
|
719
|
+
{
|
|
720
|
+
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
721
|
+
}
|
|
722
|
+
);
|
|
686
723
|
return true;
|
|
687
724
|
} catch (_error) {
|
|
688
725
|
return false;
|
|
@@ -696,10 +733,9 @@ export default class WebUsbTransport {
|
|
|
696
733
|
|
|
697
734
|
return probeProtocolV2Helper({
|
|
698
735
|
call: (name, data, options) => this.callProtocolV2(path, name, data, options),
|
|
699
|
-
timeoutMs:
|
|
736
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
|
|
700
737
|
logger: this.Log,
|
|
701
738
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
702
|
-
onProbeFailed: () => this.resetConnectionAfterProbe(path),
|
|
703
739
|
});
|
|
704
740
|
}
|
|
705
741
|
|
|
@@ -729,10 +765,6 @@ export default class WebUsbTransport {
|
|
|
729
765
|
);
|
|
730
766
|
}
|
|
731
767
|
|
|
732
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
733
|
-
this.Log.debug('transport call', createTransportCallLog(name, protocol));
|
|
734
|
-
}
|
|
735
|
-
|
|
736
768
|
if (protocol === 'V2') {
|
|
737
769
|
return this.callProtocolV2(path, name, data, options);
|
|
738
770
|
}
|
|
@@ -780,83 +812,7 @@ export default class WebUsbTransport {
|
|
|
780
812
|
data: Record<string, unknown>,
|
|
781
813
|
options?: TransportCallOptions
|
|
782
814
|
) {
|
|
783
|
-
|
|
784
|
-
if (!this.messagesV2) {
|
|
785
|
-
throw ERRORS.TypedError(
|
|
786
|
-
HardwareErrorCode.TransportNotConfigured,
|
|
787
|
-
'Protocol V2 schema not configured'
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
if (!protocolV1Messages) {
|
|
791
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
let session = this.protocolV2Sessions.get(path);
|
|
795
|
-
if (!session) {
|
|
796
|
-
session = new ProtocolV2Session({
|
|
797
|
-
schemas: {
|
|
798
|
-
protocolV1: protocolV1Messages,
|
|
799
|
-
protocolV2: this.messagesV2,
|
|
800
|
-
},
|
|
801
|
-
router: PROTOCOL_V2_CHANNEL_USB,
|
|
802
|
-
writeFrame: (frame: Uint8Array) => this.transferOutOnce(path, frame),
|
|
803
|
-
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
804
|
-
logger: this.Log,
|
|
805
|
-
logPrefix: 'ProtocolV2 WebUSB',
|
|
806
|
-
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
807
|
-
new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
808
|
-
});
|
|
809
|
-
this.protocolV2Sessions.set(path, session);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
|
|
813
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
814
|
-
try {
|
|
815
|
-
return await session.call(name, data, options);
|
|
816
|
-
} finally {
|
|
817
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
|
|
822
|
-
let assembler = this.protocolV2Assemblers.get(path);
|
|
823
|
-
if (!assembler) {
|
|
824
|
-
assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
825
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
|
|
829
|
-
const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
830
|
-
|
|
831
|
-
while (!frame) {
|
|
832
|
-
const cancelToken = { cancelled: false };
|
|
833
|
-
const transferIn = this.transferInWithRetry(path, PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken);
|
|
834
|
-
const dataView = deadline
|
|
835
|
-
? await this.withProtocolReadTimeout(
|
|
836
|
-
path,
|
|
837
|
-
transferIn,
|
|
838
|
-
Math.max(deadline - Date.now(), 1),
|
|
839
|
-
'V2',
|
|
840
|
-
() => {
|
|
841
|
-
cancelToken.cancelled = true;
|
|
842
|
-
}
|
|
843
|
-
)
|
|
844
|
-
: await transferIn;
|
|
845
|
-
const bytes = new Uint8Array(
|
|
846
|
-
this.toArrayBuffer(
|
|
847
|
-
dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
|
|
848
|
-
)
|
|
849
|
-
);
|
|
850
|
-
try {
|
|
851
|
-
frame = assembler.push(bytes);
|
|
852
|
-
} catch (error) {
|
|
853
|
-
throw ERRORS.TypedError(
|
|
854
|
-
HardwareErrorCode.NetworkError,
|
|
855
|
-
error instanceof Error ? error.message : String(error)
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
return frame;
|
|
815
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
860
816
|
}
|
|
861
817
|
|
|
862
818
|
/**
|
|
@@ -911,18 +867,63 @@ export default class WebUsbTransport {
|
|
|
911
867
|
* Release device
|
|
912
868
|
*/
|
|
913
869
|
async release(path: string) {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
917
|
-
await device.releaseInterface(ifaceNum);
|
|
918
|
-
await device.close();
|
|
870
|
+
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
871
|
+
await this.closeOpenDevice(path);
|
|
919
872
|
this.deviceProtocol.delete(path);
|
|
920
873
|
this.deviceProtocolHints.delete(path);
|
|
921
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
922
|
-
this.protocolV2Assemblers.delete(path);
|
|
923
874
|
this.deviceEndpoints.delete(path);
|
|
924
875
|
}
|
|
925
876
|
|
|
877
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
878
|
+
if (!this.messages || !this.messagesV2) {
|
|
879
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
880
|
+
}
|
|
881
|
+
return {
|
|
882
|
+
protocolV1: this.messages,
|
|
883
|
+
protocolV2: this.messagesV2,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
protected getProtocolV2UsbLogger() {
|
|
888
|
+
return this.Log;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
protected async writeProtocolV2UsbPacket(
|
|
892
|
+
path: string,
|
|
893
|
+
frame: Uint8Array,
|
|
894
|
+
_context: ProtocolV2CallContext
|
|
895
|
+
): Promise<void> {
|
|
896
|
+
await this.transferOutOnce(path, frame);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
protected async readProtocolV2UsbPacket(
|
|
900
|
+
path: string,
|
|
901
|
+
_context: ProtocolV2CallContext
|
|
902
|
+
): Promise<Uint8Array> {
|
|
903
|
+
const dataView = await this.transferInOnce(path, PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
904
|
+
return new Uint8Array(
|
|
905
|
+
this.toArrayBuffer(
|
|
906
|
+
dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
|
|
907
|
+
)
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
912
|
+
await this.closeOpenDevice(path);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
916
|
+
this.deviceProtocol.delete(path);
|
|
917
|
+
this.Log?.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
921
|
+
return new ProtocolV2LinkError(
|
|
922
|
+
'response-timeout',
|
|
923
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
926
927
|
/**
|
|
927
928
|
* Expose the detected protocol type for a given device path.
|
|
928
929
|
* Used by upper layers (e.g. TransportManager) to select the correct schema.
|