@onekeyfe/hd-transport-web-device 1.2.0-alpha.11 → 1.2.0-alpha.111

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/src/webusb.ts CHANGED
@@ -1,23 +1,30 @@
1
1
  /* eslint-disable no-undef */
2
2
  import transport, {
3
- LogBlockCommand,
4
3
  PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
5
4
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
6
5
  PROTOCOL_V1_REPORT_ID,
7
6
  PROTOCOL_V1_USB_PACKET_SIZE,
8
7
  PROTOCOL_V2_CHANNEL_USB,
9
8
  PROTOCOL_V2_FRAME_MAX_BYTES,
10
- ProtocolV2FrameAssembler,
11
- ProtocolV2Session,
9
+ ProtocolV2LinkError,
10
+ ProtocolV2UsbTransportBase,
12
11
  probeProtocolV2 as probeProtocolV2Helper,
13
12
  } from '@onekeyfe/hd-transport';
14
- import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
13
+ import {
14
+ ERRORS,
15
+ HardwareErrorCode,
16
+ ONEKEY_WEBUSB_FILTER,
17
+ isKnownTrezorWebUsbDevice,
18
+ wait,
19
+ } from '@onekeyfe/hd-shared';
15
20
  import ByteBuffer from 'bytebuffer';
16
21
 
17
22
  import type {
18
23
  AcquireInput,
19
24
  OneKeyDeviceInfoBase,
20
25
  ProtocolType,
26
+ ProtocolV2CallContext,
27
+ ProtocolV2Schemas,
21
28
  TransportCallOptions,
22
29
  } from '@onekeyfe/hd-transport';
23
30
 
@@ -32,23 +39,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
32
39
  const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
33
40
  const PACKET_IO_MAX_RETRIES = 3;
34
41
  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
-
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;
52
48
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
53
49
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
54
50
  }
@@ -73,38 +69,22 @@ interface TransferCancelToken {
73
69
  cancelled: boolean;
74
70
  }
75
71
 
76
- export default class WebUsbTransport {
72
+ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
77
73
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
78
74
 
79
75
  /** Protobuf schema for Protocol V2 transports. */
80
76
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
81
77
 
78
+ private protocolV2SchemaSource: string | undefined;
79
+
82
80
  /** Per-path protocol type detected by active wire-level probe. */
83
81
  private deviceProtocol: Map<string, ProtocolType> = new Map();
84
82
 
85
83
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
86
84
 
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
85
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
97
86
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
98
87
 
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
-
108
88
  name = 'WebUsbTransport';
109
89
 
110
90
  stopped = false;
@@ -127,6 +107,14 @@ export default class WebUsbTransport {
127
107
 
128
108
  interfaceId = INTERFACE_ID;
129
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
+
130
118
  /**
131
119
  * Initialize WebUSB transport
132
120
  */
@@ -156,10 +144,20 @@ export default class WebUsbTransport {
156
144
  * Cache the Protocol V2 protobuf schema.
157
145
  */
158
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;
159
154
  this.messagesV2 = parseConfigure(signedData);
160
- this.protocolV2Sessions.clear();
161
- this.protocolV2ReadTimeouts.clear();
162
- this.Log?.debug('[WebUsbTransport] Protocol V2 schema configured');
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
+ );
163
161
  }
164
162
 
165
163
  /**
@@ -191,26 +189,6 @@ export default class WebUsbTransport {
191
189
  return this.deviceList;
192
190
  }
193
191
 
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
-
214
192
  /**
215
193
  * Get list of connected devices
216
194
  */
@@ -218,14 +196,17 @@ export default class WebUsbTransport {
218
196
  if (!this.usb) return [];
219
197
 
220
198
  const devices = await this.usb.getDevices();
221
- const onekeyDevices = devices.filter(dev =>
222
- ONEKEY_WEBUSB_FILTER.some(
223
- desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId
224
- )
225
- );
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
+ });
226
207
 
227
208
  this.deviceList = onekeyDevices.map(device => {
228
- const path = this.getDevicePath(device);
209
+ const path = device.serialNumber as string;
229
210
  const protocolHint = inferProtocolHintFromDeviceName(device.productName);
230
211
  if (protocolHint) {
231
212
  this.deviceProtocolHints.set(path, protocolHint);
@@ -238,14 +219,6 @@ export default class WebUsbTransport {
238
219
  };
239
220
  });
240
221
 
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
- }
248
-
249
222
  return this.deviceList;
250
223
  }
251
224
 
@@ -255,13 +228,16 @@ export default class WebUsbTransport {
255
228
  async acquire(input: AcquireInput) {
256
229
  if (!input.path) return;
257
230
  try {
231
+ await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
258
232
  await this.closeOpenDevice(input.path);
259
233
  await this.connect(input.path ?? '', true);
260
234
  const deviceName = this.deviceList.find(device => device.path === input.path)?.device
261
235
  .productName;
262
236
  const protocolHint = input.expectedProtocol
263
237
  ? undefined
264
- : this.deviceProtocolHints.get(input.path) ?? inferProtocolHintFromDeviceName(deviceName);
238
+ : input.protocolHint ??
239
+ this.deviceProtocolHints.get(input.path) ??
240
+ inferProtocolHintFromDeviceName(deviceName);
265
241
  if (protocolHint) {
266
242
  this.deviceProtocolHints.set(input.path, protocolHint);
267
243
  }
@@ -269,6 +245,7 @@ export default class WebUsbTransport {
269
245
  return await Promise.resolve(input.path);
270
246
  } catch (e) {
271
247
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
248
+ await this.closeOpenDevice(input.path);
272
249
  throw e;
273
250
  }
274
251
  }
@@ -285,13 +262,17 @@ export default class WebUsbTransport {
285
262
  );
286
263
  }
287
264
 
288
- private createProtocolDetectionError() {
265
+ private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
289
266
  return ERRORS.TypedError(
290
- HardwareErrorCode.RuntimeError,
291
- 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
267
+ HardwareErrorCode.DeviceInitializeFailed,
268
+ `Protocol ${expected} probe timeout after ${attempts} attempts`
292
269
  );
293
270
  }
294
271
 
272
+ private createProtocolDetectionError() {
273
+ return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
274
+ }
275
+
295
276
  private async detectProtocol(
296
277
  path: string,
297
278
  expectedProtocol?: ProtocolType,
@@ -300,34 +281,56 @@ export default class WebUsbTransport {
300
281
  if (expectedProtocol === 'V1') {
301
282
  if (await this.probeProtocolV1(path)) {
302
283
  this.deviceProtocol.set(path, 'V1');
303
- this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
304
284
  return 'V1';
305
285
  }
286
+ await this.closeConnectionAfterProbe(path);
306
287
  throw this.createProtocolMismatchError(expectedProtocol);
307
288
  }
308
289
 
309
290
  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';
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
+ );
315
312
  }
316
313
 
317
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
318
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
319
- // 不能作为最终结论。
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.
320
316
  const probeOrder: ProtocolType[] =
321
317
  protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
322
318
 
323
- for (const protocol of probeOrder) {
319
+ for (const [index, protocol] of probeOrder.entries()) {
324
320
  const detected =
325
321
  protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
326
322
  if (detected) {
327
323
  this.deviceProtocol.set(path, protocol);
328
- this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
329
324
  return protocol;
330
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
+ }
331
334
  }
332
335
 
333
336
  this.deviceProtocol.delete(path);
@@ -414,13 +417,6 @@ export default class WebUsbTransport {
414
417
  */
415
418
  async connectToDevice(path: string, first: boolean) {
416
419
  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
- );
423
-
424
420
  if (!device.opened) {
425
421
  await device.open();
426
422
  }
@@ -446,16 +442,12 @@ export default class WebUsbTransport {
446
442
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
447
443
  const endpoints = this.discoverEndpoints(device);
448
444
  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
445
  await device.claimInterface(endpoints.interfaceNumber);
453
446
  await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
454
447
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
455
448
  }
456
449
 
457
450
  private async closeOpenDevice(path: string) {
458
- this.protocolV2Assemblers.get(path)?.reset();
459
451
  const current = this.deviceList.find(device => device.path === path)?.device;
460
452
  if (!current?.opened) return;
461
453
 
@@ -650,27 +642,24 @@ export default class WebUsbTransport {
650
642
  throw lastError;
651
643
  }
652
644
 
653
- private async resetConnectionAfterProbe(path: string) {
654
- this.protocolV2Assemblers.get(path)?.reset();
655
- this.protocolV2Sessions.delete(path);
656
- this.protocolV2ReadTimeouts.delete(path);
657
-
658
- try {
659
- const device = await this.findDevice(path);
660
- if (device.opened) {
661
- const endpoints = this.deviceEndpoints.get(path);
662
- const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
663
- try {
664
- await device.releaseInterface(ifaceNum);
665
- } catch (error) {
666
- this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error);
667
- }
668
- await device.close();
669
- }
670
- } catch (error) {
671
- 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');
672
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
+ }
673
655
 
656
+ private async closeConnectionAfterProbe(path: string) {
657
+ await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
658
+ await this.closeOpenDevice(path);
659
+ }
660
+
661
+ private async resetConnectionAfterProbe(path: string) {
662
+ await this.closeConnectionAfterProbe(path);
674
663
  await this.getConnectedDevices();
675
664
  await this.connect(path, false);
676
665
  }
@@ -682,7 +671,6 @@ export default class WebUsbTransport {
682
671
  protocol: ProtocolType,
683
672
  onTimeout?: () => void
684
673
  ): Promise<T> {
685
- void path;
686
674
  let timer: ReturnType<typeof setTimeout> | undefined;
687
675
  let timedOut = false;
688
676
  const waitForeverAfterTimeout = () => new Promise<never>(() => {});
@@ -702,6 +690,13 @@ export default class WebUsbTransport {
702
690
  timer = setTimeout(async () => {
703
691
  timedOut = true;
704
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
+ }
705
700
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
706
701
  }, timeoutMs);
707
702
  }),
@@ -717,10 +712,16 @@ export default class WebUsbTransport {
717
712
  }
718
713
 
719
714
  try {
720
- await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
715
+ await this.callProtocolV1(
716
+ path,
717
+ 'Initialize',
718
+ {},
719
+ {
720
+ timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
721
+ }
722
+ );
721
723
  return true;
722
- } catch (error) {
723
- this.Log.debug('[WebUsbTransport] Protocol V1 Initialize probe failed:', error);
724
+ } catch (_error) {
724
725
  return false;
725
726
  }
726
727
  }
@@ -732,10 +733,9 @@ export default class WebUsbTransport {
732
733
 
733
734
  return probeProtocolV2Helper({
734
735
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
735
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
736
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
736
737
  logger: this.Log,
737
738
  logPrefix: 'ProtocolV2 WebUSB',
738
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
739
739
  });
740
740
  }
741
741
 
@@ -765,14 +765,6 @@ export default class WebUsbTransport {
765
765
  );
766
766
  }
767
767
 
768
- if (shouldSuppressWebUsbCallLog(name)) {
769
- // 高频文件写入不要逐包发 debug 事件,否则浏览器侧会被日志处理拖慢。
770
- } else if (shouldBlockWebUsbCallDataLog(name)) {
771
- this.Log.debug('call-', ' name: ', name, ' protocol: ', protocol);
772
- } else {
773
- this.Log.debug('call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
774
- }
775
-
776
768
  if (protocol === 'V2') {
777
769
  return this.callProtocolV2(path, name, data, options);
778
770
  }
@@ -820,83 +812,7 @@ export default class WebUsbTransport {
820
812
  data: Record<string, unknown>,
821
813
  options?: TransportCallOptions
822
814
  ) {
823
- const protocolV1Messages = this.messages;
824
- if (!this.messagesV2) {
825
- throw ERRORS.TypedError(
826
- HardwareErrorCode.TransportNotConfigured,
827
- 'Protocol V2 schema not configured'
828
- );
829
- }
830
- if (!protocolV1Messages) {
831
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
832
- }
833
-
834
- let session = this.protocolV2Sessions.get(path);
835
- if (!session) {
836
- session = new ProtocolV2Session({
837
- schemas: {
838
- protocolV1: protocolV1Messages,
839
- protocolV2: this.messagesV2,
840
- },
841
- router: PROTOCOL_V2_CHANNEL_USB,
842
- writeFrame: (frame: Uint8Array) => this.transferOutOnce(path, frame),
843
- readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
844
- logger: this.Log,
845
- logPrefix: 'ProtocolV2 WebUSB',
846
- createTimeoutError: (messageName: string, timeoutMs: number) =>
847
- new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
848
- });
849
- this.protocolV2Sessions.set(path, session);
850
- }
851
-
852
- this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
853
- this.protocolV2Assemblers.get(path)?.reset();
854
- try {
855
- return await session.call(name, data, options);
856
- } finally {
857
- this.protocolV2ReadTimeouts.delete(path);
858
- }
859
- }
860
-
861
- private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
862
- let assembler = this.protocolV2Assemblers.get(path);
863
- if (!assembler) {
864
- assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
865
- this.protocolV2Assemblers.set(path, assembler);
866
- }
867
-
868
- let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
869
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
870
-
871
- while (!frame) {
872
- const cancelToken = { cancelled: false };
873
- const transferIn = this.transferInWithRetry(path, PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken);
874
- const dataView = deadline
875
- ? await this.withProtocolReadTimeout(
876
- path,
877
- transferIn,
878
- Math.max(deadline - Date.now(), 1),
879
- 'V2',
880
- () => {
881
- cancelToken.cancelled = true;
882
- }
883
- )
884
- : await transferIn;
885
- const bytes = new Uint8Array(
886
- this.toArrayBuffer(
887
- dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
888
- )
889
- );
890
- try {
891
- frame = assembler.push(bytes);
892
- } catch (error) {
893
- throw ERRORS.TypedError(
894
- HardwareErrorCode.NetworkError,
895
- error instanceof Error ? error.message : String(error)
896
- );
897
- }
898
- }
899
- return frame;
815
+ return this.callProtocolV2Usb(path, name, data, options);
900
816
  }
901
817
 
902
818
  /**
@@ -951,18 +867,63 @@ export default class WebUsbTransport {
951
867
  * Release device
952
868
  */
953
869
  async release(path: string) {
954
- const device: USBDevice = await this.findDevice(path);
955
- const endpoints = this.deviceEndpoints.get(path);
956
- const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
957
- await device.releaseInterface(ifaceNum);
958
- await device.close();
870
+ await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
871
+ await this.closeOpenDevice(path);
959
872
  this.deviceProtocol.delete(path);
960
873
  this.deviceProtocolHints.delete(path);
961
- this.protocolV2Assemblers.get(path)?.reset();
962
- this.protocolV2Assemblers.delete(path);
963
874
  this.deviceEndpoints.delete(path);
964
875
  }
965
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
+
966
927
  /**
967
928
  * Expose the detected protocol type for a given device path.
968
929
  * Used by upper layers (e.g. TransportManager) to select the correct schema.