@onekeyfe/hd-transport-web-device 1.1.27-patch.1 → 1.2.0-alpha.0
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 +231 -0
- package/dist/electron-ble-transport.d.ts +50 -13
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +80 -17
- package/dist/index.js +927 -115
- package/dist/webusb.d.ts +29 -3
- package/dist/webusb.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/electron-ble-transport.ts +612 -133
- package/src/webusb.ts +595 -47
package/src/webusb.ts
CHANGED
|
@@ -1,19 +1,57 @@
|
|
|
1
1
|
/* eslint-disable no-undef */
|
|
2
|
-
import transport, {
|
|
2
|
+
import transport, {
|
|
3
|
+
LogBlockCommand,
|
|
4
|
+
PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
|
|
5
|
+
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
6
|
+
PROTOCOL_V1_REPORT_ID,
|
|
7
|
+
PROTOCOL_V1_USB_PACKET_SIZE,
|
|
8
|
+
PROTOCOL_V2_CHANNEL_USB,
|
|
9
|
+
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
10
|
+
ProtocolV2FrameAssembler,
|
|
11
|
+
ProtocolV2Session,
|
|
12
|
+
probeProtocolV2 as probeProtocolV2Helper,
|
|
13
|
+
} from '@onekeyfe/hd-transport';
|
|
3
14
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
4
15
|
import ByteBuffer from 'bytebuffer';
|
|
5
16
|
|
|
6
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
AcquireInput,
|
|
19
|
+
OneKeyDeviceInfoBase,
|
|
20
|
+
ProtocolType,
|
|
21
|
+
TransportCallOptions,
|
|
22
|
+
} from '@onekeyfe/hd-transport';
|
|
7
23
|
|
|
8
|
-
const { parseConfigure,
|
|
24
|
+
const { parseConfigure, check, ProtocolV1 } = transport;
|
|
9
25
|
|
|
10
26
|
const CONFIGURATION_ID = 1;
|
|
11
27
|
const INTERFACE_ID = 0;
|
|
12
28
|
const ENDPOINT_ID = 1;
|
|
13
|
-
const PACKET_SIZE =
|
|
14
|
-
const
|
|
29
|
+
const PACKET_SIZE = PROTOCOL_V1_USB_PACKET_SIZE;
|
|
30
|
+
const PAYLOAD_SIZE = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE;
|
|
31
|
+
const REPORT_ID = PROTOCOL_V1_REPORT_ID;
|
|
32
|
+
const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
15
33
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
16
34
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
35
|
+
const PROTOCOL_PROBE_TIMEOUT = 1000;
|
|
36
|
+
const WEBUSB_FILE_WRITE_LOG_BLOCK_PATTERN = /(?:^|[^a-z])(?:raw)?(?:filesystem|emmc)?filewrite$/i;
|
|
37
|
+
|
|
38
|
+
function shouldSuppressWebUsbCallLog(name: string) {
|
|
39
|
+
const normalized = name.replace(/[_\s-]/g, '');
|
|
40
|
+
return WEBUSB_FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLogBlockCommand(name: string) {
|
|
44
|
+
return (LogBlockCommand as Set<string> | undefined)?.has?.(name) ?? false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function shouldBlockWebUsbCallDataLog(name: string) {
|
|
48
|
+
const normalized = name.replace(/[_\s-]/g, '');
|
|
49
|
+
return isLogBlockCommand(name) || WEBUSB_FILE_WRITE_LOG_BLOCK_PATTERN.test(normalized);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
53
|
+
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
54
|
+
}
|
|
17
55
|
|
|
18
56
|
/**
|
|
19
57
|
* Device information with path and WebUSB device instance
|
|
@@ -21,11 +59,52 @@ const PACKET_IO_RETRY_DELAY = 300;
|
|
|
21
59
|
export interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
22
60
|
path: string;
|
|
23
61
|
device: USBDevice;
|
|
62
|
+
protocolType?: ProtocolType;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** USB endpoint pair discovered at connect time */
|
|
66
|
+
interface DeviceEndpoints {
|
|
67
|
+
interfaceNumber: number;
|
|
68
|
+
endpointIn: number;
|
|
69
|
+
endpointOut: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface TransferCancelToken {
|
|
73
|
+
cancelled: boolean;
|
|
24
74
|
}
|
|
25
75
|
|
|
26
76
|
export default class WebUsbTransport {
|
|
27
77
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
28
78
|
|
|
79
|
+
/** Protobuf schema for Protocol V2 transports. */
|
|
80
|
+
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
81
|
+
|
|
82
|
+
/** Per-path protocol type detected by active wire-level probe. */
|
|
83
|
+
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
84
|
+
|
|
85
|
+
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
86
|
+
|
|
87
|
+
/** 按设备缓存 Protocol V2 frame assembler,保留同一次读取里多出来的后续 frame。 */
|
|
88
|
+
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
89
|
+
|
|
90
|
+
/** 按设备缓存 Protocol V2 session,保持 seq 与设备会话一致递增。 */
|
|
91
|
+
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
92
|
+
|
|
93
|
+
/** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */
|
|
94
|
+
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
95
|
+
|
|
96
|
+
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
97
|
+
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。
|
|
101
|
+
* 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path),
|
|
102
|
+
* 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
|
|
103
|
+
*/
|
|
104
|
+
private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
|
|
105
|
+
|
|
106
|
+
private mockSerialCounter = 0;
|
|
107
|
+
|
|
29
108
|
name = 'WebUsbTransport';
|
|
30
109
|
|
|
31
110
|
stopped = false;
|
|
@@ -65,7 +144,7 @@ export default class WebUsbTransport {
|
|
|
65
144
|
}
|
|
66
145
|
|
|
67
146
|
/**
|
|
68
|
-
* Configure
|
|
147
|
+
* Configure Protocol V1 protobuf schema (legacy chunked 0x3F framing).
|
|
69
148
|
*/
|
|
70
149
|
configure(signedData: any) {
|
|
71
150
|
const messages = parseConfigure(signedData);
|
|
@@ -73,6 +152,16 @@ export default class WebUsbTransport {
|
|
|
73
152
|
this.messages = messages;
|
|
74
153
|
}
|
|
75
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Cache the Protocol V2 protobuf schema.
|
|
157
|
+
*/
|
|
158
|
+
configureProtocolV2(signedData: any) {
|
|
159
|
+
this.messagesV2 = parseConfigure(signedData);
|
|
160
|
+
this.protocolV2Sessions.clear();
|
|
161
|
+
this.protocolV2ReadTimeouts.clear();
|
|
162
|
+
this.Log?.debug('[WebUsbTransport] Protocol V2 schema configured');
|
|
163
|
+
}
|
|
164
|
+
|
|
76
165
|
/**
|
|
77
166
|
* Request user to select a device
|
|
78
167
|
* This method must be called in response to a user action
|
|
@@ -102,6 +191,26 @@ export default class WebUsbTransport {
|
|
|
102
191
|
return this.deviceList;
|
|
103
192
|
}
|
|
104
193
|
|
|
194
|
+
/**
|
|
195
|
+
* 设备 path:正常设备直接用 USB serial number;
|
|
196
|
+
* 空 serial(早期工程板)回退到会话内稳定的 mock path。
|
|
197
|
+
*/
|
|
198
|
+
private getDevicePath(device: USBDevice): string {
|
|
199
|
+
if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
|
|
200
|
+
return device.serialNumber;
|
|
201
|
+
}
|
|
202
|
+
let path = this.mockSerialPaths.get(device);
|
|
203
|
+
if (!path) {
|
|
204
|
+
this.mockSerialCounter += 1;
|
|
205
|
+
path = `mock-serial:${device.vendorId.toString(16)}:${device.productId.toString(16)}:${
|
|
206
|
+
this.mockSerialCounter
|
|
207
|
+
}`;
|
|
208
|
+
this.mockSerialPaths.set(device, path);
|
|
209
|
+
this.Log?.debug(`[WebUSB] device has no serial number, using mock path: ${path}`);
|
|
210
|
+
}
|
|
211
|
+
return path;
|
|
212
|
+
}
|
|
213
|
+
|
|
105
214
|
/**
|
|
106
215
|
* Get list of connected devices
|
|
107
216
|
*/
|
|
@@ -109,19 +218,33 @@ export default class WebUsbTransport {
|
|
|
109
218
|
if (!this.usb) return [];
|
|
110
219
|
|
|
111
220
|
const devices = await this.usb.getDevices();
|
|
112
|
-
const onekeyDevices = devices.filter(dev =>
|
|
113
|
-
|
|
221
|
+
const onekeyDevices = devices.filter(dev =>
|
|
222
|
+
ONEKEY_WEBUSB_FILTER.some(
|
|
114
223
|
desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
224
|
+
)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
this.deviceList = onekeyDevices.map(device => {
|
|
228
|
+
const path = this.getDevicePath(device);
|
|
229
|
+
const protocolHint = inferProtocolHintFromDeviceName(device.productName);
|
|
230
|
+
if (protocolHint) {
|
|
231
|
+
this.deviceProtocolHints.set(path, protocolHint);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
path,
|
|
236
|
+
device,
|
|
237
|
+
commType: 'webusb',
|
|
238
|
+
};
|
|
118
239
|
});
|
|
119
240
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
241
|
+
// Debug: log all discovered devices. Protocol is detected after acquire via wire probe.
|
|
242
|
+
for (const dev of onekeyDevices) {
|
|
243
|
+
this.Log.debug(
|
|
244
|
+
`[WebUSB] Device: name="${dev.productName}" serial="${dev.serialNumber}" ` +
|
|
245
|
+
`VID=0x${dev.vendorId.toString(16)} PID=0x${dev.productId.toString(16)}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
125
248
|
|
|
126
249
|
return this.deviceList;
|
|
127
250
|
}
|
|
@@ -132,7 +255,17 @@ export default class WebUsbTransport {
|
|
|
132
255
|
async acquire(input: AcquireInput) {
|
|
133
256
|
if (!input.path) return;
|
|
134
257
|
try {
|
|
258
|
+
await this.closeOpenDevice(input.path);
|
|
135
259
|
await this.connect(input.path ?? '', true);
|
|
260
|
+
const deviceName = this.deviceList.find(device => device.path === input.path)?.device
|
|
261
|
+
.productName;
|
|
262
|
+
const protocolHint = input.expectedProtocol
|
|
263
|
+
? undefined
|
|
264
|
+
: this.deviceProtocolHints.get(input.path) ?? inferProtocolHintFromDeviceName(deviceName);
|
|
265
|
+
if (protocolHint) {
|
|
266
|
+
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
267
|
+
}
|
|
268
|
+
await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
|
|
136
269
|
return await Promise.resolve(input.path);
|
|
137
270
|
} catch (e) {
|
|
138
271
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
@@ -140,6 +273,67 @@ export default class WebUsbTransport {
|
|
|
140
273
|
}
|
|
141
274
|
}
|
|
142
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Determine protocol type after connect.
|
|
278
|
+
* Probe Protocol V1 first with Initialize. If it does not answer in time,
|
|
279
|
+
* fall back to a Protocol V2 Ping probe.
|
|
280
|
+
*/
|
|
281
|
+
private createProtocolMismatchError(expected: ProtocolType) {
|
|
282
|
+
return ERRORS.TypedError(
|
|
283
|
+
HardwareErrorCode.RuntimeError,
|
|
284
|
+
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private createProtocolDetectionError() {
|
|
289
|
+
return ERRORS.TypedError(
|
|
290
|
+
HardwareErrorCode.RuntimeError,
|
|
291
|
+
'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private async detectProtocol(
|
|
296
|
+
path: string,
|
|
297
|
+
expectedProtocol?: ProtocolType,
|
|
298
|
+
protocolHint?: ProtocolType
|
|
299
|
+
): Promise<ProtocolType> {
|
|
300
|
+
if (expectedProtocol === 'V1') {
|
|
301
|
+
if (await this.probeProtocolV1(path)) {
|
|
302
|
+
this.deviceProtocol.set(path, 'V1');
|
|
303
|
+
this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
304
|
+
return 'V1';
|
|
305
|
+
}
|
|
306
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (expectedProtocol === 'V2') {
|
|
310
|
+
// 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
|
|
311
|
+
// 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
|
|
312
|
+
this.deviceProtocol.set(path, 'V2');
|
|
313
|
+
this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
314
|
+
return 'V2';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
|
|
318
|
+
// 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
|
|
319
|
+
// 不能作为最终结论。
|
|
320
|
+
const probeOrder: ProtocolType[] =
|
|
321
|
+
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
322
|
+
|
|
323
|
+
for (const protocol of probeOrder) {
|
|
324
|
+
const detected =
|
|
325
|
+
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
326
|
+
if (detected) {
|
|
327
|
+
this.deviceProtocol.set(path, protocol);
|
|
328
|
+
this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
|
|
329
|
+
return protocol;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.deviceProtocol.delete(path);
|
|
334
|
+
throw this.createProtocolDetectionError();
|
|
335
|
+
}
|
|
336
|
+
|
|
143
337
|
/**
|
|
144
338
|
* Find device by path
|
|
145
339
|
*/
|
|
@@ -182,22 +376,116 @@ export default class WebUsbTransport {
|
|
|
182
376
|
}
|
|
183
377
|
|
|
184
378
|
/**
|
|
185
|
-
*
|
|
379
|
+
* Discover vendor-class (0xFF) interface and its IN/OUT endpoint numbers from USB descriptors.
|
|
380
|
+
* Falls back to legacy hardcoded values if no vendor interface is found.
|
|
381
|
+
*/
|
|
382
|
+
private discoverEndpoints(device: USBDevice): DeviceEndpoints {
|
|
383
|
+
for (const config of device.configurations) {
|
|
384
|
+
for (const iface of config.interfaces) {
|
|
385
|
+
for (const alt of iface.alternates) {
|
|
386
|
+
if (alt.interfaceClass === 0xff) {
|
|
387
|
+
let endpointIn = this.endpointId;
|
|
388
|
+
let endpointOut = this.endpointId;
|
|
389
|
+
for (const ep of alt.endpoints) {
|
|
390
|
+
if (ep.direction === 'in') endpointIn = ep.endpointNumber;
|
|
391
|
+
else endpointOut = ep.endpointNumber;
|
|
392
|
+
}
|
|
393
|
+
this.Log?.debug(
|
|
394
|
+
`[WebUsbTransport] discovered vendor interface ${iface.interfaceNumber}, ` +
|
|
395
|
+
`endpointIn=${endpointIn}, endpointOut=${endpointOut}`
|
|
396
|
+
);
|
|
397
|
+
return { interfaceNumber: iface.interfaceNumber, endpointIn, endpointOut };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Fallback: legacy hardcoded values
|
|
403
|
+
this.Log?.debug('[WebUsbTransport] no vendor interface found, using defaults');
|
|
404
|
+
return {
|
|
405
|
+
interfaceNumber: this.interfaceId,
|
|
406
|
+
endpointIn: this.endpointId,
|
|
407
|
+
endpointOut: this.endpointId,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Connect to specific device.
|
|
413
|
+
* Discovers interface/endpoint numbers from USB descriptors on first connection.
|
|
186
414
|
*/
|
|
187
415
|
async connectToDevice(path: string, first: boolean) {
|
|
188
|
-
|
|
189
|
-
|
|
416
|
+
let device: USBDevice = await this.findDevice(path);
|
|
417
|
+
this.Log.debug(
|
|
418
|
+
'[WebUsbTransport] connecting to device:',
|
|
419
|
+
device.productName,
|
|
420
|
+
'PID:',
|
|
421
|
+
device.productId
|
|
422
|
+
);
|
|
190
423
|
|
|
191
|
-
if (
|
|
424
|
+
if (!device.opened) {
|
|
425
|
+
await device.open();
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
await device.reset();
|
|
429
|
+
} catch (error) {
|
|
430
|
+
this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error);
|
|
431
|
+
}
|
|
432
|
+
await this.getConnectedDevices();
|
|
433
|
+
device = await this.findDevice(path);
|
|
434
|
+
if (!device.opened) {
|
|
435
|
+
await device.open();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (
|
|
439
|
+
first ||
|
|
440
|
+
!device.configuration ||
|
|
441
|
+
device.configuration.configurationValue !== this.configurationId
|
|
442
|
+
) {
|
|
192
443
|
await device.selectConfiguration(this.configurationId);
|
|
193
|
-
try {
|
|
194
|
-
await device.reset();
|
|
195
|
-
} catch (error) {
|
|
196
|
-
// Ignore reset errors
|
|
197
|
-
}
|
|
198
444
|
}
|
|
199
445
|
|
|
200
|
-
|
|
446
|
+
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
447
|
+
const endpoints = this.discoverEndpoints(device);
|
|
448
|
+
this.deviceEndpoints.set(path, endpoints);
|
|
449
|
+
this.protocolV2Assemblers.get(path)?.reset();
|
|
450
|
+
this.protocolV2Assemblers.set(path, new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES));
|
|
451
|
+
|
|
452
|
+
await device.claimInterface(endpoints.interfaceNumber);
|
|
453
|
+
await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
|
|
454
|
+
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async closeOpenDevice(path: string) {
|
|
458
|
+
this.protocolV2Assemblers.get(path)?.reset();
|
|
459
|
+
const current = this.deviceList.find(device => device.path === path)?.device;
|
|
460
|
+
if (!current?.opened) return;
|
|
461
|
+
|
|
462
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
463
|
+
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
464
|
+
try {
|
|
465
|
+
await current.releaseInterface(ifaceNum);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error);
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
await current.close();
|
|
471
|
+
} catch (error) {
|
|
472
|
+
this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async clearEndpointHalt(
|
|
477
|
+
device: USBDevice,
|
|
478
|
+
direction: USBDirection,
|
|
479
|
+
endpointNumber: number
|
|
480
|
+
) {
|
|
481
|
+
try {
|
|
482
|
+
await device.clearHalt(direction, endpointNumber);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
this.Log?.debug(
|
|
485
|
+
`[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`,
|
|
486
|
+
error
|
|
487
|
+
);
|
|
488
|
+
}
|
|
201
489
|
}
|
|
202
490
|
|
|
203
491
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
@@ -243,8 +531,10 @@ export default class WebUsbTransport {
|
|
|
243
531
|
try {
|
|
244
532
|
const currentDevice = await this.findDevice(path);
|
|
245
533
|
if (currentDevice.opened) {
|
|
534
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
535
|
+
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
246
536
|
try {
|
|
247
|
-
await currentDevice.releaseInterface(
|
|
537
|
+
await currentDevice.releaseInterface(ifaceNum);
|
|
248
538
|
} catch (releaseError) {
|
|
249
539
|
this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError);
|
|
250
540
|
}
|
|
@@ -285,10 +575,12 @@ export default class WebUsbTransport {
|
|
|
285
575
|
if (!device.opened) {
|
|
286
576
|
await this.connect(path, false);
|
|
287
577
|
}
|
|
578
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
579
|
+
const endpointOut = endpoints?.endpointOut ?? this.endpointId;
|
|
288
580
|
const transferBuffer = this.toArrayBuffer(
|
|
289
581
|
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
290
582
|
);
|
|
291
|
-
await device.transferOut(
|
|
583
|
+
await device.transferOut(endpointOut, transferBuffer);
|
|
292
584
|
return;
|
|
293
585
|
} catch (error) {
|
|
294
586
|
lastError = error;
|
|
@@ -311,18 +603,30 @@ export default class WebUsbTransport {
|
|
|
311
603
|
throw lastError;
|
|
312
604
|
}
|
|
313
605
|
|
|
314
|
-
private async transferInWithRetry(
|
|
606
|
+
private async transferInWithRetry(
|
|
607
|
+
path: string,
|
|
608
|
+
length: number,
|
|
609
|
+
cancelToken?: TransferCancelToken
|
|
610
|
+
): Promise<DataView> {
|
|
315
611
|
let lastError: unknown;
|
|
316
612
|
for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt += 1) {
|
|
613
|
+
if (cancelToken?.cancelled) {
|
|
614
|
+
throw new Error('transferIn cancelled');
|
|
615
|
+
}
|
|
317
616
|
try {
|
|
318
617
|
const device = await this.findDevice(path);
|
|
319
618
|
if (!device.opened) {
|
|
320
619
|
await this.connect(path, false);
|
|
321
620
|
}
|
|
322
|
-
const
|
|
621
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
622
|
+
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
623
|
+
const result = await device.transferIn(endpointIn, length);
|
|
323
624
|
return this.getTransferInData(result);
|
|
324
625
|
} catch (error) {
|
|
325
626
|
lastError = error;
|
|
627
|
+
if (cancelToken?.cancelled) {
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
326
630
|
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error);
|
|
327
631
|
if (!shouldRetry) {
|
|
328
632
|
throw error;
|
|
@@ -342,10 +646,104 @@ export default class WebUsbTransport {
|
|
|
342
646
|
throw lastError;
|
|
343
647
|
}
|
|
344
648
|
|
|
649
|
+
private async resetConnectionAfterProbe(path: string) {
|
|
650
|
+
this.protocolV2Assemblers.get(path)?.reset();
|
|
651
|
+
this.protocolV2Sessions.delete(path);
|
|
652
|
+
this.protocolV2ReadTimeouts.delete(path);
|
|
653
|
+
|
|
654
|
+
try {
|
|
655
|
+
const device = await this.findDevice(path);
|
|
656
|
+
if (device.opened) {
|
|
657
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
658
|
+
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
659
|
+
try {
|
|
660
|
+
await device.releaseInterface(ifaceNum);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error);
|
|
663
|
+
}
|
|
664
|
+
await device.close();
|
|
665
|
+
}
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.Log.debug('[WebUsbTransport] close after protocol probe error:', error);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
await this.getConnectedDevices();
|
|
671
|
+
await this.connect(path, false);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private async withProtocolReadTimeout<T>(
|
|
675
|
+
path: string,
|
|
676
|
+
promise: Promise<T>,
|
|
677
|
+
timeoutMs: number,
|
|
678
|
+
protocol: ProtocolType,
|
|
679
|
+
onTimeout?: () => void
|
|
680
|
+
): Promise<T> {
|
|
681
|
+
void path;
|
|
682
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
683
|
+
let timedOut = false;
|
|
684
|
+
const waitForeverAfterTimeout = () => new Promise<never>(() => {});
|
|
685
|
+
const guardedPromise = promise.then(
|
|
686
|
+
value => (timedOut ? waitForeverAfterTimeout() : value),
|
|
687
|
+
error => {
|
|
688
|
+
if (timedOut) {
|
|
689
|
+
return waitForeverAfterTimeout();
|
|
690
|
+
}
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
try {
|
|
695
|
+
return await Promise.race([
|
|
696
|
+
guardedPromise,
|
|
697
|
+
new Promise<never>((_, reject) => {
|
|
698
|
+
timer = setTimeout(async () => {
|
|
699
|
+
timedOut = true;
|
|
700
|
+
onTimeout?.();
|
|
701
|
+
reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
|
|
702
|
+
}, timeoutMs);
|
|
703
|
+
}),
|
|
704
|
+
]);
|
|
705
|
+
} finally {
|
|
706
|
+
if (timer) clearTimeout(timer);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private async probeProtocolV1(path: string) {
|
|
711
|
+
if (!this.messages) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
717
|
+
return true;
|
|
718
|
+
} catch (error) {
|
|
719
|
+
this.Log.debug('[WebUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private async probeProtocolV2(path: string) {
|
|
725
|
+
if (!this.messages || !this.messagesV2) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return probeProtocolV2Helper({
|
|
730
|
+
call: (name, data, options) => this.callProtocolV2(path, name, data, options),
|
|
731
|
+
timeoutMs: PROTOCOL_PROBE_TIMEOUT,
|
|
732
|
+
logger: this.Log,
|
|
733
|
+
logPrefix: 'ProtocolV2 WebUSB',
|
|
734
|
+
onProbeFailed: () => this.resetConnectionAfterProbe(path),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
345
738
|
/**
|
|
346
|
-
* Call device method
|
|
739
|
+
* Call device method — branches to Protocol V1 or Protocol V2 based on active probe.
|
|
347
740
|
*/
|
|
348
|
-
async call(
|
|
741
|
+
async call(
|
|
742
|
+
path: string,
|
|
743
|
+
name: string,
|
|
744
|
+
data: Record<string, unknown>,
|
|
745
|
+
options?: TransportCallOptions
|
|
746
|
+
) {
|
|
349
747
|
if (this.messages == null) {
|
|
350
748
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
351
749
|
}
|
|
@@ -355,37 +753,172 @@ export default class WebUsbTransport {
|
|
|
355
753
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
356
754
|
}
|
|
357
755
|
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
|
|
756
|
+
const protocol = this.deviceProtocol.get(path);
|
|
757
|
+
if (!protocol) {
|
|
758
|
+
throw ERRORS.TypedError(
|
|
759
|
+
HardwareErrorCode.RuntimeError,
|
|
760
|
+
`Device protocol has not been detected for ${path}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (shouldSuppressWebUsbCallLog(name)) {
|
|
765
|
+
// 高频文件写入不要逐包发 debug 事件,否则浏览器侧会被日志处理拖慢。
|
|
766
|
+
} else if (shouldBlockWebUsbCallDataLog(name)) {
|
|
767
|
+
this.Log.debug('call-', ' name: ', name, ' protocol: ', protocol);
|
|
361
768
|
} else {
|
|
362
|
-
this.Log.debug('call-', ' name: ', name, ' data: ', data);
|
|
769
|
+
this.Log.debug('call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (protocol === 'V2') {
|
|
773
|
+
return this.callProtocolV2(path, name, data, options);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return this.callProtocolV1(path, name, data, options);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
private async callProtocolV1(
|
|
780
|
+
path: string,
|
|
781
|
+
name: string,
|
|
782
|
+
data: Record<string, unknown>,
|
|
783
|
+
options?: TransportCallOptions
|
|
784
|
+
) {
|
|
785
|
+
const { messages } = this;
|
|
786
|
+
if (!messages) {
|
|
787
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
363
788
|
}
|
|
364
|
-
|
|
789
|
+
|
|
790
|
+
const encodeBuffers = ProtocolV1.encodeMessageChunks(messages, name, data);
|
|
365
791
|
|
|
366
792
|
for (const buffer of encodeBuffers) {
|
|
367
793
|
const newArray: Uint8Array = new Uint8Array(PACKET_SIZE);
|
|
368
|
-
newArray[0] =
|
|
794
|
+
newArray[0] = REPORT_ID;
|
|
369
795
|
newArray.set(new Uint8Array(buffer), 1);
|
|
370
|
-
// console.log('send packet: ', newArray);
|
|
371
796
|
await this.transferOutWithRetry(path, newArray);
|
|
372
797
|
}
|
|
373
798
|
|
|
374
|
-
const resData = await this.receiveData(path);
|
|
799
|
+
const resData = await this.receiveData(path, options?.timeoutMs);
|
|
375
800
|
if (typeof resData !== 'string') {
|
|
376
801
|
throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
|
|
377
802
|
}
|
|
378
|
-
const jsonData =
|
|
803
|
+
const jsonData = ProtocolV1.decodeMessage(messages, resData);
|
|
379
804
|
return check.call(jsonData);
|
|
380
805
|
}
|
|
381
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Send/receive a single call over Protocol V2 (0x5A framing).
|
|
809
|
+
*
|
|
810
|
+
* Encoding: protobuf message → 2-byte LE messageTypeId + pb bytes → Protocol V2 frame
|
|
811
|
+
* Decoding: Protocol V2 frame → messageTypeId + pb bytes → protobuf message
|
|
812
|
+
*/
|
|
813
|
+
private async callProtocolV2(
|
|
814
|
+
path: string,
|
|
815
|
+
name: string,
|
|
816
|
+
data: Record<string, unknown>,
|
|
817
|
+
options?: TransportCallOptions
|
|
818
|
+
) {
|
|
819
|
+
const protocolV1Messages = this.messages;
|
|
820
|
+
if (!this.messagesV2) {
|
|
821
|
+
throw ERRORS.TypedError(
|
|
822
|
+
HardwareErrorCode.TransportNotConfigured,
|
|
823
|
+
'Protocol V2 schema not configured'
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (!protocolV1Messages) {
|
|
827
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
let session = this.protocolV2Sessions.get(path);
|
|
831
|
+
if (!session) {
|
|
832
|
+
session = new ProtocolV2Session({
|
|
833
|
+
schemas: {
|
|
834
|
+
protocolV1: protocolV1Messages,
|
|
835
|
+
protocolV2: this.messagesV2,
|
|
836
|
+
},
|
|
837
|
+
router: PROTOCOL_V2_CHANNEL_USB,
|
|
838
|
+
writeFrame: (frame: Uint8Array) => this.transferOutWithRetry(path, frame),
|
|
839
|
+
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
840
|
+
logger: this.Log,
|
|
841
|
+
logPrefix: 'ProtocolV2 WebUSB',
|
|
842
|
+
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
843
|
+
new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
844
|
+
});
|
|
845
|
+
this.protocolV2Sessions.set(path, session);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
|
|
849
|
+
this.protocolV2Assemblers.get(path)?.reset();
|
|
850
|
+
try {
|
|
851
|
+
return await session.call(name, data, options);
|
|
852
|
+
} finally {
|
|
853
|
+
this.protocolV2ReadTimeouts.delete(path);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
|
|
858
|
+
let assembler = this.protocolV2Assemblers.get(path);
|
|
859
|
+
if (!assembler) {
|
|
860
|
+
assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
861
|
+
this.protocolV2Assemblers.set(path, assembler);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
|
|
865
|
+
const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
866
|
+
|
|
867
|
+
while (!frame) {
|
|
868
|
+
const cancelToken = { cancelled: false };
|
|
869
|
+
const transferIn = this.transferInWithRetry(path, PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken);
|
|
870
|
+
const dataView = deadline
|
|
871
|
+
? await this.withProtocolReadTimeout(
|
|
872
|
+
path,
|
|
873
|
+
transferIn,
|
|
874
|
+
Math.max(deadline - Date.now(), 1),
|
|
875
|
+
'V2',
|
|
876
|
+
() => {
|
|
877
|
+
cancelToken.cancelled = true;
|
|
878
|
+
}
|
|
879
|
+
)
|
|
880
|
+
: await transferIn;
|
|
881
|
+
const bytes = new Uint8Array(
|
|
882
|
+
this.toArrayBuffer(
|
|
883
|
+
dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
try {
|
|
887
|
+
frame = assembler.push(bytes);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
throw ERRORS.TypedError(
|
|
890
|
+
HardwareErrorCode.NetworkError,
|
|
891
|
+
error instanceof Error ? error.message : String(error)
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return frame;
|
|
896
|
+
}
|
|
897
|
+
|
|
382
898
|
/**
|
|
383
899
|
* Receive data from device
|
|
384
900
|
*/
|
|
385
|
-
async receiveData(path: string) {
|
|
386
|
-
const
|
|
901
|
+
async receiveData(path: string, timeoutMs?: number) {
|
|
902
|
+
const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
903
|
+
const readPacket = async () => {
|
|
904
|
+
const cancelToken = { cancelled: false };
|
|
905
|
+
const transferIn = this.transferInWithRetry(path, PACKET_SIZE, cancelToken);
|
|
906
|
+
return deadline
|
|
907
|
+
? this.withProtocolReadTimeout(
|
|
908
|
+
path,
|
|
909
|
+
transferIn,
|
|
910
|
+
Math.max(deadline - Date.now(), 1),
|
|
911
|
+
'V1',
|
|
912
|
+
() => {
|
|
913
|
+
cancelToken.cancelled = true;
|
|
914
|
+
}
|
|
915
|
+
)
|
|
916
|
+
: transferIn;
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const firstPacketData = await readPacket();
|
|
387
920
|
const firstData = this.toArrayBuffer(firstPacketData.buffer.slice(1));
|
|
388
|
-
const { length, typeId, restBuffer } =
|
|
921
|
+
const { length, typeId, restBuffer } = ProtocolV1.decodeFirstChunk(firstData);
|
|
389
922
|
|
|
390
923
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
391
924
|
const lengthWithHeader = Number(length + HEADER_LENGTH);
|
|
@@ -397,9 +930,9 @@ export default class WebUsbTransport {
|
|
|
397
930
|
}
|
|
398
931
|
|
|
399
932
|
while (decoded.offset < lengthWithHeader) {
|
|
400
|
-
const packetData = await
|
|
933
|
+
const packetData = await readPacket();
|
|
401
934
|
const buffer = this.toArrayBuffer(packetData.buffer.slice(1));
|
|
402
|
-
if (lengthWithHeader - decoded.offset >=
|
|
935
|
+
if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
|
|
403
936
|
decoded.append(buffer);
|
|
404
937
|
} else {
|
|
405
938
|
decoded.append(buffer.slice(0, lengthWithHeader - decoded.offset));
|
|
@@ -415,7 +948,22 @@ export default class WebUsbTransport {
|
|
|
415
948
|
*/
|
|
416
949
|
async release(path: string) {
|
|
417
950
|
const device: USBDevice = await this.findDevice(path);
|
|
418
|
-
|
|
951
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
952
|
+
const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
|
|
953
|
+
await device.releaseInterface(ifaceNum);
|
|
419
954
|
await device.close();
|
|
955
|
+
this.deviceProtocol.delete(path);
|
|
956
|
+
this.deviceProtocolHints.delete(path);
|
|
957
|
+
this.protocolV2Assemblers.get(path)?.reset();
|
|
958
|
+
this.protocolV2Assemblers.delete(path);
|
|
959
|
+
this.deviceEndpoints.delete(path);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Expose the detected protocol type for a given device path.
|
|
964
|
+
* Used by upper layers (e.g. TransportManager) to select the correct schema.
|
|
965
|
+
*/
|
|
966
|
+
getProtocolType(path: string): ProtocolType | undefined {
|
|
967
|
+
return this.deviceProtocol.get(path);
|
|
420
968
|
}
|
|
421
969
|
}
|