@onekeyfe/hd-transport-usb 1.1.26-alpha.0 → 1.1.26-alpha.10

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
@@ -7,19 +7,37 @@ declare const PACKET_SIZE = 64;
7
7
  declare class NodeUsbTransport {
8
8
  messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
9
9
  name: string;
10
+ version: string;
10
11
  configured: boolean;
12
+ isOutdated: boolean;
11
13
  Log?: any;
12
14
  emitter?: EventEmitter;
13
15
  private serialToBusId;
14
16
  private openDevices;
15
- init(logger: any, emitter?: EventEmitter): void;
16
- configure(signedData: any): void;
17
+ private reconnectLocks;
18
+ private cancelled;
19
+ init(logger: any, emitter?: EventEmitter): Promise<string>;
20
+ configure(signedData: any): Promise<void>;
17
21
  listen(): void;
22
+ stop(): void;
23
+ post(path: string, name: string, data: Record<string, unknown>): Promise<void>;
24
+ read(path: string): Promise<{
25
+ message: {
26
+ [key: string]: any;
27
+ };
28
+ type: string;
29
+ }>;
18
30
  enumerate(): Promise<OneKeyDeviceInfo[]>;
19
31
  acquire(input: AcquireInput): Promise<string>;
20
32
  release(path: string, _onclose?: boolean): Promise<void>;
21
33
  call(path: string, name: string, data: Record<string, unknown>): Promise<transport.MessageFromOneKey>;
22
34
  cancel(): void;
35
+ private getOpenDevice;
36
+ private getErrorMessage;
37
+ private isRetryableError;
38
+ private reconnectForRetry;
39
+ private sendAllChunksWithRetry;
40
+ private transferInWithRetry;
23
41
  private openDevice;
24
42
  private receiveData;
25
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAA8B,MAAM,wBAAwB,CAAC;AAKpE,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA6K7E,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAElE,IAAI,SAAsB;IAE1B,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAGvB,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAiC;IAMpD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAKxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAMzB,MAAM;IASA,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA8BxC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB7C,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;IAqCpE,MAAM;YAUQ,UAAU;YAuDV,WAAW;CAiC1B;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAA8B,MAAM,wBAAwB,CAAC;AAKpE,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA0J7E,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;IAM1B,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;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -71,6 +71,7 @@ const INTERFACE_NUMBER = 0;
71
71
  const ENDPOINT_IN = 0x81;
72
72
  const ENDPOINT_OUT = 0x01;
73
73
  const TRANSFER_TIMEOUT_MS = 30000;
74
+ const SERIAL_READ_TIMEOUT_MS = 5000;
74
75
  const PACKET_IO_MAX_RETRIES = 3;
75
76
  const PACKET_IO_RETRY_DELAY = 300;
76
77
  function getBusId(dev) {
@@ -89,29 +90,47 @@ function readSerialNumber(dev, openDevices) {
89
90
  }
90
91
  }
91
92
  return new Promise(resolve => {
93
+ let settled = false;
94
+ const settle = (value) => {
95
+ if (settled)
96
+ return;
97
+ settled = true;
98
+ resolve(value);
99
+ };
100
+ const timer = setTimeout(() => {
101
+ try {
102
+ dev.close();
103
+ }
104
+ catch (_a) {
105
+ }
106
+ settle(busId);
107
+ }, SERIAL_READ_TIMEOUT_MS);
92
108
  try {
93
109
  dev.open();
94
110
  try {
95
111
  dev.getStringDescriptor(iSerialNumber, (_err, data) => {
112
+ clearTimeout(timer);
96
113
  try {
97
114
  dev.close();
98
115
  }
99
116
  catch (_a) {
100
117
  }
101
- resolve(data || busId);
118
+ settle(data || busId);
102
119
  });
103
120
  }
104
121
  catch (_a) {
122
+ clearTimeout(timer);
105
123
  try {
106
124
  dev.close();
107
125
  }
108
126
  catch (_b) {
109
127
  }
110
- resolve(busId);
128
+ settle(busId);
111
129
  }
112
130
  }
113
131
  catch (_c) {
114
- resolve(busId);
132
+ clearTimeout(timer);
133
+ settle(busId);
115
134
  }
116
135
  });
117
136
  }
@@ -135,45 +154,6 @@ function transferOutOnce(ep, data) {
135
154
  });
136
155
  });
137
156
  }
138
- function wait(ms) {
139
- return new Promise(resolve => {
140
- setTimeout(resolve, ms);
141
- });
142
- }
143
- function transferIn(ep, length) {
144
- return __awaiter(this, void 0, void 0, function* () {
145
- let lastError;
146
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
147
- try {
148
- return yield transferInOnce(ep, length);
149
- }
150
- catch (err) {
151
- lastError = err;
152
- if (attempt < PACKET_IO_MAX_RETRIES) {
153
- yield wait(attempt * PACKET_IO_RETRY_DELAY);
154
- }
155
- }
156
- }
157
- throw lastError;
158
- });
159
- }
160
- function transferOut(ep, data) {
161
- return __awaiter(this, void 0, void 0, function* () {
162
- let lastError;
163
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
164
- try {
165
- return yield transferOutOnce(ep, data);
166
- }
167
- catch (err) {
168
- lastError = err;
169
- if (attempt < PACKET_IO_MAX_RETRIES) {
170
- yield wait(attempt * PACKET_IO_RETRY_DELAY);
171
- }
172
- }
173
- }
174
- throw lastError;
175
- });
176
- }
177
157
  function skipReportByte(packet) {
178
158
  if (packet[0] === REPORT_ID) {
179
159
  return packet.subarray(1);
@@ -186,21 +166,51 @@ function toArrayBuffer(buf) {
186
166
  class NodeUsbTransport {
187
167
  constructor() {
188
168
  this.name = 'NodeUsbTransport';
169
+ this.version = '';
189
170
  this.configured = false;
171
+ this.isOutdated = false;
190
172
  this.serialToBusId = new Map();
191
173
  this.openDevices = new Map();
174
+ this.reconnectLocks = new Map();
175
+ this.cancelled = false;
192
176
  }
193
177
  init(logger, emitter) {
194
178
  this.Log = logger;
195
179
  this.emitter = emitter;
180
+ return Promise.resolve('');
196
181
  }
197
182
  configure(signedData) {
198
183
  const messages = parseConfigure(signedData);
199
184
  this.configured = true;
200
185
  this.messages = messages;
186
+ return Promise.resolve();
201
187
  }
202
188
  listen() {
203
189
  }
190
+ stop() {
191
+ }
192
+ post(path, name, data) {
193
+ return __awaiter(this, void 0, void 0, function* () {
194
+ if (!this.messages) {
195
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
196
+ }
197
+ const encodeBuffers = buildEncodeBuffers(this.messages, name, data);
198
+ yield this.sendAllChunksWithRetry(path, encodeBuffers);
199
+ });
200
+ }
201
+ read(path) {
202
+ return __awaiter(this, void 0, void 0, function* () {
203
+ const dev = this.getOpenDevice(path);
204
+ const resData = yield this.receiveData(path, dev);
205
+ if (typeof resData !== 'string') {
206
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, 'Returning data is not string.');
207
+ }
208
+ if (!this.messages) {
209
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
210
+ }
211
+ return receiveOne(this.messages, resData);
212
+ });
213
+ }
204
214
  enumerate() {
205
215
  return __awaiter(this, void 0, void 0, function* () {
206
216
  const allDevices = usb__namespace.getDeviceList();
@@ -228,20 +238,18 @@ class NodeUsbTransport {
228
238
  }
229
239
  acquire(input) {
230
240
  var _a, _b, _c;
231
- return __awaiter(this, void 0, void 0, function* () {
232
- const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
233
- if (!path) {
234
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
235
- }
236
- try {
237
- yield this.openDevice(path);
238
- return path;
239
- }
240
- catch (error) {
241
- (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
242
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_c = error.message) !== null && _c !== void 0 ? _c : String(error));
243
- }
244
- });
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
+ }
245
253
  }
246
254
  release(path, _onclose) {
247
255
  return __awaiter(this, void 0, void 0, function* () {
@@ -273,11 +281,11 @@ class NodeUsbTransport {
273
281
  call(path, name, data) {
274
282
  var _a, _b;
275
283
  return __awaiter(this, void 0, void 0, function* () {
284
+ this.cancelled = false;
276
285
  if (!this.messages) {
277
286
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
278
287
  }
279
- const openDev = this.openDevices.get(path);
280
- if (!openDev) {
288
+ if (!this.openDevices.get(path)) {
281
289
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
282
290
  }
283
291
  const { messages } = this;
@@ -288,13 +296,8 @@ class NodeUsbTransport {
288
296
  (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
289
297
  }
290
298
  const encodeBuffers = buildEncodeBuffers(messages, name, data);
291
- for (const buffer of encodeBuffers) {
292
- const packet = new Uint8Array(PACKET_SIZE);
293
- packet[0] = REPORT_ID;
294
- packet.set(new Uint8Array(buffer), 1);
295
- yield transferOut(openDev.epOut, Buffer.from(packet));
296
- }
297
- const resData = yield this.receiveData(openDev);
299
+ yield this.sendAllChunksWithRetry(path, encodeBuffers);
300
+ const resData = yield this.receiveData(path, this.getOpenDevice(path));
298
301
  if (typeof resData !== 'string') {
299
302
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, 'Returning data is not string.');
300
303
  }
@@ -305,20 +308,147 @@ class NodeUsbTransport {
305
308
  cancel() {
306
309
  var _a;
307
310
  (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport cancel');
311
+ this.cancelled = true;
308
312
  }
309
- openDevice(path) {
313
+ getOpenDevice(path) {
314
+ const dev = this.openDevices.get(path);
315
+ if (!dev) {
316
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
317
+ }
318
+ return dev;
319
+ }
320
+ getErrorMessage(error) {
321
+ if (!error)
322
+ return '';
323
+ if (typeof error === 'string')
324
+ return error;
325
+ if (typeof error === 'object' && 'message' in error) {
326
+ const { message } = error;
327
+ return typeof message === 'string' ? message : String(message !== null && message !== void 0 ? message : '');
328
+ }
329
+ return String(error);
330
+ }
331
+ isRetryableError(error) {
332
+ const message = this.getErrorMessage(error).toLowerCase();
333
+ return (message.includes('libusb') ||
334
+ message.includes('transfer') ||
335
+ message.includes('disconnected') ||
336
+ message.includes('device not found') ||
337
+ message.includes('busy') ||
338
+ message.includes('pipe') ||
339
+ message.includes('empty usb transfer') ||
340
+ message.includes('network') ||
341
+ message.includes('timeout') ||
342
+ message.includes('interrupt'));
343
+ }
344
+ reconnectForRetry(path, direction, attempt, error) {
345
+ const existing = this.reconnectLocks.get(path);
346
+ if (existing)
347
+ return existing;
348
+ const doReconnect = () => __awaiter(this, void 0, void 0, function* () {
349
+ var _a, _b;
350
+ (_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
+ yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
352
+ try {
353
+ yield this.release(path);
354
+ }
355
+ catch (releaseError) {
356
+ (_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('[NodeUsbTransport] release before retry error:', releaseError);
357
+ }
358
+ yield this.enumerate();
359
+ this.openDevice(path);
360
+ const openDev = this.openDevices.get(path);
361
+ if (!openDev) {
362
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device not found after reconnect: ${path}`);
363
+ }
364
+ return openDev;
365
+ });
366
+ const promise = doReconnect().finally(() => {
367
+ this.reconnectLocks.delete(path);
368
+ });
369
+ this.reconnectLocks.set(path, promise);
370
+ return promise;
371
+ }
372
+ sendAllChunksWithRetry(path, encodeBuffers) {
310
373
  var _a;
311
374
  return __awaiter(this, void 0, void 0, function* () {
312
- const existing = this.openDevices.get(path);
313
- if (existing)
314
- return;
315
- const busId = (_a = this.serialToBusId.get(path)) !== null && _a !== void 0 ? _a : path;
316
- const allDevices = usb__namespace.getDeviceList();
317
- const dev = allDevices.find(d => getBusId(d) === busId);
318
- if (!dev) {
319
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
375
+ let lastError;
376
+ for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
377
+ if (this.cancelled) {
378
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
379
+ }
380
+ try {
381
+ for (const buffer of encodeBuffers) {
382
+ const packet = new Uint8Array(PACKET_SIZE);
383
+ packet[0] = REPORT_ID;
384
+ packet.set(new Uint8Array(buffer), 1);
385
+ yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
386
+ }
387
+ return;
388
+ }
389
+ catch (error) {
390
+ lastError = error;
391
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
392
+ if (!shouldRetry) {
393
+ throw error;
394
+ }
395
+ try {
396
+ yield this.reconnectForRetry(path, 'out', attempt, error);
397
+ }
398
+ catch (reconnectError) {
399
+ lastError = reconnectError;
400
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] reconnect failed on send retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
401
+ break;
402
+ }
403
+ }
320
404
  }
321
- dev.open();
405
+ throw lastError;
406
+ });
407
+ }
408
+ transferInWithRetry(path, openDev, length) {
409
+ var _a;
410
+ return __awaiter(this, void 0, void 0, function* () {
411
+ let lastError;
412
+ let currentDev = openDev;
413
+ for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
414
+ if (this.cancelled) {
415
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
416
+ }
417
+ try {
418
+ return yield transferInOnce(currentDev.epIn, length);
419
+ }
420
+ catch (error) {
421
+ lastError = error;
422
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
423
+ if (!shouldRetry) {
424
+ throw error;
425
+ }
426
+ try {
427
+ currentDev = yield this.reconnectForRetry(path, 'in', attempt, error);
428
+ }
429
+ catch (reconnectError) {
430
+ lastError = reconnectError;
431
+ (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
432
+ break;
433
+ }
434
+ }
435
+ }
436
+ throw lastError;
437
+ });
438
+ }
439
+ openDevice(path) {
440
+ var _a;
441
+ const existing = this.openDevices.get(path);
442
+ if (existing)
443
+ return;
444
+ const busId = (_a = this.serialToBusId.get(path)) !== null && _a !== void 0 ? _a : path;
445
+ const allDevices = usb__namespace.getDeviceList();
446
+ const dev = allDevices.find(d => getBusId(d) === busId);
447
+ if (!dev) {
448
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
449
+ }
450
+ dev.open();
451
+ try {
322
452
  dev.timeout = TRANSFER_TIMEOUT_MS;
323
453
  const iface = dev.interface(INTERFACE_NUMBER);
324
454
  if (process.platform === 'linux') {
@@ -334,17 +464,24 @@ class NodeUsbTransport {
334
464
  const epIn = iface.endpoints.find((e) => e.direction === 'in' && e.address === ENDPOINT_IN);
335
465
  const epOut = iface.endpoints.find((e) => e.direction === 'out' && e.address === ENDPOINT_OUT);
336
466
  if (!epIn || !epOut) {
337
- dev.close();
338
467
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'USB endpoints not found (expected IN 0x81, OUT 0x01)');
339
468
  }
340
469
  epIn.timeout = TRANSFER_TIMEOUT_MS;
341
470
  epOut.timeout = TRANSFER_TIMEOUT_MS;
342
471
  this.openDevices.set(path, { device: dev, iface, epIn, epOut });
343
- });
472
+ }
473
+ catch (err) {
474
+ try {
475
+ dev.close();
476
+ }
477
+ catch (_c) {
478
+ }
479
+ throw err;
480
+ }
344
481
  }
345
- receiveData(dev) {
482
+ receiveData(path, dev) {
346
483
  return __awaiter(this, void 0, void 0, function* () {
347
- const firstPacket = yield transferIn(dev.epIn, PACKET_SIZE);
484
+ const firstPacket = yield this.transferInWithRetry(path, dev, PACKET_SIZE);
348
485
  const firstData = skipReportByte(firstPacket);
349
486
  const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
350
487
  const lengthWithHeader = Number(length) + HEADER_LENGTH;
@@ -355,7 +492,7 @@ class NodeUsbTransport {
355
492
  decoded.append(restBuffer);
356
493
  }
357
494
  while (decoded.offset < lengthWithHeader) {
358
- const packet = yield transferIn(dev.epIn, PACKET_SIZE);
495
+ const packet = yield this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE);
359
496
  const pktData = skipReportByte(packet);
360
497
  const buf = toArrayBuffer(pktData);
361
498
  if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-usb",
3
- "version": "1.1.26-alpha.0",
3
+ "version": "1.1.26-alpha.10",
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.26-alpha.0",
24
- "@onekeyfe/hd-transport": "1.1.26-alpha.0",
23
+ "@onekeyfe/hd-shared": "1.1.26-alpha.10",
24
+ "@onekeyfe/hd-transport": "1.1.26-alpha.10",
25
25
  "bytebuffer": "^5.0.1",
26
26
  "usb": "^2.14.0"
27
27
  },
28
- "gitHead": "8d3072a406b0672c9d20e2f58e25cef94a9759fb"
28
+ "gitHead": "4cab4ba97dee894aa87145ced1e629c06f0ab8b7"
29
29
  }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import ByteBuffer from 'bytebuffer';
2
2
  import * as usb from 'usb';
3
3
  import transport, { LogBlockCommand } from '@onekeyfe/hd-transport';
4
- import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
4
+ import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
5
5
 
6
6
  import { HEADER_LENGTH, PACKET_SIZE, PAYLOAD_SIZE, REPORT_ID } from './constants';
7
7
 
@@ -19,6 +19,9 @@ const ENDPOINT_OUT = 0x01;
19
19
  /** Transfer timeout in milliseconds */
20
20
  const TRANSFER_TIMEOUT_MS = 30000;
21
21
 
22
+ /** Timeout for reading serial number descriptor during enumeration */
23
+ const SERIAL_READ_TIMEOUT_MS = 5000;
24
+
22
25
  /** Packet I/O retry configuration (matches WebUsbTransport) */
23
26
  const PACKET_IO_MAX_RETRIES = 3;
24
27
  const PACKET_IO_RETRY_DELAY = 300;
@@ -59,29 +62,49 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
59
62
  }
60
63
  }
61
64
 
62
- return new Promise(resolve => {
65
+ return new Promise<string>(resolve => {
66
+ let settled = false;
67
+ const settle = (value: string) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ resolve(value);
71
+ };
72
+
73
+ // Guard against getStringDescriptor never calling back
74
+ const timer = setTimeout(() => {
75
+ try {
76
+ dev.close();
77
+ } catch {
78
+ /* ignore */
79
+ }
80
+ settle(busId);
81
+ }, SERIAL_READ_TIMEOUT_MS);
82
+
63
83
  try {
64
84
  dev.open();
65
85
  try {
66
86
  dev.getStringDescriptor(iSerialNumber, (_err: Error | undefined, data?: string) => {
87
+ clearTimeout(timer);
67
88
  try {
68
89
  dev.close();
69
90
  } catch {
70
91
  /* ignore */
71
92
  }
72
- resolve(data || busId);
93
+ settle(data || busId);
73
94
  });
74
95
  } catch {
96
+ clearTimeout(timer);
75
97
  try {
76
98
  dev.close();
77
99
  } catch {
78
100
  /* ignore */
79
101
  }
80
- resolve(busId);
102
+ settle(busId);
81
103
  }
82
104
  } catch {
83
105
  // dev.open() failed (e.g. LIBUSB_ERROR_BUSY if already open elsewhere)
84
- resolve(busId);
106
+ clearTimeout(timer);
107
+ settle(busId);
85
108
  }
86
109
  });
87
110
  }
@@ -111,48 +134,6 @@ function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
111
134
  });
112
135
  }
113
136
 
114
- function wait(ms: number): Promise<void> {
115
- return new Promise(resolve => {
116
- setTimeout(resolve, ms);
117
- });
118
- }
119
-
120
- /**
121
- * USB IN transfer with retry.
122
- */
123
- async function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
124
- let lastError: unknown;
125
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
126
- try {
127
- return await transferInOnce(ep, length);
128
- } catch (err) {
129
- lastError = err;
130
- if (attempt < PACKET_IO_MAX_RETRIES) {
131
- await wait(attempt * PACKET_IO_RETRY_DELAY);
132
- }
133
- }
134
- }
135
- throw lastError;
136
- }
137
-
138
- /**
139
- * USB OUT transfer with retry.
140
- */
141
- async function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
142
- let lastError: unknown;
143
- for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
144
- try {
145
- return await transferOutOnce(ep, data);
146
- } catch (err) {
147
- lastError = err;
148
- if (attempt < PACKET_IO_MAX_RETRIES) {
149
- await wait(attempt * PACKET_IO_RETRY_DELAY);
150
- }
151
- }
152
- }
153
- throw lastError;
154
- }
155
-
156
137
  /**
157
138
  * Skip the 0x3F protocol marker byte from a USB packet.
158
139
  */
@@ -184,8 +165,12 @@ export default class NodeUsbTransport {
184
165
 
185
166
  name = 'NodeUsbTransport';
186
167
 
168
+ version = '';
169
+
187
170
  configured = false;
188
171
 
172
+ isOutdated = false;
173
+
189
174
  Log?: any;
190
175
 
191
176
  emitter?: EventEmitter;
@@ -196,6 +181,12 @@ export default class NodeUsbTransport {
196
181
  /** path → opened device state */
197
182
  private openDevices = new Map<string, OpenDevice>();
198
183
 
184
+ /** per-path reconnect lock to prevent concurrent reconnects */
185
+ private reconnectLocks = new Map<string, Promise<OpenDevice>>();
186
+
187
+ /** set to true when cancel() is called; checked by retry loops */
188
+ private cancelled = false;
189
+
199
190
  /**
200
191
  * Initialize transport.
201
192
  * Signature matches the Transport.init interface (logger, emitter).
@@ -203,18 +194,52 @@ export default class NodeUsbTransport {
203
194
  init(logger: any, emitter?: EventEmitter) {
204
195
  this.Log = logger;
205
196
  this.emitter = emitter;
197
+ return Promise.resolve('');
206
198
  }
207
199
 
208
200
  configure(signedData: any) {
209
201
  const messages = parseConfigure(signedData);
210
202
  this.configured = true;
211
203
  this.messages = messages;
204
+ return Promise.resolve();
212
205
  }
213
206
 
214
207
  listen() {
215
208
  // empty — could add hotplug events via usb.on('attach'/'detach')
216
209
  }
217
210
 
211
+ stop() {
212
+ // Placeholder — no background listeners to tear down
213
+ }
214
+
215
+ /**
216
+ * Low-level post (send only, no response). Not used by NodeUsbTransport
217
+ * since call() handles the full send+receive cycle, but required by the Transport interface.
218
+ */
219
+ async post(path: string, name: string, data: Record<string, unknown>): Promise<void> {
220
+ if (!this.messages) {
221
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
222
+ }
223
+ const encodeBuffers = buildEncodeBuffers(this.messages, name, data);
224
+ await this.sendAllChunksWithRetry(path, encodeBuffers);
225
+ }
226
+
227
+ /**
228
+ * Low-level read (receive only). Not used by NodeUsbTransport
229
+ * since call() handles the full send+receive cycle, but required by the Transport interface.
230
+ */
231
+ async read(path: string) {
232
+ const dev = this.getOpenDevice(path);
233
+ const resData = await this.receiveData(path, dev);
234
+ if (typeof resData !== 'string') {
235
+ throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
236
+ }
237
+ if (!this.messages) {
238
+ throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
239
+ }
240
+ return receiveOne(this.messages, resData);
241
+ }
242
+
218
243
  /**
219
244
  * Enumerate connected OneKey USB devices.
220
245
  * Opens each device briefly to read its serial number (used as `path`),
@@ -250,15 +275,15 @@ export default class NodeUsbTransport {
250
275
  /**
251
276
  * Acquire device — open USB device, claim interface, return path (string).
252
277
  */
253
- async acquire(input: AcquireInput): Promise<string> {
278
+ acquire(input: AcquireInput): Promise<string> {
254
279
  const path = input.path ?? '';
255
280
  if (!path) {
256
281
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'No device path provided');
257
282
  }
258
283
 
259
284
  try {
260
- await this.openDevice(path);
261
- return path;
285
+ this.openDevice(path);
286
+ return Promise.resolve(path);
262
287
  } catch (error: any) {
263
288
  this.Log?.debug('NodeUsbTransport acquire error: ', error);
264
289
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
@@ -298,12 +323,13 @@ export default class NodeUsbTransport {
298
323
  * This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
299
324
  */
300
325
  async call(path: string, name: string, data: Record<string, unknown>) {
326
+ this.cancelled = false;
327
+
301
328
  if (!this.messages) {
302
329
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
303
330
  }
304
331
 
305
- const openDev = this.openDevices.get(path);
306
- if (!openDev) {
332
+ if (!this.openDevices.get(path)) {
307
333
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
308
334
  }
309
335
 
@@ -317,16 +343,12 @@ export default class NodeUsbTransport {
317
343
  // Encode protobuf message into 63-byte chunks (same as WebUsbTransport)
318
344
  const encodeBuffers = buildEncodeBuffers(messages, name, data);
319
345
 
320
- // Send each chunk with 0x3F report ID prefix
321
- for (const buffer of encodeBuffers) {
322
- const packet = new Uint8Array(PACKET_SIZE);
323
- packet[0] = REPORT_ID;
324
- packet.set(new Uint8Array(buffer), 1);
325
- await transferOut(openDev.epOut, Buffer.from(packet));
326
- }
346
+ // Send all chunks with retry if any chunk fails and reconnects,
347
+ // restart the entire send sequence from chunk 0 (device resets state on reconnect)
348
+ await this.sendAllChunksWithRetry(path, encodeBuffers);
327
349
 
328
- // Receive response
329
- const resData = await this.receiveData(openDev);
350
+ // Receive response — re-resolve in case reconnect happened during send
351
+ const resData = await this.receiveData(path, this.getOpenDevice(path));
330
352
  if (typeof resData !== 'string') {
331
353
  throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
332
354
  }
@@ -336,15 +358,184 @@ export default class NodeUsbTransport {
336
358
 
337
359
  cancel() {
338
360
  this.Log?.debug('NodeUsbTransport cancel');
361
+ this.cancelled = true;
339
362
  }
340
363
 
341
364
  // --- Private helpers ---
342
365
 
366
+ /**
367
+ * Get the current open device for a path, re-resolving from the map
368
+ * so callers always use a fresh reference after reconnect.
369
+ */
370
+ private getOpenDevice(path: string): OpenDevice {
371
+ const dev = this.openDevices.get(path);
372
+ if (!dev) {
373
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
374
+ }
375
+ return dev;
376
+ }
377
+
378
+ private getErrorMessage(error: unknown): string {
379
+ if (!error) return '';
380
+ if (typeof error === 'string') return error;
381
+ if (typeof error === 'object' && 'message' in error) {
382
+ const { message } = error as { message?: unknown };
383
+ return typeof message === 'string' ? message : String(message ?? '');
384
+ }
385
+ return String(error);
386
+ }
387
+
388
+ private isRetryableError(error: unknown): boolean {
389
+ const message = this.getErrorMessage(error).toLowerCase();
390
+ return (
391
+ message.includes('libusb') ||
392
+ message.includes('transfer') ||
393
+ message.includes('disconnected') ||
394
+ message.includes('device not found') ||
395
+ message.includes('busy') ||
396
+ message.includes('pipe') ||
397
+ message.includes('empty usb transfer') ||
398
+ message.includes('network') ||
399
+ message.includes('timeout') ||
400
+ message.includes('interrupt')
401
+ );
402
+ }
403
+
404
+ /**
405
+ * Reconnect device before retrying a failed transfer (aligned with WebUsbTransport).
406
+ * Uses per-path lock to prevent concurrent reconnects to the same device.
407
+ */
408
+ private reconnectForRetry(
409
+ path: string,
410
+ direction: 'in' | 'out',
411
+ attempt: number,
412
+ error: unknown
413
+ ): Promise<OpenDevice> {
414
+ // If a reconnect is already in progress for this path, reuse it
415
+ const existing = this.reconnectLocks.get(path);
416
+ if (existing) return existing;
417
+
418
+ const doReconnect = async (): Promise<OpenDevice> => {
419
+ this.Log?.debug(
420
+ `[NodeUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
421
+ error
422
+ )}`
423
+ );
424
+ await wait(attempt * PACKET_IO_RETRY_DELAY);
425
+
426
+ // Close the existing device
427
+ try {
428
+ await this.release(path);
429
+ } catch (releaseError) {
430
+ this.Log?.debug('[NodeUsbTransport] release before retry error:', releaseError);
431
+ }
432
+
433
+ // Re-enumerate to refresh device list, then re-open
434
+ await this.enumerate();
435
+ this.openDevice(path);
436
+
437
+ const openDev = this.openDevices.get(path);
438
+ if (!openDev) {
439
+ throw ERRORS.TypedError(
440
+ HardwareErrorCode.DeviceNotFound,
441
+ `Device not found after reconnect: ${path}`
442
+ );
443
+ }
444
+ return openDev;
445
+ };
446
+
447
+ const promise = doReconnect().finally(() => {
448
+ this.reconnectLocks.delete(path);
449
+ });
450
+ this.reconnectLocks.set(path, promise);
451
+ return promise;
452
+ }
453
+
454
+ /**
455
+ * Send all encoded chunks to the device with retry.
456
+ * If a chunk fails and triggers reconnect, the entire sequence restarts
457
+ * from chunk 0 because the device resets protocol state on reconnect.
458
+ */
459
+ private async sendAllChunksWithRetry(path: string, encodeBuffers: ArrayBuffer[]): Promise<void> {
460
+ let lastError: unknown;
461
+ for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
462
+ if (this.cancelled) {
463
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
464
+ }
465
+ try {
466
+ for (const buffer of encodeBuffers) {
467
+ const packet = new Uint8Array(PACKET_SIZE);
468
+ packet[0] = REPORT_ID;
469
+ packet.set(new Uint8Array(buffer), 1);
470
+ await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
471
+ }
472
+ return; // all chunks sent successfully
473
+ } catch (error) {
474
+ lastError = error;
475
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
476
+ if (!shouldRetry) {
477
+ throw error;
478
+ }
479
+ try {
480
+ await this.reconnectForRetry(path, 'out', attempt, error);
481
+ // Reconnected — loop will restart from chunk 0
482
+ } catch (reconnectError) {
483
+ lastError = reconnectError;
484
+ this.Log?.debug(
485
+ `[NodeUsbTransport] reconnect failed on send retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
486
+ reconnectError
487
+ )}`
488
+ );
489
+ // Reconnect failed — no point retrying with a dead device
490
+ break;
491
+ }
492
+ }
493
+ }
494
+ throw lastError;
495
+ }
496
+
497
+ /**
498
+ * USB IN transfer with retry and reconnect (aligned with WebUsbTransport).
499
+ */
500
+ private async transferInWithRetry(
501
+ path: string,
502
+ openDev: OpenDevice,
503
+ length: number
504
+ ): Promise<Buffer> {
505
+ let lastError: unknown;
506
+ let currentDev = openDev;
507
+ for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
508
+ if (this.cancelled) {
509
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
510
+ }
511
+ try {
512
+ return await transferInOnce(currentDev.epIn, length);
513
+ } catch (error) {
514
+ lastError = error;
515
+ const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
516
+ if (!shouldRetry) {
517
+ throw error;
518
+ }
519
+ try {
520
+ currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
521
+ } catch (reconnectError) {
522
+ lastError = reconnectError;
523
+ this.Log?.debug(
524
+ `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
525
+ reconnectError
526
+ )}`
527
+ );
528
+ break;
529
+ }
530
+ }
531
+ }
532
+ throw lastError;
533
+ }
534
+
343
535
  /**
344
536
  * Open a USB device by path (serial number), claim interface, cache endpoints.
345
537
  */
346
- // eslint-disable-next-line @typescript-eslint/require-await
347
- private async openDevice(path: string): Promise<void> {
538
+ private openDevice(path: string): void {
348
539
  const existing = this.openDevices.get(path);
349
540
  if (existing) return;
350
541
 
@@ -357,51 +548,60 @@ export default class NodeUsbTransport {
357
548
  }
358
549
 
359
550
  dev.open();
360
- dev.timeout = TRANSFER_TIMEOUT_MS;
361
551
 
362
- const iface = dev.interface(INTERFACE_NUMBER);
552
+ try {
553
+ dev.timeout = TRANSFER_TIMEOUT_MS;
363
554
 
364
- // On Linux, detach kernel driver if active
365
- if (process.platform === 'linux') {
366
- try {
367
- if (iface.isKernelDriverActive()) {
368
- iface.detachKernelDriver();
555
+ const iface = dev.interface(INTERFACE_NUMBER);
556
+
557
+ // On Linux, detach kernel driver if active
558
+ if (process.platform === 'linux') {
559
+ try {
560
+ if (iface.isKernelDriverActive()) {
561
+ iface.detachKernelDriver();
562
+ }
563
+ } catch {
564
+ // May not be supported — continue
369
565
  }
370
- } catch {
371
- // May not be supported — continue
372
566
  }
373
- }
374
-
375
- iface.claim();
376
567
 
377
- const epIn = iface.endpoints.find(
378
- (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
379
- );
380
- const epOut = iface.endpoints.find(
381
- (e): e is usb.OutEndpoint => e.direction === 'out' && e.address === ENDPOINT_OUT
382
- );
568
+ iface.claim();
383
569
 
384
- if (!epIn || !epOut) {
385
- dev.close();
386
- throw ERRORS.TypedError(
387
- HardwareErrorCode.DeviceNotFound,
388
- 'USB endpoints not found (expected IN 0x81, OUT 0x01)'
570
+ const epIn = iface.endpoints.find(
571
+ (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
572
+ );
573
+ const epOut = iface.endpoints.find(
574
+ (e): e is usb.OutEndpoint => e.direction === 'out' && e.address === ENDPOINT_OUT
389
575
  );
390
- }
391
576
 
392
- epIn.timeout = TRANSFER_TIMEOUT_MS;
393
- epOut.timeout = TRANSFER_TIMEOUT_MS;
577
+ if (!epIn || !epOut) {
578
+ throw ERRORS.TypedError(
579
+ HardwareErrorCode.DeviceNotFound,
580
+ 'USB endpoints not found (expected IN 0x81, OUT 0x01)'
581
+ );
582
+ }
583
+
584
+ epIn.timeout = TRANSFER_TIMEOUT_MS;
585
+ epOut.timeout = TRANSFER_TIMEOUT_MS;
394
586
 
395
- this.openDevices.set(path, { device: dev, iface, epIn, epOut });
587
+ this.openDevices.set(path, { device: dev, iface, epIn, epOut });
588
+ } catch (err) {
589
+ try {
590
+ dev.close();
591
+ } catch {
592
+ // ignore close errors during cleanup
593
+ }
594
+ throw err;
595
+ }
396
596
  }
397
597
 
398
598
  /**
399
599
  * Receive a complete protobuf response from the device.
400
600
  * Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
401
601
  */
402
- private async receiveData(dev: OpenDevice): Promise<string> {
602
+ private async receiveData(path: string, dev: OpenDevice): Promise<string> {
403
603
  // Read first packet, skip report byte
404
- const firstPacket = await transferIn(dev.epIn, PACKET_SIZE);
604
+ const firstPacket = await this.transferInWithRetry(path, dev, PACKET_SIZE);
405
605
  const firstData = skipReportByte(firstPacket);
406
606
 
407
607
  // Decode header: ## marker → { typeId, length, restBuffer }
@@ -417,8 +617,9 @@ export default class NodeUsbTransport {
417
617
  }
418
618
 
419
619
  // Read subsequent packets until complete
620
+ // Re-resolve device on each iteration so we use a fresh handle after any reconnect
420
621
  while (decoded.offset < lengthWithHeader) {
421
- const packet = await transferIn(dev.epIn, PACKET_SIZE);
622
+ const packet = await this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE);
422
623
  const pktData = skipReportByte(packet);
423
624
  const buf = toArrayBuffer(pktData);
424
625
  if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {