@onekeyfe/hd-transport-web-device 1.2.0-alpha.16 → 1.2.0-alpha.161
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 +551 -27
- package/__tests__/webusb-protocol-cache.test.ts +244 -0
- package/__tests__/webusb-protocol-v2-timeout.test.ts +363 -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 +19 -4
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +169 -15
- package/dist/index.js +451 -296
- package/dist/transportLog.d.ts +1 -6
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/webusb.d.ts +25 -11
- 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 +291 -130
- package/src/transportLog.ts +1 -11
- package/src/webusb.ts +327 -205
package/src/webusb.ts
CHANGED
|
@@ -6,25 +6,29 @@ 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
|
+
TRANSPORT_EVENT,
|
|
11
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
12
13
|
} from '@onekeyfe/hd-transport';
|
|
13
14
|
import {
|
|
14
15
|
ERRORS,
|
|
15
16
|
HardwareErrorCode,
|
|
16
17
|
ONEKEY_WEBUSB_FILTER,
|
|
18
|
+
inferProtocolHintFromUsbId,
|
|
17
19
|
isKnownTrezorWebUsbDevice,
|
|
20
|
+
resolveOneKeyUsbDevicePath,
|
|
18
21
|
wait,
|
|
19
22
|
} from '@onekeyfe/hd-shared';
|
|
20
23
|
import ByteBuffer from 'bytebuffer';
|
|
21
24
|
|
|
22
|
-
import
|
|
23
|
-
|
|
25
|
+
import type EventEmitter from 'events';
|
|
24
26
|
import type {
|
|
25
27
|
AcquireInput,
|
|
26
28
|
OneKeyDeviceInfoBase,
|
|
27
29
|
ProtocolType,
|
|
30
|
+
ProtocolV2CallContext,
|
|
31
|
+
ProtocolV2Schemas,
|
|
28
32
|
TransportCallOptions,
|
|
29
33
|
} from '@onekeyfe/hd-transport';
|
|
30
34
|
|
|
@@ -39,10 +43,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
|
|
|
39
43
|
const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
40
44
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
41
45
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
// Legacy devices can take longer to answer Initialize after a WebUSB reset.
|
|
47
|
+
// Keep this aligned with Node USB so a slow Protocol V1 response is not
|
|
48
|
+
// misreported as a protocol mismatch during acquire.
|
|
49
|
+
const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
|
|
50
|
+
const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
|
|
51
|
+
const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
|
|
46
52
|
|
|
47
53
|
/**
|
|
48
54
|
* Device information with path and WebUSB device instance
|
|
@@ -64,37 +70,65 @@ interface TransferCancelToken {
|
|
|
64
70
|
cancelled: boolean;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The navigator.usb disconnect listener is module-scoped and attached at most
|
|
75
|
+
* once: listeners on navigator.usb are global and never garbage collected, so a
|
|
76
|
+
* per-instance listener would retain every transport instance created across
|
|
77
|
+
* SDK re-initializations. Events are routed to the most recently initialized
|
|
78
|
+
* transport instance instead.
|
|
79
|
+
*/
|
|
80
|
+
let activeWebUsbTransport: WebUsbTransport | undefined;
|
|
81
|
+
let usbDisconnectListenerAttached = false;
|
|
82
|
+
|
|
83
|
+
function registerActiveWebUsbTransport(instance: WebUsbTransport, usb: USB) {
|
|
84
|
+
activeWebUsbTransport = instance;
|
|
85
|
+
if (usbDisconnectListenerAttached) return;
|
|
86
|
+
usbDisconnectListenerAttached = true;
|
|
87
|
+
usb.addEventListener('disconnect', event => {
|
|
88
|
+
if (!event.device) return;
|
|
89
|
+
const path = resolveOneKeyUsbDevicePath(event.device);
|
|
90
|
+
if (!path) return;
|
|
91
|
+
activeWebUsbTransport?.markProtocolStale(path);
|
|
92
|
+
// WebUSB has no device-list poller behind it, so this event is the only
|
|
93
|
+
// signal that the device is gone. Without it consumers never see a
|
|
94
|
+
// DEVICE.DISCONNECT for USB (OK-60486).
|
|
95
|
+
activeWebUsbTransport?.emitDeviceDisconnect(path, event.device);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
68
100
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
69
101
|
|
|
70
102
|
/** Protobuf schema for Protocol V2 transports. */
|
|
71
103
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
72
104
|
|
|
105
|
+
private protocolV2SchemaSource: string | undefined;
|
|
106
|
+
|
|
73
107
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
74
108
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
75
109
|
|
|
76
110
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
77
111
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
/** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */
|
|
85
|
-
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
112
|
+
/**
|
|
113
|
+
* Paths whose cached protocol must be re-probed on the next acquire (a USB
|
|
114
|
+
* disconnect was seen, or a transfer-level reconnect happened mid-call).
|
|
115
|
+
*/
|
|
116
|
+
private staleProtocolPaths: Set<string> = new Set();
|
|
86
117
|
|
|
87
|
-
/**
|
|
88
|
-
private
|
|
118
|
+
/** Paths currently acquired (between a successful acquire() and its release()). */
|
|
119
|
+
private acquiredPaths: Set<string> = new Set();
|
|
89
120
|
|
|
90
121
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
122
|
+
* The exact USBDevice object each cached protocol was probed against. The
|
|
123
|
+
* browser returns the same object identity for a device as long as it stays
|
|
124
|
+
* connected, and a replug/reboot always yields a new object — so an identity
|
|
125
|
+
* mismatch proves the device was re-enumerated since the probe, even when the
|
|
126
|
+
* disconnect event itself was delayed or missed.
|
|
94
127
|
*/
|
|
95
|
-
private
|
|
128
|
+
private probedDeviceObjects: Map<string, USBDevice> = new Map();
|
|
96
129
|
|
|
97
|
-
|
|
130
|
+
/** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
|
|
131
|
+
private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
|
|
98
132
|
|
|
99
133
|
name = 'WebUsbTransport';
|
|
100
134
|
|
|
@@ -106,6 +140,8 @@ export default class WebUsbTransport {
|
|
|
106
140
|
|
|
107
141
|
usb?: USB;
|
|
108
142
|
|
|
143
|
+
emitter?: EventEmitter;
|
|
144
|
+
|
|
109
145
|
/**
|
|
110
146
|
* Cached list of connected devices
|
|
111
147
|
* This is essential for maintaining device references between operations
|
|
@@ -118,11 +154,20 @@ export default class WebUsbTransport {
|
|
|
118
154
|
|
|
119
155
|
interfaceId = INTERFACE_ID;
|
|
120
156
|
|
|
157
|
+
constructor() {
|
|
158
|
+
super({
|
|
159
|
+
router: PROTOCOL_V2_CHANNEL_USB,
|
|
160
|
+
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
161
|
+
logPrefix: 'ProtocolV2 WebUSB',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
121
165
|
/**
|
|
122
166
|
* Initialize WebUSB transport
|
|
123
167
|
*/
|
|
124
|
-
init(logger: any) {
|
|
168
|
+
init(logger: any, emitter?: EventEmitter) {
|
|
125
169
|
this.Log = logger;
|
|
170
|
+
this.emitter = emitter;
|
|
126
171
|
|
|
127
172
|
const { usb } = navigator;
|
|
128
173
|
if (!usb) {
|
|
@@ -132,6 +177,34 @@ export default class WebUsbTransport {
|
|
|
132
177
|
);
|
|
133
178
|
}
|
|
134
179
|
this.usb = usb;
|
|
180
|
+
registerActiveWebUsbTransport(this, usb);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Announce that a USB device left. Called from the module-scoped disconnect
|
|
185
|
+
* listener, which is why it is public rather than inlined.
|
|
186
|
+
*/
|
|
187
|
+
emitDeviceDisconnect(path: string, device?: USBDevice) {
|
|
188
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
189
|
+
name: device?.productName ?? '',
|
|
190
|
+
id: path,
|
|
191
|
+
connectId: path,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Protocol type is a property of the physical device keyed by USB serial number.
|
|
197
|
+
* It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
|
|
198
|
+
* a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
|
|
199
|
+
* reconnects, which cover a missed disconnect event) only MARK the cached probe
|
|
200
|
+
* result stale instead of deleting it: an in-flight session keeps using the old
|
|
201
|
+
* value so the transfer-level reconnect retries can absorb a transient
|
|
202
|
+
* re-enumeration exactly as they did before the cache existed, while the next
|
|
203
|
+
* acquire re-probes from scratch.
|
|
204
|
+
*/
|
|
205
|
+
markProtocolStale(path: string) {
|
|
206
|
+
this.staleProtocolPaths.add(path);
|
|
207
|
+
this.probedDeviceObjects.delete(path);
|
|
135
208
|
}
|
|
136
209
|
|
|
137
210
|
/**
|
|
@@ -147,9 +220,20 @@ export default class WebUsbTransport {
|
|
|
147
220
|
* Cache the Protocol V2 protobuf schema.
|
|
148
221
|
*/
|
|
149
222
|
configureProtocolV2(signedData: any) {
|
|
223
|
+
const schemaSource =
|
|
224
|
+
typeof signedData === 'string'
|
|
225
|
+
? signedData
|
|
226
|
+
: JSON.stringify(signedData) ?? String(signedData);
|
|
227
|
+
if (schemaSource === this.protocolV2SchemaSource) return;
|
|
228
|
+
|
|
229
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
150
230
|
this.messagesV2 = parseConfigure(signedData);
|
|
151
|
-
this.
|
|
152
|
-
|
|
231
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
232
|
+
if (!hadProtocolV2Schema) return;
|
|
233
|
+
|
|
234
|
+
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
235
|
+
this.Log?.debug('[WebUsbTransport] schema link cleanup failed:', error)
|
|
236
|
+
);
|
|
153
237
|
}
|
|
154
238
|
|
|
155
239
|
/**
|
|
@@ -181,26 +265,6 @@ export default class WebUsbTransport {
|
|
|
181
265
|
return this.deviceList;
|
|
182
266
|
}
|
|
183
267
|
|
|
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
268
|
/**
|
|
205
269
|
* Get list of connected devices
|
|
206
270
|
*/
|
|
@@ -216,18 +280,21 @@ export default class WebUsbTransport {
|
|
|
216
280
|
return isOneKey && !isKnownTrezorWebUsbDevice(dev);
|
|
217
281
|
});
|
|
218
282
|
|
|
219
|
-
this.deviceList = onekeyDevices.
|
|
220
|
-
const path =
|
|
221
|
-
|
|
283
|
+
this.deviceList = onekeyDevices.flatMap(device => {
|
|
284
|
+
const path = resolveOneKeyUsbDevicePath(device);
|
|
285
|
+
if (!path) return [];
|
|
286
|
+
const protocolHint = inferProtocolHintFromUsbId(device.vendorId, device.productId);
|
|
222
287
|
if (protocolHint) {
|
|
223
288
|
this.deviceProtocolHints.set(path, protocolHint);
|
|
224
289
|
}
|
|
225
290
|
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
291
|
+
return [
|
|
292
|
+
{
|
|
293
|
+
path,
|
|
294
|
+
device,
|
|
295
|
+
commType: 'webusb',
|
|
296
|
+
},
|
|
297
|
+
];
|
|
231
298
|
});
|
|
232
299
|
|
|
233
300
|
return this.deviceList;
|
|
@@ -239,20 +306,60 @@ export default class WebUsbTransport {
|
|
|
239
306
|
async acquire(input: AcquireInput) {
|
|
240
307
|
if (!input.path) return;
|
|
241
308
|
try {
|
|
309
|
+
await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
|
|
242
310
|
await this.closeOpenDevice(input.path);
|
|
243
311
|
await this.connect(input.path ?? '', true);
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.deviceProtocolHints.
|
|
312
|
+
if (input.forceProtocolDetection) {
|
|
313
|
+
// Explicit recovery/discovery (e.g. detectDeviceConnectProtocol) must
|
|
314
|
+
// probe on the wire regardless of any cached result — the cached
|
|
315
|
+
// binding may be exactly what the caller is trying to recover from.
|
|
316
|
+
this.staleProtocolPaths.delete(input.path);
|
|
317
|
+
this.deviceProtocol.delete(input.path);
|
|
318
|
+
this.deviceProtocolHints.delete(input.path);
|
|
319
|
+
}
|
|
320
|
+
if (this.staleProtocolPaths.has(input.path)) {
|
|
321
|
+
// The device disconnected (possibly rebooting into another mode) since
|
|
322
|
+
// the protocol was probed — drop the cache so it is re-probed below.
|
|
323
|
+
this.staleProtocolPaths.delete(input.path);
|
|
324
|
+
this.deviceProtocol.delete(input.path);
|
|
325
|
+
}
|
|
326
|
+
if (this.deviceProtocol.has(input.path)) {
|
|
327
|
+
const currentDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
328
|
+
if (!currentDevice || currentDevice !== this.probedDeviceObjects.get(input.path)) {
|
|
329
|
+
// The OS re-enumerated the device since the probe (a replug/reboot we
|
|
330
|
+
// may not have seen a disconnect event for) — the cached protocol can
|
|
331
|
+
// no longer be trusted; re-probe on the wire below.
|
|
332
|
+
this.deviceProtocol.delete(input.path);
|
|
333
|
+
}
|
|
251
334
|
}
|
|
252
|
-
|
|
335
|
+
const cachedProtocol = this.deviceProtocol.get(input.path);
|
|
336
|
+
if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
|
|
337
|
+
// The caller expects a different protocol than the cached probe result;
|
|
338
|
+
// the cache is stale — drop it and re-probe on the wire below.
|
|
339
|
+
this.deviceProtocol.delete(input.path);
|
|
340
|
+
}
|
|
341
|
+
if (!this.deviceProtocol.has(input.path)) {
|
|
342
|
+
const usbDevice = this.deviceList.find(device => device.path === input.path)?.device;
|
|
343
|
+
const protocolHint = input.expectedProtocol
|
|
344
|
+
? undefined
|
|
345
|
+
: input.protocolHint ??
|
|
346
|
+
this.deviceProtocolHints.get(input.path) ??
|
|
347
|
+
inferProtocolHintFromUsbId(usbDevice?.vendorId, usbDevice?.productId);
|
|
348
|
+
if (protocolHint) {
|
|
349
|
+
this.deviceProtocolHints.set(input.path, protocolHint);
|
|
350
|
+
}
|
|
351
|
+
await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
|
|
352
|
+
const probedDevice = this.deviceList.find(d => d.path === input.path)?.device;
|
|
353
|
+
if (probedDevice) {
|
|
354
|
+
this.probedDeviceObjects.set(input.path, probedDevice);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.acquiredPaths.add(input.path);
|
|
253
358
|
return await Promise.resolve(input.path);
|
|
254
359
|
} catch (e) {
|
|
255
360
|
this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
|
361
|
+
this.acquiredPaths.delete(input.path);
|
|
362
|
+
await this.closeOpenDevice(input.path);
|
|
256
363
|
throw e;
|
|
257
364
|
}
|
|
258
365
|
}
|
|
@@ -269,13 +376,17 @@ export default class WebUsbTransport {
|
|
|
269
376
|
);
|
|
270
377
|
}
|
|
271
378
|
|
|
272
|
-
private
|
|
379
|
+
private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
|
|
273
380
|
return ERRORS.TypedError(
|
|
274
|
-
HardwareErrorCode.
|
|
275
|
-
|
|
381
|
+
HardwareErrorCode.DeviceInitializeFailed,
|
|
382
|
+
`Protocol ${expected} probe timeout after ${attempts} attempts`
|
|
276
383
|
);
|
|
277
384
|
}
|
|
278
385
|
|
|
386
|
+
private createProtocolDetectionError() {
|
|
387
|
+
return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
|
|
388
|
+
}
|
|
389
|
+
|
|
279
390
|
private async detectProtocol(
|
|
280
391
|
path: string,
|
|
281
392
|
expectedProtocol?: ProtocolType,
|
|
@@ -286,29 +397,54 @@ export default class WebUsbTransport {
|
|
|
286
397
|
this.deviceProtocol.set(path, 'V1');
|
|
287
398
|
return 'V1';
|
|
288
399
|
}
|
|
400
|
+
await this.closeConnectionAfterProbe(path);
|
|
289
401
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
290
402
|
}
|
|
291
403
|
|
|
292
404
|
if (expectedProtocol === 'V2') {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
405
|
+
for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
|
|
406
|
+
if (await this.probeProtocolV2(path)) {
|
|
407
|
+
this.deviceProtocol.set(path, 'V2');
|
|
408
|
+
return 'V2';
|
|
409
|
+
}
|
|
410
|
+
if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
|
|
411
|
+
await this.resetConnectionAfterProbe(path);
|
|
412
|
+
this.Log?.debug(
|
|
413
|
+
`[WebUsbTransport] Protocol V2 probe timed out, retrying ${
|
|
414
|
+
attempt + 1
|
|
415
|
+
}/${EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS}`
|
|
416
|
+
);
|
|
417
|
+
} else {
|
|
418
|
+
await this.closeConnectionAfterProbe(path);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
this.deviceProtocol.delete(path);
|
|
422
|
+
throw this.createProtocolProbeTimeoutError(
|
|
423
|
+
expectedProtocol,
|
|
424
|
+
EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS
|
|
425
|
+
);
|
|
297
426
|
}
|
|
298
427
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// 不能作为最终结论。
|
|
428
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
429
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
302
430
|
const probeOrder: ProtocolType[] =
|
|
303
431
|
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
304
432
|
|
|
305
|
-
for (const protocol of probeOrder) {
|
|
433
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
306
434
|
const detected =
|
|
307
435
|
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
308
436
|
if (detected) {
|
|
309
437
|
this.deviceProtocol.set(path, protocol);
|
|
310
438
|
return protocol;
|
|
311
439
|
}
|
|
440
|
+
if (index < probeOrder.length - 1) {
|
|
441
|
+
// A timed-out WebUSB transferIn cannot be cancelled in place. Closing and
|
|
442
|
+
// reopening the device guarantees the next protocol probe cannot consume a
|
|
443
|
+
// late response from the previous protocol generation.
|
|
444
|
+
await this.resetConnectionAfterProbe(path);
|
|
445
|
+
} else {
|
|
446
|
+
await this.closeConnectionAfterProbe(path);
|
|
447
|
+
}
|
|
312
448
|
}
|
|
313
449
|
|
|
314
450
|
this.deviceProtocol.delete(path);
|
|
@@ -420,16 +556,12 @@ export default class WebUsbTransport {
|
|
|
420
556
|
// Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
|
|
421
557
|
const endpoints = this.discoverEndpoints(device);
|
|
422
558
|
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
559
|
await device.claimInterface(endpoints.interfaceNumber);
|
|
427
560
|
await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
|
|
428
561
|
await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
|
|
429
562
|
}
|
|
430
563
|
|
|
431
564
|
private async closeOpenDevice(path: string) {
|
|
432
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
433
565
|
const current = this.deviceList.find(device => device.path === path)?.device;
|
|
434
566
|
if (!current?.opened) return;
|
|
435
567
|
|
|
@@ -462,7 +594,25 @@ export default class WebUsbTransport {
|
|
|
462
594
|
}
|
|
463
595
|
}
|
|
464
596
|
|
|
597
|
+
/**
|
|
598
|
+
* With the protocol cache surviving release(), deviceProtocol presence no
|
|
599
|
+
* longer implies an active session. Guard call()/post() explicitly so a
|
|
600
|
+
* post-release straggler fails fast instead of silently reopening the device
|
|
601
|
+
* and driving it outside any session (pre-cache behavior: the deleted
|
|
602
|
+
* protocol entry produced the same fail-fast).
|
|
603
|
+
*/
|
|
604
|
+
private assertAcquired(path: string) {
|
|
605
|
+
if (!this.acquiredPaths.has(path)) {
|
|
606
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
465
610
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
611
|
+
this.assertAcquired(session);
|
|
612
|
+
if (this.deviceProtocol.get(session) === 'V2') {
|
|
613
|
+
await this.sendProtocolV2UsbFlowControl(session, name, data);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
466
616
|
await this.call(session, name, data);
|
|
467
617
|
}
|
|
468
618
|
|
|
@@ -500,6 +650,11 @@ export default class WebUsbTransport {
|
|
|
500
650
|
error
|
|
501
651
|
)}`
|
|
502
652
|
);
|
|
653
|
+
// The device dropped off the bus mid-call and may have rebooted into a
|
|
654
|
+
// different mode. The in-flight retry keeps the known protocol, but the
|
|
655
|
+
// next acquire must re-probe — this also self-heals a stale cache when the
|
|
656
|
+
// USB disconnect event itself was missed.
|
|
657
|
+
this.staleProtocolPaths.add(path);
|
|
503
658
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
504
659
|
|
|
505
660
|
try {
|
|
@@ -624,33 +779,30 @@ export default class WebUsbTransport {
|
|
|
624
779
|
throw lastError;
|
|
625
780
|
}
|
|
626
781
|
|
|
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);
|
|
782
|
+
private async transferInOnce(path: string, length: number): Promise<DataView> {
|
|
783
|
+
const device = await this.findDevice(path);
|
|
784
|
+
if (!device.opened) {
|
|
785
|
+
throw new Error('USBDevice is not open for transferIn');
|
|
646
786
|
}
|
|
787
|
+
const endpoints = this.deviceEndpoints.get(path);
|
|
788
|
+
const endpointIn = endpoints?.endpointIn ?? this.endpointId;
|
|
789
|
+
const result = await device.transferIn(endpointIn, length);
|
|
790
|
+
return this.getTransferInData(result);
|
|
791
|
+
}
|
|
647
792
|
|
|
793
|
+
private async closeConnectionAfterProbe(path: string) {
|
|
794
|
+
await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
|
|
795
|
+
await this.closeOpenDevice(path);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private async resetConnectionAfterProbe(path: string) {
|
|
799
|
+
await this.closeConnectionAfterProbe(path);
|
|
648
800
|
await this.getConnectedDevices();
|
|
649
801
|
await this.connect(path, false);
|
|
650
802
|
}
|
|
651
803
|
|
|
652
804
|
private async withProtocolReadTimeout<T>(
|
|
653
|
-
|
|
805
|
+
path: string,
|
|
654
806
|
promise: Promise<T>,
|
|
655
807
|
timeoutMs: number,
|
|
656
808
|
protocol: ProtocolType,
|
|
@@ -672,9 +824,16 @@ export default class WebUsbTransport {
|
|
|
672
824
|
return await Promise.race([
|
|
673
825
|
guardedPromise,
|
|
674
826
|
new Promise<never>((_, reject) => {
|
|
675
|
-
timer = setTimeout(() => {
|
|
827
|
+
timer = setTimeout(async () => {
|
|
676
828
|
timedOut = true;
|
|
677
829
|
onTimeout?.();
|
|
830
|
+
if (protocol === 'V1') {
|
|
831
|
+
try {
|
|
832
|
+
await this.resetConnectionAfterProbe(path);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
678
837
|
reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
|
|
679
838
|
}, timeoutMs);
|
|
680
839
|
}),
|
|
@@ -690,7 +849,14 @@ export default class WebUsbTransport {
|
|
|
690
849
|
}
|
|
691
850
|
|
|
692
851
|
try {
|
|
693
|
-
await this.callProtocolV1(
|
|
852
|
+
await this.callProtocolV1(
|
|
853
|
+
path,
|
|
854
|
+
'Initialize',
|
|
855
|
+
{},
|
|
856
|
+
{
|
|
857
|
+
timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
|
|
858
|
+
}
|
|
859
|
+
);
|
|
694
860
|
return true;
|
|
695
861
|
} catch (_error) {
|
|
696
862
|
return false;
|
|
@@ -704,10 +870,9 @@ export default class WebUsbTransport {
|
|
|
704
870
|
|
|
705
871
|
return probeProtocolV2Helper({
|
|
706
872
|
call: (name, data, options) => this.callProtocolV2(path, name, data, options),
|
|
707
|
-
timeoutMs:
|
|
873
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
|
|
708
874
|
logger: this.Log,
|
|
709
875
|
logPrefix: 'ProtocolV2 WebUSB',
|
|
710
|
-
onProbeFailed: () => this.resetConnectionAfterProbe(path),
|
|
711
876
|
});
|
|
712
877
|
}
|
|
713
878
|
|
|
@@ -723,6 +888,7 @@ export default class WebUsbTransport {
|
|
|
723
888
|
if (this.messages == null) {
|
|
724
889
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
725
890
|
}
|
|
891
|
+
this.assertAcquired(path);
|
|
726
892
|
|
|
727
893
|
const device = await this.findDevice(path);
|
|
728
894
|
if (!device) {
|
|
@@ -737,10 +903,6 @@ export default class WebUsbTransport {
|
|
|
737
903
|
);
|
|
738
904
|
}
|
|
739
905
|
|
|
740
|
-
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
741
|
-
this.Log.debug('transport call', createTransportCallLog(name, protocol));
|
|
742
|
-
}
|
|
743
|
-
|
|
744
906
|
if (protocol === 'V2') {
|
|
745
907
|
return this.callProtocolV2(path, name, data, options);
|
|
746
908
|
}
|
|
@@ -788,94 +950,7 @@ export default class WebUsbTransport {
|
|
|
788
950
|
data: Record<string, unknown>,
|
|
789
951
|
options?: TransportCallOptions
|
|
790
952
|
) {
|
|
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;
|
|
953
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
879
954
|
}
|
|
880
955
|
|
|
881
956
|
/**
|
|
@@ -930,20 +1005,67 @@ export default class WebUsbTransport {
|
|
|
930
1005
|
* Release device
|
|
931
1006
|
*/
|
|
932
1007
|
async release(path: string) {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
this.protocolV2Assemblers.delete(path);
|
|
942
|
-
this.protocolV2Sessions.delete(path);
|
|
943
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
1008
|
+
this.acquiredPaths.delete(path);
|
|
1009
|
+
await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
|
|
1010
|
+
await this.closeOpenDevice(path);
|
|
1011
|
+
// Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
|
|
1012
|
+
// physical-device property, so the next acquire can skip the wire-level probe.
|
|
1013
|
+
// V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
|
|
1014
|
+
// invalidation above, and any entry is re-probed after a USB disconnect or a
|
|
1015
|
+
// transfer-level reconnect (see markProtocolStale).
|
|
944
1016
|
this.deviceEndpoints.delete(path);
|
|
945
1017
|
}
|
|
946
1018
|
|
|
1019
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
1020
|
+
if (!this.messages || !this.messagesV2) {
|
|
1021
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
protocolV1: this.messages,
|
|
1025
|
+
protocolV2: this.messagesV2,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
protected getProtocolV2UsbLogger() {
|
|
1030
|
+
return this.Log;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
protected async writeProtocolV2UsbPacket(
|
|
1034
|
+
path: string,
|
|
1035
|
+
frame: Uint8Array,
|
|
1036
|
+
_context: ProtocolV2CallContext
|
|
1037
|
+
): Promise<void> {
|
|
1038
|
+
await this.transferOutOnce(path, frame);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
protected async readProtocolV2UsbPacket(
|
|
1042
|
+
path: string,
|
|
1043
|
+
_context: ProtocolV2CallContext
|
|
1044
|
+
): Promise<Uint8Array> {
|
|
1045
|
+
const dataView = await this.transferInOnce(path, PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
1046
|
+
return new Uint8Array(
|
|
1047
|
+
this.toArrayBuffer(
|
|
1048
|
+
dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1054
|
+
await this.closeOpenDevice(path);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
1058
|
+
this.deviceProtocol.delete(path);
|
|
1059
|
+
this.Log?.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
1063
|
+
return new ProtocolV2LinkError(
|
|
1064
|
+
'response-timeout',
|
|
1065
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
947
1069
|
/**
|
|
948
1070
|
* Expose the detected protocol type for a given device path.
|
|
949
1071
|
* Used by upper layers (e.g. TransportManager) to select the correct schema.
|