@onekeyfe/hd-transport-web-device 1.2.0-alpha.12 → 1.2.0-alpha.121

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,19 +6,25 @@ 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
- 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';
14
20
  import ByteBuffer from 'bytebuffer';
15
21
 
16
- import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
17
-
18
22
  import type {
19
23
  AcquireInput,
20
24
  OneKeyDeviceInfoBase,
21
25
  ProtocolType,
26
+ ProtocolV2CallContext,
27
+ ProtocolV2Schemas,
22
28
  TransportCallOptions,
23
29
  } from '@onekeyfe/hd-transport';
24
30
 
@@ -33,7 +39,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
33
39
  const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
34
40
  const PACKET_IO_MAX_RETRIES = 3;
35
41
  const PACKET_IO_RETRY_DELAY = 300;
36
- 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;
37
48
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
38
49
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
39
50
  }
@@ -58,38 +69,22 @@ interface TransferCancelToken {
58
69
  cancelled: boolean;
59
70
  }
60
71
 
61
- export default class WebUsbTransport {
72
+ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
62
73
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
63
74
 
64
75
  /** Protobuf schema for Protocol V2 transports. */
65
76
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
66
77
 
78
+ private protocolV2SchemaSource: string | undefined;
79
+
67
80
  /** Per-path protocol type detected by active wire-level probe. */
68
81
  private deviceProtocol: Map<string, ProtocolType> = new Map();
69
82
 
70
83
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
71
84
 
72
- /** 按设备缓存 Protocol V2 frame assembler,保留同一次读取里多出来的后续 frame。 */
73
- private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
74
-
75
- /** 按设备缓存 Protocol V2 session,保持 seq 与设备会话一致递增。 */
76
- private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
77
-
78
- /** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */
79
- private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
80
-
81
85
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
82
86
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
83
87
 
84
- /**
85
- * 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。
86
- * 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path),
87
- * 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。
88
- */
89
- private mockSerialPaths: WeakMap<USBDevice, string> = new WeakMap();
90
-
91
- private mockSerialCounter = 0;
92
-
93
88
  name = 'WebUsbTransport';
94
89
 
95
90
  stopped = false;
@@ -112,6 +107,14 @@ export default class WebUsbTransport {
112
107
 
113
108
  interfaceId = INTERFACE_ID;
114
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
+
115
118
  /**
116
119
  * Initialize WebUSB transport
117
120
  */
@@ -141,9 +144,20 @@ export default class WebUsbTransport {
141
144
  * Cache the Protocol V2 protobuf schema.
142
145
  */
143
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;
144
154
  this.messagesV2 = parseConfigure(signedData);
145
- this.protocolV2Sessions.clear();
146
- 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
+ );
147
161
  }
148
162
 
149
163
  /**
@@ -175,26 +189,6 @@ export default class WebUsbTransport {
175
189
  return this.deviceList;
176
190
  }
177
191
 
178
- /**
179
- * 设备 path:正常设备直接用 USB serial number;
180
- * 空 serial(早期工程板)回退到会话内稳定的 mock path。
181
- */
182
- private getDevicePath(device: USBDevice): string {
183
- if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) {
184
- return device.serialNumber;
185
- }
186
- let path = this.mockSerialPaths.get(device);
187
- if (!path) {
188
- this.mockSerialCounter += 1;
189
- path = `mock-serial:${device.vendorId.toString(16)}:${device.productId.toString(16)}:${
190
- this.mockSerialCounter
191
- }`;
192
- this.mockSerialPaths.set(device, path);
193
- this.Log?.debug(`[WebUSB] device has no serial number, using mock path: ${path}`);
194
- }
195
- return path;
196
- }
197
-
198
192
  /**
199
193
  * Get list of connected devices
200
194
  */
@@ -202,14 +196,17 @@ export default class WebUsbTransport {
202
196
  if (!this.usb) return [];
203
197
 
204
198
  const devices = await this.usb.getDevices();
205
- const onekeyDevices = devices.filter(dev =>
206
- ONEKEY_WEBUSB_FILTER.some(
207
- desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId
208
- )
209
- );
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
+ });
210
207
 
211
208
  this.deviceList = onekeyDevices.map(device => {
212
- const path = this.getDevicePath(device);
209
+ const path = device.serialNumber as string;
213
210
  const protocolHint = inferProtocolHintFromDeviceName(device.productName);
214
211
  if (protocolHint) {
215
212
  this.deviceProtocolHints.set(path, protocolHint);
@@ -231,13 +228,16 @@ export default class WebUsbTransport {
231
228
  async acquire(input: AcquireInput) {
232
229
  if (!input.path) return;
233
230
  try {
231
+ await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
234
232
  await this.closeOpenDevice(input.path);
235
233
  await this.connect(input.path ?? '', true);
236
234
  const deviceName = this.deviceList.find(device => device.path === input.path)?.device
237
235
  .productName;
238
236
  const protocolHint = input.expectedProtocol
239
237
  ? undefined
240
- : this.deviceProtocolHints.get(input.path) ?? inferProtocolHintFromDeviceName(deviceName);
238
+ : input.protocolHint ??
239
+ this.deviceProtocolHints.get(input.path) ??
240
+ inferProtocolHintFromDeviceName(deviceName);
241
241
  if (protocolHint) {
242
242
  this.deviceProtocolHints.set(input.path, protocolHint);
243
243
  }
@@ -245,6 +245,7 @@ export default class WebUsbTransport {
245
245
  return await Promise.resolve(input.path);
246
246
  } catch (e) {
247
247
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
248
+ await this.closeOpenDevice(input.path);
248
249
  throw e;
249
250
  }
250
251
  }
@@ -261,13 +262,17 @@ export default class WebUsbTransport {
261
262
  );
262
263
  }
263
264
 
264
- private createProtocolDetectionError() {
265
+ private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
265
266
  return ERRORS.TypedError(
266
- HardwareErrorCode.RuntimeError,
267
- '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`
268
269
  );
269
270
  }
270
271
 
272
+ private createProtocolDetectionError() {
273
+ return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
274
+ }
275
+
271
276
  private async detectProtocol(
272
277
  path: string,
273
278
  expectedProtocol?: ProtocolType,
@@ -278,29 +283,54 @@ export default class WebUsbTransport {
278
283
  this.deviceProtocol.set(path, 'V1');
279
284
  return 'V1';
280
285
  }
286
+ await this.closeConnectionAfterProbe(path);
281
287
  throw this.createProtocolMismatchError(expectedProtocol);
282
288
  }
283
289
 
284
290
  if (expectedProtocol === 'V2') {
285
- // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景,
286
- // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。
287
- this.deviceProtocol.set(path, 'V2');
288
- 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
+ );
289
312
  }
290
313
 
291
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
292
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
293
- // 不能作为最终结论。
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.
294
316
  const probeOrder: ProtocolType[] =
295
317
  protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
296
318
 
297
- for (const protocol of probeOrder) {
319
+ for (const [index, protocol] of probeOrder.entries()) {
298
320
  const detected =
299
321
  protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
300
322
  if (detected) {
301
323
  this.deviceProtocol.set(path, protocol);
302
324
  return protocol;
303
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
+ }
304
334
  }
305
335
 
306
336
  this.deviceProtocol.delete(path);
@@ -412,16 +442,12 @@ export default class WebUsbTransport {
412
442
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
413
443
  const endpoints = this.discoverEndpoints(device);
414
444
  this.deviceEndpoints.set(path, endpoints);
415
- this.protocolV2Assemblers.get(path)?.reset();
416
- this.protocolV2Assemblers.set(path, new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES));
417
-
418
445
  await device.claimInterface(endpoints.interfaceNumber);
419
446
  await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
420
447
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
421
448
  }
422
449
 
423
450
  private async closeOpenDevice(path: string) {
424
- this.protocolV2Assemblers.get(path)?.reset();
425
451
  const current = this.deviceList.find(device => device.path === path)?.device;
426
452
  if (!current?.opened) return;
427
453
 
@@ -616,33 +642,30 @@ export default class WebUsbTransport {
616
642
  throw lastError;
617
643
  }
618
644
 
619
- private async resetConnectionAfterProbe(path: string) {
620
- this.protocolV2Assemblers.get(path)?.reset();
621
- this.protocolV2Sessions.delete(path);
622
- this.protocolV2ReadTimeouts.delete(path);
623
-
624
- try {
625
- const device = await this.findDevice(path);
626
- if (device.opened) {
627
- const endpoints = this.deviceEndpoints.get(path);
628
- const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
629
- try {
630
- await device.releaseInterface(ifaceNum);
631
- } catch (error) {
632
- this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error);
633
- }
634
- await device.close();
635
- }
636
- } catch (error) {
637
- 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');
638
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
+ }
655
+
656
+ private async closeConnectionAfterProbe(path: string) {
657
+ await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
658
+ await this.closeOpenDevice(path);
659
+ }
639
660
 
661
+ private async resetConnectionAfterProbe(path: string) {
662
+ await this.closeConnectionAfterProbe(path);
640
663
  await this.getConnectedDevices();
641
664
  await this.connect(path, false);
642
665
  }
643
666
 
644
667
  private async withProtocolReadTimeout<T>(
645
- _path: string,
668
+ path: string,
646
669
  promise: Promise<T>,
647
670
  timeoutMs: number,
648
671
  protocol: ProtocolType,
@@ -664,9 +687,16 @@ export default class WebUsbTransport {
664
687
  return await Promise.race([
665
688
  guardedPromise,
666
689
  new Promise<never>((_, reject) => {
667
- timer = setTimeout(() => {
690
+ timer = setTimeout(async () => {
668
691
  timedOut = true;
669
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
+ }
670
700
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
671
701
  }, timeoutMs);
672
702
  }),
@@ -682,7 +712,14 @@ export default class WebUsbTransport {
682
712
  }
683
713
 
684
714
  try {
685
- 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
+ );
686
723
  return true;
687
724
  } catch (_error) {
688
725
  return false;
@@ -696,10 +733,9 @@ export default class WebUsbTransport {
696
733
 
697
734
  return probeProtocolV2Helper({
698
735
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
699
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
736
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
700
737
  logger: this.Log,
701
738
  logPrefix: 'ProtocolV2 WebUSB',
702
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
703
739
  });
704
740
  }
705
741
 
@@ -729,10 +765,6 @@ export default class WebUsbTransport {
729
765
  );
730
766
  }
731
767
 
732
- if (!shouldSuppressHighVolumeCallLog(name)) {
733
- this.Log.debug('transport call', createTransportCallLog(name, protocol));
734
- }
735
-
736
768
  if (protocol === 'V2') {
737
769
  return this.callProtocolV2(path, name, data, options);
738
770
  }
@@ -780,83 +812,7 @@ export default class WebUsbTransport {
780
812
  data: Record<string, unknown>,
781
813
  options?: TransportCallOptions
782
814
  ) {
783
- const protocolV1Messages = this.messages;
784
- if (!this.messagesV2) {
785
- throw ERRORS.TypedError(
786
- HardwareErrorCode.TransportNotConfigured,
787
- 'Protocol V2 schema not configured'
788
- );
789
- }
790
- if (!protocolV1Messages) {
791
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
792
- }
793
-
794
- let session = this.protocolV2Sessions.get(path);
795
- if (!session) {
796
- session = new ProtocolV2Session({
797
- schemas: {
798
- protocolV1: protocolV1Messages,
799
- protocolV2: this.messagesV2,
800
- },
801
- router: PROTOCOL_V2_CHANNEL_USB,
802
- writeFrame: (frame: Uint8Array) => this.transferOutOnce(path, frame),
803
- readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
804
- logger: this.Log,
805
- logPrefix: 'ProtocolV2 WebUSB',
806
- createTimeoutError: (messageName: string, timeoutMs: number) =>
807
- new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
808
- });
809
- this.protocolV2Sessions.set(path, session);
810
- }
811
-
812
- this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
813
- this.protocolV2Assemblers.get(path)?.reset();
814
- try {
815
- return await session.call(name, data, options);
816
- } finally {
817
- this.protocolV2ReadTimeouts.delete(path);
818
- }
819
- }
820
-
821
- private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
822
- let assembler = this.protocolV2Assemblers.get(path);
823
- if (!assembler) {
824
- assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
825
- this.protocolV2Assemblers.set(path, assembler);
826
- }
827
-
828
- let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
829
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
830
-
831
- while (!frame) {
832
- const cancelToken = { cancelled: false };
833
- const transferIn = this.transferInWithRetry(path, PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken);
834
- const dataView = deadline
835
- ? await this.withProtocolReadTimeout(
836
- path,
837
- transferIn,
838
- Math.max(deadline - Date.now(), 1),
839
- 'V2',
840
- () => {
841
- cancelToken.cancelled = true;
842
- }
843
- )
844
- : await transferIn;
845
- const bytes = new Uint8Array(
846
- this.toArrayBuffer(
847
- dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
848
- )
849
- );
850
- try {
851
- frame = assembler.push(bytes);
852
- } catch (error) {
853
- throw ERRORS.TypedError(
854
- HardwareErrorCode.NetworkError,
855
- error instanceof Error ? error.message : String(error)
856
- );
857
- }
858
- }
859
- return frame;
815
+ return this.callProtocolV2Usb(path, name, data, options);
860
816
  }
861
817
 
862
818
  /**
@@ -911,18 +867,63 @@ export default class WebUsbTransport {
911
867
  * Release device
912
868
  */
913
869
  async release(path: string) {
914
- const device: USBDevice = await this.findDevice(path);
915
- const endpoints = this.deviceEndpoints.get(path);
916
- const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId;
917
- await device.releaseInterface(ifaceNum);
918
- await device.close();
870
+ await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
871
+ await this.closeOpenDevice(path);
919
872
  this.deviceProtocol.delete(path);
920
873
  this.deviceProtocolHints.delete(path);
921
- this.protocolV2Assemblers.get(path)?.reset();
922
- this.protocolV2Assemblers.delete(path);
923
874
  this.deviceEndpoints.delete(path);
924
875
  }
925
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
+
926
927
  /**
927
928
  * Expose the detected protocol type for a given device path.
928
929
  * Used by upper layers (e.g. TransportManager) to select the correct schema.