@onekeyfe/hd-transport-usb 1.1.27-alpha.30 → 1.1.27-alpha.32

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/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as transport from '@onekeyfe/hd-transport';
2
- import transport__default, { ProtocolType, OneKeyDeviceInfo, AcquireInput } from '@onekeyfe/hd-transport';
2
+ import transport__default, { OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolType } from '@onekeyfe/hd-transport';
3
3
  import EventEmitter from 'events';
4
4
 
5
5
  declare class NodeUsbTransport {
6
6
  messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
7
+ messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
7
8
  name: string;
8
9
  version: string;
9
10
  configured: boolean;
@@ -12,11 +13,13 @@ declare class NodeUsbTransport {
12
13
  emitter?: EventEmitter;
13
14
  private serialToBusId;
14
15
  private openDevices;
16
+ private deviceProtocol;
17
+ private protocolV2Assemblers;
15
18
  private reconnectLocks;
16
19
  private cancelled;
17
- getProtocolType(_path: string): ProtocolType;
18
20
  init(logger: any, emitter?: EventEmitter): Promise<string>;
19
21
  configure(signedData: any): Promise<void>;
22
+ configureProtocolV2(signedData: any): void;
20
23
  listen(): void;
21
24
  stop(): void;
22
25
  post(path: string, name: string, data: Record<string, unknown>): Promise<void>;
@@ -29,16 +32,29 @@ declare class NodeUsbTransport {
29
32
  enumerate(): Promise<OneKeyDeviceInfo[]>;
30
33
  acquire(input: AcquireInput): Promise<string>;
31
34
  release(path: string, _onclose?: boolean): Promise<void>;
32
- call(path: string, name: string, data: Record<string, unknown>): Promise<transport.MessageFromOneKey>;
35
+ private closeOpenDevice;
36
+ call(path: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
37
+ private callProtocolV1;
33
38
  cancel(): void;
34
39
  private getOpenDevice;
35
40
  private getErrorMessage;
36
41
  private isRetryableError;
42
+ private getDeviceInterface;
37
43
  private reconnectForRetry;
38
44
  private sendAllChunksWithRetry;
39
45
  private transferInWithRetry;
40
46
  private openDevice;
47
+ private createProtocolMismatchError;
48
+ private detectProtocol;
49
+ private resetConnectionAfterProbe;
50
+ private withProtocolReadTimeout;
51
+ private probeProtocolV1;
52
+ private probeProtocolV2;
53
+ private writeProtocolV2Frame;
54
+ private receiveProtocolV2Frame;
55
+ private callProtocolV2;
41
56
  private receiveData;
57
+ getProtocolType(path: string): ProtocolType;
42
58
  }
43
59
 
44
60
  export { NodeUsbTransport as default };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAMN,MAAM,wBAAwB,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AA+J3F,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAElE,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,CAA0C;IAGhE,OAAO,CAAC,SAAS,CAAS;IAG1B,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY;IAQ5C,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAMxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAOzB,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;IA8B9C,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAkBvC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BxD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAkCpE,MAAM;IAWN,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,iBAAiB;YAmDX,sBAAsB;YAyCtB,mBAAmB;IAsCjC,OAAO,CAAC,UAAU;YAgEJ,WAAW;CAkC1B"}
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;AAgKhC,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,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;IAKnC,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;IAmB7C,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;YAkClB,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;IAsCjC,OAAO,CAAC,UAAU;IA8DlB,OAAO,CAAC,2BAA2B;YAOrB,cAAc;YAqCd,yBAAyB;YAazB,uBAAuB;YA0CvB,eAAe;YAcf,eAAe;YAcf,oBAAoB;YA+BpB,sBAAsB;YAuCtB,cAAc;YAuCd,WAAW;IA6CzB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY;CAG5C"}
package/dist/index.js CHANGED
@@ -71,6 +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
75
  function getBusId(dev) {
75
76
  return `usb:${dev.busNumber}:${dev.deviceAddress}`;
76
77
  }
@@ -168,12 +169,11 @@ class NodeUsbTransport {
168
169
  this.isOutdated = false;
169
170
  this.serialToBusId = new Map();
170
171
  this.openDevices = new Map();
172
+ this.deviceProtocol = new Map();
173
+ this.protocolV2Assemblers = new Map();
171
174
  this.reconnectLocks = new Map();
172
175
  this.cancelled = false;
173
176
  }
174
- getProtocolType(_path) {
175
- return 'V1';
176
- }
177
177
  init(logger, emitter) {
178
178
  this.Log = logger;
179
179
  this.emitter = emitter;
@@ -185,6 +185,11 @@ class NodeUsbTransport {
185
185
  this.messages = messages;
186
186
  return Promise.resolve();
187
187
  }
188
+ configureProtocolV2(signedData) {
189
+ var _a;
190
+ this.messagesV2 = parseConfigure(signedData);
191
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V2 schema configured');
192
+ }
188
193
  listen() {
189
194
  }
190
195
  stop() {
@@ -238,20 +243,30 @@ class NodeUsbTransport {
238
243
  }
239
244
  acquire(input) {
240
245
  var _a, _b, _c;
241
- const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
242
- if (!path) {
243
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
244
- }
245
- try {
246
- this.openDevice(path);
247
- return Promise.resolve(path);
248
- }
249
- catch (error) {
250
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
251
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_c = error.message) !== null && _c !== void 0 ? _c : String(error));
252
- }
246
+ return __awaiter(this, void 0, void 0, function* () {
247
+ const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
248
+ if (!path) {
249
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
250
+ }
251
+ try {
252
+ this.openDevice(path);
253
+ yield this.detectProtocol(path, input.expectedProtocol);
254
+ return path;
255
+ }
256
+ catch (error) {
257
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
258
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_c = error.message) !== null && _c !== void 0 ? _c : String(error));
259
+ }
260
+ });
253
261
  }
254
262
  release(path, _onclose) {
263
+ return __awaiter(this, void 0, void 0, function* () {
264
+ yield this.closeOpenDevice(path);
265
+ this.deviceProtocol.delete(path);
266
+ this.protocolV2Assemblers.delete(path);
267
+ });
268
+ }
269
+ closeOpenDevice(path) {
255
270
  return __awaiter(this, void 0, void 0, function* () {
256
271
  const openDev = this.openDevices.get(path);
257
272
  if (!openDev)
@@ -278,8 +293,8 @@ class NodeUsbTransport {
278
293
  this.openDevices.delete(path);
279
294
  });
280
295
  }
281
- call(path, name, data) {
282
- var _a, _b;
296
+ call(path, name, data, options) {
297
+ var _a, _b, _c;
283
298
  return __awaiter(this, void 0, void 0, function* () {
284
299
  this.cancelled = false;
285
300
  if (!this.messages) {
@@ -288,16 +303,28 @@ class NodeUsbTransport {
288
303
  if (!this.openDevices.get(path)) {
289
304
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
290
305
  }
291
- const { messages } = this;
306
+ const protocol = (_a = this.deviceProtocol.get(path)) !== null && _a !== void 0 ? _a : 'V1';
292
307
  if (transport.LogBlockCommand.has(name)) {
293
- (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport call-', ' name: ', name);
308
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
294
309
  }
295
310
  else {
296
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
311
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
312
+ }
313
+ if (protocol === 'V2') {
314
+ return this.callProtocolV2(path, name, data, options);
315
+ }
316
+ return this.callProtocolV1(path, name, data, options);
317
+ });
318
+ }
319
+ callProtocolV1(path, name, data, options) {
320
+ return __awaiter(this, void 0, void 0, function* () {
321
+ const { messages } = this;
322
+ if (!messages) {
323
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
297
324
  }
298
325
  const encodeBuffers = ProtocolV1.encodeMessageChunks(messages, name, data);
299
326
  yield this.sendAllChunksWithRetry(path, encodeBuffers);
300
- const resData = yield this.receiveData(path, this.getOpenDevice(path));
327
+ const resData = yield this.receiveData(path, this.getOpenDevice(path), options === null || options === void 0 ? void 0 : options.timeoutMs);
301
328
  if (typeof resData !== 'string') {
302
329
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, 'Returning data is not string.');
303
330
  }
@@ -341,6 +368,17 @@ class NodeUsbTransport {
341
368
  message.includes('timeout') ||
342
369
  message.includes('interrupt'));
343
370
  }
371
+ getDeviceInterface(dev) {
372
+ var _a;
373
+ const { interfaces } = dev;
374
+ if (!(interfaces === null || interfaces === void 0 ? void 0 : interfaces.length)) {
375
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'USB interface not found');
376
+ }
377
+ const vendorInterface = interfaces.find(iface => iface.descriptor.bInterfaceClass === 0xff);
378
+ const defaultInterface = interfaces.find(iface => iface.descriptor.bInterfaceNumber === INTERFACE_NUMBER);
379
+ const iface = (_a = vendorInterface !== null && vendorInterface !== void 0 ? vendorInterface : defaultInterface) !== null && _a !== void 0 ? _a : interfaces[0];
380
+ return iface;
381
+ }
344
382
  reconnectForRetry(path, direction, attempt, error) {
345
383
  const existing = this.reconnectLocks.get(path);
346
384
  if (existing)
@@ -350,7 +388,7 @@ class NodeUsbTransport {
350
388
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
351
389
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
352
390
  try {
353
- yield this.release(path);
391
+ yield this.closeOpenDevice(path);
354
392
  }
355
393
  catch (releaseError) {
356
394
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[NodeUsbTransport] release before retry error:', releaseError);
@@ -437,7 +475,7 @@ class NodeUsbTransport {
437
475
  });
438
476
  }
439
477
  openDevice(path) {
440
- var _a;
478
+ var _a, _b, _c;
441
479
  const existing = this.openDevices.get(path);
442
480
  if (existing)
443
481
  return;
@@ -450,19 +488,19 @@ class NodeUsbTransport {
450
488
  dev.open();
451
489
  try {
452
490
  dev.timeout = TRANSFER_TIMEOUT_MS;
453
- const iface = dev.interface(INTERFACE_NUMBER);
491
+ const iface = this.getDeviceInterface(dev);
454
492
  if (process.platform === 'linux') {
455
493
  try {
456
494
  if (iface.isKernelDriverActive()) {
457
495
  iface.detachKernelDriver();
458
496
  }
459
497
  }
460
- catch (_b) {
498
+ catch (_d) {
461
499
  }
462
500
  }
463
501
  iface.claim();
464
- const epIn = iface.endpoints.find((e) => e.direction === 'in' && e.address === ENDPOINT_IN);
465
- const epOut = iface.endpoints.find((e) => e.direction === 'out' && e.address === ENDPOINT_OUT);
502
+ const epIn = (_b = iface.endpoints.find((e) => e.direction === 'in' && e.address === ENDPOINT_IN)) !== null && _b !== void 0 ? _b : iface.endpoints.find((e) => e.direction === 'in');
503
+ const epOut = (_c = iface.endpoints.find((e) => e.direction === 'out' && e.address === ENDPOINT_OUT)) !== null && _c !== void 0 ? _c : iface.endpoints.find((e) => e.direction === 'out');
466
504
  if (!epIn || !epOut) {
467
505
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'USB endpoints not found (expected IN 0x81, OUT 0x01)');
468
506
  }
@@ -474,14 +512,222 @@ class NodeUsbTransport {
474
512
  try {
475
513
  dev.close();
476
514
  }
477
- catch (_c) {
515
+ catch (_e) {
478
516
  }
479
517
  throw err;
480
518
  }
481
519
  }
482
- receiveData(path, dev) {
520
+ createProtocolMismatchError(expected) {
521
+ return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
522
+ }
523
+ detectProtocol(path, expectedProtocol) {
524
+ var _a, _b, _c, _d;
525
+ return __awaiter(this, void 0, void 0, function* () {
526
+ if (expectedProtocol === 'V1') {
527
+ if (yield this.probeProtocolV1(path)) {
528
+ this.deviceProtocol.set(path, 'V1');
529
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
530
+ return 'V1';
531
+ }
532
+ throw this.createProtocolMismatchError(expectedProtocol);
533
+ }
534
+ if (expectedProtocol === 'V2') {
535
+ if (yield this.probeProtocolV2(path)) {
536
+ this.deviceProtocol.set(path, 'V2');
537
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
538
+ return 'V2';
539
+ }
540
+ throw this.createProtocolMismatchError(expectedProtocol);
541
+ }
542
+ if (this.deviceProtocol.get(path) === 'V2' && (yield this.probeProtocolV2(path))) {
543
+ this.deviceProtocol.set(path, 'V2');
544
+ (_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
545
+ return 'V2';
546
+ }
547
+ let protocol = 'V1';
548
+ if (!(yield this.probeProtocolV1(path)) && (yield this.probeProtocolV2(path))) {
549
+ protocol = 'V2';
550
+ }
551
+ this.deviceProtocol.set(path, protocol);
552
+ (_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
553
+ return protocol;
554
+ });
555
+ }
556
+ resetConnectionAfterProbe(path) {
557
+ var _a, _b;
558
+ return __awaiter(this, void 0, void 0, function* () {
559
+ (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
560
+ try {
561
+ yield this.closeOpenDevice(path);
562
+ }
563
+ catch (error) {
564
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[NodeUsbTransport] close after protocol probe error:', error);
565
+ }
566
+ yield this.enumerate();
567
+ this.openDevice(path);
568
+ });
569
+ }
570
+ withProtocolReadTimeout(path, promise, timeoutMs, protocol) {
571
+ return __awaiter(this, void 0, void 0, function* () {
572
+ let timer;
573
+ let timedOut = false;
574
+ const waitForeverAfterTimeout = () => new Promise(() => { });
575
+ const guardedPromise = promise.then(value => (timedOut ? waitForeverAfterTimeout() : value), error => {
576
+ if (timedOut) {
577
+ return waitForeverAfterTimeout();
578
+ }
579
+ throw error;
580
+ });
581
+ try {
582
+ return yield Promise.race([
583
+ guardedPromise,
584
+ new Promise((_, reject) => {
585
+ timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
586
+ var _a;
587
+ timedOut = true;
588
+ try {
589
+ yield this.resetConnectionAfterProbe(path);
590
+ }
591
+ catch (error) {
592
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] reset after Protocol ${protocol} timeout failed:`, error);
593
+ }
594
+ finally {
595
+ reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
596
+ }
597
+ }), timeoutMs);
598
+ }),
599
+ ]);
600
+ }
601
+ finally {
602
+ if (timer)
603
+ clearTimeout(timer);
604
+ }
605
+ });
606
+ }
607
+ probeProtocolV1(path) {
608
+ var _a;
483
609
  return __awaiter(this, void 0, void 0, function* () {
484
- const firstPacket = yield this.transferInWithRetry(path, dev, PACKET_SIZE);
610
+ if (!this.messages) {
611
+ return false;
612
+ }
613
+ try {
614
+ yield this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
615
+ return true;
616
+ }
617
+ catch (error) {
618
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
619
+ return false;
620
+ }
621
+ });
622
+ }
623
+ probeProtocolV2(path) {
624
+ return __awaiter(this, void 0, void 0, function* () {
625
+ if (!this.messages || !this.messagesV2) {
626
+ return false;
627
+ }
628
+ return transport.probeProtocolV2({
629
+ call: (name, data, options) => this.callProtocolV2(path, name, data, options),
630
+ timeoutMs: PROTOCOL_PROBE_TIMEOUT,
631
+ logger: this.Log,
632
+ logPrefix: 'ProtocolV2 NodeUSB',
633
+ onProbeFailed: () => this.resetConnectionAfterProbe(path),
634
+ });
635
+ });
636
+ }
637
+ writeProtocolV2Frame(path, frame) {
638
+ var _a;
639
+ return __awaiter(this, void 0, void 0, function* () {
640
+ let lastError;
641
+ for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
642
+ if (this.cancelled) {
643
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
644
+ }
645
+ try {
646
+ yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
647
+ return;
648
+ }
649
+ catch (error) {
650
+ lastError = error;
651
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
652
+ if (!shouldRetry) {
653
+ throw error;
654
+ }
655
+ try {
656
+ yield this.reconnectForRetry(path, 'out', attempt, error);
657
+ }
658
+ catch (reconnectError) {
659
+ lastError = reconnectError;
660
+ (_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)}`);
661
+ break;
662
+ }
663
+ }
664
+ }
665
+ throw lastError;
666
+ });
667
+ }
668
+ receiveProtocolV2Frame(path, timeoutMs) {
669
+ return __awaiter(this, void 0, void 0, function* () {
670
+ let assembler = this.protocolV2Assemblers.get(path);
671
+ if (!assembler) {
672
+ assembler = new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_FRAME_MAX_BYTES);
673
+ this.protocolV2Assemblers.set(path, assembler);
674
+ }
675
+ let frame = assembler.push(new Uint8Array(0));
676
+ const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
677
+ while (!frame) {
678
+ const transferIn = this.transferInWithRetry(path, this.getOpenDevice(path), transport.PROTOCOL_V2_FRAME_MAX_BYTES);
679
+ const packet = deadline
680
+ ? yield this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V2')
681
+ : yield transferIn;
682
+ const bytes = new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
683
+ try {
684
+ frame = assembler.push(bytes);
685
+ }
686
+ catch (error) {
687
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, error instanceof Error ? error.message : String(error));
688
+ }
689
+ }
690
+ return frame;
691
+ });
692
+ }
693
+ callProtocolV2(path, name, data, options) {
694
+ var _a;
695
+ return __awaiter(this, void 0, void 0, function* () {
696
+ const protocolV1Messages = this.messages;
697
+ if (!this.messagesV2) {
698
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured, 'Protocol V2 schema not configured');
699
+ }
700
+ if (!protocolV1Messages) {
701
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
702
+ }
703
+ const session = new transport.ProtocolV2Session({
704
+ schemas: {
705
+ protocolV1: protocolV1Messages,
706
+ protocolV2: this.messagesV2,
707
+ },
708
+ router: transport.PROTOCOL_V2_CHANNEL_USB,
709
+ writeFrame: (frame) => this.writeProtocolV2Frame(path, frame),
710
+ readFrame: () => this.receiveProtocolV2Frame(path, options === null || options === void 0 ? void 0 : options.timeoutMs),
711
+ logger: this.Log,
712
+ logPrefix: 'ProtocolV2 NodeUSB',
713
+ createTimeoutError: (_messageName, timeoutMs) => new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
714
+ });
715
+ (_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
716
+ return session.call(name, data, options);
717
+ });
718
+ }
719
+ receiveData(path, dev, timeoutMs) {
720
+ return __awaiter(this, void 0, void 0, function* () {
721
+ const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
722
+ const readPacket = () => __awaiter(this, void 0, void 0, function* () {
723
+ const transferIn = this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE);
724
+ return deadline
725
+ ? this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V1')
726
+ : transferIn;
727
+ });
728
+ const firstPacket = timeoutMs
729
+ ? yield readPacket()
730
+ : yield this.transferInWithRetry(path, dev, PACKET_SIZE);
485
731
  const firstData = skipReportByte(firstPacket);
486
732
  const { length, typeId, restBuffer } = ProtocolV1.decodeFirstChunk(toArrayBuffer(firstData));
487
733
  const lengthWithHeader = Number(length) + HEADER_LENGTH;
@@ -492,7 +738,7 @@ class NodeUsbTransport {
492
738
  decoded.append(restBuffer);
493
739
  }
494
740
  while (decoded.offset < lengthWithHeader) {
495
- const packet = yield this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE);
741
+ const packet = yield readPacket();
496
742
  const pktData = skipReportByte(packet);
497
743
  const buf = toArrayBuffer(pktData);
498
744
  if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
@@ -507,6 +753,10 @@ class NodeUsbTransport {
507
753
  return Buffer.from(result).toString('hex');
508
754
  });
509
755
  }
756
+ getProtocolType(path) {
757
+ var _a;
758
+ return (_a = this.deviceProtocol.get(path)) !== null && _a !== void 0 ? _a : 'V1';
759
+ }
510
760
  }
511
761
 
512
762
  module.exports = NodeUsbTransport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-usb",
3
- "version": "1.1.27-alpha.30",
3
+ "version": "1.1.27-alpha.32",
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.1.27-alpha.30",
24
- "@onekeyfe/hd-transport": "1.1.27-alpha.30",
23
+ "@onekeyfe/hd-shared": "1.1.27-alpha.32",
24
+ "@onekeyfe/hd-transport": "1.1.27-alpha.32",
25
25
  "bytebuffer": "^5.0.1",
26
26
  "usb": "^2.14.0"
27
27
  },
28
- "gitHead": "f1e3a93e766463007436eaa9aff4a271ffd8830c"
28
+ "gitHead": "04f92052a18dcf17e4a347f3003b4c4bbf062681"
29
29
  }
package/src/index.ts CHANGED
@@ -6,11 +6,21 @@ import transport, {
6
6
  PROTOCOL_V1_MESSAGE_HEADER_SIZE,
7
7
  PROTOCOL_V1_REPORT_ID,
8
8
  PROTOCOL_V1_USB_PACKET_SIZE,
9
+ PROTOCOL_V2_CHANNEL_USB,
10
+ PROTOCOL_V2_FRAME_MAX_BYTES,
11
+ ProtocolV2FrameAssembler,
12
+ ProtocolV2Session,
13
+ probeProtocolV2 as probeProtocolV2Helper,
9
14
  } from '@onekeyfe/hd-transport';
10
15
  import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
11
16
 
12
17
  import type EventEmitter from 'events';
13
- import type { AcquireInput, OneKeyDeviceInfo, ProtocolType } from '@onekeyfe/hd-transport';
18
+ import type {
19
+ AcquireInput,
20
+ OneKeyDeviceInfo,
21
+ ProtocolType,
22
+ TransportCallOptions,
23
+ } from '@onekeyfe/hd-transport';
14
24
 
15
25
  const { parseConfigure, ProtocolV1, check } = transport;
16
26
 
@@ -34,6 +44,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
34
44
  /** Packet I/O retry configuration (matches WebUsbTransport) */
35
45
  const PACKET_IO_MAX_RETRIES = 3;
36
46
  const PACKET_IO_RETRY_DELAY = 300;
47
+ const PROTOCOL_PROBE_TIMEOUT = 1000;
37
48
 
38
49
  /**
39
50
  * Opened device state — holds the USB device, claimed interface, and endpoints.
@@ -172,6 +183,9 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
172
183
  export default class NodeUsbTransport {
173
184
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
174
185
 
186
+ /** Protobuf schema for Protocol V2 transports. */
187
+ messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
188
+
175
189
  name = 'NodeUsbTransport';
176
190
 
177
191
  version = '';
@@ -190,17 +204,18 @@ export default class NodeUsbTransport {
190
204
  /** path → opened device state */
191
205
  private openDevices = new Map<string, OpenDevice>();
192
206
 
207
+ /** Per-path protocol type detected by active wire-level probe. */
208
+ private deviceProtocol: Map<string, ProtocolType> = new Map();
209
+
210
+ /** Per-path Protocol V2 frame assembler, preserving buffered frames during reads. */
211
+ private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
212
+
193
213
  /** per-path reconnect lock to prevent concurrent reconnects */
194
214
  private reconnectLocks = new Map<string, Promise<OpenDevice>>();
195
215
 
196
216
  /** set to true when cancel() is called; checked by retry loops */
197
217
  private cancelled = false;
198
218
 
199
- // NodeUsbTransport speaks Protocol V1 only (no Pro2 USB support yet via libusb).
200
- getProtocolType(_path: string): ProtocolType {
201
- return 'V1';
202
- }
203
-
204
219
  /**
205
220
  * Initialize transport.
206
221
  * Signature matches the Transport.init interface (logger, emitter).
@@ -218,6 +233,11 @@ export default class NodeUsbTransport {
218
233
  return Promise.resolve();
219
234
  }
220
235
 
236
+ configureProtocolV2(signedData: any) {
237
+ this.messagesV2 = parseConfigure(signedData);
238
+ this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
239
+ }
240
+
221
241
  listen() {
222
242
  // empty — could add hotplug events via usb.on('attach'/'detach')
223
243
  }
@@ -289,7 +309,7 @@ export default class NodeUsbTransport {
289
309
  /**
290
310
  * Acquire device — open USB device, claim interface, return path (string).
291
311
  */
292
- acquire(input: AcquireInput): Promise<string> {
312
+ async acquire(input: AcquireInput): Promise<string> {
293
313
  const path = input.path ?? '';
294
314
  if (!path) {
295
315
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'No device path provided');
@@ -297,7 +317,8 @@ export default class NodeUsbTransport {
297
317
 
298
318
  try {
299
319
  this.openDevice(path);
300
- return Promise.resolve(path);
320
+ await this.detectProtocol(path, input.expectedProtocol);
321
+ return path;
301
322
  } catch (error: any) {
302
323
  this.Log?.debug('NodeUsbTransport acquire error: ', error);
303
324
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
@@ -308,6 +329,12 @@ export default class NodeUsbTransport {
308
329
  * Release device — release interface and close.
309
330
  */
310
331
  async release(path: string, _onclose?: boolean): Promise<void> {
332
+ await this.closeOpenDevice(path);
333
+ this.deviceProtocol.delete(path);
334
+ this.protocolV2Assemblers.delete(path);
335
+ }
336
+
337
+ private async closeOpenDevice(path: string): Promise<void> {
311
338
  const openDev = this.openDevices.get(path);
312
339
  if (!openDev) return;
313
340
 
@@ -336,7 +363,12 @@ export default class NodeUsbTransport {
336
363
  * Call device method — encode protobuf, send packets, receive response.
337
364
  * This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
338
365
  */
339
- async call(path: string, name: string, data: Record<string, unknown>) {
366
+ async call(
367
+ path: string,
368
+ name: string,
369
+ data: Record<string, unknown>,
370
+ options?: TransportCallOptions
371
+ ) {
340
372
  this.cancelled = false;
341
373
 
342
374
  if (!this.messages) {
@@ -347,13 +379,38 @@ export default class NodeUsbTransport {
347
379
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
348
380
  }
349
381
 
350
- const { messages } = this;
382
+ const protocol = this.deviceProtocol.get(path) ?? 'V1';
351
383
  if (LogBlockCommand.has(name)) {
352
- this.Log?.debug('NodeUsbTransport call-', ' name: ', name);
384
+ this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
353
385
  } else {
354
- this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
386
+ this.Log?.debug(
387
+ 'NodeUsbTransport call-',
388
+ ' name: ',
389
+ name,
390
+ ' data: ',
391
+ data,
392
+ ' protocol: ',
393
+ protocol
394
+ );
395
+ }
396
+
397
+ if (protocol === 'V2') {
398
+ return this.callProtocolV2(path, name, data, options);
355
399
  }
356
400
 
401
+ return this.callProtocolV1(path, name, data, options);
402
+ }
403
+
404
+ private async callProtocolV1(
405
+ path: string,
406
+ name: string,
407
+ data: Record<string, unknown>,
408
+ options?: TransportCallOptions
409
+ ) {
410
+ const { messages } = this;
411
+ if (!messages) {
412
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
413
+ }
357
414
  // Encode protobuf message into 63-byte chunks (same as WebUsbTransport)
358
415
  const encodeBuffers = ProtocolV1.encodeMessageChunks(messages, name, data);
359
416
 
@@ -362,7 +419,7 @@ export default class NodeUsbTransport {
362
419
  await this.sendAllChunksWithRetry(path, encodeBuffers);
363
420
 
364
421
  // Receive response — re-resolve in case reconnect happened during send
365
- const resData = await this.receiveData(path, this.getOpenDevice(path));
422
+ const resData = await this.receiveData(path, this.getOpenDevice(path), options?.timeoutMs);
366
423
  if (typeof resData !== 'string') {
367
424
  throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
368
425
  }
@@ -415,6 +472,21 @@ export default class NodeUsbTransport {
415
472
  );
416
473
  }
417
474
 
475
+ private getDeviceInterface(dev: usb.Device): usb.Interface {
476
+ const { interfaces } = dev;
477
+ if (!interfaces?.length) {
478
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'USB interface not found');
479
+ }
480
+
481
+ const vendorInterface = interfaces.find(iface => iface.descriptor.bInterfaceClass === 0xff);
482
+ const defaultInterface = interfaces.find(
483
+ iface => iface.descriptor.bInterfaceNumber === INTERFACE_NUMBER
484
+ );
485
+ const iface = vendorInterface ?? defaultInterface ?? interfaces[0];
486
+
487
+ return iface;
488
+ }
489
+
418
490
  /**
419
491
  * Reconnect device before retrying a failed transfer (aligned with WebUsbTransport).
420
492
  * Uses per-path lock to prevent concurrent reconnects to the same device.
@@ -437,9 +509,9 @@ export default class NodeUsbTransport {
437
509
  );
438
510
  await wait(attempt * PACKET_IO_RETRY_DELAY);
439
511
 
440
- // Close the existing device
512
+ // Close the existing device without clearing the detected protocol cache.
441
513
  try {
442
- await this.release(path);
514
+ await this.closeOpenDevice(path);
443
515
  } catch (releaseError) {
444
516
  this.Log?.debug('[NodeUsbTransport] release before retry error:', releaseError);
445
517
  }
@@ -566,7 +638,7 @@ export default class NodeUsbTransport {
566
638
  try {
567
639
  dev.timeout = TRANSFER_TIMEOUT_MS;
568
640
 
569
- const iface = dev.interface(INTERFACE_NUMBER);
641
+ const iface = this.getDeviceInterface(dev);
570
642
 
571
643
  // On Linux, detach kernel driver if active
572
644
  if (process.platform === 'linux') {
@@ -581,12 +653,14 @@ export default class NodeUsbTransport {
581
653
 
582
654
  iface.claim();
583
655
 
584
- const epIn = iface.endpoints.find(
585
- (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
586
- );
587
- const epOut = iface.endpoints.find(
588
- (e): e is usb.OutEndpoint => e.direction === 'out' && e.address === ENDPOINT_OUT
589
- );
656
+ const epIn =
657
+ iface.endpoints.find(
658
+ (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
659
+ ) ?? iface.endpoints.find((e): e is usb.InEndpoint => e.direction === 'in');
660
+ const epOut =
661
+ iface.endpoints.find(
662
+ (e): e is usb.OutEndpoint => e.direction === 'out' && e.address === ENDPOINT_OUT
663
+ ) ?? iface.endpoints.find((e): e is usb.OutEndpoint => e.direction === 'out');
590
664
 
591
665
  if (!epIn || !epOut) {
592
666
  throw ERRORS.TypedError(
@@ -609,13 +683,255 @@ export default class NodeUsbTransport {
609
683
  }
610
684
  }
611
685
 
686
+ private createProtocolMismatchError(expected: ProtocolType) {
687
+ return ERRORS.TypedError(
688
+ HardwareErrorCode.RuntimeError,
689
+ `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
690
+ );
691
+ }
692
+
693
+ private async detectProtocol(
694
+ path: string,
695
+ expectedProtocol?: ProtocolType
696
+ ): Promise<ProtocolType> {
697
+ if (expectedProtocol === 'V1') {
698
+ if (await this.probeProtocolV1(path)) {
699
+ this.deviceProtocol.set(path, 'V1');
700
+ this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
701
+ return 'V1';
702
+ }
703
+ throw this.createProtocolMismatchError(expectedProtocol);
704
+ }
705
+
706
+ if (expectedProtocol === 'V2') {
707
+ if (await this.probeProtocolV2(path)) {
708
+ this.deviceProtocol.set(path, 'V2');
709
+ this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
710
+ return 'V2';
711
+ }
712
+ throw this.createProtocolMismatchError(expectedProtocol);
713
+ }
714
+
715
+ if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) {
716
+ this.deviceProtocol.set(path, 'V2');
717
+ this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
718
+ return 'V2';
719
+ }
720
+
721
+ let protocol: ProtocolType = 'V1';
722
+ if (!(await this.probeProtocolV1(path)) && (await this.probeProtocolV2(path))) {
723
+ protocol = 'V2';
724
+ }
725
+ this.deviceProtocol.set(path, protocol);
726
+ this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> ${protocol}`);
727
+ return protocol;
728
+ }
729
+
730
+ private async resetConnectionAfterProbe(path: string) {
731
+ this.protocolV2Assemblers.get(path)?.reset();
732
+
733
+ try {
734
+ await this.closeOpenDevice(path);
735
+ } catch (error) {
736
+ this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
737
+ }
738
+
739
+ await this.enumerate();
740
+ this.openDevice(path);
741
+ }
742
+
743
+ private async withProtocolReadTimeout<T>(
744
+ path: string,
745
+ promise: Promise<T>,
746
+ timeoutMs: number,
747
+ protocol: ProtocolType
748
+ ): Promise<T> {
749
+ let timer: ReturnType<typeof setTimeout> | undefined;
750
+ let timedOut = false;
751
+ const waitForeverAfterTimeout = () => new Promise<never>(() => {});
752
+ const guardedPromise = promise.then(
753
+ value => (timedOut ? waitForeverAfterTimeout() : value),
754
+ error => {
755
+ if (timedOut) {
756
+ return waitForeverAfterTimeout();
757
+ }
758
+ throw error;
759
+ }
760
+ );
761
+ try {
762
+ return await Promise.race([
763
+ guardedPromise,
764
+ new Promise<never>((_, reject) => {
765
+ timer = setTimeout(async () => {
766
+ timedOut = true;
767
+ try {
768
+ await this.resetConnectionAfterProbe(path);
769
+ } catch (error) {
770
+ this.Log?.debug(
771
+ `[NodeUsbTransport] reset after Protocol ${protocol} timeout failed:`,
772
+ error
773
+ );
774
+ } finally {
775
+ reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`));
776
+ }
777
+ }, timeoutMs);
778
+ }),
779
+ ]);
780
+ } finally {
781
+ if (timer) clearTimeout(timer);
782
+ }
783
+ }
784
+
785
+ private async probeProtocolV1(path: string) {
786
+ if (!this.messages) {
787
+ return false;
788
+ }
789
+
790
+ try {
791
+ await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
792
+ return true;
793
+ } catch (error) {
794
+ this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
795
+ return false;
796
+ }
797
+ }
798
+
799
+ private async probeProtocolV2(path: string) {
800
+ if (!this.messages || !this.messagesV2) {
801
+ return false;
802
+ }
803
+
804
+ return probeProtocolV2Helper({
805
+ call: (name, data, options) => this.callProtocolV2(path, name, data, options),
806
+ timeoutMs: PROTOCOL_PROBE_TIMEOUT,
807
+ logger: this.Log,
808
+ logPrefix: 'ProtocolV2 NodeUSB',
809
+ onProbeFailed: () => this.resetConnectionAfterProbe(path),
810
+ });
811
+ }
812
+
813
+ private async writeProtocolV2Frame(path: string, frame: Uint8Array) {
814
+ let lastError: unknown;
815
+ for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
816
+ if (this.cancelled) {
817
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
818
+ }
819
+ try {
820
+ await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
821
+ return;
822
+ } catch (error) {
823
+ lastError = error;
824
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
825
+ if (!shouldRetry) {
826
+ throw error;
827
+ }
828
+ try {
829
+ await this.reconnectForRetry(path, 'out', attempt, error);
830
+ } catch (reconnectError) {
831
+ lastError = reconnectError;
832
+ this.Log?.debug(
833
+ `[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
834
+ reconnectError
835
+ )}`
836
+ );
837
+ break;
838
+ }
839
+ }
840
+ }
841
+ throw lastError;
842
+ }
843
+
844
+ private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise<Uint8Array> {
845
+ let assembler = this.protocolV2Assemblers.get(path);
846
+ if (!assembler) {
847
+ assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
848
+ this.protocolV2Assemblers.set(path, assembler);
849
+ }
850
+
851
+ let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0));
852
+ const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
853
+
854
+ while (!frame) {
855
+ const transferIn = this.transferInWithRetry(
856
+ path,
857
+ this.getOpenDevice(path),
858
+ PROTOCOL_V2_FRAME_MAX_BYTES
859
+ );
860
+ const packet = deadline
861
+ ? await this.withProtocolReadTimeout(
862
+ path,
863
+ transferIn,
864
+ Math.max(deadline - Date.now(), 1),
865
+ 'V2'
866
+ )
867
+ : await transferIn;
868
+ const bytes = new Uint8Array(
869
+ packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
870
+ );
871
+ try {
872
+ frame = assembler.push(bytes);
873
+ } catch (error) {
874
+ throw ERRORS.TypedError(
875
+ HardwareErrorCode.NetworkError,
876
+ error instanceof Error ? error.message : String(error)
877
+ );
878
+ }
879
+ }
880
+ return frame;
881
+ }
882
+
883
+ private async callProtocolV2(
884
+ path: string,
885
+ name: string,
886
+ data: Record<string, unknown>,
887
+ options?: TransportCallOptions
888
+ ) {
889
+ const protocolV1Messages = this.messages;
890
+ if (!this.messagesV2) {
891
+ throw ERRORS.TypedError(
892
+ HardwareErrorCode.TransportNotConfigured,
893
+ 'Protocol V2 schema not configured'
894
+ );
895
+ }
896
+ if (!protocolV1Messages) {
897
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
898
+ }
899
+
900
+ const session = new ProtocolV2Session({
901
+ schemas: {
902
+ protocolV1: protocolV1Messages,
903
+ protocolV2: this.messagesV2,
904
+ },
905
+ router: PROTOCOL_V2_CHANNEL_USB,
906
+ writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(path, frame),
907
+ readFrame: () => this.receiveProtocolV2Frame(path, options?.timeoutMs),
908
+ logger: this.Log,
909
+ logPrefix: 'ProtocolV2 NodeUSB',
910
+ createTimeoutError: (_messageName: string, timeoutMs: number) =>
911
+ new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
912
+ });
913
+
914
+ this.protocolV2Assemblers.get(path)?.reset();
915
+ return session.call(name, data, options);
916
+ }
917
+
612
918
  /**
613
919
  * Receive a complete protobuf response from the device.
614
920
  * Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
615
921
  */
616
- private async receiveData(path: string, dev: OpenDevice): Promise<string> {
922
+ private async receiveData(path: string, dev: OpenDevice, timeoutMs?: number): Promise<string> {
923
+ const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
924
+ const readPacket = async () => {
925
+ const transferIn = this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE);
926
+ return deadline
927
+ ? this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V1')
928
+ : transferIn;
929
+ };
930
+
617
931
  // Read first packet, skip report byte
618
- const firstPacket = await this.transferInWithRetry(path, dev, PACKET_SIZE);
932
+ const firstPacket = timeoutMs
933
+ ? await readPacket()
934
+ : await this.transferInWithRetry(path, dev, PACKET_SIZE);
619
935
  const firstData = skipReportByte(firstPacket);
620
936
 
621
937
  // Decode header: ## marker → { typeId, length, restBuffer }
@@ -633,7 +949,7 @@ export default class NodeUsbTransport {
633
949
  // Read subsequent packets until complete
634
950
  // Re-resolve device on each iteration so we use a fresh handle after any reconnect
635
951
  while (decoded.offset < lengthWithHeader) {
636
- const packet = await this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE);
952
+ const packet = await readPacket();
637
953
  const pktData = skipReportByte(packet);
638
954
  const buf = toArrayBuffer(pktData);
639
955
  if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
@@ -647,4 +963,8 @@ export default class NodeUsbTransport {
647
963
  const result = decoded.toBuffer();
648
964
  return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
649
965
  }
966
+
967
+ getProtocolType(path: string): ProtocolType {
968
+ return this.deviceProtocol.get(path) ?? 'V1';
969
+ }
650
970
  }