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