@onekeyfe/hd-transport-usb 1.2.0-alpha.1 → 1.2.0-alpha.11

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,260 @@
1
+ import transportPackage, { PROTOCOL_V2_CHANNEL_USB, ProtocolV2 } from '@onekeyfe/hd-transport';
2
+
3
+ import NodeUsbTransport from '../src';
4
+
5
+ let mockUsbDevices: any[] = [];
6
+
7
+ jest.mock('usb', () => ({
8
+ getDeviceList: jest.fn(() => mockUsbDevices),
9
+ }));
10
+
11
+ const { parseConfigure } = transportPackage;
12
+
13
+ const protocolV1Schema = {
14
+ nested: {
15
+ Initialize: { fields: {} },
16
+ Success: {
17
+ fields: {
18
+ message: { type: 'string', id: 1 },
19
+ },
20
+ },
21
+ MessageType: {
22
+ values: {
23
+ MessageType_Initialize: 1,
24
+ MessageType_Success: 2,
25
+ },
26
+ },
27
+ },
28
+ };
29
+
30
+ const protocolV2Schema = {
31
+ nested: {
32
+ Ping: {
33
+ fields: {
34
+ message: { type: 'string', id: 1 },
35
+ },
36
+ },
37
+ Success: {
38
+ fields: {
39
+ message: { type: 'string', id: 1 },
40
+ },
41
+ },
42
+ MessageType: {
43
+ values: {
44
+ MessageType_Ping: 60206,
45
+ MessageType_Success: 60207,
46
+ },
47
+ },
48
+ },
49
+ };
50
+
51
+ const schemas = {
52
+ protocolV1: parseConfigure(protocolV1Schema),
53
+ protocolV2: parseConfigure(protocolV2Schema),
54
+ };
55
+
56
+ type PendingRead = {
57
+ started: Promise<void>;
58
+ fail: (error?: Error) => void;
59
+ };
60
+
61
+ const createHarness = () => {
62
+ const path = '6136';
63
+ const responseQueue: Buffer[] = [];
64
+ const sentSeqs: number[] = [];
65
+ let writeError: Error | undefined;
66
+ let holdNextRead:
67
+ | {
68
+ markStarted: () => void;
69
+ started: Promise<void>;
70
+ callback?: (error?: Error, data?: Buffer) => void;
71
+ }
72
+ | undefined;
73
+
74
+ const epIn = {
75
+ direction: 'in',
76
+ address: 0x81,
77
+ timeout: 30_000,
78
+ transfer: jest.fn((_length: number, callback: (error?: Error, data?: Buffer) => void) => {
79
+ if (epIn.timeout === 50) {
80
+ callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
81
+ return;
82
+ }
83
+ if (holdNextRead) {
84
+ const pending = holdNextRead;
85
+ holdNextRead = undefined;
86
+ pending.callback = callback;
87
+ pending.markStarted();
88
+ return;
89
+ }
90
+ const response = responseQueue.shift();
91
+ if (!response) {
92
+ callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
93
+ return;
94
+ }
95
+ callback(undefined, response);
96
+ }),
97
+ };
98
+
99
+ const epOut = {
100
+ direction: 'out',
101
+ address: 0x01,
102
+ timeout: 30_000,
103
+ transfer: jest.fn((data: Buffer, callback: (error?: Error) => void) => {
104
+ const seq = data[6];
105
+ sentSeqs.push(seq);
106
+ if (writeError) {
107
+ const error = writeError;
108
+ writeError = undefined;
109
+ callback(error);
110
+ return;
111
+ }
112
+ responseQueue.push(
113
+ Buffer.from(
114
+ ProtocolV2.encodeFrame(
115
+ schemas,
116
+ 'Success',
117
+ { message: 'ok' },
118
+ { router: PROTOCOL_V2_CHANNEL_USB, seq }
119
+ )
120
+ )
121
+ );
122
+ callback();
123
+ }),
124
+ };
125
+
126
+ const iface = {
127
+ descriptor: { bInterfaceClass: 0xff, bInterfaceNumber: 0 },
128
+ endpoints: [epIn, epOut],
129
+ claim: jest.fn(),
130
+ release: jest.fn((callback: () => void) => callback()),
131
+ isKernelDriverActive: jest.fn(() => false),
132
+ detachKernelDriver: jest.fn(),
133
+ };
134
+
135
+ const device = {
136
+ busNumber: 1,
137
+ deviceAddress: 2,
138
+ timeout: 30_000,
139
+ deviceDescriptor: {
140
+ idVendor: 0x1209,
141
+ idProduct: 0x4f4a,
142
+ iSerialNumber: 1,
143
+ },
144
+ interfaces: [iface],
145
+ open: jest.fn(),
146
+ close: jest.fn(),
147
+ getStringDescriptor: jest.fn(
148
+ (_index: number, callback: (error?: Error, value?: string) => void) =>
149
+ callback(undefined, path)
150
+ ),
151
+ };
152
+
153
+ mockUsbDevices = [device];
154
+ const transport = new NodeUsbTransport();
155
+ transport.init({ debug: jest.fn(), error: jest.fn() });
156
+ transport.configure(protocolV1Schema);
157
+ transport.configureProtocolV2(protocolV2Schema);
158
+
159
+ return {
160
+ transport,
161
+ path,
162
+ device,
163
+ iface,
164
+ epIn,
165
+ epOut,
166
+ sentSeqs,
167
+ async acquire() {
168
+ await transport.enumerate();
169
+ await transport.acquire({ path, expectedProtocol: 'V2' });
170
+ },
171
+ failNextWrite(error: Error) {
172
+ writeError = error;
173
+ },
174
+ holdRead(): PendingRead {
175
+ let markStarted: () => void = () => undefined;
176
+ const started = new Promise<void>(resolve => {
177
+ markStarted = resolve;
178
+ });
179
+ const pending = { markStarted, started, callback: undefined };
180
+ holdNextRead = pending;
181
+ return {
182
+ started,
183
+ fail(error = new Error('read released after test')) {
184
+ pending.callback?.(error);
185
+ },
186
+ };
187
+ },
188
+ };
189
+ };
190
+
191
+ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
192
+ test('keeps seq across probe, call and reacquire', async () => {
193
+ const harness = createHarness();
194
+ const { transport, path, sentSeqs } = harness;
195
+
196
+ await harness.acquire();
197
+ await transport.call(path, 'Ping', { message: 'first' });
198
+ await transport.release(path);
199
+ await harness.acquire();
200
+ await transport.call(path, 'Ping', { message: 'second' });
201
+
202
+ expect(sentSeqs).toEqual([1, 2, 3, 4]);
203
+ await transport.release(path);
204
+ });
205
+
206
+ test('does not resend a Protocol V2 frame after transferOut fails', async () => {
207
+ const harness = createHarness();
208
+ const { transport, path, epOut } = harness;
209
+ await harness.acquire();
210
+ epOut.transfer.mockClear();
211
+ harness.failNextWrite(new Error('LIBUSB_ERROR_IO'));
212
+
213
+ await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow(
214
+ 'LIBUSB_ERROR_IO'
215
+ );
216
+
217
+ expect(epOut.transfer).toHaveBeenCalledTimes(1);
218
+ });
219
+
220
+ test('rejects a pending read when release invalidates the link', async () => {
221
+ const harness = createHarness();
222
+ const { transport, path } = harness;
223
+ await harness.acquire();
224
+ const pendingRead = harness.holdRead();
225
+
226
+ const call = transport.call(path, 'Ping', { message: 'pending' }, { timeoutMs: 5000 });
227
+ const outcome = call.then(
228
+ () => 'resolved',
229
+ error => error.message
230
+ );
231
+ await pendingRead.started;
232
+ await transport.release(path);
233
+ const settled = await Promise.race([
234
+ outcome,
235
+ new Promise<string>(resolve => {
236
+ setTimeout(() => resolve('still pending'), 50);
237
+ }),
238
+ ]);
239
+ pendingRead.fail();
240
+
241
+ expect(settled).not.toBe('still pending');
242
+ });
243
+
244
+ test('keeps the cursor after a response timeout rebuilds the USB connection', async () => {
245
+ const harness = createHarness();
246
+ const { transport, path, sentSeqs } = harness;
247
+ await harness.acquire();
248
+ const pendingRead = harness.holdRead();
249
+
250
+ await expect(
251
+ transport.call(path, 'Ping', { message: 'timeout' }, { timeoutMs: 20 })
252
+ ).rejects.toThrow('20ms');
253
+ pendingRead.fail();
254
+ await harness.acquire();
255
+ await transport.call(path, 'Ping', { message: 'after-timeout' });
256
+
257
+ expect(sentSeqs).toEqual([1, 2, 3, 4]);
258
+ await transport.release(path);
259
+ });
260
+ });
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as transport from '@onekeyfe/hd-transport';
2
- import transport__default, { OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolType } from '@onekeyfe/hd-transport';
2
+ import transport__default, { ProtocolV2UsbTransportBase, OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolV2Schemas, ProtocolV2CallContext, ProtocolType } from '@onekeyfe/hd-transport';
3
3
  import EventEmitter from 'events';
4
4
 
5
- declare class NodeUsbTransport {
5
+ declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
6
6
  messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
7
7
  messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
8
8
  name: string;
@@ -14,11 +14,9 @@ declare class NodeUsbTransport {
14
14
  private serialToBusId;
15
15
  private openDevices;
16
16
  private deviceProtocol;
17
- private protocolV2Assemblers;
18
- private protocolV2Sessions;
19
- private protocolV2ReadTimeouts;
20
17
  private reconnectLocks;
21
18
  private cancelled;
19
+ constructor();
22
20
  init(logger: any, emitter?: EventEmitter): Promise<string>;
23
21
  configure(signedData: any): Promise<void>;
24
22
  configureProtocolV2(signedData: any): void;
@@ -41,6 +39,7 @@ declare class NodeUsbTransport {
41
39
  private getOpenDevice;
42
40
  private getErrorMessage;
43
41
  private isRetryableError;
42
+ private isUsbTransferTimeout;
44
43
  private getDeviceInterface;
45
44
  private reconnectForRetry;
46
45
  private sendAllChunksWithRetry;
@@ -54,8 +53,13 @@ declare class NodeUsbTransport {
54
53
  private withProtocolReadTimeout;
55
54
  private probeProtocolV1;
56
55
  private probeProtocolV2;
57
- private writeProtocolV2Frame;
58
- private receiveProtocolV2Frame;
56
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas;
57
+ protected getProtocolV2UsbLogger(): any;
58
+ protected writeProtocolV2UsbPacket(path: string, frame: Uint8Array, _context: ProtocolV2CallContext): Promise<void>;
59
+ protected readProtocolV2UsbPacket(path: string, _context: ProtocolV2CallContext): Promise<Uint8Array>;
60
+ protected resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void>;
61
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string): void;
62
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
59
63
  private callProtocolV2;
60
64
  private receiveData;
61
65
  getProtocolType(path: string): ProtocolType | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAWN,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AA0LhC,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAGlE,UAAU,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEpE,IAAI,SAAsB;IAE1B,OAAO,SAAM;IAEb,UAAU,UAAS;IAEnB,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAGvB,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAiC;IAGpD,OAAO,CAAC,cAAc,CAAwC;IAG9D,OAAO,CAAC,oBAAoB,CAAoD;IAGhF,OAAO,CAAC,kBAAkB,CAA6C;IAGvE,OAAO,CAAC,sBAAsB,CAA8C;IAG5E,OAAO,CAAC,cAAc,CAA0C;IAGhE,OAAO,CAAC,SAAS,CAAS;IAM1B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAMxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAOzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAOnC,MAAM;IAIN,IAAI;IAQE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9E,IAAI,CAAC,IAAI,EAAE,MAAM;;;;;;IAiBjB,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA8BxC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAsB7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAMhD,eAAe;IA6BvB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YAwClB,cAAc;IA0B5B,MAAM;IAWN,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,iBAAiB;YAmDX,sBAAsB;YAyCtB,mBAAmB;YAsCnB,UAAU;YA0EV,eAAe;IAiB7B,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,4BAA4B;YAOtB,cAAc;YA4Cd,yBAAyB;YAezB,uBAAuB;YA0CvB,eAAe;YAcf,eAAe;YAcf,oBAAoB;YA+BpB,sBAAsB;YAuCtB,cAAc;YAgDd,WAAW;IA6CzB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -71,7 +71,7 @@ const TRANSFER_TIMEOUT_MS = 30000;
71
71
  const SERIAL_READ_TIMEOUT_MS = 5000;
72
72
  const PACKET_IO_MAX_RETRIES = 3;
73
73
  const PACKET_IO_RETRY_DELAY = 300;
74
- const PROTOCOL_PROBE_TIMEOUT = 1000;
74
+ const PROTOCOL_PROBE_TIMEOUT = 5000;
75
75
  function getBusId(dev) {
76
76
  return `usb:${dev.busNumber}:${dev.deviceAddress}`;
77
77
  }
@@ -152,26 +152,6 @@ function transferOutOnce(ep, data) {
152
152
  });
153
153
  });
154
154
  }
155
- function resetUsbDevice(dev) {
156
- return new Promise(resolve => {
157
- const reset = dev.reset;
158
- if (typeof reset !== 'function') {
159
- resolve();
160
- return;
161
- }
162
- reset.call(dev, () => resolve());
163
- });
164
- }
165
- function clearEndpointHalt(ep) {
166
- return new Promise(resolve => {
167
- const clearHalt = ep.clearHalt;
168
- if (typeof clearHalt !== 'function') {
169
- resolve();
170
- return;
171
- }
172
- clearHalt.call(ep, () => resolve());
173
- });
174
- }
175
155
  function skipReportByte(packet) {
176
156
  if (packet[0] === REPORT_ID) {
177
157
  return packet.subarray(1);
@@ -181,8 +161,13 @@ function skipReportByte(packet) {
181
161
  function toArrayBuffer(buf) {
182
162
  return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
183
163
  }
184
- class NodeUsbTransport {
164
+ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
185
165
  constructor() {
166
+ super({
167
+ router: transport.PROTOCOL_V2_CHANNEL_USB,
168
+ maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
169
+ logPrefix: 'ProtocolV2 NodeUSB',
170
+ });
186
171
  this.name = 'NodeUsbTransport';
187
172
  this.version = '';
188
173
  this.configured = false;
@@ -190,9 +175,6 @@ class NodeUsbTransport {
190
175
  this.serialToBusId = new Map();
191
176
  this.openDevices = new Map();
192
177
  this.deviceProtocol = new Map();
193
- this.protocolV2Assemblers = new Map();
194
- this.protocolV2Sessions = new Map();
195
- this.protocolV2ReadTimeouts = new Map();
196
178
  this.reconnectLocks = new Map();
197
179
  this.cancelled = false;
198
180
  }
@@ -210,13 +192,13 @@ class NodeUsbTransport {
210
192
  configureProtocolV2(signedData) {
211
193
  var _a;
212
194
  this.messagesV2 = parseConfigure(signedData);
213
- this.protocolV2Sessions.clear();
214
- this.protocolV2ReadTimeouts.clear();
195
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] schema link cleanup failed:', error); });
215
196
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V2 schema configured');
216
197
  }
217
198
  listen() {
218
199
  }
219
200
  stop() {
201
+ this.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] stop link cleanup failed:', error); });
220
202
  }
221
203
  post(path, name, data) {
222
204
  return __awaiter(this, void 0, void 0, function* () {
@@ -274,6 +256,7 @@ class NodeUsbTransport {
274
256
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
275
257
  }
276
258
  try {
259
+ yield this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
277
260
  yield this.closeOpenDevice(path);
278
261
  yield this.openDevice(path);
279
262
  yield this.detectProtocol(path, input.expectedProtocol);
@@ -287,9 +270,9 @@ class NodeUsbTransport {
287
270
  }
288
271
  release(path, _onclose) {
289
272
  return __awaiter(this, void 0, void 0, function* () {
273
+ yield this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
290
274
  yield this.closeOpenDevice(path);
291
275
  this.deviceProtocol.delete(path);
292
- this.protocolV2Assemblers.delete(path);
293
276
  });
294
277
  }
295
278
  closeOpenDevice(path) {
@@ -397,6 +380,10 @@ class NodeUsbTransport {
397
380
  message.includes('timeout') ||
398
381
  message.includes('interrupt'));
399
382
  }
383
+ isUsbTransferTimeout(error) {
384
+ const message = this.getErrorMessage(error).toLowerCase();
385
+ return message.includes('timeout') || message.includes('timed_out');
386
+ }
400
387
  getDeviceInterface(dev) {
401
388
  var _a;
402
389
  const { interfaces } = dev;
@@ -416,6 +403,7 @@ class NodeUsbTransport {
416
403
  var _a, _b;
417
404
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
418
405
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
406
+ yield this.rotateProtocolV2UsbGeneration(path, `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`);
419
407
  try {
420
408
  yield this.closeOpenDevice(path);
421
409
  }
@@ -472,7 +460,7 @@ class NodeUsbTransport {
472
460
  throw lastError;
473
461
  });
474
462
  }
475
- transferInWithRetry(path, openDev, length) {
463
+ transferInWithRetry(path, openDev, length, options) {
476
464
  var _a;
477
465
  return __awaiter(this, void 0, void 0, function* () {
478
466
  let lastError;
@@ -486,17 +474,22 @@ class NodeUsbTransport {
486
474
  }
487
475
  catch (error) {
488
476
  lastError = error;
489
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
490
- if (!shouldRetry) {
491
- throw error;
492
- }
493
- try {
494
- currentDev = yield this.reconnectForRetry(path, 'in', attempt, error);
477
+ if ((options === null || options === void 0 ? void 0 : options.waitIndefinitelyOnTimeout) && this.isUsbTransferTimeout(error)) {
478
+ attempt -= 1;
495
479
  }
496
- catch (reconnectError) {
497
- lastError = reconnectError;
498
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
499
- break;
480
+ else {
481
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
482
+ if (!shouldRetry) {
483
+ throw error;
484
+ }
485
+ try {
486
+ currentDev = yield this.reconnectForRetry(path, 'in', attempt, error);
487
+ }
488
+ catch (reconnectError) {
489
+ lastError = reconnectError;
490
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
491
+ break;
492
+ }
500
493
  }
501
494
  }
502
495
  }
@@ -518,13 +511,6 @@ class NodeUsbTransport {
518
511
  try {
519
512
  dev.open();
520
513
  dev.timeout = TRANSFER_TIMEOUT_MS;
521
- yield resetUsbDevice(dev);
522
- try {
523
- dev.open();
524
- }
525
- catch (_d) {
526
- }
527
- dev.timeout = TRANSFER_TIMEOUT_MS;
528
514
  const iface = this.getDeviceInterface(dev);
529
515
  if (process.platform === 'linux') {
530
516
  try {
@@ -532,7 +518,7 @@ class NodeUsbTransport {
532
518
  iface.detachKernelDriver();
533
519
  }
534
520
  }
535
- catch (_e) {
521
+ catch (_d) {
536
522
  }
537
523
  }
538
524
  iface.claim();
@@ -543,8 +529,6 @@ class NodeUsbTransport {
543
529
  }
544
530
  epIn.timeout = TRANSFER_TIMEOUT_MS;
545
531
  epOut.timeout = TRANSFER_TIMEOUT_MS;
546
- yield clearEndpointHalt(epIn);
547
- yield clearEndpointHalt(epOut);
548
532
  yield this.drainStaleInput(epIn);
549
533
  this.openDevices.set(path, { device: dev, iface, epIn, epOut });
550
534
  }
@@ -552,7 +536,7 @@ class NodeUsbTransport {
552
536
  try {
553
537
  dev.close();
554
538
  }
555
- catch (_f) {
539
+ catch (_e) {
556
540
  }
557
541
  throw err;
558
542
  }
@@ -622,16 +606,14 @@ class NodeUsbTransport {
622
606
  });
623
607
  }
624
608
  resetConnectionAfterProbe(path) {
625
- var _a, _b;
609
+ var _a;
626
610
  return __awaiter(this, void 0, void 0, function* () {
627
- (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
628
- this.protocolV2Sessions.delete(path);
629
- this.protocolV2ReadTimeouts.delete(path);
611
+ yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
630
612
  try {
631
613
  yield this.closeOpenDevice(path);
632
614
  }
633
615
  catch (error) {
634
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[NodeUsbTransport] close after protocol probe error:', error);
616
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] close after protocol probe error:', error);
635
617
  }
636
618
  yield this.enumerate();
637
619
  yield this.openDevice(path);
@@ -704,96 +686,60 @@ class NodeUsbTransport {
704
686
  });
705
687
  });
706
688
  }
707
- writeProtocolV2Frame(path, frame) {
708
- var _a;
689
+ getProtocolV2UsbSchemas() {
690
+ if (!this.messages || !this.messagesV2) {
691
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
692
+ }
693
+ return {
694
+ protocolV1: this.messages,
695
+ protocolV2: this.messagesV2,
696
+ };
697
+ }
698
+ getProtocolV2UsbLogger() {
699
+ return this.Log;
700
+ }
701
+ writeProtocolV2UsbPacket(path, frame, _context) {
709
702
  return __awaiter(this, void 0, void 0, function* () {
710
- let lastError;
711
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
703
+ if (this.cancelled) {
704
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
705
+ }
706
+ yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
707
+ });
708
+ }
709
+ readProtocolV2UsbPacket(path, _context) {
710
+ return __awaiter(this, void 0, void 0, function* () {
711
+ for (;;) {
712
712
  if (this.cancelled) {
713
713
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
714
714
  }
715
715
  try {
716
- yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
717
- return;
716
+ const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
717
+ return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
718
718
  }
719
719
  catch (error) {
720
- lastError = error;
721
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
722
- if (!shouldRetry) {
720
+ if (!this.isUsbTransferTimeout(error)) {
723
721
  throw error;
724
722
  }
725
- try {
726
- yield this.reconnectForRetry(path, 'out', attempt, error);
727
- }
728
- catch (reconnectError) {
729
- lastError = reconnectError;
730
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
731
- break;
732
- }
733
723
  }
734
724
  }
735
- throw lastError;
736
725
  });
737
726
  }
738
- receiveProtocolV2Frame(path, timeoutMs) {
727
+ resetProtocolV2UsbNativeLink(path, _reason) {
739
728
  return __awaiter(this, void 0, void 0, function* () {
740
- let assembler = this.protocolV2Assemblers.get(path);
741
- if (!assembler) {
742
- assembler = new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_FRAME_MAX_BYTES);
743
- this.protocolV2Assemblers.set(path, assembler);
744
- }
745
- let frame = assembler.push(new Uint8Array(0));
746
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
747
- while (!frame) {
748
- const transferIn = this.transferInWithRetry(path, this.getOpenDevice(path), transport.PROTOCOL_V2_FRAME_MAX_BYTES);
749
- const packet = deadline
750
- ? yield this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V2')
751
- : yield transferIn;
752
- const bytes = new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
753
- try {
754
- frame = assembler.push(bytes);
755
- }
756
- catch (error) {
757
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, error instanceof Error ? error.message : String(error));
758
- }
759
- }
760
- return frame;
729
+ yield this.closeOpenDevice(path);
761
730
  });
762
731
  }
763
- callProtocolV2(path, name, data, options) {
732
+ onProtocolV2UsbLinkInvalidated(path, reason) {
764
733
  var _a;
734
+ this.deviceProtocol.delete(path);
735
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
736
+ }
737
+ createProtocolV2UsbTimeoutError(name, timeoutMs) {
738
+ return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
739
+ }
740
+ callProtocolV2(path, name, data, options) {
765
741
  return __awaiter(this, void 0, void 0, function* () {
766
- const protocolV1Messages = this.messages;
767
- if (!this.messagesV2) {
768
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured, 'Protocol V2 schema not configured');
769
- }
770
- if (!protocolV1Messages) {
771
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
772
- }
773
- let session = this.protocolV2Sessions.get(path);
774
- if (!session) {
775
- session = new transport.ProtocolV2Session({
776
- schemas: {
777
- protocolV1: protocolV1Messages,
778
- protocolV2: this.messagesV2,
779
- },
780
- router: transport.PROTOCOL_V2_CHANNEL_USB,
781
- writeFrame: (frame) => this.writeProtocolV2Frame(path, frame),
782
- readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
783
- logger: this.Log,
784
- logPrefix: 'ProtocolV2 NodeUSB',
785
- createTimeoutError: (messageName, timeoutMs) => new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
786
- });
787
- this.protocolV2Sessions.set(path, session);
788
- }
789
- this.protocolV2ReadTimeouts.set(path, options === null || options === void 0 ? void 0 : options.timeoutMs);
790
- (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
791
- try {
792
- return yield session.call(name, data, options);
793
- }
794
- finally {
795
- this.protocolV2ReadTimeouts.delete(path);
796
- }
742
+ return this.callProtocolV2Usb(path, name, data, options);
797
743
  });
798
744
  }
799
745
  receiveData(path, dev, timeoutMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-usb",
3
- "version": "1.2.0-alpha.1",
3
+ "version": "1.2.0-alpha.11",
4
4
  "description": "OneKey hardware wallet direct USB transport plugin (libusb)",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -20,10 +20,10 @@
20
20
  "lint:fix": "eslint . --fix"
21
21
  },
22
22
  "dependencies": {
23
- "@onekeyfe/hd-shared": "1.2.0-alpha.1",
24
- "@onekeyfe/hd-transport": "1.2.0-alpha.1",
23
+ "@onekeyfe/hd-shared": "1.2.0-alpha.11",
24
+ "@onekeyfe/hd-transport": "1.2.0-alpha.11",
25
25
  "bytebuffer": "^5.0.1",
26
26
  "usb": "^2.14.0"
27
27
  },
28
- "gitHead": "4f4aae3a47be7bd54b4db4a10c400056784e01bd"
28
+ "gitHead": "05d3421ec00e07e99f6665e6436a612503e32dfc"
29
29
  }
package/src/index.ts CHANGED
@@ -8,8 +8,7 @@ import transport, {
8
8
  PROTOCOL_V1_USB_PACKET_SIZE,
9
9
  PROTOCOL_V2_CHANNEL_USB,
10
10
  PROTOCOL_V2_FRAME_MAX_BYTES,
11
- ProtocolV2FrameAssembler,
12
- ProtocolV2Session,
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';
@@ -19,6 +18,8 @@ import type {
19
18
  AcquireInput,
20
19
  OneKeyDeviceInfo,
21
20
  ProtocolType,
21
+ ProtocolV2CallContext,
22
+ ProtocolV2Schemas,
22
23
  TransportCallOptions,
23
24
  } from '@onekeyfe/hd-transport';
24
25
 
@@ -44,7 +45,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
44
45
  /** Packet I/O retry configuration (matches WebUsbTransport) */
45
46
  const PACKET_IO_MAX_RETRIES = 3;
46
47
  const PACKET_IO_RETRY_DELAY = 300;
47
- const PROTOCOL_PROBE_TIMEOUT = 1000;
48
+ const PROTOCOL_PROBE_TIMEOUT = 5000;
48
49
 
49
50
  /**
50
51
  * Opened device state — holds the USB device, claimed interface, and endpoints.
@@ -154,32 +155,6 @@ function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
154
155
  });
155
156
  }
156
157
 
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
158
  /**
184
159
  * Skip the 0x3F protocol marker byte from a USB packet.
185
160
  */
@@ -206,7 +181,7 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
206
181
  *
207
182
  * Modeled after WebUsbTransport.
208
183
  */
209
- export default class NodeUsbTransport {
184
+ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
210
185
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
211
186
 
212
187
  /** Protobuf schema for Protocol V2 transports. */
@@ -233,21 +208,20 @@ export default class NodeUsbTransport {
233
208
  /** Per-path protocol type detected by active wire-level probe. */
234
209
  private deviceProtocol: Map<string, ProtocolType> = new Map();
235
210
 
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
211
  /** per-path reconnect lock to prevent concurrent reconnects */
246
212
  private reconnectLocks = new Map<string, Promise<OpenDevice>>();
247
213
 
248
214
  /** set to true when cancel() is called; checked by retry loops */
249
215
  private cancelled = false;
250
216
 
217
+ constructor() {
218
+ super({
219
+ router: PROTOCOL_V2_CHANNEL_USB,
220
+ maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
221
+ logPrefix: 'ProtocolV2 NodeUSB',
222
+ });
223
+ }
224
+
251
225
  /**
252
226
  * Initialize transport.
253
227
  * Signature matches the Transport.init interface (logger, emitter).
@@ -267,8 +241,9 @@ export default class NodeUsbTransport {
267
241
 
268
242
  configureProtocolV2(signedData: any) {
269
243
  this.messagesV2 = parseConfigure(signedData);
270
- this.protocolV2Sessions.clear();
271
- this.protocolV2ReadTimeouts.clear();
244
+ this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
245
+ this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
246
+ );
272
247
  this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
273
248
  }
274
249
 
@@ -277,7 +252,9 @@ export default class NodeUsbTransport {
277
252
  }
278
253
 
279
254
  stop() {
280
- // Placeholder no background listeners to tear down
255
+ this.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error =>
256
+ this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error)
257
+ );
281
258
  }
282
259
 
283
260
  /**
@@ -352,6 +329,7 @@ export default class NodeUsbTransport {
352
329
  }
353
330
 
354
331
  try {
332
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
355
333
  await this.closeOpenDevice(path);
356
334
  await this.openDevice(path);
357
335
  await this.detectProtocol(path, input.expectedProtocol);
@@ -366,9 +344,9 @@ export default class NodeUsbTransport {
366
344
  * Release device — release interface and close.
367
345
  */
368
346
  async release(path: string, _onclose?: boolean): Promise<void> {
347
+ await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
369
348
  await this.closeOpenDevice(path);
370
349
  this.deviceProtocol.delete(path);
371
- this.protocolV2Assemblers.delete(path);
372
350
  }
373
351
 
374
352
  private async closeOpenDevice(path: string): Promise<void> {
@@ -515,6 +493,11 @@ export default class NodeUsbTransport {
515
493
  );
516
494
  }
517
495
 
496
+ private isUsbTransferTimeout(error: unknown): boolean {
497
+ const message = this.getErrorMessage(error).toLowerCase();
498
+ return message.includes('timeout') || message.includes('timed_out');
499
+ }
500
+
518
501
  private getDeviceInterface(dev: usb.Device): usb.Interface {
519
502
  const { interfaces } = dev;
520
503
  if (!interfaces?.length) {
@@ -552,6 +535,10 @@ export default class NodeUsbTransport {
552
535
  );
553
536
  await wait(attempt * PACKET_IO_RETRY_DELAY);
554
537
 
538
+ await this.rotateProtocolV2UsbGeneration(
539
+ path,
540
+ `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
541
+ );
555
542
  // Close the existing device without clearing the detected protocol cache.
556
543
  try {
557
544
  await this.closeOpenDevice(path);
@@ -629,7 +616,8 @@ export default class NodeUsbTransport {
629
616
  private async transferInWithRetry(
630
617
  path: string,
631
618
  openDev: OpenDevice,
632
- length: number
619
+ length: number,
620
+ options?: { waitIndefinitelyOnTimeout?: boolean }
633
621
  ): Promise<Buffer> {
634
622
  let lastError: unknown;
635
623
  let currentDev = openDev;
@@ -641,20 +629,24 @@ export default class NodeUsbTransport {
641
629
  return await transferInOnce(currentDev.epIn, length);
642
630
  } catch (error) {
643
631
  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;
632
+ if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
633
+ attempt -= 1;
634
+ } else {
635
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
636
+ if (!shouldRetry) {
637
+ throw error;
638
+ }
639
+ try {
640
+ currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
641
+ } catch (reconnectError) {
642
+ lastError = reconnectError;
643
+ this.Log?.debug(
644
+ `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
645
+ reconnectError
646
+ )}`
647
+ );
648
+ break;
649
+ }
658
650
  }
659
651
  }
660
652
  }
@@ -680,15 +672,6 @@ export default class NodeUsbTransport {
680
672
  dev.open();
681
673
  dev.timeout = TRANSFER_TIMEOUT_MS;
682
674
 
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
675
  const iface = this.getDeviceInterface(dev);
693
676
 
694
677
  // On Linux, detach kernel driver if active
@@ -723,8 +706,6 @@ export default class NodeUsbTransport {
723
706
  epIn.timeout = TRANSFER_TIMEOUT_MS;
724
707
  epOut.timeout = TRANSFER_TIMEOUT_MS;
725
708
 
726
- await clearEndpointHalt(epIn);
727
- await clearEndpointHalt(epOut);
728
709
  await this.drainStaleInput(epIn);
729
710
 
730
711
  this.openDevices.set(path, { device: dev, iface, epIn, epOut });
@@ -814,9 +795,7 @@ export default class NodeUsbTransport {
814
795
  }
815
796
 
816
797
  private async resetConnectionAfterProbe(path: string) {
817
- this.protocolV2Assemblers.get(path)?.reset();
818
- this.protocolV2Sessions.delete(path);
819
- this.protocolV2ReadTimeouts.delete(path);
798
+ await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
820
799
 
821
800
  try {
822
801
  await this.closeOpenDevice(path);
@@ -898,74 +877,66 @@ export default class NodeUsbTransport {
898
877
  });
899
878
  }
900
879
 
901
- private async writeProtocolV2Frame(path: string, frame: Uint8Array) {
902
- let lastError: unknown;
903
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
880
+ protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
881
+ if (!this.messages || !this.messagesV2) {
882
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
883
+ }
884
+ return {
885
+ protocolV1: this.messages,
886
+ protocolV2: this.messagesV2,
887
+ };
888
+ }
889
+
890
+ protected getProtocolV2UsbLogger() {
891
+ return this.Log;
892
+ }
893
+
894
+ protected async writeProtocolV2UsbPacket(
895
+ path: string,
896
+ frame: Uint8Array,
897
+ _context: ProtocolV2CallContext
898
+ ): Promise<void> {
899
+ if (this.cancelled) {
900
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
901
+ }
902
+ await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
903
+ }
904
+
905
+ protected async readProtocolV2UsbPacket(
906
+ path: string,
907
+ _context: ProtocolV2CallContext
908
+ ): Promise<Uint8Array> {
909
+ for (;;) {
904
910
  if (this.cancelled) {
905
911
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
906
912
  }
907
913
  try {
908
- await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
909
- return;
914
+ const packet = await transferInOnce(
915
+ this.getOpenDevice(path).epIn,
916
+ PROTOCOL_V2_FRAME_MAX_BYTES
917
+ );
918
+ return new Uint8Array(
919
+ packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
920
+ );
910
921
  } catch (error) {
911
- lastError = error;
912
- const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
913
- if (!shouldRetry) {
922
+ if (!this.isUsbTransferTimeout(error)) {
914
923
  throw error;
915
924
  }
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
925
  }
928
926
  }
929
- throw lastError;
930
927
  }
931
928
 
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
- }
929
+ protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
930
+ await this.closeOpenDevice(path);
931
+ }
938
932
 
939
- let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
940
- const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
933
+ protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
934
+ this.deviceProtocol.delete(path);
935
+ this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
936
+ }
941
937
 
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;
938
+ protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
939
+ return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
969
940
  }
970
941
 
971
942
  private async callProtocolV2(
@@ -974,42 +945,7 @@ export default class NodeUsbTransport {
974
945
  data: Record<string, unknown>,
975
946
  options?: TransportCallOptions
976
947
  ) {
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
- }
948
+ return this.callProtocolV2Usb(path, name, data, options);
1013
949
  }
1014
950
 
1015
951
  /**