@onekeyfe/hd-transport-usb 1.2.0-alpha.2 → 1.2.0-alpha.21

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,26 @@
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
+ ProtocolV2UsbTransportBase,
13
11
  probeProtocolV2 as probeProtocolV2Helper,
14
12
  } from '@onekeyfe/hd-transport';
15
13
  import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
16
14
 
15
+ import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
16
+
17
17
  import type EventEmitter from 'events';
18
18
  import type {
19
19
  AcquireInput,
20
20
  OneKeyDeviceInfo,
21
21
  ProtocolType,
22
+ ProtocolV2CallContext,
23
+ ProtocolV2Schemas,
22
24
  TransportCallOptions,
23
25
  } from '@onekeyfe/hd-transport';
24
26
 
@@ -44,7 +46,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
44
46
  /** Packet I/O retry configuration (matches WebUsbTransport) */
45
47
  const PACKET_IO_MAX_RETRIES = 3;
46
48
  const PACKET_IO_RETRY_DELAY = 300;
47
- const PROTOCOL_PROBE_TIMEOUT = 1000;
49
+ const PROTOCOL_PROBE_TIMEOUT = 5000;
48
50
 
49
51
  /**
50
52
  * Opened device state — holds the USB device, claimed interface, and endpoints.
@@ -129,57 +131,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
129
131
  });
130
132
  }
131
133
 
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
134
  /**
184
135
  * Skip the 0x3F protocol marker byte from a USB packet.
185
136
  */
@@ -206,7 +157,7 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
206
157
  *
207
158
  * Modeled after WebUsbTransport.
208
159
  */
209
- export default class NodeUsbTransport {
160
+ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
210
161
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
211
162
 
212
163
  /** Protobuf schema for Protocol V2 transports. */
@@ -233,21 +184,29 @@ export default class NodeUsbTransport {
233
184
  /** Per-path protocol type detected by active wire-level probe. */
234
185
  private deviceProtocol: Map<string, ProtocolType> = new Map();
235
186
 
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
187
  /** per-path reconnect lock to prevent concurrent reconnects */
246
188
  private reconnectLocks = new Map<string, Promise<OpenDevice>>();
247
189
 
190
+ /**
191
+ * Retain the low-level Transfer so release()/stop() can cancel native pending reads.
192
+ * Endpoint.transfer() hides it and may keep the CLI alive after output completes.
193
+ */
194
+ private activeTransfers = new Map<
195
+ usb.Transfer,
196
+ { path: string; settled: Promise<void>; resolveSettled: () => void }
197
+ >();
198
+
248
199
  /** set to true when cancel() is called; checked by retry loops */
249
200
  private cancelled = false;
250
201
 
202
+ constructor() {
203
+ super({
204
+ router: PROTOCOL_V2_CHANNEL_USB,
205
+ maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
206
+ logPrefix: 'ProtocolV2 NodeUSB',
207
+ });
208
+ }
209
+
251
210
  /**
252
211
  * Initialize transport.
253
212
  * Signature matches the Transport.init interface (logger, emitter).
@@ -267,17 +226,36 @@ export default class NodeUsbTransport {
267
226
 
268
227
  configureProtocolV2(signedData: any) {
269
228
  this.messagesV2 = parseConfigure(signedData);
270
- this.protocolV2Sessions.clear();
271
- this.protocolV2ReadTimeouts.clear();
272
- this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
229
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
230
+ this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
231
+ );
273
232
  }
274
233
 
275
234
  listen() {
276
235
  // empty — could add hotplug events via usb.on('attach'/'detach')
277
236
  }
278
237
 
279
- stop() {
280
- // Placeholder — no background listeners to tear down
238
+ async stop(): Promise<void> {
239
+ this.cancelled = true;
240
+
241
+ await this.cancelActiveTransfers();
242
+
243
+ try {
244
+ await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
245
+ } catch (error) {
246
+ this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
247
+ }
248
+
249
+ // Reconnect may already be in flight when dispose starts. Wait for it to
250
+ // settle, then close every remaining handle so it cannot reopen USB after
251
+ // the first cleanup pass.
252
+ await Promise.allSettled(this.reconnectLocks.values());
253
+ await this.cancelActiveTransfers();
254
+ await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
255
+
256
+ this.reconnectLocks.clear();
257
+ this.deviceProtocol.clear();
258
+ this.serialToBusId.clear();
281
259
  }
282
260
 
283
261
  /**
@@ -352,6 +330,7 @@ export default class NodeUsbTransport {
352
330
  }
353
331
 
354
332
  try {
333
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
355
334
  await this.closeOpenDevice(path);
356
335
  await this.openDevice(path);
357
336
  await this.detectProtocol(path, input.expectedProtocol);
@@ -366,9 +345,91 @@ export default class NodeUsbTransport {
366
345
  * Release device — release interface and close.
367
346
  */
368
347
  async release(path: string, _onclose?: boolean): Promise<void> {
348
+ await this.cancelActiveTransfers(path);
349
+ await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
369
350
  await this.closeOpenDevice(path);
370
351
  this.deviceProtocol.delete(path);
371
- this.protocolV2Assemblers.delete(path);
352
+ }
353
+
354
+ private async cancelActiveTransfers(path?: string): Promise<void> {
355
+ const transfers = Array.from(this.activeTransfers.entries()).filter(
356
+ ([, active]) => path === undefined || active.path === path
357
+ );
358
+ transfers.forEach(([transfer]) => {
359
+ try {
360
+ transfer.cancel();
361
+ } catch {
362
+ // A transfer may finish between the snapshot and cancel().
363
+ }
364
+ });
365
+ await Promise.allSettled(transfers.map(([, active]) => active.settled));
366
+ }
367
+
368
+ private createTrackedTransfer(
369
+ path: string,
370
+ endpoint: usb.InEndpoint | usb.OutEndpoint,
371
+ callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
372
+ ): usb.Transfer {
373
+ let resolveSettled: () => void = () => undefined;
374
+ const settled = new Promise<void>(resolve => {
375
+ resolveSettled = resolve;
376
+ });
377
+ const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
378
+ this.activeTransfers.delete(transfer);
379
+ resolveSettled();
380
+ callback(error, buffer, actualLength);
381
+ });
382
+ this.activeTransfers.set(transfer, { path, settled, resolveSettled });
383
+ return transfer;
384
+ }
385
+
386
+ private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
387
+ return new Promise((resolve, reject) => {
388
+ const buffer = Buffer.alloc(length);
389
+ const transfer = this.createTrackedTransfer(
390
+ path,
391
+ ep,
392
+ (error, _submittedBuffer, actualLength) => {
393
+ if (error) {
394
+ reject(error);
395
+ return;
396
+ }
397
+ if (actualLength <= 0) {
398
+ reject(new Error('Empty USB transfer'));
399
+ return;
400
+ }
401
+ resolve(buffer.subarray(0, actualLength));
402
+ }
403
+ );
404
+ try {
405
+ transfer.submit(buffer);
406
+ } catch (error) {
407
+ const active = this.activeTransfers.get(transfer);
408
+ this.activeTransfers.delete(transfer);
409
+ active?.resolveSettled();
410
+ reject(error);
411
+ }
412
+ });
413
+ }
414
+
415
+ private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
416
+ return new Promise((resolve, reject) => {
417
+ const transfer = this.createTrackedTransfer(path, ep, error => {
418
+ if (error) {
419
+ reject(error);
420
+ return;
421
+ }
422
+ resolve();
423
+ });
424
+ try {
425
+ transfer.submit(data);
426
+ } catch (error) {
427
+ const active = this.activeTransfers.get(transfer);
428
+ this.activeTransfers.delete(transfer);
429
+ active?.resolveSettled();
430
+ reject(error);
431
+ }
432
+ });
372
433
  }
373
434
 
374
435
  private async closeOpenDevice(path: string): Promise<void> {
@@ -423,18 +484,8 @@ export default class NodeUsbTransport {
423
484
  `Device protocol has not been detected for ${path}`
424
485
  );
425
486
  }
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
- );
487
+ if (!shouldSuppressHighVolumeCallLog(name)) {
488
+ this.Log?.debug('transport call', createTransportCallLog(name, protocol));
438
489
  }
439
490
 
440
491
  if (protocol === 'V2') {
@@ -471,7 +522,6 @@ export default class NodeUsbTransport {
471
522
  }
472
523
 
473
524
  cancel() {
474
- this.Log?.debug('NodeUsbTransport cancel');
475
525
  this.cancelled = true;
476
526
  }
477
527
 
@@ -501,6 +551,12 @@ export default class NodeUsbTransport {
501
551
 
502
552
  private isRetryableError(error: unknown): boolean {
503
553
  const message = this.getErrorMessage(error).toLowerCase();
554
+ // cancelActiveTransfers() terminates timed-out or releasing native requests.
555
+ // LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
556
+ // read on the rebuilt interface and race protocol probing against release.
557
+ if (message.includes('cancelled') || message.includes('canceled')) {
558
+ return false;
559
+ }
504
560
  return (
505
561
  message.includes('libusb') ||
506
562
  message.includes('transfer') ||
@@ -515,6 +571,11 @@ export default class NodeUsbTransport {
515
571
  );
516
572
  }
517
573
 
574
+ private isUsbTransferTimeout(error: unknown): boolean {
575
+ const message = this.getErrorMessage(error).toLowerCase();
576
+ return message.includes('timeout') || message.includes('timed_out');
577
+ }
578
+
518
579
  private getDeviceInterface(dev: usb.Device): usb.Interface {
519
580
  const { interfaces } = dev;
520
581
  if (!interfaces?.length) {
@@ -552,6 +613,10 @@ export default class NodeUsbTransport {
552
613
  );
553
614
  await wait(attempt * PACKET_IO_RETRY_DELAY);
554
615
 
616
+ await this.rotateProtocolV2UsbGeneration(
617
+ path,
618
+ `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
619
+ );
555
620
  // Close the existing device without clearing the detected protocol cache.
556
621
  try {
557
622
  await this.closeOpenDevice(path);
@@ -596,7 +661,7 @@ export default class NodeUsbTransport {
596
661
  const packet = new Uint8Array(PACKET_SIZE);
597
662
  packet[0] = REPORT_ID;
598
663
  packet.set(new Uint8Array(buffer), 1);
599
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
664
+ await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
600
665
  }
601
666
  return; // all chunks sent successfully
602
667
  } catch (error) {
@@ -629,7 +694,8 @@ export default class NodeUsbTransport {
629
694
  private async transferInWithRetry(
630
695
  path: string,
631
696
  openDev: OpenDevice,
632
- length: number
697
+ length: number,
698
+ options?: { waitIndefinitelyOnTimeout?: boolean }
633
699
  ): Promise<Buffer> {
634
700
  let lastError: unknown;
635
701
  let currentDev = openDev;
@@ -638,23 +704,27 @@ export default class NodeUsbTransport {
638
704
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
639
705
  }
640
706
  try {
641
- return await transferInOnce(currentDev.epIn, length);
707
+ return await this.transferInOnce(path, currentDev.epIn, length);
642
708
  } catch (error) {
643
709
  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;
710
+ if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
711
+ attempt -= 1;
712
+ } else {
713
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
714
+ if (!shouldRetry) {
715
+ throw error;
716
+ }
717
+ try {
718
+ currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
719
+ } catch (reconnectError) {
720
+ lastError = reconnectError;
721
+ this.Log?.debug(
722
+ `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
723
+ reconnectError
724
+ )}`
725
+ );
726
+ break;
727
+ }
658
728
  }
659
729
  }
660
730
  }
@@ -680,15 +750,6 @@ export default class NodeUsbTransport {
680
750
  dev.open();
681
751
  dev.timeout = TRANSFER_TIMEOUT_MS;
682
752
 
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
753
  const iface = this.getDeviceInterface(dev);
693
754
 
694
755
  // On Linux, detach kernel driver if active
@@ -723,9 +784,7 @@ export default class NodeUsbTransport {
723
784
  epIn.timeout = TRANSFER_TIMEOUT_MS;
724
785
  epOut.timeout = TRANSFER_TIMEOUT_MS;
725
786
 
726
- await clearEndpointHalt(epIn);
727
- await clearEndpointHalt(epOut);
728
- await this.drainStaleInput(epIn);
787
+ await this.drainStaleInput(path, epIn);
729
788
 
730
789
  this.openDevices.set(path, { device: dev, iface, epIn, epOut });
731
790
  } catch (err) {
@@ -738,14 +797,14 @@ export default class NodeUsbTransport {
738
797
  }
739
798
  }
740
799
 
741
- private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
800
+ private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
742
801
  const originalTimeout = epIn.timeout;
743
802
  epIn.timeout = 50;
744
803
  try {
745
804
  // Drain a small bounded number of packets left by the previous USB session.
746
805
  for (let index = 0; index < 16; index += 1) {
747
806
  try {
748
- await transferInOnce(epIn, PACKET_SIZE);
807
+ await this.transferInOnce(path, epIn, PACKET_SIZE);
749
808
  } catch {
750
809
  break;
751
810
  }
@@ -773,39 +832,35 @@ export default class NodeUsbTransport {
773
832
  path: string,
774
833
  expectedProtocol?: ProtocolType
775
834
  ): Promise<ProtocolType> {
835
+ if (expectedProtocol === 'V2') {
836
+ // Bootloader may not answer Ping after an update reboot. An explicit V2 hint means
837
+ // the protocol was confirmed earlier; establish the link and let the first command
838
+ // verify that USB initialization completed.
839
+ this.deviceProtocol.set(path, 'V2');
840
+ this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
841
+ return 'V2';
842
+ }
843
+
776
844
  if (expectedProtocol === 'V1') {
777
845
  if (await this.probeProtocolV1(path)) {
778
846
  this.deviceProtocol.set(path, 'V1');
779
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
780
847
  return 'V1';
781
848
  }
782
849
  throw this.createProtocolMismatchError(expectedProtocol);
783
850
  }
784
851
 
785
- if (expectedProtocol === 'V2') {
786
- if (await this.probeProtocolV2(path)) {
787
- this.deviceProtocol.set(path, 'V2');
788
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
789
- return 'V2';
790
- }
791
- throw this.createProtocolMismatchError(expectedProtocol);
792
- }
793
-
794
852
  if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) {
795
853
  this.deviceProtocol.set(path, 'V2');
796
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
797
854
  return 'V2';
798
855
  }
799
856
 
800
857
  if (await this.probeProtocolV1(path)) {
801
858
  this.deviceProtocol.set(path, 'V1');
802
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
803
859
  return 'V1';
804
860
  }
805
861
 
806
862
  if (await this.probeProtocolV2(path)) {
807
863
  this.deviceProtocol.set(path, 'V2');
808
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2`);
809
864
  return 'V2';
810
865
  }
811
866
 
@@ -814,11 +869,13 @@ export default class NodeUsbTransport {
814
869
  }
815
870
 
816
871
  private async resetConnectionAfterProbe(path: string) {
817
- this.protocolV2Assemblers.get(path)?.reset();
818
- this.protocolV2Sessions.delete(path);
819
- this.protocolV2ReadTimeouts.delete(path);
872
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
820
873
 
821
874
  try {
875
+ // A timed-out probe may leave an IN transfer pending. Cancel and await it before
876
+ // closing the interface, or libusb may never callback and release()/stop() will
877
+ // wait forever, surfacing as Polling timeout (809).
878
+ await this.cancelActiveTransfers(path);
822
879
  await this.closeOpenDevice(path);
823
880
  } catch (error) {
824
881
  this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
@@ -878,8 +935,7 @@ export default class NodeUsbTransport {
878
935
  try {
879
936
  await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
880
937
  return true;
881
- } catch (error) {
882
- this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
938
+ } catch (_error) {
883
939
  return false;
884
940
  }
885
941
  }
@@ -898,74 +954,68 @@ export default class NodeUsbTransport {
898
954
  });
899
955
  }
900
956
 
901
- private async writeProtocolV2Frame(path: string, frame: Uint8Array) {
902
- let lastError: unknown;
903
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
957
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
958
+ if (!this.messages || !this.messagesV2) {
959
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
960
+ }
961
+ return {
962
+ protocolV1: this.messages,
963
+ protocolV2: this.messagesV2,
964
+ };
965
+ }
966
+
967
+ protected getProtocolV2UsbLogger() {
968
+ return this.Log;
969
+ }
970
+
971
+ protected async writeProtocolV2UsbPacket(
972
+ path: string,
973
+ frame: Uint8Array,
974
+ _context: ProtocolV2CallContext
975
+ ): Promise<void> {
976
+ if (this.cancelled) {
977
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
978
+ }
979
+ await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
980
+ }
981
+
982
+ protected async readProtocolV2UsbPacket(
983
+ path: string,
984
+ _context: ProtocolV2CallContext
985
+ ): Promise<Uint8Array> {
986
+ for (;;) {
904
987
  if (this.cancelled) {
905
988
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
906
989
  }
907
990
  try {
908
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
909
- return;
991
+ const packet = await this.transferInOnce(
992
+ path,
993
+ this.getOpenDevice(path).epIn,
994
+ PROTOCOL_V2_FRAME_MAX_BYTES
995
+ );
996
+ return new Uint8Array(
997
+ packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
998
+ );
910
999
  } catch (error) {
911
- lastError = error;
912
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
913
- if (!shouldRetry) {
1000
+ if (!this.isUsbTransferTimeout(error)) {
914
1001
  throw error;
915
1002
  }
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
1003
  }
928
1004
  }
929
- throw lastError;
930
1005
  }
931
1006
 
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
- }
1007
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
1008
+ await this.cancelActiveTransfers(path);
1009
+ await this.closeOpenDevice(path);
1010
+ }
938
1011
 
939
- let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
940
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
1012
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
1013
+ this.deviceProtocol.delete(path);
1014
+ this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
1015
+ }
941
1016
 
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;
1017
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
1018
+ return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
969
1019
  }
970
1020
 
971
1021
  private async callProtocolV2(
@@ -974,42 +1024,7 @@ export default class NodeUsbTransport {
974
1024
  data: Record<string, unknown>,
975
1025
  options?: TransportCallOptions
976
1026
  ) {
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
- }
1027
+ return this.callProtocolV2Usb(path, name, data, options);
1013
1028
  }
1014
1029
 
1015
1030
  /**