@onekeyfe/hd-transport-usb 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/index.ts CHANGED
@@ -1,24 +1,27 @@
1
1
  import ByteBuffer from 'bytebuffer';
2
2
  import * as usb from 'usb';
3
3
  import transport, {
4
- LogBlockCommand,
5
4
  PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
6
5
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
7
6
  PROTOCOL_V1_REPORT_ID,
8
7
  PROTOCOL_V1_USB_PACKET_SIZE,
9
8
  PROTOCOL_V2_CHANNEL_USB,
10
9
  PROTOCOL_V2_FRAME_MAX_BYTES,
11
- ProtocolV2FrameAssembler,
12
- ProtocolV2Session,
10
+ ProtocolV2LinkError,
11
+ ProtocolV2UsbTransportBase,
13
12
  probeProtocolV2 as probeProtocolV2Helper,
14
13
  } from '@onekeyfe/hd-transport';
15
14
  import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
16
15
 
16
+ import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
17
+
17
18
  import type EventEmitter from 'events';
18
19
  import type {
19
20
  AcquireInput,
20
21
  OneKeyDeviceInfo,
21
22
  ProtocolType,
23
+ ProtocolV2CallContext,
24
+ ProtocolV2Schemas,
22
25
  TransportCallOptions,
23
26
  } from '@onekeyfe/hd-transport';
24
27
 
@@ -44,7 +47,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
44
47
  /** Packet I/O retry configuration (matches WebUsbTransport) */
45
48
  const PACKET_IO_MAX_RETRIES = 3;
46
49
  const PACKET_IO_RETRY_DELAY = 300;
47
- const PROTOCOL_PROBE_TIMEOUT = 1000;
50
+ const PROTOCOL_PROBE_TIMEOUT = 5000;
48
51
 
49
52
  /**
50
53
  * Opened device state — holds the USB device, claimed interface, and endpoints.
@@ -129,57 +132,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
129
132
  });
130
133
  }
131
134
 
132
- /**
133
- * Promisified USB IN transfer (single attempt).
134
- */
135
- function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
136
- return new Promise((resolve, reject) => {
137
- ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
138
- if (err) return reject(err);
139
- if (!data || data.length === 0) return reject(new Error('Empty USB transfer'));
140
- resolve(data);
141
- });
142
- });
143
- }
144
-
145
- /**
146
- * Promisified USB OUT transfer (single attempt).
147
- */
148
- function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
149
- return new Promise((resolve, reject) => {
150
- ep.transfer(data, (err: Error | undefined) => {
151
- if (err) return reject(err);
152
- resolve();
153
- });
154
- });
155
- }
156
-
157
- function resetUsbDevice(dev: usb.Device): Promise<void> {
158
- return new Promise(resolve => {
159
- const reset = (dev as usb.Device & { reset?: (callback: (err?: Error) => void) => void }).reset;
160
- if (typeof reset !== 'function') {
161
- resolve();
162
- return;
163
- }
164
- reset.call(dev, () => resolve());
165
- });
166
- }
167
-
168
- function clearEndpointHalt(ep: usb.InEndpoint | usb.OutEndpoint): Promise<void> {
169
- return new Promise(resolve => {
170
- const clearHalt = (
171
- ep as (usb.InEndpoint | usb.OutEndpoint) & {
172
- clearHalt?: (callback: (err?: Error) => void) => void;
173
- }
174
- ).clearHalt;
175
- if (typeof clearHalt !== 'function') {
176
- resolve();
177
- return;
178
- }
179
- clearHalt.call(ep, () => resolve());
180
- });
181
- }
182
-
183
135
  /**
184
136
  * Skip the 0x3F protocol marker byte from a USB packet.
185
137
  */
@@ -206,12 +158,14 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
206
158
  *
207
159
  * Modeled after WebUsbTransport.
208
160
  */
209
- export default class NodeUsbTransport {
161
+ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
210
162
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
211
163
 
212
164
  /** Protobuf schema for Protocol V2 transports. */
213
165
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
214
166
 
167
+ private protocolV2SchemaSource: string | undefined;
168
+
215
169
  name = 'NodeUsbTransport';
216
170
 
217
171
  version = '';
@@ -233,21 +187,29 @@ export default class NodeUsbTransport {
233
187
  /** Per-path protocol type detected by active wire-level probe. */
234
188
  private deviceProtocol: Map<string, ProtocolType> = new Map();
235
189
 
236
- /** Per-path Protocol V2 frame assembler, preserving buffered frames during reads. */
237
- private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
238
-
239
- /** Per-path Protocol V2 session, preserving seq across API calls on the same device path. */
240
- private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
241
-
242
- /** Current Protocol V2 read timeout, consumed by cached session readFrame closures. */
243
- private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
244
-
245
190
  /** per-path reconnect lock to prevent concurrent reconnects */
246
191
  private reconnectLocks = new Map<string, Promise<OpenDevice>>();
247
192
 
193
+ /**
194
+ * Retain the low-level Transfer so release()/stop() can cancel native pending reads.
195
+ * Endpoint.transfer() hides it and may keep the CLI alive after output completes.
196
+ */
197
+ private activeTransfers = new Map<
198
+ usb.Transfer,
199
+ { path: string; settled: Promise<void>; resolveSettled: () => void }
200
+ >();
201
+
248
202
  /** set to true when cancel() is called; checked by retry loops */
249
203
  private cancelled = false;
250
204
 
205
+ constructor() {
206
+ super({
207
+ router: PROTOCOL_V2_CHANNEL_USB,
208
+ maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
209
+ logPrefix: 'ProtocolV2 NodeUSB',
210
+ });
211
+ }
212
+
251
213
  /**
252
214
  * Initialize transport.
253
215
  * Signature matches the Transport.init interface (logger, emitter).
@@ -266,18 +228,47 @@ export default class NodeUsbTransport {
266
228
  }
267
229
 
268
230
  configureProtocolV2(signedData: any) {
231
+ const schemaSource =
232
+ typeof signedData === 'string'
233
+ ? signedData
234
+ : JSON.stringify(signedData) ?? String(signedData);
235
+ if (schemaSource === this.protocolV2SchemaSource) return;
236
+
237
+ const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
269
238
  this.messagesV2 = parseConfigure(signedData);
270
- this.protocolV2Sessions.clear();
271
- this.protocolV2ReadTimeouts.clear();
272
- this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
239
+ this.protocolV2SchemaSource = schemaSource;
240
+ if (!hadProtocolV2Schema) return;
241
+
242
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
243
+ this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
244
+ );
273
245
  }
274
246
 
275
247
  listen() {
276
248
  // empty — could add hotplug events via usb.on('attach'/'detach')
277
249
  }
278
250
 
279
- stop() {
280
- // Placeholder — no background listeners to tear down
251
+ async stop(): Promise<void> {
252
+ this.cancelled = true;
253
+
254
+ await this.cancelActiveTransfers();
255
+
256
+ try {
257
+ await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
258
+ } catch (error) {
259
+ this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
260
+ }
261
+
262
+ // Reconnect may already be in flight when dispose starts. Wait for it to
263
+ // settle, then close every remaining handle so it cannot reopen USB after
264
+ // the first cleanup pass.
265
+ await Promise.allSettled(this.reconnectLocks.values());
266
+ await this.cancelActiveTransfers();
267
+ await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
268
+
269
+ this.reconnectLocks.clear();
270
+ this.deviceProtocol.clear();
271
+ this.serialToBusId.clear();
281
272
  }
282
273
 
283
274
  /**
@@ -352,6 +343,7 @@ export default class NodeUsbTransport {
352
343
  }
353
344
 
354
345
  try {
346
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
355
347
  await this.closeOpenDevice(path);
356
348
  await this.openDevice(path);
357
349
  await this.detectProtocol(path, input.expectedProtocol);
@@ -366,9 +358,91 @@ export default class NodeUsbTransport {
366
358
  * Release device — release interface and close.
367
359
  */
368
360
  async release(path: string, _onclose?: boolean): Promise<void> {
361
+ await this.cancelActiveTransfers(path);
362
+ await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
369
363
  await this.closeOpenDevice(path);
370
364
  this.deviceProtocol.delete(path);
371
- this.protocolV2Assemblers.delete(path);
365
+ }
366
+
367
+ private async cancelActiveTransfers(path?: string): Promise<void> {
368
+ const transfers = Array.from(this.activeTransfers.entries()).filter(
369
+ ([, active]) => path === undefined || active.path === path
370
+ );
371
+ transfers.forEach(([transfer]) => {
372
+ try {
373
+ transfer.cancel();
374
+ } catch {
375
+ // A transfer may finish between the snapshot and cancel().
376
+ }
377
+ });
378
+ await Promise.allSettled(transfers.map(([, active]) => active.settled));
379
+ }
380
+
381
+ private createTrackedTransfer(
382
+ path: string,
383
+ endpoint: usb.InEndpoint | usb.OutEndpoint,
384
+ callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
385
+ ): usb.Transfer {
386
+ let resolveSettled: () => void = () => undefined;
387
+ const settled = new Promise<void>(resolve => {
388
+ resolveSettled = resolve;
389
+ });
390
+ const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
391
+ this.activeTransfers.delete(transfer);
392
+ resolveSettled();
393
+ callback(error, buffer, actualLength);
394
+ });
395
+ this.activeTransfers.set(transfer, { path, settled, resolveSettled });
396
+ return transfer;
397
+ }
398
+
399
+ private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
400
+ return new Promise((resolve, reject) => {
401
+ const buffer = Buffer.alloc(length);
402
+ const transfer = this.createTrackedTransfer(
403
+ path,
404
+ ep,
405
+ (error, _submittedBuffer, actualLength) => {
406
+ if (error) {
407
+ reject(error);
408
+ return;
409
+ }
410
+ if (actualLength <= 0) {
411
+ reject(new Error('Empty USB transfer'));
412
+ return;
413
+ }
414
+ resolve(buffer.subarray(0, actualLength));
415
+ }
416
+ );
417
+ try {
418
+ transfer.submit(buffer);
419
+ } catch (error) {
420
+ const active = this.activeTransfers.get(transfer);
421
+ this.activeTransfers.delete(transfer);
422
+ active?.resolveSettled();
423
+ reject(error);
424
+ }
425
+ });
426
+ }
427
+
428
+ private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
429
+ return new Promise((resolve, reject) => {
430
+ const transfer = this.createTrackedTransfer(path, ep, error => {
431
+ if (error) {
432
+ reject(error);
433
+ return;
434
+ }
435
+ resolve();
436
+ });
437
+ try {
438
+ transfer.submit(data);
439
+ } catch (error) {
440
+ const active = this.activeTransfers.get(transfer);
441
+ this.activeTransfers.delete(transfer);
442
+ active?.resolveSettled();
443
+ reject(error);
444
+ }
445
+ });
372
446
  }
373
447
 
374
448
  private async closeOpenDevice(path: string): Promise<void> {
@@ -423,18 +497,8 @@ export default class NodeUsbTransport {
423
497
  `Device protocol has not been detected for ${path}`
424
498
  );
425
499
  }
426
- if (LogBlockCommand.has(name)) {
427
- this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
428
- } else {
429
- this.Log?.debug(
430
- 'NodeUsbTransport call-',
431
- ' name: ',
432
- name,
433
- ' data: ',
434
- data,
435
- ' protocol: ',
436
- protocol
437
- );
500
+ if (!shouldSuppressHighVolumeCallLog(name)) {
501
+ this.Log?.debug('transport call', createTransportCallLog(name, protocol));
438
502
  }
439
503
 
440
504
  if (protocol === 'V2') {
@@ -471,7 +535,6 @@ export default class NodeUsbTransport {
471
535
  }
472
536
 
473
537
  cancel() {
474
- this.Log?.debug('NodeUsbTransport cancel');
475
538
  this.cancelled = true;
476
539
  }
477
540
 
@@ -501,6 +564,12 @@ export default class NodeUsbTransport {
501
564
 
502
565
  private isRetryableError(error: unknown): boolean {
503
566
  const message = this.getErrorMessage(error).toLowerCase();
567
+ // cancelActiveTransfers() terminates timed-out or releasing native requests.
568
+ // LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
569
+ // read on the rebuilt interface and race protocol probing against release.
570
+ if (message.includes('cancelled') || message.includes('canceled')) {
571
+ return false;
572
+ }
504
573
  return (
505
574
  message.includes('libusb') ||
506
575
  message.includes('transfer') ||
@@ -515,6 +584,11 @@ export default class NodeUsbTransport {
515
584
  );
516
585
  }
517
586
 
587
+ private isUsbTransferTimeout(error: unknown): boolean {
588
+ const message = this.getErrorMessage(error).toLowerCase();
589
+ return message.includes('timeout') || message.includes('timed_out');
590
+ }
591
+
518
592
  private getDeviceInterface(dev: usb.Device): usb.Interface {
519
593
  const { interfaces } = dev;
520
594
  if (!interfaces?.length) {
@@ -552,6 +626,10 @@ export default class NodeUsbTransport {
552
626
  );
553
627
  await wait(attempt * PACKET_IO_RETRY_DELAY);
554
628
 
629
+ await this.rotateProtocolV2UsbGeneration(
630
+ path,
631
+ `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
632
+ );
555
633
  // Close the existing device without clearing the detected protocol cache.
556
634
  try {
557
635
  await this.closeOpenDevice(path);
@@ -596,7 +674,7 @@ export default class NodeUsbTransport {
596
674
  const packet = new Uint8Array(PACKET_SIZE);
597
675
  packet[0] = REPORT_ID;
598
676
  packet.set(new Uint8Array(buffer), 1);
599
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
677
+ await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
600
678
  }
601
679
  return; // all chunks sent successfully
602
680
  } catch (error) {
@@ -629,7 +707,8 @@ export default class NodeUsbTransport {
629
707
  private async transferInWithRetry(
630
708
  path: string,
631
709
  openDev: OpenDevice,
632
- length: number
710
+ length: number,
711
+ options?: { waitIndefinitelyOnTimeout?: boolean }
633
712
  ): Promise<Buffer> {
634
713
  let lastError: unknown;
635
714
  let currentDev = openDev;
@@ -638,23 +717,27 @@ export default class NodeUsbTransport {
638
717
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
639
718
  }
640
719
  try {
641
- return await transferInOnce(currentDev.epIn, length);
720
+ return await this.transferInOnce(path, currentDev.epIn, length);
642
721
  } catch (error) {
643
722
  lastError = error;
644
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
645
- if (!shouldRetry) {
646
- throw error;
647
- }
648
- try {
649
- currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
650
- } catch (reconnectError) {
651
- lastError = reconnectError;
652
- this.Log?.debug(
653
- `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
654
- reconnectError
655
- )}`
656
- );
657
- break;
723
+ if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
724
+ attempt -= 1;
725
+ } else {
726
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
727
+ if (!shouldRetry) {
728
+ throw error;
729
+ }
730
+ try {
731
+ currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
732
+ } catch (reconnectError) {
733
+ lastError = reconnectError;
734
+ this.Log?.debug(
735
+ `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
736
+ reconnectError
737
+ )}`
738
+ );
739
+ break;
740
+ }
658
741
  }
659
742
  }
660
743
  }
@@ -680,15 +763,6 @@ export default class NodeUsbTransport {
680
763
  dev.open();
681
764
  dev.timeout = TRANSFER_TIMEOUT_MS;
682
765
 
683
- await resetUsbDevice(dev);
684
-
685
- try {
686
- dev.open();
687
- } catch {
688
- // libusb keeps some devices open across reset; continue with the current handle.
689
- }
690
- dev.timeout = TRANSFER_TIMEOUT_MS;
691
-
692
766
  const iface = this.getDeviceInterface(dev);
693
767
 
694
768
  // On Linux, detach kernel driver if active
@@ -723,9 +797,7 @@ export default class NodeUsbTransport {
723
797
  epIn.timeout = TRANSFER_TIMEOUT_MS;
724
798
  epOut.timeout = TRANSFER_TIMEOUT_MS;
725
799
 
726
- await clearEndpointHalt(epIn);
727
- await clearEndpointHalt(epOut);
728
- await this.drainStaleInput(epIn);
800
+ await this.drainStaleInput(path, epIn);
729
801
 
730
802
  this.openDevices.set(path, { device: dev, iface, epIn, epOut });
731
803
  } catch (err) {
@@ -738,14 +810,14 @@ export default class NodeUsbTransport {
738
810
  }
739
811
  }
740
812
 
741
- private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
813
+ private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
742
814
  const originalTimeout = epIn.timeout;
743
815
  epIn.timeout = 50;
744
816
  try {
745
817
  // Drain a small bounded number of packets left by the previous USB session.
746
818
  for (let index = 0; index < 16; index += 1) {
747
819
  try {
748
- await transferInOnce(epIn, PACKET_SIZE);
820
+ await this.transferInOnce(path, epIn, PACKET_SIZE);
749
821
  } catch {
750
822
  break;
751
823
  }
@@ -773,15 +845,6 @@ export default class NodeUsbTransport {
773
845
  path: string,
774
846
  expectedProtocol?: ProtocolType
775
847
  ): Promise<ProtocolType> {
776
- if (expectedProtocol === 'V1') {
777
- if (await this.probeProtocolV1(path)) {
778
- this.deviceProtocol.set(path, 'V1');
779
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
780
- return 'V1';
781
- }
782
- throw this.createProtocolMismatchError(expectedProtocol);
783
- }
784
-
785
848
  if (expectedProtocol === 'V2') {
786
849
  if (await this.probeProtocolV2(path)) {
787
850
  this.deviceProtocol.set(path, 'V2');
@@ -791,21 +854,26 @@ export default class NodeUsbTransport {
791
854
  throw this.createProtocolMismatchError(expectedProtocol);
792
855
  }
793
856
 
857
+ if (expectedProtocol === 'V1') {
858
+ if (await this.probeProtocolV1(path)) {
859
+ this.deviceProtocol.set(path, 'V1');
860
+ return 'V1';
861
+ }
862
+ throw this.createProtocolMismatchError(expectedProtocol);
863
+ }
864
+
794
865
  if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) {
795
866
  this.deviceProtocol.set(path, 'V2');
796
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
797
867
  return 'V2';
798
868
  }
799
869
 
800
870
  if (await this.probeProtocolV1(path)) {
801
871
  this.deviceProtocol.set(path, 'V1');
802
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
803
872
  return 'V1';
804
873
  }
805
874
 
806
875
  if (await this.probeProtocolV2(path)) {
807
876
  this.deviceProtocol.set(path, 'V2');
808
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2`);
809
877
  return 'V2';
810
878
  }
811
879
 
@@ -814,11 +882,13 @@ export default class NodeUsbTransport {
814
882
  }
815
883
 
816
884
  private async resetConnectionAfterProbe(path: string) {
817
- this.protocolV2Assemblers.get(path)?.reset();
818
- this.protocolV2Sessions.delete(path);
819
- this.protocolV2ReadTimeouts.delete(path);
885
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
820
886
 
821
887
  try {
888
+ // A timed-out probe may leave an IN transfer pending. Cancel and await it before
889
+ // closing the interface, or libusb may never callback and release()/stop() will
890
+ // wait forever, surfacing as Polling timeout (809).
891
+ await this.cancelActiveTransfers(path);
822
892
  await this.closeOpenDevice(path);
823
893
  } catch (error) {
824
894
  this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
@@ -878,8 +948,7 @@ export default class NodeUsbTransport {
878
948
  try {
879
949
  await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
880
950
  return true;
881
- } catch (error) {
882
- this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
951
+ } catch (_error) {
883
952
  return false;
884
953
  }
885
954
  }
@@ -898,74 +967,71 @@ export default class NodeUsbTransport {
898
967
  });
899
968
  }
900
969
 
901
- private async writeProtocolV2Frame(path: string, frame: Uint8Array) {
902
- let lastError: unknown;
903
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
970
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
971
+ if (!this.messages || !this.messagesV2) {
972
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
973
+ }
974
+ return {
975
+ protocolV1: this.messages,
976
+ protocolV2: this.messagesV2,
977
+ };
978
+ }
979
+
980
+ protected getProtocolV2UsbLogger() {
981
+ return this.Log;
982
+ }
983
+
984
+ protected async writeProtocolV2UsbPacket(
985
+ path: string,
986
+ frame: Uint8Array,
987
+ _context: ProtocolV2CallContext
988
+ ): Promise<void> {
989
+ if (this.cancelled) {
990
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
991
+ }
992
+ await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
993
+ }
994
+
995
+ protected async readProtocolV2UsbPacket(
996
+ path: string,
997
+ _context: ProtocolV2CallContext
998
+ ): Promise<Uint8Array> {
999
+ for (;;) {
904
1000
  if (this.cancelled) {
905
1001
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
906
1002
  }
907
1003
  try {
908
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
909
- return;
1004
+ const packet = await this.transferInOnce(
1005
+ path,
1006
+ this.getOpenDevice(path).epIn,
1007
+ PROTOCOL_V2_FRAME_MAX_BYTES
1008
+ );
1009
+ return new Uint8Array(
1010
+ packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
1011
+ );
910
1012
  } catch (error) {
911
- lastError = error;
912
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
913
- if (!shouldRetry) {
1013
+ if (!this.isUsbTransferTimeout(error)) {
914
1014
  throw error;
915
1015
  }
916
- try {
917
- await this.reconnectForRetry(path, 'out', attempt, error);
918
- } catch (reconnectError) {
919
- lastError = reconnectError;
920
- this.Log?.debug(
921
- `[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
922
- reconnectError
923
- )}`
924
- );
925
- break;
926
- }
927
1016
  }
928
1017
  }
929
- throw lastError;
930
1018
  }
931
1019
 
932
- private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
933
- let assembler = this.protocolV2Assemblers.get(path);
934
- if (!assembler) {
935
- assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
936
- this.protocolV2Assemblers.set(path, assembler);
937
- }
1020
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
1021
+ await this.cancelActiveTransfers(path);
1022
+ await this.closeOpenDevice(path);
1023
+ }
938
1024
 
939
- let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
940
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
1025
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
1026
+ this.deviceProtocol.delete(path);
1027
+ this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
1028
+ }
941
1029
 
942
- while (!frame) {
943
- const transferIn = this.transferInWithRetry(
944
- path,
945
- this.getOpenDevice(path),
946
- PROTOCOL_V2_FRAME_MAX_BYTES
947
- );
948
- const packet = deadline
949
- ? await this.withProtocolReadTimeout(
950
- path,
951
- transferIn,
952
- Math.max(deadline - Date.now(), 1),
953
- 'V2'
954
- )
955
- : await transferIn;
956
- const bytes = new Uint8Array(
957
- packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
958
- );
959
- try {
960
- frame = assembler.push(bytes);
961
- } catch (error) {
962
- throw ERRORS.TypedError(
963
- HardwareErrorCode.NetworkError,
964
- error instanceof Error ? error.message : String(error)
965
- );
966
- }
967
- }
968
- return frame;
1030
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
1031
+ return new ProtocolV2LinkError(
1032
+ 'response-timeout',
1033
+ `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
1034
+ );
969
1035
  }
970
1036
 
971
1037
  private async callProtocolV2(
@@ -974,42 +1040,7 @@ export default class NodeUsbTransport {
974
1040
  data: Record<string, unknown>,
975
1041
  options?: TransportCallOptions
976
1042
  ) {
977
- const protocolV1Messages = this.messages;
978
- if (!this.messagesV2) {
979
- throw ERRORS.TypedError(
980
- HardwareErrorCode.TransportNotConfigured,
981
- 'Protocol V2 schema not configured'
982
- );
983
- }
984
- if (!protocolV1Messages) {
985
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
986
- }
987
-
988
- let session = this.protocolV2Sessions.get(path);
989
- if (!session) {
990
- session = new ProtocolV2Session({
991
- schemas: {
992
- protocolV1: protocolV1Messages,
993
- protocolV2: this.messagesV2,
994
- },
995
- router: PROTOCOL_V2_CHANNEL_USB,
996
- writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(path, frame),
997
- readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
998
- logger: this.Log,
999
- logPrefix: 'ProtocolV2 NodeUSB',
1000
- createTimeoutError: (messageName: string, timeoutMs: number) =>
1001
- new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
1002
- });
1003
- this.protocolV2Sessions.set(path, session);
1004
- }
1005
-
1006
- this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
1007
- this.protocolV2Assemblers.get(path)?.reset();
1008
- try {
1009
- return await session.call(name, data, options);
1010
- } finally {
1011
- this.protocolV2ReadTimeouts.delete(path);
1012
- }
1043
+ return this.callProtocolV2Usb(path, name, data, options);
1013
1044
  }
1014
1045
 
1015
1046
  /**