@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 +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +281 -31
- package/package.json +4 -4
- package/src/index.ts +345 -25
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as transport from '@onekeyfe/hd-transport';
|
|
2
|
-
import transport__default, {
|
|
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
|
-
|
|
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 };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
306
|
+
const protocol = (_a = this.deviceProtocol.get(path)) !== null && _a !== void 0 ? _a : 'V1';
|
|
292
307
|
if (transport.LogBlockCommand.has(name)) {
|
|
293
|
-
(
|
|
308
|
+
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
|
|
294
309
|
}
|
|
295
310
|
else {
|
|
296
|
-
(
|
|
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.
|
|
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 =
|
|
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 (
|
|
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 (
|
|
515
|
+
catch (_e) {
|
|
478
516
|
}
|
|
479
517
|
throw err;
|
|
480
518
|
}
|
|
481
519
|
}
|
|
482
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.1.27-alpha.
|
|
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": "
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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.
|
|
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 =
|
|
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 =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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 =
|
|
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
|
|
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
|
}
|