@onekeyfe/hd-transport-usb 1.1.26 → 1.1.27-alpha.31
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 +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +292 -43
- package/package.json +4 -4
- package/src/index.ts +363 -31
- package/dist/constants.d.ts +0 -5
- package/dist/constants.d.ts.map +0 -1
- package/src/constants.ts +0 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import * as transport from '@onekeyfe/hd-transport';
|
|
2
|
-
import transport__default, { 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
|
-
declare const PACKET_SIZE = 64;
|
|
6
|
-
|
|
7
5
|
declare class NodeUsbTransport {
|
|
8
6
|
messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
7
|
+
messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
9
8
|
name: string;
|
|
10
9
|
version: string;
|
|
11
10
|
configured: boolean;
|
|
@@ -14,10 +13,13 @@ declare class NodeUsbTransport {
|
|
|
14
13
|
emitter?: EventEmitter;
|
|
15
14
|
private serialToBusId;
|
|
16
15
|
private openDevices;
|
|
16
|
+
private deviceProtocol;
|
|
17
|
+
private protocolV2Assemblers;
|
|
17
18
|
private reconnectLocks;
|
|
18
19
|
private cancelled;
|
|
19
20
|
init(logger: any, emitter?: EventEmitter): Promise<string>;
|
|
20
21
|
configure(signedData: any): Promise<void>;
|
|
22
|
+
configureProtocolV2(signedData: any): void;
|
|
21
23
|
listen(): void;
|
|
22
24
|
stop(): void;
|
|
23
25
|
post(path: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
@@ -30,16 +32,29 @@ declare class NodeUsbTransport {
|
|
|
30
32
|
enumerate(): Promise<OneKeyDeviceInfo[]>;
|
|
31
33
|
acquire(input: AcquireInput): Promise<string>;
|
|
32
34
|
release(path: string, _onclose?: boolean): Promise<void>;
|
|
33
|
-
|
|
35
|
+
private closeOpenDevice;
|
|
36
|
+
call(path: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
|
|
37
|
+
private callProtocolV1;
|
|
34
38
|
cancel(): void;
|
|
35
39
|
private getOpenDevice;
|
|
36
40
|
private getErrorMessage;
|
|
37
41
|
private isRetryableError;
|
|
42
|
+
private getDeviceInterface;
|
|
38
43
|
private reconnectForRetry;
|
|
39
44
|
private sendAllChunksWithRetry;
|
|
40
45
|
private transferInWithRetry;
|
|
41
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;
|
|
42
56
|
private receiveData;
|
|
57
|
+
getProtocolType(path: string): ProtocolType;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
export {
|
|
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
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
3
|
var ByteBuffer = require('bytebuffer');
|
|
6
4
|
var usb = require('usb');
|
|
7
5
|
var transport = require('@onekeyfe/hd-transport');
|
|
@@ -61,12 +59,11 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
61
59
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
62
60
|
};
|
|
63
61
|
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport__default["default"];
|
|
62
|
+
const { parseConfigure, ProtocolV1, check } = transport__default["default"];
|
|
63
|
+
const PACKET_SIZE = transport.PROTOCOL_V1_USB_PACKET_SIZE;
|
|
64
|
+
const REPORT_ID = transport.PROTOCOL_V1_REPORT_ID;
|
|
65
|
+
const PAYLOAD_SIZE = transport.PROTOCOL_V1_CHUNK_PAYLOAD_SIZE;
|
|
66
|
+
const HEADER_LENGTH = transport.PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
70
67
|
const INTERFACE_NUMBER = 0;
|
|
71
68
|
const ENDPOINT_IN = 0x81;
|
|
72
69
|
const ENDPOINT_OUT = 0x01;
|
|
@@ -74,6 +71,7 @@ const TRANSFER_TIMEOUT_MS = 30000;
|
|
|
74
71
|
const SERIAL_READ_TIMEOUT_MS = 5000;
|
|
75
72
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
76
73
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
74
|
+
const PROTOCOL_PROBE_TIMEOUT = 1000;
|
|
77
75
|
function getBusId(dev) {
|
|
78
76
|
return `usb:${dev.busNumber}:${dev.deviceAddress}`;
|
|
79
77
|
}
|
|
@@ -171,6 +169,8 @@ class NodeUsbTransport {
|
|
|
171
169
|
this.isOutdated = false;
|
|
172
170
|
this.serialToBusId = new Map();
|
|
173
171
|
this.openDevices = new Map();
|
|
172
|
+
this.deviceProtocol = new Map();
|
|
173
|
+
this.protocolV2Assemblers = new Map();
|
|
174
174
|
this.reconnectLocks = new Map();
|
|
175
175
|
this.cancelled = false;
|
|
176
176
|
}
|
|
@@ -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() {
|
|
@@ -194,7 +199,7 @@ class NodeUsbTransport {
|
|
|
194
199
|
if (!this.messages) {
|
|
195
200
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
196
201
|
}
|
|
197
|
-
const encodeBuffers =
|
|
202
|
+
const encodeBuffers = ProtocolV1.encodeMessageChunks(this.messages, name, data);
|
|
198
203
|
yield this.sendAllChunksWithRetry(path, encodeBuffers);
|
|
199
204
|
});
|
|
200
205
|
}
|
|
@@ -208,7 +213,7 @@ class NodeUsbTransport {
|
|
|
208
213
|
if (!this.messages) {
|
|
209
214
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
210
215
|
}
|
|
211
|
-
return
|
|
216
|
+
return ProtocolV1.decodeMessage(this.messages, resData);
|
|
212
217
|
});
|
|
213
218
|
}
|
|
214
219
|
enumerate() {
|
|
@@ -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,20 +303,32 @@ 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);
|
|
297
315
|
}
|
|
298
|
-
|
|
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);
|
|
324
|
+
}
|
|
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
|
}
|
|
304
|
-
const jsonData =
|
|
331
|
+
const jsonData = ProtocolV1.decodeMessage(messages, resData);
|
|
305
332
|
return check.call(jsonData);
|
|
306
333
|
});
|
|
307
334
|
}
|
|
@@ -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,16 +512,224 @@ 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;
|
|
609
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
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) {
|
|
483
669
|
return __awaiter(this, void 0, void 0, function* () {
|
|
484
|
-
|
|
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
|
-
const { length, typeId, restBuffer } =
|
|
732
|
+
const { length, typeId, restBuffer } = ProtocolV1.decodeFirstChunk(toArrayBuffer(firstData));
|
|
487
733
|
const lengthWithHeader = Number(length) + HEADER_LENGTH;
|
|
488
734
|
const decoded = new ByteBuffer__default["default"](lengthWithHeader);
|
|
489
735
|
decoded.writeUint16(typeId);
|
|
@@ -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,7 +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
|
-
exports
|
|
513
|
-
exports["default"] = NodeUsbTransport;
|
|
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.
|
|
3
|
+
"version": "1.1.27-alpha.31",
|
|
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.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.1.
|
|
23
|
+
"@onekeyfe/hd-shared": "1.1.27-alpha.31",
|
|
24
|
+
"@onekeyfe/hd-transport": "1.1.27-alpha.31",
|
|
25
25
|
"bytebuffer": "^5.0.1",
|
|
26
26
|
"usb": "^2.14.0"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "73a8ecbc0e5e47038966b2f9a5ab30dc2f947d8f"
|
|
29
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import ByteBuffer from 'bytebuffer';
|
|
2
2
|
import * as usb from 'usb';
|
|
3
|
-
import transport, {
|
|
3
|
+
import transport, {
|
|
4
|
+
LogBlockCommand,
|
|
5
|
+
PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
|
|
6
|
+
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
7
|
+
PROTOCOL_V1_REPORT_ID,
|
|
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,
|
|
14
|
+
} from '@onekeyfe/hd-transport';
|
|
4
15
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
5
16
|
|
|
6
|
-
import { HEADER_LENGTH, PACKET_SIZE, PAYLOAD_SIZE, REPORT_ID } from './constants';
|
|
7
|
-
|
|
8
17
|
import type EventEmitter from 'events';
|
|
9
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
AcquireInput,
|
|
20
|
+
OneKeyDeviceInfo,
|
|
21
|
+
ProtocolType,
|
|
22
|
+
TransportCallOptions,
|
|
23
|
+
} from '@onekeyfe/hd-transport';
|
|
24
|
+
|
|
25
|
+
const { parseConfigure, ProtocolV1, check } = transport;
|
|
10
26
|
|
|
11
|
-
const
|
|
27
|
+
const PACKET_SIZE = PROTOCOL_V1_USB_PACKET_SIZE;
|
|
28
|
+
const REPORT_ID = PROTOCOL_V1_REPORT_ID;
|
|
29
|
+
const PAYLOAD_SIZE = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE;
|
|
30
|
+
const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE;
|
|
12
31
|
|
|
13
32
|
/** USB interface number for vendor-specific communication */
|
|
14
33
|
const INTERFACE_NUMBER = 0;
|
|
@@ -25,6 +44,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
|
|
|
25
44
|
/** Packet I/O retry configuration (matches WebUsbTransport) */
|
|
26
45
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
27
46
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
47
|
+
const PROTOCOL_PROBE_TIMEOUT = 1000;
|
|
28
48
|
|
|
29
49
|
/**
|
|
30
50
|
* Opened device state — holds the USB device, claimed interface, and endpoints.
|
|
@@ -163,6 +183,9 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
|
163
183
|
export default class NodeUsbTransport {
|
|
164
184
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
165
185
|
|
|
186
|
+
/** Protobuf schema for Protocol V2 transports. */
|
|
187
|
+
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
188
|
+
|
|
166
189
|
name = 'NodeUsbTransport';
|
|
167
190
|
|
|
168
191
|
version = '';
|
|
@@ -181,6 +204,12 @@ export default class NodeUsbTransport {
|
|
|
181
204
|
/** path → opened device state */
|
|
182
205
|
private openDevices = new Map<string, OpenDevice>();
|
|
183
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
|
+
|
|
184
213
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
185
214
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
186
215
|
|
|
@@ -204,6 +233,11 @@ export default class NodeUsbTransport {
|
|
|
204
233
|
return Promise.resolve();
|
|
205
234
|
}
|
|
206
235
|
|
|
236
|
+
configureProtocolV2(signedData: any) {
|
|
237
|
+
this.messagesV2 = parseConfigure(signedData);
|
|
238
|
+
this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
|
|
239
|
+
}
|
|
240
|
+
|
|
207
241
|
listen() {
|
|
208
242
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
209
243
|
}
|
|
@@ -220,7 +254,7 @@ export default class NodeUsbTransport {
|
|
|
220
254
|
if (!this.messages) {
|
|
221
255
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
222
256
|
}
|
|
223
|
-
const encodeBuffers =
|
|
257
|
+
const encodeBuffers = ProtocolV1.encodeMessageChunks(this.messages, name, data);
|
|
224
258
|
await this.sendAllChunksWithRetry(path, encodeBuffers);
|
|
225
259
|
}
|
|
226
260
|
|
|
@@ -237,7 +271,7 @@ export default class NodeUsbTransport {
|
|
|
237
271
|
if (!this.messages) {
|
|
238
272
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
239
273
|
}
|
|
240
|
-
return
|
|
274
|
+
return ProtocolV1.decodeMessage(this.messages, resData);
|
|
241
275
|
}
|
|
242
276
|
|
|
243
277
|
/**
|
|
@@ -275,7 +309,7 @@ export default class NodeUsbTransport {
|
|
|
275
309
|
/**
|
|
276
310
|
* Acquire device — open USB device, claim interface, return path (string).
|
|
277
311
|
*/
|
|
278
|
-
acquire(input: AcquireInput): Promise<string> {
|
|
312
|
+
async acquire(input: AcquireInput): Promise<string> {
|
|
279
313
|
const path = input.path ?? '';
|
|
280
314
|
if (!path) {
|
|
281
315
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'No device path provided');
|
|
@@ -283,7 +317,8 @@ export default class NodeUsbTransport {
|
|
|
283
317
|
|
|
284
318
|
try {
|
|
285
319
|
this.openDevice(path);
|
|
286
|
-
|
|
320
|
+
await this.detectProtocol(path, input.expectedProtocol);
|
|
321
|
+
return path;
|
|
287
322
|
} catch (error: any) {
|
|
288
323
|
this.Log?.debug('NodeUsbTransport acquire error: ', error);
|
|
289
324
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
@@ -294,6 +329,12 @@ export default class NodeUsbTransport {
|
|
|
294
329
|
* Release device — release interface and close.
|
|
295
330
|
*/
|
|
296
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> {
|
|
297
338
|
const openDev = this.openDevices.get(path);
|
|
298
339
|
if (!openDev) return;
|
|
299
340
|
|
|
@@ -322,7 +363,12 @@ export default class NodeUsbTransport {
|
|
|
322
363
|
* Call device method — encode protobuf, send packets, receive response.
|
|
323
364
|
* This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
|
|
324
365
|
*/
|
|
325
|
-
async call(
|
|
366
|
+
async call(
|
|
367
|
+
path: string,
|
|
368
|
+
name: string,
|
|
369
|
+
data: Record<string, unknown>,
|
|
370
|
+
options?: TransportCallOptions
|
|
371
|
+
) {
|
|
326
372
|
this.cancelled = false;
|
|
327
373
|
|
|
328
374
|
if (!this.messages) {
|
|
@@ -333,26 +379,51 @@ export default class NodeUsbTransport {
|
|
|
333
379
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
|
|
334
380
|
}
|
|
335
381
|
|
|
336
|
-
const
|
|
382
|
+
const protocol = this.deviceProtocol.get(path) ?? 'V1';
|
|
337
383
|
if (LogBlockCommand.has(name)) {
|
|
338
|
-
this.Log?.debug('NodeUsbTransport call-', ' name: ', name);
|
|
384
|
+
this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
|
|
339
385
|
} else {
|
|
340
|
-
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);
|
|
341
399
|
}
|
|
342
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
|
+
}
|
|
343
414
|
// Encode protobuf message into 63-byte chunks (same as WebUsbTransport)
|
|
344
|
-
const encodeBuffers =
|
|
415
|
+
const encodeBuffers = ProtocolV1.encodeMessageChunks(messages, name, data);
|
|
345
416
|
|
|
346
417
|
// Send all chunks with retry — if any chunk fails and reconnects,
|
|
347
418
|
// restart the entire send sequence from chunk 0 (device resets state on reconnect)
|
|
348
419
|
await this.sendAllChunksWithRetry(path, encodeBuffers);
|
|
349
420
|
|
|
350
421
|
// Receive response — re-resolve in case reconnect happened during send
|
|
351
|
-
const resData = await this.receiveData(path, this.getOpenDevice(path));
|
|
422
|
+
const resData = await this.receiveData(path, this.getOpenDevice(path), options?.timeoutMs);
|
|
352
423
|
if (typeof resData !== 'string') {
|
|
353
424
|
throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
|
|
354
425
|
}
|
|
355
|
-
const jsonData =
|
|
426
|
+
const jsonData = ProtocolV1.decodeMessage(messages, resData);
|
|
356
427
|
return check.call(jsonData);
|
|
357
428
|
}
|
|
358
429
|
|
|
@@ -401,6 +472,21 @@ export default class NodeUsbTransport {
|
|
|
401
472
|
);
|
|
402
473
|
}
|
|
403
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
|
+
|
|
404
490
|
/**
|
|
405
491
|
* Reconnect device before retrying a failed transfer (aligned with WebUsbTransport).
|
|
406
492
|
* Uses per-path lock to prevent concurrent reconnects to the same device.
|
|
@@ -423,9 +509,9 @@ export default class NodeUsbTransport {
|
|
|
423
509
|
);
|
|
424
510
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
425
511
|
|
|
426
|
-
// Close the existing device
|
|
512
|
+
// Close the existing device without clearing the detected protocol cache.
|
|
427
513
|
try {
|
|
428
|
-
await this.
|
|
514
|
+
await this.closeOpenDevice(path);
|
|
429
515
|
} catch (releaseError) {
|
|
430
516
|
this.Log?.debug('[NodeUsbTransport] release before retry error:', releaseError);
|
|
431
517
|
}
|
|
@@ -552,7 +638,7 @@ export default class NodeUsbTransport {
|
|
|
552
638
|
try {
|
|
553
639
|
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
554
640
|
|
|
555
|
-
const iface =
|
|
641
|
+
const iface = this.getDeviceInterface(dev);
|
|
556
642
|
|
|
557
643
|
// On Linux, detach kernel driver if active
|
|
558
644
|
if (process.platform === 'linux') {
|
|
@@ -567,12 +653,14 @@ export default class NodeUsbTransport {
|
|
|
567
653
|
|
|
568
654
|
iface.claim();
|
|
569
655
|
|
|
570
|
-
const epIn =
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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');
|
|
576
664
|
|
|
577
665
|
if (!epIn || !epOut) {
|
|
578
666
|
throw ERRORS.TypedError(
|
|
@@ -595,17 +683,259 @@ export default class NodeUsbTransport {
|
|
|
595
683
|
}
|
|
596
684
|
}
|
|
597
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
|
+
|
|
598
918
|
/**
|
|
599
919
|
* Receive a complete protobuf response from the device.
|
|
600
920
|
* Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
|
|
601
921
|
*/
|
|
602
|
-
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
|
+
|
|
603
931
|
// Read first packet, skip report byte
|
|
604
|
-
const firstPacket =
|
|
932
|
+
const firstPacket = timeoutMs
|
|
933
|
+
? await readPacket()
|
|
934
|
+
: await this.transferInWithRetry(path, dev, PACKET_SIZE);
|
|
605
935
|
const firstData = skipReportByte(firstPacket);
|
|
606
936
|
|
|
607
937
|
// Decode header: ## marker → { typeId, length, restBuffer }
|
|
608
|
-
const { length, typeId, restBuffer } =
|
|
938
|
+
const { length, typeId, restBuffer } = ProtocolV1.decodeFirstChunk(toArrayBuffer(firstData));
|
|
609
939
|
|
|
610
940
|
// Allocate result: typeId(2) + length(4) + payload(length)
|
|
611
941
|
const lengthWithHeader = Number(length) + HEADER_LENGTH;
|
|
@@ -619,7 +949,7 @@ export default class NodeUsbTransport {
|
|
|
619
949
|
// Read subsequent packets until complete
|
|
620
950
|
// Re-resolve device on each iteration so we use a fresh handle after any reconnect
|
|
621
951
|
while (decoded.offset < lengthWithHeader) {
|
|
622
|
-
const packet = await
|
|
952
|
+
const packet = await readPacket();
|
|
623
953
|
const pktData = skipReportByte(packet);
|
|
624
954
|
const buf = toArrayBuffer(pktData);
|
|
625
955
|
if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
|
|
@@ -633,6 +963,8 @@ export default class NodeUsbTransport {
|
|
|
633
963
|
const result = decoded.toBuffer();
|
|
634
964
|
return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
|
|
635
965
|
}
|
|
636
|
-
}
|
|
637
966
|
|
|
638
|
-
|
|
967
|
+
getProtocolType(path: string): ProtocolType {
|
|
968
|
+
return this.deviceProtocol.get(path) ?? 'V1';
|
|
969
|
+
}
|
|
970
|
+
}
|
package/dist/constants.d.ts
DELETED
package/dist/constants.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW,KAAK,CAAC;AAG9B,eAAO,MAAM,SAAS,KAAO,CAAC;AAG9B,eAAO,MAAM,YAAY,QAAkB,CAAC;AAG5C,eAAO,MAAM,aAAa,IAAI,CAAC"}
|
package/src/constants.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** USB packet size in bytes */
|
|
2
|
-
export const PACKET_SIZE = 64;
|
|
3
|
-
|
|
4
|
-
/** Protocol marker byte (0x3F = '?') — first byte of every packet */
|
|
5
|
-
export const REPORT_ID = 0x3f;
|
|
6
|
-
|
|
7
|
-
/** Usable payload per packet after stripping the 0x3F report byte */
|
|
8
|
-
export const PAYLOAD_SIZE = PACKET_SIZE - 1;
|
|
9
|
-
|
|
10
|
-
/** Protocol header length: typeId (2 bytes) + length (4 bytes) */
|
|
11
|
-
export const HEADER_LENGTH = 6;
|