@onekeyfe/hd-transport-web-device 1.2.0-alpha.7 → 1.2.0-alpha.71

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.
@@ -0,0 +1 @@
1
+ export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
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
 
@@ -32,23 +41,12 @@ const REPORT_ID = PROTOCOL_V1_REPORT_ID;
32
41
  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
- 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
-
44
+ // Legacy devices can take longer to answer Initialize after a WebUSB reset.
45
+ // Keep this aligned with Node USB so a slow Protocol V1 response is not
46
+ // misreported as a protocol mismatch during acquire.
47
+ const PROTOCOL_V1_PROBE_TIMEOUT = 5000;
48
+ const PROTOCOL_V2_PROBE_TIMEOUT = 1000;
49
+ const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
52
50
  function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
53
51
  return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
54
52
  }
@@ -73,38 +71,22 @@ interface TransferCancelToken {
73
71
  cancelled: boolean;
74
72
  }
75
73
 
76
- export default class WebUsbTransport {
74
+ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
77
75
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
78
76
 
79
77
  /** Protobuf schema for Protocol V2 transports. */
80
78
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
81
79
 
80
+ private protocolV2SchemaSource: string | undefined;
81
+
82
82
  /** Per-path protocol type detected by active wire-level probe. */
83
83
  private deviceProtocol: Map<string, ProtocolType> = new Map();
84
84
 
85
85
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
86
86
 
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
87
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
97
88
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
98
89
 
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
90
  name = 'WebUsbTransport';
109
91
 
110
92
  stopped = false;
@@ -127,6 +109,14 @@ export default class WebUsbTransport {
127
109
 
128
110
  interfaceId = INTERFACE_ID;
129
111
 
112
+ constructor() {
113
+ super({
114
+ router: PROTOCOL_V2_CHANNEL_USB,
115
+ maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
116
+ logPrefix: 'ProtocolV2 WebUSB',
117
+ });
118
+ }
119
+
130
120
  /**
131
121
  * Initialize WebUSB transport
132
122
  */
@@ -156,10 +146,20 @@ export default class WebUsbTransport {
156
146
  * Cache the Protocol V2 protobuf schema.
157
147
  */
158
148
  configureProtocolV2(signedData: any) {
149
+ const schemaSource =
150
+ typeof signedData === 'string'
151
+ ? signedData
152
+ : JSON.stringify(signedData) ?? String(signedData);
153
+ if (schemaSource === this.protocolV2SchemaSource) return;
154
+
155
+ const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
159
156
  this.messagesV2 = parseConfigure(signedData);
160
- this.protocolV2Sessions.clear();
161
- this.protocolV2ReadTimeouts.clear();
162
- this.Log?.debug('[WebUsbTransport] Protocol V2 schema configured');
157
+ this.protocolV2SchemaSource = schemaSource;
158
+ if (!hadProtocolV2Schema) return;
159
+
160
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
161
+ this.Log?.debug('[WebUsbTransport] schema link cleanup failed:', error)
162
+ );
163
163
  }
164
164
 
165
165
  /**
@@ -191,26 +191,6 @@ export default class WebUsbTransport {
191
191
  return this.deviceList;
192
192
  }
193
193
 
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
194
  /**
215
195
  * Get list of connected devices
216
196
  */
@@ -218,14 +198,17 @@ export default class WebUsbTransport {
218
198
  if (!this.usb) return [];
219
199
 
220
200
  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
- );
201
+ const onekeyDevices = devices.filter(dev => {
202
+ const isOneKey = ONEKEY_WEBUSB_FILTER.some(
203
+ (desc: { vendorId: number; productId: number }) =>
204
+ dev.vendorId === desc.vendorId && dev.productId === desc.productId
205
+ );
206
+ const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0;
207
+ return isOneKey && hasSerialNumber && !isKnownTrezorWebUsbDevice(dev);
208
+ });
226
209
 
227
210
  this.deviceList = onekeyDevices.map(device => {
228
- const path = this.getDevicePath(device);
211
+ const path = device.serialNumber as string;
229
212
  const protocolHint = inferProtocolHintFromDeviceName(device.productName);
230
213
  if (protocolHint) {
231
214
  this.deviceProtocolHints.set(path, protocolHint);
@@ -238,14 +221,6 @@ export default class WebUsbTransport {
238
221
  };
239
222
  });
240
223
 
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
224
  return this.deviceList;
250
225
  }
251
226
 
@@ -255,13 +230,16 @@ export default class WebUsbTransport {
255
230
  async acquire(input: AcquireInput) {
256
231
  if (!input.path) return;
257
232
  try {
233
+ await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
258
234
  await this.closeOpenDevice(input.path);
259
235
  await this.connect(input.path ?? '', true);
260
236
  const deviceName = this.deviceList.find(device => device.path === input.path)?.device
261
237
  .productName;
262
238
  const protocolHint = input.expectedProtocol
263
239
  ? undefined
264
- : this.deviceProtocolHints.get(input.path) ?? inferProtocolHintFromDeviceName(deviceName);
240
+ : input.protocolHint ??
241
+ this.deviceProtocolHints.get(input.path) ??
242
+ inferProtocolHintFromDeviceName(deviceName);
265
243
  if (protocolHint) {
266
244
  this.deviceProtocolHints.set(input.path, protocolHint);
267
245
  }
@@ -269,6 +247,7 @@ export default class WebUsbTransport {
269
247
  return await Promise.resolve(input.path);
270
248
  } catch (e) {
271
249
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
250
+ await this.closeOpenDevice(input.path);
272
251
  throw e;
273
252
  }
274
253
  }
@@ -285,13 +264,17 @@ export default class WebUsbTransport {
285
264
  );
286
265
  }
287
266
 
288
- private createProtocolDetectionError() {
267
+ private createProtocolProbeTimeoutError(expected: ProtocolType, attempts: number) {
289
268
  return ERRORS.TypedError(
290
269
  HardwareErrorCode.RuntimeError,
291
- 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
270
+ `Protocol ${expected} probe timeout after ${attempts} attempts`
292
271
  );
293
272
  }
294
273
 
274
+ private createProtocolDetectionError() {
275
+ return ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
276
+ }
277
+
295
278
  private async detectProtocol(
296
279
  path: string,
297
280
  expectedProtocol?: ProtocolType,
@@ -300,34 +283,56 @@ export default class WebUsbTransport {
300
283
  if (expectedProtocol === 'V1') {
301
284
  if (await this.probeProtocolV1(path)) {
302
285
  this.deviceProtocol.set(path, 'V1');
303
- this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
304
286
  return 'V1';
305
287
  }
288
+ await this.closeConnectionAfterProbe(path);
306
289
  throw this.createProtocolMismatchError(expectedProtocol);
307
290
  }
308
291
 
309
292
  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';
293
+ for (let attempt = 1; attempt <= EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS; attempt += 1) {
294
+ if (await this.probeProtocolV2(path)) {
295
+ this.deviceProtocol.set(path, 'V2');
296
+ return 'V2';
297
+ }
298
+ if (attempt < EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS) {
299
+ await this.resetConnectionAfterProbe(path);
300
+ this.Log?.debug(
301
+ `[WebUsbTransport] Protocol V2 probe timed out, retrying ${
302
+ attempt + 1
303
+ }/${EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS}`
304
+ );
305
+ } else {
306
+ await this.closeConnectionAfterProbe(path);
307
+ }
308
+ }
309
+ this.deviceProtocol.delete(path);
310
+ throw this.createProtocolProbeTimeoutError(
311
+ expectedProtocol,
312
+ EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS
313
+ );
315
314
  }
316
315
 
317
- // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
318
- // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
319
- // 不能作为最终结论。
316
+ // Protocol must be actively probed after connection. Name, PID, and descriptors only
317
+ // influence probe order; a V2 hint probes V2 first and falls back to V1.
320
318
  const probeOrder: ProtocolType[] =
321
319
  protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
322
320
 
323
- for (const protocol of probeOrder) {
321
+ for (const [index, protocol] of probeOrder.entries()) {
324
322
  const detected =
325
323
  protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
326
324
  if (detected) {
327
325
  this.deviceProtocol.set(path, protocol);
328
- this.Log.debug(`[WebUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
329
326
  return protocol;
330
327
  }
328
+ if (index < probeOrder.length - 1) {
329
+ // A timed-out WebUSB transferIn cannot be cancelled in place. Closing and
330
+ // reopening the device guarantees the next protocol probe cannot consume a
331
+ // late response from the previous protocol generation.
332
+ await this.resetConnectionAfterProbe(path);
333
+ } else {
334
+ await this.closeConnectionAfterProbe(path);
335
+ }
331
336
  }
332
337
 
333
338
  this.deviceProtocol.delete(path);
@@ -414,13 +419,6 @@ export default class WebUsbTransport {
414
419
  */
415
420
  async connectToDevice(path: string, first: boolean) {
416
421
  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
422
  if (!device.opened) {
425
423
  await device.open();
426
424
  }
@@ -446,16 +444,12 @@ export default class WebUsbTransport {
446
444
  // Discover endpoints from USB descriptors; descriptors are not used for protocol selection.
447
445
  const endpoints = this.discoverEndpoints(device);
448
446
  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
447
  await device.claimInterface(endpoints.interfaceNumber);
453
448
  await this.clearEndpointHalt(device, 'in', endpoints.endpointIn);
454
449
  await this.clearEndpointHalt(device, 'out', endpoints.endpointOut);
455
450
  }
456
451
 
457
452
  private async closeOpenDevice(path: string) {
458
- this.protocolV2Assemblers.get(path)?.reset();
459
453
  const current = this.deviceList.find(device => device.path === path)?.device;
460
454
  if (!current?.opened) return;
461
455
 
@@ -650,27 +644,24 @@ export default class WebUsbTransport {
650
644
  throw lastError;
651
645
  }
652
646
 
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);
647
+ private async transferInOnce(path: string, length: number): Promise<DataView> {
648
+ const device = await this.findDevice(path);
649
+ if (!device.opened) {
650
+ throw new Error('USBDevice is not open for transferIn');
672
651
  }
652
+ const endpoints = this.deviceEndpoints.get(path);
653
+ const endpointIn = endpoints?.endpointIn ?? this.endpointId;
654
+ const result = await device.transferIn(endpointIn, length);
655
+ return this.getTransferInData(result);
656
+ }
657
+
658
+ private async closeConnectionAfterProbe(path: string) {
659
+ await this.rotateProtocolV2UsbGeneration(path, 'WebUSB protocol probe reset');
660
+ await this.closeOpenDevice(path);
661
+ }
673
662
 
663
+ private async resetConnectionAfterProbe(path: string) {
664
+ await this.closeConnectionAfterProbe(path);
674
665
  await this.getConnectedDevices();
675
666
  await this.connect(path, false);
676
667
  }
@@ -682,7 +673,6 @@ export default class WebUsbTransport {
682
673
  protocol: ProtocolType,
683
674
  onTimeout?: () => void
684
675
  ): Promise<T> {
685
- void path;
686
676
  let timer: ReturnType<typeof setTimeout> | undefined;
687
677
  let timedOut = false;
688
678
  const waitForeverAfterTimeout = () => new Promise<never>(() => {});
@@ -702,6 +692,13 @@ export default class WebUsbTransport {
702
692
  timer = setTimeout(async () => {
703
693
  timedOut = true;
704
694
  onTimeout?.();
695
+ if (protocol === 'V1') {
696
+ try {
697
+ await this.resetConnectionAfterProbe(path);
698
+ } catch (error) {
699
+ this.Log.debug('[WebUsbTransport] reset after Protocol V1 timeout failed:', error);
700
+ }
701
+ }
705
702
  reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
706
703
  }, timeoutMs);
707
704
  }),
@@ -717,10 +714,16 @@ export default class WebUsbTransport {
717
714
  }
718
715
 
719
716
  try {
720
- await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
717
+ await this.callProtocolV1(
718
+ path,
719
+ 'Initialize',
720
+ {},
721
+ {
722
+ timeoutMs: PROTOCOL_V1_PROBE_TIMEOUT,
723
+ }
724
+ );
721
725
  return true;
722
- } catch (error) {
723
- this.Log.debug('[WebUsbTransport] Protocol V1 Initialize probe failed:', error);
726
+ } catch (_error) {
724
727
  return false;
725
728
  }
726
729
  }
@@ -732,10 +735,9 @@ export default class WebUsbTransport {
732
735
 
733
736
  return probeProtocolV2Helper({
734
737
  call: (name, data, options) => this.callProtocolV2(path, name, data, options),
735
- timeoutMs: PROTOCOL_PROBE_TIMEOUT,
738
+ timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT,
736
739
  logger: this.Log,
737
740
  logPrefix: 'ProtocolV2 WebUSB',
738
- onProbeFailed: () => this.resetConnectionAfterProbe(path),
739
741
  });
740
742
  }
741
743
 
@@ -765,12 +767,8 @@ export default class WebUsbTransport {
765
767
  );
766
768
  }
767
769
 
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);
770
+ if (!shouldSuppressHighVolumeCallLog(name)) {
771
+ this.Log.debug('transport call', createTransportCallLog(name, protocol, data));
774
772
  }
775
773
 
776
774
  if (protocol === 'V2') {
@@ -820,83 +818,7 @@ export default class WebUsbTransport {
820
818
  data: Record<string, unknown>,
821
819
  options?: TransportCallOptions
822
820
  ) {
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;
821
+ return this.callProtocolV2Usb(path, name, data, options);
900
822
  }
901
823
 
902
824
  /**
@@ -951,18 +873,63 @@ export default class WebUsbTransport {
951
873
  * Release device
952
874
  */
953
875
  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();
876
+ await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
877
+ await this.closeOpenDevice(path);
959
878
  this.deviceProtocol.delete(path);
960
879
  this.deviceProtocolHints.delete(path);
961
- this.protocolV2Assemblers.get(path)?.reset();
962
- this.protocolV2Assemblers.delete(path);
963
880
  this.deviceEndpoints.delete(path);
964
881
  }
965
882
 
883
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
884
+ if (!this.messages || !this.messagesV2) {
885
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
886
+ }
887
+ return {
888
+ protocolV1: this.messages,
889
+ protocolV2: this.messagesV2,
890
+ };
891
+ }
892
+
893
+ protected getProtocolV2UsbLogger() {
894
+ return this.Log;
895
+ }
896
+
897
+ protected async writeProtocolV2UsbPacket(
898
+ path: string,
899
+ frame: Uint8Array,
900
+ _context: ProtocolV2CallContext
901
+ ): Promise<void> {
902
+ await this.transferOutOnce(path, frame);
903
+ }
904
+
905
+ protected async readProtocolV2UsbPacket(
906
+ path: string,
907
+ _context: ProtocolV2CallContext
908
+ ): Promise<Uint8Array> {
909
+ const dataView = await this.transferInOnce(path, PROTOCOL_V2_FRAME_MAX_BYTES);
910
+ return new Uint8Array(
911
+ this.toArrayBuffer(
912
+ dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)
913
+ )
914
+ );
915
+ }
916
+
917
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
918
+ await this.closeOpenDevice(path);
919
+ }
920
+
921
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
922
+ this.deviceProtocol.delete(path);
923
+ this.Log?.debug(`[WebUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
924
+ }
925
+
926
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
927
+ return new ProtocolV2LinkError(
928
+ 'response-timeout',
929
+ `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
930
+ );
931
+ }
932
+
966
933
  /**
967
934
  * Expose the detected protocol type for a given device path.
968
935
  * Used by upper layers (e.g. TransportManager) to select the correct schema.