@onekeyfe/hd-transport-web-device 1.2.0-alpha.3 → 1.2.0-alpha.31

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,32 @@
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
 
22
+ import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
23
+
17
24
  import type {
18
25
  AcquireInput,
19
26
  OneKeyDeviceInfoBase,
20
27
  ProtocolType,
28
+ ProtocolV2CallContext,
29
+ ProtocolV2Schemas,
21
30
  TransportCallOptions,
22
31
  } from '@onekeyfe/hd-transport';
23
32
 
@@ -33,22 +42,7 @@ const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
33
42
  const PACKET_IO_MAX_RETRIES = 3;
34
43
  const PACKET_IO_RETRY_DELAY = 300;
35
44
  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
-
45
+ const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
52
46
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
53
47
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
54
48
  }
@@ -73,33 +67,25 @@ interface TransferCancelToken {
73
67
  cancelled: boolean;
74
68
  }
75
69
 
76
- export default class WebUsbTransport {
70
+ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
77
71
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
78
72
 
79
73
  /** Protobuf schema for Protocol V2 transports. */
80
74
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
81
75
 
76
+ private protocolV2SchemaSource: string | undefined;
77
+
82
78
  /** Per-path protocol type detected by active wire-level probe. */
83
79
  private deviceProtocol: Map<string, ProtocolType> = new Map();
84
80
 
85
81
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
86
82
 
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
83
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
97
84
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
98
85
 
99
86
  /**
100
- * 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。
101
- * 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path),
102
- * 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
87
+ * Early Pro2 boards have no USB serial number. Assign a session-stable mock path per
88
+ * USBDevice instance so discovery retains them; reconnecting creates a new instance/path.
103
89
  */
104
90
  private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
105
91
 
@@ -127,6 +113,14 @@ export default class WebUsbTransport {
127
113
 
128
114
  interfaceId = INTERFACE_ID;
129
115
 
116
+ constructor() {
117
+ super({
118
+ router: PROTOCOL_V2_CHANNEL_USB,
119
+ maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
120
+ logPrefix: 'ProtocolV2 WebUSB',
121
+ });
122
+ }
123
+
130
124
  /**
131
125
  * Initialize WebUSB transport
132
126
  */
@@ -156,10 +150,20 @@ export default class WebUsbTransport {
156
150
  * Cache the Protocol V2 protobuf schema.
157
151
  */
158
152
  configureProtocolV2(signedData: any) {
153
+ const schemaSource =
154
+ typeof signedData === 'string'
155
+ ? signedData
156
+ : JSON.stringify(signedData) ?? String(signedData);
157
+ if (schemaSource === this.protocolV2SchemaSource) return;
158
+
159
+ const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
159
160
  this.messagesV2 = parseConfigure(signedData);
160
- this.protocolV2Sessions.clear();
161
- this.protocolV2ReadTimeouts.clear();
162
- this.Log?.debug('[WebUsbTransport] Protocol V2 schema configured');
161
+ this.protocolV2SchemaSource = schemaSource;
162
+ if (!hadProtocolV2Schema) return;
163
+
164
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
165
+ this.Log?.debug('[WebUsbTransport] schema link cleanup failed:', error)
166
+ );
163
167
  }
164
168
 
165
169
  /**
@@ -192,8 +196,7 @@ export default class WebUsbTransport {
192
196
  }
193
197
 
194
198
  /**
195
- * 设备 path:正常设备直接用 USB serial number;
196
- * 空 serial(早期工程板)回退到会话内稳定的 mock path。
199
+ * Use the USB serial as the device path, falling back to a session-stable mock path.
197
200
  */
198
201
  private getDevicePath(device: USBDevice): string {
199
202
  if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
@@ -218,11 +221,13 @@ export default class WebUsbTransport {
218
221
  if (!this.usb) return [];
219
222
 
220
223
  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
- );
224
+ const onekeyDevices = devices.filter(dev => {
225
+ const isOneKey = ONEKEY_WEBUSB_FILTER.some(
226
+ (desc: { vendorId: number; productId: number }) =>
227
+ dev.vendorId === desc.vendorId && dev.productId === desc.productId
228
+ );
229
+ return isOneKey && !isKnownTrezorWebUsbDevice(dev);
230
+ });
226
231
 
227
232
  this.deviceList = onekeyDevices.map(device => {
228
233
  const path = this.getDevicePath(device);
@@ -238,14 +243,6 @@ export default class WebUsbTransport {
238
243
  };
239
244
  });
240
245
 
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
246
  return this.deviceList;
250
247
  }
251
248
 
@@ -255,6 +252,7 @@ export default class WebUsbTransport {
255
252
  async acquire(input: AcquireInput) {
256
253
  if (!input.path) return;
257
254
  try {
255
+ await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
258
256
  await this.closeOpenDevice(input.path);
259
257
  await this.connect(input.path ?? '', true);
260
258
  const deviceName = this.deviceList.find(device => device.path === input.path)?.device
@@ -285,6 +283,13 @@ export default class WebUsbTransport {
285
283
  );
286
284
  }
287
285
 
286
+ private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
287
+ return ERRORS.TypedError(
288
+ HardwareErrorCode.RuntimeError,
289
+ `Protocol ${expected} probe timeout after ${attempts} attempts`
290
+ );
291
+ }
292
+
288
293
  private createProtocolDetectionError() {
289
294
  return ERRORS.TypedError(
290
295
  HardwareErrorCode.RuntimeError,
@@ -300,23 +305,36 @@ export default class WebUsbTransport {
300
305
  if (expectedProtocol === 'V1') {
301
306
  if (await this.probeProtocolV1(path)) {
302
307
  this.deviceProtocol.set(path, 'V1');
303
- this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
304
308
  return 'V1';
305
309
  }
310
+ await this.resetConnectionAfterProbe(path);
306
311
  throw this.createProtocolMismatchError(expectedProtocol);
307
312
  }
308
313
 
309
314
  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
+ for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
316
+ if (await this.probeProtocolV2(path)) {
317
+ this.deviceProtocol.set(path, 'V2');
318
+ return 'V2';
319
+ }
320
+ await this.resetConnectionAfterProbe(path);
321
+ if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
322
+ this.Log?.debug(
323
+ `[WebUsbTransport] Protocol V2 probe timed out, retrying ${
324
+ attempt + 1
325
+ }/${EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS}`
326
+ );
327
+ }
328
+ }
329
+ this.deviceProtocol.delete(path);
330
+ throw this.createProtocolProbeTimeoutError(
331
+ expectedProtocol,
332
+ EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS
333
+ );
315
334
  }
316
335
 
317
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
318
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
319
- // 不能作为最终结论。
336
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
337
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
320
338
  const probeOrder: ProtocolType[] =
321
339
  protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
322
340
 
@@ -325,9 +343,12 @@ export default class WebUsbTransport {
325
343
  protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
326
344
  if (detected) {
327
345
  this.deviceProtocol.set(path, protocol);
328
- this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
329
346
  return protocol;
330
347
  }
348
+ // A timed-out WebUSB transferIn cannot be cancelled in place. Closing and
349
+ // reopening the device guarantees the next protocol probe cannot consume a
350
+ // late response from the previous protocol generation.
351
+ await this.resetConnectionAfterProbe(path);
331
352
  }
332
353
 
333
354
  this.deviceProtocol.delete(path);
@@ -414,13 +435,6 @@ export default class WebUsbTransport {
414
435
  */
415
436
  async connectToDevice(path: string, first: boolean) {
416
437
  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
438
  if (!device.opened) {
425
439
  await device.open();
426
440
  }
@@ -446,16 +460,12 @@ export default class WebUsbTransport {
446
460
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
447
461
  const endpoints = this.discoverEndpoints(device);
448
462
  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
463
  await device.claimInterface(endpoints.interfaceNumber);
453
464
  await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
454
465
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
455
466
  }
456
467
 
457
468
  private async closeOpenDevice(path: string) {
458
- this.protocolV2Assemblers.get(path)?.reset();
459
469
  const current = this.deviceList.find(device => device.path === path)?.device;
460
470
  if (!current?.opened) return;
461
471
 
@@ -571,16 +581,7 @@ export default class WebUsbTransport {
571
581
  let lastError: unknown;
572
582
  for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt += 1) {
573
583
  try {
574
- const device = await this.findDevice(path);
575
- if (!device.opened) {
576
- await this.connect(path, false);
577
- }
578
- const endpoints = this.deviceEndpoints.get(path);
579
- const endpointOut = endpoints?.endpointOut ?? this.endpointId;
580
- const transferBuffer = this.toArrayBuffer(
581
- packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
582
- );
583
- await device.transferOut(endpointOut, transferBuffer);
584
+ await this.transferOutOnce(path, packet);
584
585
  return;
585
586
  } catch (error) {
586
587
  lastError = error;
@@ -603,6 +604,19 @@ export default class WebUsbTransport {
603
604
  throw lastError;
604
605
  }
605
606
 
607
+ private async transferOutOnce(path: string, packet: Uint8Array) {
608
+ const device = await this.findDevice(path);
609
+ if (!device.opened) {
610
+ await this.connect(path, false);
611
+ }
612
+ const endpoints = this.deviceEndpoints.get(path);
613
+ const endpointOut = endpoints?.endpointOut ?? this.endpointId;
614
+ const transferBuffer = this.toArrayBuffer(
615
+ packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
616
+ );
617
+ await device.transferOut(endpointOut, transferBuffer);
618
+ }
619
+
606
620
  private async transferInWithRetry(
607
621
  path: string,
608
622
  length: number,
@@ -646,10 +660,19 @@ export default class WebUsbTransport {
646
660
  throw lastError;
647
661
  }
648
662
 
663
+ private async transferInOnce(path: string, length: number): Promise<DataView> {
664
+ const device = await this.findDevice(path);
665
+ if (!device.opened) {
666
+ throw new Error('USBDevice is not open for transferIn');
667
+ }
668
+ const endpoints = this.deviceEndpoints.get(path);
669
+ const endpointIn = endpoints?.endpointIn ?? this.endpointId;
670
+ const result = await device.transferIn(endpointIn, length);
671
+ return this.getTransferInData(result);
672
+ }
673
+
649
674
  private async resetConnectionAfterProbe(path: string) {
650
- this.protocolV2Assemblers.get(path)?.reset();
651
- this.protocolV2Sessions.delete(path);
652
- this.protocolV2ReadTimeouts.delete(path);
675
+ await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
653
676
 
654
677
  try {
655
678
  const device = await this.findDevice(path);
@@ -678,7 +701,6 @@ export default class WebUsbTransport {
678
701
  protocol: ProtocolType,
679
702
  onTimeout?: () => void
680
703
  ): Promise<T> {
681
- void path;
682
704
  let timer: ReturnType<typeof setTimeout> | undefined;
683
705
  let timedOut = false;
684
706
  const waitForeverAfterTimeout = () => new Promise<never>(() => {});
@@ -698,6 +720,13 @@ export default class WebUsbTransport {
698
720
  timer = setTimeout(async () => {
699
721
  timedOut = true;
700
722
  onTimeout?.();
723
+ if (protocol === 'V1') {
724
+ try {
725
+ await this.resetConnectionAfterProbe(path);
726
+ } catch (error) {
727
+ this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
728
+ }
729
+ }
701
730
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
702
731
  }, timeoutMs);
703
732
  }),
@@ -715,8 +744,7 @@ export default class WebUsbTransport {
715
744
  try {
716
745
  await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
717
746
  return true;
718
- } catch (error) {
719
- this.Log.debug('[WebUsbTransport] Protocol V1 Initialize probe failed:', error);
747
+ } catch (_error) {
720
748
  return false;
721
749
  }
722
750
  }
@@ -731,7 +759,6 @@ export default class WebUsbTransport {
731
759
  timeoutMs: PROTOCOL_PROBE_TIMEOUT,
732
760
  logger: this.Log,
733
761
  logPrefix: 'ProtocolV2 WebUSB',
734
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
735
762
  });
736
763
  }
737
764
 
@@ -761,12 +788,8 @@ export default class WebUsbTransport {
761
788
  );
762
789
  }
763
790
 
764
- if (shouldSuppressWebUsbCallLog(name)) {
765
- // 高频文件写入不要逐包发 debug 事件,否则浏览器侧会被日志处理拖慢。
766
- } else if (shouldBlockWebUsbCallDataLog(name)) {
767
- this.Log.debug('call-', ' name: ', name, ' protocol: ', protocol);
768
- } else {
769
- this.Log.debug('call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
791
+ if (!shouldSuppressHighVolumeCallLog(name)) {
792
+ this.Log.debug('transport call', createTransportCallLog(name, protocol));
770
793
  }
771
794
 
772
795
  if (protocol === 'V2') {
@@ -816,83 +839,7 @@ export default class WebUsbTransport {
816
839
  data: Record<string, unknown>,
817
840
  options?: TransportCallOptions
818
841
  ) {
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;
842
+ return this.callProtocolV2Usb(path, name, data, options);
896
843
  }
897
844
 
898
845
  /**
@@ -947,18 +894,63 @@ export default class WebUsbTransport {
947
894
  * Release device
948
895
  */
949
896
  async release(path: string) {
950
- const device: USBDevice = await this.findDevice(path);
951
- const endpoints = this.deviceEndpoints.get(path);
952
- const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
953
- await device.releaseInterface(ifaceNum);
954
- await device.close();
897
+ await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
898
+ await this.closeOpenDevice(path);
955
899
  this.deviceProtocol.delete(path);
956
900
  this.deviceProtocolHints.delete(path);
957
- this.protocolV2Assemblers.get(path)?.reset();
958
- this.protocolV2Assemblers.delete(path);
959
901
  this.deviceEndpoints.delete(path);
960
902
  }
961
903
 
904
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
905
+ if (!this.messages || !this.messagesV2) {
906
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
907
+ }
908
+ return {
909
+ protocolV1: this.messages,
910
+ protocolV2: this.messagesV2,
911
+ };
912
+ }
913
+
914
+ protected getProtocolV2UsbLogger() {
915
+ return this.Log;
916
+ }
917
+
918
+ protected async writeProtocolV2UsbPacket(
919
+ path: string,
920
+ frame: Uint8Array,
921
+ _context: ProtocolV2CallContext
922
+ ): Promise<void> {
923
+ await this.transferOutOnce(path, frame);
924
+ }
925
+
926
+ protected async readProtocolV2UsbPacket(
927
+ path: string,
928
+ _context: ProtocolV2CallContext
929
+ ): Promise<Uint8Array> {
930
+ const dataView = await this.transferInOnce(path, PROTOCOL_V2_FRAME_MAX_BYTES);
931
+ return new Uint8Array(
932
+ this.toArrayBuffer(
933
+ dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
934
+ )
935
+ );
936
+ }
937
+
938
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
939
+ await this.closeOpenDevice(path);
940
+ }
941
+
942
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
943
+ this.deviceProtocol.delete(path);
944
+ this.Log?.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
945
+ }
946
+
947
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
948
+ return new ProtocolV2LinkError(
949
+ 'response-timeout',
950
+ `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
951
+ );
952
+ }
953
+
962
954
  /**
963
955
  * Expose the detected protocol type for a given device path.
964
956
  * Used by upper layers (e.g. TransportManager) to select the correct schema.