@onekeyfe/hd-transport-usb 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.
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
 
@@ -129,31 +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
135
  /**
158
136
  * Skip the 0x3F protocol marker byte from a USB packet.
159
137
  */
@@ -180,12 +158,14 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
180
158
  *
181
159
  * Modeled after WebUsbTransport.
182
160
  */
183
- export default class NodeUsbTransport {
161
+ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
184
162
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
185
163
 
186
164
  /** Protobuf schema for Protocol V2 transports. */
187
165
  messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
188
166
 
167
+ private protocolV2SchemaSource: string | undefined;
168
+
189
169
  name = 'NodeUsbTransport';
190
170
 
191
171
  version = '';
@@ -207,21 +187,29 @@ export default class NodeUsbTransport {
207
187
  /** Per-path protocol type detected by active wire-level probe. */
208
188
  private deviceProtocol: Map<string, ProtocolType> = new Map();
209
189
 
210
- /** Per-path Protocol V2 frame assembler, preserving buffered frames during reads. */
211
- private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
212
-
213
- /** Per-path Protocol V2 session, preserving seq across API calls on the same device path. */
214
- private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
215
-
216
- /** Current Protocol V2 read timeout, consumed by cached session readFrame closures. */
217
- private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
218
-
219
190
  /** per-path reconnect lock to prevent concurrent reconnects */
220
191
  private reconnectLocks = new Map<string, Promise<OpenDevice>>();
221
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
+
222
202
  /** set to true when cancel() is called; checked by retry loops */
223
203
  private cancelled = false;
224
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
+
225
213
  /**
226
214
  * Initialize transport.
227
215
  * Signature matches the Transport.init interface (logger, emitter).
@@ -240,18 +228,47 @@ export default class NodeUsbTransport {
240
228
  }
241
229
 
242
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;
243
238
  this.messagesV2 = parseConfigure(signedData);
244
- this.protocolV2Sessions.clear();
245
- this.protocolV2ReadTimeouts.clear();
246
- 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
+ );
247
245
  }
248
246
 
249
247
  listen() {
250
248
  // empty — could add hotplug events via usb.on('attach'/'detach')
251
249
  }
252
250
 
253
- stop() {
254
- // 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();
255
272
  }
256
273
 
257
274
  /**
@@ -326,12 +343,18 @@ export default class NodeUsbTransport {
326
343
  }
327
344
 
328
345
  try {
346
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
329
347
  await this.closeOpenDevice(path);
330
348
  await this.openDevice(path);
331
- await this.detectProtocol(path, input.expectedProtocol);
349
+ await this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
332
350
  return path;
333
351
  } catch (error: any) {
334
352
  this.Log?.debug('NodeUsbTransport acquire error: ', error);
353
+ try {
354
+ await this.release(path);
355
+ } catch (cleanupError) {
356
+ this.Log?.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
357
+ }
335
358
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
336
359
  }
337
360
  }
@@ -340,9 +363,91 @@ export default class NodeUsbTransport {
340
363
  * Release device — release interface and close.
341
364
  */
342
365
  async release(path: string, _onclose?: boolean): Promise<void> {
366
+ await this.cancelActiveTransfers(path);
367
+ await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
343
368
  await this.closeOpenDevice(path);
344
369
  this.deviceProtocol.delete(path);
345
- this.protocolV2Assemblers.delete(path);
370
+ }
371
+
372
+ private async cancelActiveTransfers(path?: string): Promise<void> {
373
+ const transfers = Array.from(this.activeTransfers.entries()).filter(
374
+ ([, active]) => path === undefined || active.path === path
375
+ );
376
+ transfers.forEach(([transfer]) => {
377
+ try {
378
+ transfer.cancel();
379
+ } catch {
380
+ // A transfer may finish between the snapshot and cancel().
381
+ }
382
+ });
383
+ await Promise.allSettled(transfers.map(([, active]) => active.settled));
384
+ }
385
+
386
+ private createTrackedTransfer(
387
+ path: string,
388
+ endpoint: usb.InEndpoint | usb.OutEndpoint,
389
+ callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
390
+ ): usb.Transfer {
391
+ let resolveSettled: () => void = () => undefined;
392
+ const settled = new Promise<void>(resolve => {
393
+ resolveSettled = resolve;
394
+ });
395
+ const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
396
+ this.activeTransfers.delete(transfer);
397
+ resolveSettled();
398
+ callback(error, buffer, actualLength);
399
+ });
400
+ this.activeTransfers.set(transfer, { path, settled, resolveSettled });
401
+ return transfer;
402
+ }
403
+
404
+ private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
405
+ return new Promise((resolve, reject) => {
406
+ const buffer = Buffer.alloc(length);
407
+ const transfer = this.createTrackedTransfer(
408
+ path,
409
+ ep,
410
+ (error, _submittedBuffer, actualLength) => {
411
+ if (error) {
412
+ reject(error);
413
+ return;
414
+ }
415
+ if (actualLength <= 0) {
416
+ reject(new Error('Empty USB transfer'));
417
+ return;
418
+ }
419
+ resolve(buffer.subarray(0, actualLength));
420
+ }
421
+ );
422
+ try {
423
+ transfer.submit(buffer);
424
+ } catch (error) {
425
+ const active = this.activeTransfers.get(transfer);
426
+ this.activeTransfers.delete(transfer);
427
+ active?.resolveSettled();
428
+ reject(error);
429
+ }
430
+ });
431
+ }
432
+
433
+ private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
434
+ return new Promise((resolve, reject) => {
435
+ const transfer = this.createTrackedTransfer(path, ep, error => {
436
+ if (error) {
437
+ reject(error);
438
+ return;
439
+ }
440
+ resolve();
441
+ });
442
+ try {
443
+ transfer.submit(data);
444
+ } catch (error) {
445
+ const active = this.activeTransfers.get(transfer);
446
+ this.activeTransfers.delete(transfer);
447
+ active?.resolveSettled();
448
+ reject(error);
449
+ }
450
+ });
346
451
  }
347
452
 
348
453
  private async closeOpenDevice(path: string): Promise<void> {
@@ -397,18 +502,8 @@ export default class NodeUsbTransport {
397
502
  `Device protocol has not been detected for ${path}`
398
503
  );
399
504
  }
400
- if (LogBlockCommand.has(name)) {
401
- this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
402
- } else {
403
- this.Log?.debug(
404
- 'NodeUsbTransport call-',
405
- ' name: ',
406
- name,
407
- ' data: ',
408
- data,
409
- ' protocol: ',
410
- protocol
411
- );
505
+ if (!shouldSuppressHighVolumeCallLog(name)) {
506
+ this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
412
507
  }
413
508
 
414
509
  if (protocol === 'V2') {
@@ -445,7 +540,6 @@ export default class NodeUsbTransport {
445
540
  }
446
541
 
447
542
  cancel() {
448
- this.Log?.debug('NodeUsbTransport cancel');
449
543
  this.cancelled = true;
450
544
  }
451
545
 
@@ -475,6 +569,12 @@ export default class NodeUsbTransport {
475
569
 
476
570
  private isRetryableError(error: unknown): boolean {
477
571
  const message = this.getErrorMessage(error).toLowerCase();
572
+ // cancelActiveTransfers() terminates timed-out or releasing native requests.
573
+ // LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
574
+ // read on the rebuilt interface and race protocol probing against release.
575
+ if (message.includes('cancelled') || message.includes('canceled')) {
576
+ return false;
577
+ }
478
578
  return (
479
579
  message.includes('libusb') ||
480
580
  message.includes('transfer') ||
@@ -531,6 +631,10 @@ export default class NodeUsbTransport {
531
631
  );
532
632
  await wait(attempt * PACKET_IO_RETRY_DELAY);
533
633
 
634
+ await this.rotateProtocolV2UsbGeneration(
635
+ path,
636
+ `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
637
+ );
534
638
  // Close the existing device without clearing the detected protocol cache.
535
639
  try {
536
640
  await this.closeOpenDevice(path);
@@ -575,7 +679,7 @@ export default class NodeUsbTransport {
575
679
  const packet = new Uint8Array(PACKET_SIZE);
576
680
  packet[0] = REPORT_ID;
577
681
  packet.set(new Uint8Array(buffer), 1);
578
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
682
+ await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
579
683
  }
580
684
  return; // all chunks sent successfully
581
685
  } catch (error) {
@@ -618,27 +722,27 @@ export default class NodeUsbTransport {
618
722
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
619
723
  }
620
724
  try {
621
- return await transferInOnce(currentDev.epIn, length);
725
+ return await this.transferInOnce(path, currentDev.epIn, length);
622
726
  } catch (error) {
623
727
  lastError = error;
624
728
  if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
625
729
  attempt -= 1;
626
- continue;
627
- }
628
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
629
- if (!shouldRetry) {
630
- throw error;
631
- }
632
- try {
633
- currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
634
- } catch (reconnectError) {
635
- lastError = reconnectError;
636
- this.Log?.debug(
637
- `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
638
- reconnectError
639
- )}`
640
- );
641
- break;
730
+ } else {
731
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
732
+ if (!shouldRetry) {
733
+ throw error;
734
+ }
735
+ try {
736
+ currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
737
+ } catch (reconnectError) {
738
+ lastError = reconnectError;
739
+ this.Log?.debug(
740
+ `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
741
+ reconnectError
742
+ )}`
743
+ );
744
+ break;
745
+ }
642
746
  }
643
747
  }
644
748
  }
@@ -698,7 +802,7 @@ export default class NodeUsbTransport {
698
802
  epIn.timeout = TRANSFER_TIMEOUT_MS;
699
803
  epOut.timeout = TRANSFER_TIMEOUT_MS;
700
804
 
701
- await this.drainStaleInput(epIn);
805
+ await this.drainStaleInput(path, epIn);
702
806
 
703
807
  this.openDevices.set(path, { device: dev, iface, epIn, epOut });
704
808
  } catch (err) {
@@ -711,14 +815,14 @@ export default class NodeUsbTransport {
711
815
  }
712
816
  }
713
817
 
714
- private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
818
+ private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
715
819
  const originalTimeout = epIn.timeout;
716
820
  epIn.timeout = 50;
717
821
  try {
718
822
  // Drain a small bounded number of packets left by the previous USB session.
719
823
  for (let index = 0; index < 16; index += 1) {
720
824
  try {
721
- await transferInOnce(epIn, PACKET_SIZE);
825
+ await this.transferInOnce(path, epIn, PACKET_SIZE);
722
826
  } catch {
723
827
  break;
724
828
  }
@@ -744,17 +848,9 @@ export default class NodeUsbTransport {
744
848
 
745
849
  private async detectProtocol(
746
850
  path: string,
747
- expectedProtocol?: ProtocolType
851
+ expectedProtocol?: ProtocolType,
852
+ protocolHint?: ProtocolType
748
853
  ): Promise<ProtocolType> {
749
- if (expectedProtocol === 'V1') {
750
- if (await this.probeProtocolV1(path)) {
751
- this.deviceProtocol.set(path, 'V1');
752
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
753
- return 'V1';
754
- }
755
- throw this.createProtocolMismatchError(expectedProtocol);
756
- }
757
-
758
854
  if (expectedProtocol === 'V2') {
759
855
  if (await this.probeProtocolV2(path)) {
760
856
  this.deviceProtocol.set(path, 'V2');
@@ -764,22 +860,27 @@ export default class NodeUsbTransport {
764
860
  throw this.createProtocolMismatchError(expectedProtocol);
765
861
  }
766
862
 
767
- if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) {
768
- this.deviceProtocol.set(path, 'V2');
769
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
770
- return 'V2';
863
+ if (expectedProtocol === 'V1') {
864
+ if (await this.probeProtocolV1(path)) {
865
+ this.deviceProtocol.set(path, 'V1');
866
+ return 'V1';
867
+ }
868
+ throw this.createProtocolMismatchError(expectedProtocol);
771
869
  }
772
870
 
773
- if (await this.probeProtocolV1(path)) {
774
- this.deviceProtocol.set(path, 'V1');
775
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
776
- return 'V1';
777
- }
871
+ const probeOrder: ProtocolType[] =
872
+ protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
778
873
 
779
- if (await this.probeProtocolV2(path)) {
780
- this.deviceProtocol.set(path, 'V2');
781
- this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2`);
782
- return 'V2';
874
+ for (const [index, protocol] of probeOrder.entries()) {
875
+ if (index > 0) {
876
+ await this.resetConnectionAfterProbe(path);
877
+ }
878
+ const detected =
879
+ protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
880
+ if (detected) {
881
+ this.deviceProtocol.set(path, protocol);
882
+ return protocol;
883
+ }
783
884
  }
784
885
 
785
886
  this.deviceProtocol.delete(path);
@@ -787,11 +888,13 @@ export default class NodeUsbTransport {
787
888
  }
788
889
 
789
890
  private async resetConnectionAfterProbe(path: string) {
790
- this.protocolV2Assemblers.get(path)?.reset();
791
- this.protocolV2Sessions.delete(path);
792
- this.protocolV2ReadTimeouts.delete(path);
891
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
793
892
 
794
893
  try {
894
+ // A timed-out probe may leave an IN transfer pending. Cancel and await it before
895
+ // closing the interface, or libusb may never callback and release()/stop() will
896
+ // wait forever, surfacing as Polling timeout (809).
897
+ await this.cancelActiveTransfers(path);
795
898
  await this.closeOpenDevice(path);
796
899
  } catch (error) {
797
900
  this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
@@ -851,8 +954,7 @@ export default class NodeUsbTransport {
851
954
  try {
852
955
  await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
853
956
  return true;
854
- } catch (error) {
855
- this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
957
+ } catch (_error) {
856
958
  return false;
857
959
  }
858
960
  }
@@ -871,75 +973,71 @@ export default class NodeUsbTransport {
871
973
  });
872
974
  }
873
975
 
874
- private async writeProtocolV2Frame(path: string, frame: Uint8Array) {
875
- let lastError: unknown;
876
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
976
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
977
+ if (!this.messages || !this.messagesV2) {
978
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
979
+ }
980
+ return {
981
+ protocolV1: this.messages,
982
+ protocolV2: this.messagesV2,
983
+ };
984
+ }
985
+
986
+ protected getProtocolV2UsbLogger() {
987
+ return this.Log;
988
+ }
989
+
990
+ protected async writeProtocolV2UsbPacket(
991
+ path: string,
992
+ frame: Uint8Array,
993
+ _context: ProtocolV2CallContext
994
+ ): Promise<void> {
995
+ if (this.cancelled) {
996
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
997
+ }
998
+ await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
999
+ }
1000
+
1001
+ protected async readProtocolV2UsbPacket(
1002
+ path: string,
1003
+ _context: ProtocolV2CallContext
1004
+ ): Promise<Uint8Array> {
1005
+ for (;;) {
877
1006
  if (this.cancelled) {
878
1007
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
879
1008
  }
880
1009
  try {
881
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
882
- return;
1010
+ const packet = await this.transferInOnce(
1011
+ path,
1012
+ this.getOpenDevice(path).epIn,
1013
+ PROTOCOL_V2_FRAME_MAX_BYTES
1014
+ );
1015
+ return new Uint8Array(
1016
+ packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
1017
+ );
883
1018
  } catch (error) {
884
- lastError = error;
885
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
886
- if (!shouldRetry) {
1019
+ if (!this.isUsbTransferTimeout(error)) {
887
1020
  throw error;
888
1021
  }
889
- try {
890
- await this.reconnectForRetry(path, 'out', attempt, error);
891
- } catch (reconnectError) {
892
- lastError = reconnectError;
893
- this.Log?.debug(
894
- `[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
895
- reconnectError
896
- )}`
897
- );
898
- break;
899
- }
900
1022
  }
901
1023
  }
902
- throw lastError;
903
1024
  }
904
1025
 
905
- private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
906
- let assembler = this.protocolV2Assemblers.get(path);
907
- if (!assembler) {
908
- assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
909
- this.protocolV2Assemblers.set(path, assembler);
910
- }
1026
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
1027
+ await this.cancelActiveTransfers(path);
1028
+ await this.closeOpenDevice(path);
1029
+ }
911
1030
 
912
- let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
913
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
1031
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
1032
+ this.deviceProtocol.delete(path);
1033
+ this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
1034
+ }
914
1035
 
915
- while (!frame) {
916
- const transferIn = this.transferInWithRetry(
917
- path,
918
- this.getOpenDevice(path),
919
- PROTOCOL_V2_FRAME_MAX_BYTES,
920
- { waitIndefinitelyOnTimeout: !deadline }
921
- );
922
- const packet = deadline
923
- ? await this.withProtocolReadTimeout(
924
- path,
925
- transferIn,
926
- Math.max(deadline - Date.now(), 1),
927
- 'V2'
928
- )
929
- : await transferIn;
930
- const bytes = new Uint8Array(
931
- packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
932
- );
933
- try {
934
- frame = assembler.push(bytes);
935
- } catch (error) {
936
- throw ERRORS.TypedError(
937
- HardwareErrorCode.NetworkError,
938
- error instanceof Error ? error.message : String(error)
939
- );
940
- }
941
- }
942
- return frame;
1036
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
1037
+ return new ProtocolV2LinkError(
1038
+ 'response-timeout',
1039
+ `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
1040
+ );
943
1041
  }
944
1042
 
945
1043
  private async callProtocolV2(
@@ -948,42 +1046,7 @@ export default class NodeUsbTransport {
948
1046
  data: Record<string, unknown>,
949
1047
  options?: TransportCallOptions
950
1048
  ) {
951
- const protocolV1Messages = this.messages;
952
- if (!this.messagesV2) {
953
- throw ERRORS.TypedError(
954
- HardwareErrorCode.TransportNotConfigured,
955
- 'Protocol V2 schema not configured'
956
- );
957
- }
958
- if (!protocolV1Messages) {
959
- throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
960
- }
961
-
962
- let session = this.protocolV2Sessions.get(path);
963
- if (!session) {
964
- session = new ProtocolV2Session({
965
- schemas: {
966
- protocolV1: protocolV1Messages,
967
- protocolV2: this.messagesV2,
968
- },
969
- router: PROTOCOL_V2_CHANNEL_USB,
970
- writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(path, frame),
971
- readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
972
- logger: this.Log,
973
- logPrefix: 'ProtocolV2 NodeUSB',
974
- createTimeoutError: (messageName: string, timeoutMs: number) =>
975
- new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
976
- });
977
- this.protocolV2Sessions.set(path, session);
978
- }
979
-
980
- this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
981
- this.protocolV2Assemblers.get(path)?.reset();
982
- try {
983
- return await session.call(name, data, options);
984
- } finally {
985
- this.protocolV2ReadTimeouts.delete(path);
986
- }
1049
+ return this.callProtocolV2Usb(path, name, data, options);
987
1050
  }
988
1051
 
989
1052
  /**
@@ -0,0 +1 @@
1
+ export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';