@onekeyfe/hd-transport-web-device 1.2.0-alpha.13 → 1.2.0-alpha.131

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
@@ -6,8 +6,8 @@ import transport, {
6
6
  PROTOCOL_V1_USB_PACKET_SIZE,
7
7
  PROTOCOL_V2_CHANNEL_USB,
8
8
  PROTOCOL_V2_FRAME_MAX_BYTES,
9
- ProtocolV2FrameAssembler,
10
- ProtocolV2Session,
9
+ ProtocolV2LinkError,
10
+ ProtocolV2UsbTransportBase,
11
11
  probeProtocolV2 as probeProtocolV2Helper,
12
12
  } from '@onekeyfe/hd-transport';
13
13
  import {
@@ -19,12 +19,12 @@ import {
19
19
  } from '@onekeyfe/hd-shared';
20
20
  import ByteBuffer from 'bytebuffer';
21
21
 
22
- import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
23
-
24
22
  import type {
25
23
  AcquireInput,
26
24
  OneKeyDeviceInfoBase,
27
25
  ProtocolType,
26
+ ProtocolV2CallContext,
27
+ ProtocolV2Schemas,
28
28
  TransportCallOptions,
29
29
  } from '@onekeyfe/hd-transport';
30
30
 
@@ -39,7 +39,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
39
39
  const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
40
40
  const PACKET_IO_MAX_RETRIES = 3;
41
41
  const PACKET_IO_RETRY_DELAY = 300;
42
- const PROTOCOL_PROBE_TIMEOUT = 1000;
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;
43
48
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
44
49
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
45
50
  }
@@ -64,38 +69,22 @@ interface TransferCancelToken {
64
69
  cancelled: boolean;
65
70
  }
66
71
 
67
- export default class WebUsbTransport {
72
+ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
68
73
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
69
74
 
70
75
  /** Protobuf schema for Protocol V2 transports. */
71
76
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
72
77
 
78
+ private protocolV2SchemaSource: string | undefined;
79
+
73
80
  /** Per-path protocol type detected by active wire-level probe. */
74
81
  private deviceProtocol: Map<string, ProtocolType> = new Map();
75
82
 
76
83
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
77
84
 
78
- /** 按设备缓存 Protocol V2 frame assembler,保留同一次读取里多出来的后续 frame。 */
79
- private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
80
-
81
- /** 按设备缓存 Protocol V2 session,保持 seq 与设备会话一致递增。 */
82
- private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
83
-
84
- /** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */
85
- private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
86
-
87
85
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
88
86
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
89
87
 
90
- /**
91
- * 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。
92
- * 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path),
93
- * 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
94
- */
95
- private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
96
-
97
- private mockSerialCounter = 0;
98
-
99
88
  name = 'WebUsbTransport';
100
89
 
101
90
  stopped = false;
@@ -118,6 +107,14 @@ export default class WebUsbTransport {
118
107
 
119
108
  interfaceId = INTERFACE_ID;
120
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
+
121
118
  /**
122
119
  * Initialize WebUSB transport
123
120
  */
@@ -147,9 +144,20 @@ export default class WebUsbTransport {
147
144
  * Cache the Protocol V2 protobuf schema.
148
145
  */
149
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;
150
154
  this.messagesV2 = parseConfigure(signedData);
151
- this.protocolV2Sessions.clear();
152
- this.protocolV2ReadTimeouts.clear();
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
+ );
153
161
  }
154
162
 
155
163
  /**
@@ -181,26 +189,6 @@ export default class WebUsbTransport {
181
189
  return this.deviceList;
182
190
  }
183
191
 
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
192
  /**
205
193
  * Get list of connected devices
206
194
  */
@@ -213,11 +201,12 @@ export default class WebUsbTransport {
213
201
  (desc: { vendorId: number; productId: number }) =>
214
202
  dev.vendorId === desc.vendorId && dev.productId === desc.productId
215
203
  );
216
- return isOneKey && !isKnownTrezorWebUsbDevice(dev);
204
+ const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0;
205
+ return isOneKey && hasSerialNumber && !isKnownTrezorWebUsbDevice(dev);
217
206
  });
218
207
 
219
208
  this.deviceList = onekeyDevices.map(device => {
220
- const path = this.getDevicePath(device);
209
+ const path = device.serialNumber as string;
221
210
  const protocolHint = inferProtocolHintFromDeviceName(device.productName);
222
211
  if (protocolHint) {
223
212
  this.deviceProtocolHints.set(path, protocolHint);
@@ -239,13 +228,16 @@ export default class WebUsbTransport {
239
228
  async acquire(input: AcquireInput) {
240
229
  if (!input.path) return;
241
230
  try {
231
+ await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
242
232
  await this.closeOpenDevice(input.path);
243
233
  await this.connect(input.path ?? '', true);
244
234
  const deviceName = this.deviceList.find(device => device.path === input.path)?.device
245
235
  .productName;
246
236
  const protocolHint = input.expectedProtocol
247
237
  ? undefined
248
- : this.deviceProtocolHints.get(input.path) ?? inferProtocolHintFromDeviceName(deviceName);
238
+ : input.protocolHint ??
239
+ this.deviceProtocolHints.get(input.path) ??
240
+ inferProtocolHintFromDeviceName(deviceName);
249
241
  if (protocolHint) {
250
242
  this.deviceProtocolHints.set(input.path, protocolHint);
251
243
  }
@@ -253,6 +245,7 @@ export default class WebUsbTransport {
253
245
  return await Promise.resolve(input.path);
254
246
  } catch (e) {
255
247
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
248
+ await this.closeOpenDevice(input.path);
256
249
  throw e;
257
250
  }
258
251
  }
@@ -269,13 +262,17 @@ export default class WebUsbTransport {
269
262
  );
270
263
  }
271
264
 
272
- private createProtocolDetectionError() {
265
+ private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
273
266
  return ERRORS.TypedError(
274
- HardwareErrorCode.RuntimeError,
275
- '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`
276
269
  );
277
270
  }
278
271
 
272
+ private createProtocolDetectionError() {
273
+ return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
274
+ }
275
+
279
276
  private async detectProtocol(
280
277
  path: string,
281
278
  expectedProtocol?: ProtocolType,
@@ -286,29 +283,54 @@ export default class WebUsbTransport {
286
283
  this.deviceProtocol.set(path, 'V1');
287
284
  return 'V1';
288
285
  }
286
+ await this.closeConnectionAfterProbe(path);
289
287
  throw this.createProtocolMismatchError(expectedProtocol);
290
288
  }
291
289
 
292
290
  if (expectedProtocol === 'V2') {
293
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
294
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
295
- this.deviceProtocol.set(path, 'V2');
296
- 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
+ );
297
312
  }
298
313
 
299
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
300
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
301
- // 不能作为最终结论。
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.
302
316
  const probeOrder: ProtocolType[] =
303
317
  protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
304
318
 
305
- for (const protocol of probeOrder) {
319
+ for (const [index, protocol] of probeOrder.entries()) {
306
320
  const detected =
307
321
  protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
308
322
  if (detected) {
309
323
  this.deviceProtocol.set(path, protocol);
310
324
  return protocol;
311
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
+ }
312
334
  }
313
335
 
314
336
  this.deviceProtocol.delete(path);
@@ -420,16 +442,12 @@ export default class WebUsbTransport {
420
442
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
421
443
  const endpoints = this.discoverEndpoints(device);
422
444
  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
445
  await device.claimInterface(endpoints.interfaceNumber);
427
446
  await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
428
447
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
429
448
  }
430
449
 
431
450
  private async closeOpenDevice(path: string) {
432
- this.protocolV2Assemblers.get(path)?.reset();
433
451
  const current = this.deviceList.find(device => device.path === path)?.device;
434
452
  if (!current?.opened) return;
435
453
 
@@ -463,6 +481,10 @@ export default class WebUsbTransport {
463
481
  }
464
482
 
465
483
  async post(session: string, name: string, data: Record<string, unknown>) {
484
+ if (this.deviceProtocol.get(session) === 'V2') {
485
+ await this.sendProtocolV2UsbFlowControl(session, name, data);
486
+ return;
487
+ }
466
488
  await this.call(session, name, data);
467
489
  }
468
490
 
@@ -624,33 +646,30 @@ export default class WebUsbTransport {
624
646
  throw lastError;
625
647
  }
626
648
 
627
- private async resetConnectionAfterProbe(path: string) {
628
- this.protocolV2Assemblers.get(path)?.reset();
629
- this.protocolV2Sessions.delete(path);
630
- this.protocolV2ReadTimeouts.delete(path);
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);
649
+ private async transferInOnce(path: string, length: number): Promise<DataView> {
650
+ const device = await this.findDevice(path);
651
+ if (!device.opened) {
652
+ throw new Error('USBDevice is not open for transferIn');
646
653
  }
654
+ const endpoints = this.deviceEndpoints.get(path);
655
+ const endpointIn = endpoints?.endpointIn ?? this.endpointId;
656
+ const result = await device.transferIn(endpointIn, length);
657
+ return this.getTransferInData(result);
658
+ }
659
+
660
+ private async closeConnectionAfterProbe(path: string) {
661
+ await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
662
+ await this.closeOpenDevice(path);
663
+ }
647
664
 
665
+ private async resetConnectionAfterProbe(path: string) {
666
+ await this.closeConnectionAfterProbe(path);
648
667
  await this.getConnectedDevices();
649
668
  await this.connect(path, false);
650
669
  }
651
670
 
652
671
  private async withProtocolReadTimeout<T>(
653
- _path: string,
672
+ path: string,
654
673
  promise: Promise<T>,
655
674
  timeoutMs: number,
656
675
  protocol: ProtocolType,
@@ -672,9 +691,16 @@ export default class WebUsbTransport {
672
691
  return await Promise.race([
673
692
  guardedPromise,
674
693
  new Promise<never>((_, reject) => {
675
- timer = setTimeout(() => {
694
+ timer = setTimeout(async () => {
676
695
  timedOut = true;
677
696
  onTimeout?.();
697
+ if (protocol === 'V1') {
698
+ try {
699
+ await this.resetConnectionAfterProbe(path);
700
+ } catch (error) {
701
+ this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
702
+ }
703
+ }
678
704
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
679
705
  }, timeoutMs);
680
706
  }),
@@ -690,7 +716,14 @@ export default class WebUsbTransport {
690
716
  }
691
717
 
692
718
  try {
693
- await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
719
+ await this.callProtocolV1(
720
+ path,
721
+ 'Initialize',
722
+ {},
723
+ {
724
+ timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
725
+ }
726
+ );
694
727
  return true;
695
728
  } catch (_error) {
696
729
  return false;
@@ -704,10 +737,9 @@ export default class WebUsbTransport {
704
737
 
705
738
  return probeProtocolV2Helper({
706
739
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
707
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
740
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
708
741
  logger: this.Log,
709
742
  logPrefix: 'ProtocolV2 WebUSB',
710
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
711
743
  });
712
744
  }
713
745
 
@@ -737,10 +769,6 @@ export default class WebUsbTransport {
737
769
  );
738
770
  }
739
771
 
740
- if (!shouldSuppressHighVolumeCallLog(name)) {
741
- this.Log.debug('transport call', createTransportCallLog(name, protocol));
742
- }
743
-
744
772
  if (protocol === 'V2') {
745
773
  return this.callProtocolV2(path, name, data, options);
746
774
  }
@@ -788,94 +816,7 @@ export default class WebUsbTransport {
788
816
  data: Record<string, unknown>,
789
817
  options?: TransportCallOptions
790
818
  ) {
791
- const protocolV1Messages = this.messages;
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;
819
+ return this.callProtocolV2Usb(path, name, data, options);
879
820
  }
880
821
 
881
822
  /**
@@ -930,20 +871,63 @@ export default class WebUsbTransport {
930
871
  * Release device
931
872
  */
932
873
  async release(path: string) {
933
- const device: USBDevice = await this.findDevice(path);
934
- const endpoints = this.deviceEndpoints.get(path);
935
- const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
936
- await device.releaseInterface(ifaceNum);
937
- await device.close();
874
+ await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
875
+ await this.closeOpenDevice(path);
938
876
  this.deviceProtocol.delete(path);
939
877
  this.deviceProtocolHints.delete(path);
940
- this.protocolV2Assemblers.get(path)?.reset();
941
- this.protocolV2Assemblers.delete(path);
942
- this.protocolV2Sessions.delete(path);
943
- this.protocolV2ReadTimeouts.delete(path);
944
878
  this.deviceEndpoints.delete(path);
945
879
  }
946
880
 
881
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
882
+ if (!this.messages || !this.messagesV2) {
883
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
884
+ }
885
+ return {
886
+ protocolV1: this.messages,
887
+ protocolV2: this.messagesV2,
888
+ };
889
+ }
890
+
891
+ protected getProtocolV2UsbLogger() {
892
+ return this.Log;
893
+ }
894
+
895
+ protected async writeProtocolV2UsbPacket(
896
+ path: string,
897
+ frame: Uint8Array,
898
+ _context: ProtocolV2CallContext
899
+ ): Promise<void> {
900
+ await this.transferOutOnce(path, frame);
901
+ }
902
+
903
+ protected async readProtocolV2UsbPacket(
904
+ path: string,
905
+ _context: ProtocolV2CallContext
906
+ ): Promise<Uint8Array> {
907
+ const dataView = await this.transferInOnce(path, PROTOCOL_V2_FRAME_MAX_BYTES);
908
+ return new Uint8Array(
909
+ this.toArrayBuffer(
910
+ dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
911
+ )
912
+ );
913
+ }
914
+
915
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
916
+ await this.closeOpenDevice(path);
917
+ }
918
+
919
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
920
+ this.deviceProtocol.delete(path);
921
+ this.Log?.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
922
+ }
923
+
924
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
925
+ return new ProtocolV2LinkError(
926
+ 'response-timeout',
927
+ `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
928
+ );
929
+ }
930
+
947
931
  /**
948
932
  * Expose the detected protocol type for a given device path.
949
933
  * Used by upper layers (e.g. TransportManager) to select the correct schema.