@onekeyfe/hd-transport-usb 1.2.0-alpha.3 → 1.2.0-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/__tests__/protocol-v2-link.test.ts +398 -0
- package/dist/index.d.ts +87 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +202 -177
- package/dist/transportLog.d.ts +7 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/jest.config.js +5 -0
- package/package.json +5 -4
- package/src/index.ts +254 -223
- package/src/transportLog.ts +11 -0
package/src/index.ts
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import ByteBuffer from 'bytebuffer';
|
|
2
2
|
import * as usb from 'usb';
|
|
3
3
|
import transport, {
|
|
4
|
-
LogBlockCommand,
|
|
5
4
|
PROTOCOL_V1_CHUNK_PAYLOAD_SIZE,
|
|
6
5
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
7
6
|
PROTOCOL_V1_REPORT_ID,
|
|
8
7
|
PROTOCOL_V1_USB_PACKET_SIZE,
|
|
9
8
|
PROTOCOL_V2_CHANNEL_USB,
|
|
10
9
|
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
ProtocolV2LinkError,
|
|
11
|
+
ProtocolV2UsbTransportBase,
|
|
13
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
14
13
|
} from '@onekeyfe/hd-transport';
|
|
15
14
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
16
15
|
|
|
16
|
+
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
17
|
+
|
|
17
18
|
import type EventEmitter from 'events';
|
|
18
19
|
import type {
|
|
19
20
|
AcquireInput,
|
|
20
21
|
OneKeyDeviceInfo,
|
|
21
22
|
ProtocolType,
|
|
23
|
+
ProtocolV2CallContext,
|
|
24
|
+
ProtocolV2Schemas,
|
|
22
25
|
TransportCallOptions,
|
|
23
26
|
} from '@onekeyfe/hd-transport';
|
|
24
27
|
|
|
@@ -44,7 +47,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
|
|
|
44
47
|
/** Packet I/O retry configuration (matches WebUsbTransport) */
|
|
45
48
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
46
49
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
47
|
-
const PROTOCOL_PROBE_TIMEOUT =
|
|
50
|
+
const PROTOCOL_PROBE_TIMEOUT = 5000;
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
53
|
* Opened device state — holds the USB device, claimed interface, and endpoints.
|
|
@@ -129,57 +132,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
/**
|
|
133
|
-
* Promisified USB IN transfer (single attempt).
|
|
134
|
-
*/
|
|
135
|
-
function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
|
|
138
|
-
if (err) return reject(err);
|
|
139
|
-
if (!data || data.length === 0) return reject(new Error('Empty USB transfer'));
|
|
140
|
-
resolve(data);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Promisified USB OUT transfer (single attempt).
|
|
147
|
-
*/
|
|
148
|
-
function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
149
|
-
return new Promise((resolve, reject) => {
|
|
150
|
-
ep.transfer(data, (err: Error | undefined) => {
|
|
151
|
-
if (err) return reject(err);
|
|
152
|
-
resolve();
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function resetUsbDevice(dev: usb.Device): Promise<void> {
|
|
158
|
-
return new Promise(resolve => {
|
|
159
|
-
const reset = (dev as usb.Device & { reset?: (callback: (err?: Error) => void) => void }).reset;
|
|
160
|
-
if (typeof reset !== 'function') {
|
|
161
|
-
resolve();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
reset.call(dev, () => resolve());
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function clearEndpointHalt(ep: usb.InEndpoint | usb.OutEndpoint): Promise<void> {
|
|
169
|
-
return new Promise(resolve => {
|
|
170
|
-
const clearHalt = (
|
|
171
|
-
ep as (usb.InEndpoint | usb.OutEndpoint) & {
|
|
172
|
-
clearHalt?: (callback: (err?: Error) => void) => void;
|
|
173
|
-
}
|
|
174
|
-
).clearHalt;
|
|
175
|
-
if (typeof clearHalt !== 'function') {
|
|
176
|
-
resolve();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
clearHalt.call(ep, () => resolve());
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
135
|
/**
|
|
184
136
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
185
137
|
*/
|
|
@@ -206,12 +158,14 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
|
206
158
|
*
|
|
207
159
|
* Modeled after WebUsbTransport.
|
|
208
160
|
*/
|
|
209
|
-
export default class NodeUsbTransport {
|
|
161
|
+
export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
210
162
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
211
163
|
|
|
212
164
|
/** Protobuf schema for Protocol V2 transports. */
|
|
213
165
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
214
166
|
|
|
167
|
+
private protocolV2SchemaSource: string | undefined;
|
|
168
|
+
|
|
215
169
|
name = 'NodeUsbTransport';
|
|
216
170
|
|
|
217
171
|
version = '';
|
|
@@ -233,21 +187,29 @@ export default class NodeUsbTransport {
|
|
|
233
187
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
234
188
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
235
189
|
|
|
236
|
-
/** Per-path Protocol V2 frame assembler, preserving buffered frames during reads. */
|
|
237
|
-
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
238
|
-
|
|
239
|
-
/** Per-path Protocol V2 session, preserving seq across API calls on the same device path. */
|
|
240
|
-
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
241
|
-
|
|
242
|
-
/** Current Protocol V2 read timeout, consumed by cached session readFrame closures. */
|
|
243
|
-
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
244
|
-
|
|
245
190
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
246
191
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
247
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
195
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
196
|
+
*/
|
|
197
|
+
private activeTransfers = new Map<
|
|
198
|
+
usb.Transfer,
|
|
199
|
+
{ path: string; settled: Promise<void>; resolveSettled: () => void }
|
|
200
|
+
>();
|
|
201
|
+
|
|
248
202
|
/** set to true when cancel() is called; checked by retry loops */
|
|
249
203
|
private cancelled = false;
|
|
250
204
|
|
|
205
|
+
constructor() {
|
|
206
|
+
super({
|
|
207
|
+
router: PROTOCOL_V2_CHANNEL_USB,
|
|
208
|
+
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
209
|
+
logPrefix: 'ProtocolV2 NodeUSB',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
251
213
|
/**
|
|
252
214
|
* Initialize transport.
|
|
253
215
|
* Signature matches the Transport.init interface (logger, emitter).
|
|
@@ -266,18 +228,47 @@ export default class NodeUsbTransport {
|
|
|
266
228
|
}
|
|
267
229
|
|
|
268
230
|
configureProtocolV2(signedData: any) {
|
|
231
|
+
const schemaSource =
|
|
232
|
+
typeof signedData === 'string'
|
|
233
|
+
? signedData
|
|
234
|
+
: JSON.stringify(signedData) ?? String(signedData);
|
|
235
|
+
if (schemaSource === this.protocolV2SchemaSource) return;
|
|
236
|
+
|
|
237
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
269
238
|
this.messagesV2 = parseConfigure(signedData);
|
|
270
|
-
this.
|
|
271
|
-
|
|
272
|
-
|
|
239
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
240
|
+
if (!hadProtocolV2Schema) return;
|
|
241
|
+
|
|
242
|
+
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
243
|
+
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
244
|
+
);
|
|
273
245
|
}
|
|
274
246
|
|
|
275
247
|
listen() {
|
|
276
248
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
277
249
|
}
|
|
278
250
|
|
|
279
|
-
stop() {
|
|
280
|
-
|
|
251
|
+
async stop(): Promise<void> {
|
|
252
|
+
this.cancelled = true;
|
|
253
|
+
|
|
254
|
+
await this.cancelActiveTransfers();
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Reconnect may already be in flight when dispose starts. Wait for it to
|
|
263
|
+
// settle, then close every remaining handle so it cannot reopen USB after
|
|
264
|
+
// the first cleanup pass.
|
|
265
|
+
await Promise.allSettled(this.reconnectLocks.values());
|
|
266
|
+
await this.cancelActiveTransfers();
|
|
267
|
+
await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
268
|
+
|
|
269
|
+
this.reconnectLocks.clear();
|
|
270
|
+
this.deviceProtocol.clear();
|
|
271
|
+
this.serialToBusId.clear();
|
|
281
272
|
}
|
|
282
273
|
|
|
283
274
|
/**
|
|
@@ -352,6 +343,7 @@ export default class NodeUsbTransport {
|
|
|
352
343
|
}
|
|
353
344
|
|
|
354
345
|
try {
|
|
346
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
355
347
|
await this.closeOpenDevice(path);
|
|
356
348
|
await this.openDevice(path);
|
|
357
349
|
await this.detectProtocol(path, input.expectedProtocol);
|
|
@@ -366,9 +358,91 @@ export default class NodeUsbTransport {
|
|
|
366
358
|
* Release device — release interface and close.
|
|
367
359
|
*/
|
|
368
360
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
361
|
+
await this.cancelActiveTransfers(path);
|
|
362
|
+
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
369
363
|
await this.closeOpenDevice(path);
|
|
370
364
|
this.deviceProtocol.delete(path);
|
|
371
|
-
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
368
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
369
|
+
([, active]) => path === undefined || active.path === path
|
|
370
|
+
);
|
|
371
|
+
transfers.forEach(([transfer]) => {
|
|
372
|
+
try {
|
|
373
|
+
transfer.cancel();
|
|
374
|
+
} catch {
|
|
375
|
+
// A transfer may finish between the snapshot and cancel().
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private createTrackedTransfer(
|
|
382
|
+
path: string,
|
|
383
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
384
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
385
|
+
): usb.Transfer {
|
|
386
|
+
let resolveSettled: () => void = () => undefined;
|
|
387
|
+
const settled = new Promise<void>(resolve => {
|
|
388
|
+
resolveSettled = resolve;
|
|
389
|
+
});
|
|
390
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
391
|
+
this.activeTransfers.delete(transfer);
|
|
392
|
+
resolveSettled();
|
|
393
|
+
callback(error, buffer, actualLength);
|
|
394
|
+
});
|
|
395
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
396
|
+
return transfer;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
400
|
+
return new Promise((resolve, reject) => {
|
|
401
|
+
const buffer = Buffer.alloc(length);
|
|
402
|
+
const transfer = this.createTrackedTransfer(
|
|
403
|
+
path,
|
|
404
|
+
ep,
|
|
405
|
+
(error, _submittedBuffer, actualLength) => {
|
|
406
|
+
if (error) {
|
|
407
|
+
reject(error);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (actualLength <= 0) {
|
|
411
|
+
reject(new Error('Empty USB transfer'));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
resolve(buffer.subarray(0, actualLength));
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
try {
|
|
418
|
+
transfer.submit(buffer);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const active = this.activeTransfers.get(transfer);
|
|
421
|
+
this.activeTransfers.delete(transfer);
|
|
422
|
+
active?.resolveSettled();
|
|
423
|
+
reject(error);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
431
|
+
if (error) {
|
|
432
|
+
reject(error);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
resolve();
|
|
436
|
+
});
|
|
437
|
+
try {
|
|
438
|
+
transfer.submit(data);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
const active = this.activeTransfers.get(transfer);
|
|
441
|
+
this.activeTransfers.delete(transfer);
|
|
442
|
+
active?.resolveSettled();
|
|
443
|
+
reject(error);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
372
446
|
}
|
|
373
447
|
|
|
374
448
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
@@ -423,18 +497,8 @@ export default class NodeUsbTransport {
|
|
|
423
497
|
`Device protocol has not been detected for ${path}`
|
|
424
498
|
);
|
|
425
499
|
}
|
|
426
|
-
if (
|
|
427
|
-
this.Log?.debug('
|
|
428
|
-
} else {
|
|
429
|
-
this.Log?.debug(
|
|
430
|
-
'NodeUsbTransport call-',
|
|
431
|
-
' name: ',
|
|
432
|
-
name,
|
|
433
|
-
' data: ',
|
|
434
|
-
data,
|
|
435
|
-
' protocol: ',
|
|
436
|
-
protocol
|
|
437
|
-
);
|
|
500
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
501
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol));
|
|
438
502
|
}
|
|
439
503
|
|
|
440
504
|
if (protocol === 'V2') {
|
|
@@ -471,7 +535,6 @@ export default class NodeUsbTransport {
|
|
|
471
535
|
}
|
|
472
536
|
|
|
473
537
|
cancel() {
|
|
474
|
-
this.Log?.debug('NodeUsbTransport cancel');
|
|
475
538
|
this.cancelled = true;
|
|
476
539
|
}
|
|
477
540
|
|
|
@@ -501,6 +564,12 @@ export default class NodeUsbTransport {
|
|
|
501
564
|
|
|
502
565
|
private isRetryableError(error: unknown): boolean {
|
|
503
566
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
567
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
568
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
569
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
570
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
504
573
|
return (
|
|
505
574
|
message.includes('libusb') ||
|
|
506
575
|
message.includes('transfer') ||
|
|
@@ -515,6 +584,11 @@ export default class NodeUsbTransport {
|
|
|
515
584
|
);
|
|
516
585
|
}
|
|
517
586
|
|
|
587
|
+
private isUsbTransferTimeout(error: unknown): boolean {
|
|
588
|
+
const message = this.getErrorMessage(error).toLowerCase();
|
|
589
|
+
return message.includes('timeout') || message.includes('timed_out');
|
|
590
|
+
}
|
|
591
|
+
|
|
518
592
|
private getDeviceInterface(dev: usb.Device): usb.Interface {
|
|
519
593
|
const { interfaces } = dev;
|
|
520
594
|
if (!interfaces?.length) {
|
|
@@ -552,6 +626,10 @@ export default class NodeUsbTransport {
|
|
|
552
626
|
);
|
|
553
627
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
554
628
|
|
|
629
|
+
await this.rotateProtocolV2UsbGeneration(
|
|
630
|
+
path,
|
|
631
|
+
`Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
|
|
632
|
+
);
|
|
555
633
|
// Close the existing device without clearing the detected protocol cache.
|
|
556
634
|
try {
|
|
557
635
|
await this.closeOpenDevice(path);
|
|
@@ -596,7 +674,7 @@ export default class NodeUsbTransport {
|
|
|
596
674
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
597
675
|
packet[0] = REPORT_ID;
|
|
598
676
|
packet.set(new Uint8Array(buffer), 1);
|
|
599
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
677
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
600
678
|
}
|
|
601
679
|
return; // all chunks sent successfully
|
|
602
680
|
} catch (error) {
|
|
@@ -629,7 +707,8 @@ export default class NodeUsbTransport {
|
|
|
629
707
|
private async transferInWithRetry(
|
|
630
708
|
path: string,
|
|
631
709
|
openDev: OpenDevice,
|
|
632
|
-
length: number
|
|
710
|
+
length: number,
|
|
711
|
+
options?: { waitIndefinitelyOnTimeout?: boolean }
|
|
633
712
|
): Promise<Buffer> {
|
|
634
713
|
let lastError: unknown;
|
|
635
714
|
let currentDev = openDev;
|
|
@@ -638,23 +717,27 @@ export default class NodeUsbTransport {
|
|
|
638
717
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
639
718
|
}
|
|
640
719
|
try {
|
|
641
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
720
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
642
721
|
} catch (error) {
|
|
643
722
|
lastError = error;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
723
|
+
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
724
|
+
attempt -= 1;
|
|
725
|
+
} else {
|
|
726
|
+
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
727
|
+
if (!shouldRetry) {
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
|
|
732
|
+
} catch (reconnectError) {
|
|
733
|
+
lastError = reconnectError;
|
|
734
|
+
this.Log?.debug(
|
|
735
|
+
`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
736
|
+
reconnectError
|
|
737
|
+
)}`
|
|
738
|
+
);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
658
741
|
}
|
|
659
742
|
}
|
|
660
743
|
}
|
|
@@ -680,15 +763,6 @@ export default class NodeUsbTransport {
|
|
|
680
763
|
dev.open();
|
|
681
764
|
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
682
765
|
|
|
683
|
-
await resetUsbDevice(dev);
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
dev.open();
|
|
687
|
-
} catch {
|
|
688
|
-
// libusb keeps some devices open across reset; continue with the current handle.
|
|
689
|
-
}
|
|
690
|
-
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
691
|
-
|
|
692
766
|
const iface = this.getDeviceInterface(dev);
|
|
693
767
|
|
|
694
768
|
// On Linux, detach kernel driver if active
|
|
@@ -723,9 +797,7 @@ export default class NodeUsbTransport {
|
|
|
723
797
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
724
798
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
725
799
|
|
|
726
|
-
await
|
|
727
|
-
await clearEndpointHalt(epOut);
|
|
728
|
-
await this.drainStaleInput(epIn);
|
|
800
|
+
await this.drainStaleInput(path, epIn);
|
|
729
801
|
|
|
730
802
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
731
803
|
} catch (err) {
|
|
@@ -738,14 +810,14 @@ export default class NodeUsbTransport {
|
|
|
738
810
|
}
|
|
739
811
|
}
|
|
740
812
|
|
|
741
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
813
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
742
814
|
const originalTimeout = epIn.timeout;
|
|
743
815
|
epIn.timeout = 50;
|
|
744
816
|
try {
|
|
745
817
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
746
818
|
for (let index = 0; index < 16; index += 1) {
|
|
747
819
|
try {
|
|
748
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
820
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
749
821
|
} catch {
|
|
750
822
|
break;
|
|
751
823
|
}
|
|
@@ -773,15 +845,6 @@ export default class NodeUsbTransport {
|
|
|
773
845
|
path: string,
|
|
774
846
|
expectedProtocol?: ProtocolType
|
|
775
847
|
): Promise<ProtocolType> {
|
|
776
|
-
if (expectedProtocol === 'V1') {
|
|
777
|
-
if (await this.probeProtocolV1(path)) {
|
|
778
|
-
this.deviceProtocol.set(path, 'V1');
|
|
779
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
780
|
-
return 'V1';
|
|
781
|
-
}
|
|
782
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
848
|
if (expectedProtocol === 'V2') {
|
|
786
849
|
if (await this.probeProtocolV2(path)) {
|
|
787
850
|
this.deviceProtocol.set(path, 'V2');
|
|
@@ -791,21 +854,26 @@ export default class NodeUsbTransport {
|
|
|
791
854
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
792
855
|
}
|
|
793
856
|
|
|
857
|
+
if (expectedProtocol === 'V1') {
|
|
858
|
+
if (await this.probeProtocolV1(path)) {
|
|
859
|
+
this.deviceProtocol.set(path, 'V1');
|
|
860
|
+
return 'V1';
|
|
861
|
+
}
|
|
862
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
863
|
+
}
|
|
864
|
+
|
|
794
865
|
if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) {
|
|
795
866
|
this.deviceProtocol.set(path, 'V2');
|
|
796
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
|
|
797
867
|
return 'V2';
|
|
798
868
|
}
|
|
799
869
|
|
|
800
870
|
if (await this.probeProtocolV1(path)) {
|
|
801
871
|
this.deviceProtocol.set(path, 'V1');
|
|
802
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
|
|
803
872
|
return 'V1';
|
|
804
873
|
}
|
|
805
874
|
|
|
806
875
|
if (await this.probeProtocolV2(path)) {
|
|
807
876
|
this.deviceProtocol.set(path, 'V2');
|
|
808
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2`);
|
|
809
877
|
return 'V2';
|
|
810
878
|
}
|
|
811
879
|
|
|
@@ -814,11 +882,13 @@ export default class NodeUsbTransport {
|
|
|
814
882
|
}
|
|
815
883
|
|
|
816
884
|
private async resetConnectionAfterProbe(path: string) {
|
|
817
|
-
this.
|
|
818
|
-
this.protocolV2Sessions.delete(path);
|
|
819
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
885
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
820
886
|
|
|
821
887
|
try {
|
|
888
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
889
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
890
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
891
|
+
await this.cancelActiveTransfers(path);
|
|
822
892
|
await this.closeOpenDevice(path);
|
|
823
893
|
} catch (error) {
|
|
824
894
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -878,8 +948,7 @@ export default class NodeUsbTransport {
|
|
|
878
948
|
try {
|
|
879
949
|
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
880
950
|
return true;
|
|
881
|
-
} catch (
|
|
882
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
951
|
+
} catch (_error) {
|
|
883
952
|
return false;
|
|
884
953
|
}
|
|
885
954
|
}
|
|
@@ -898,74 +967,71 @@ export default class NodeUsbTransport {
|
|
|
898
967
|
});
|
|
899
968
|
}
|
|
900
969
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
970
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
971
|
+
if (!this.messages || !this.messagesV2) {
|
|
972
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
protocolV1: this.messages,
|
|
976
|
+
protocolV2: this.messagesV2,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
protected getProtocolV2UsbLogger() {
|
|
981
|
+
return this.Log;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
protected async writeProtocolV2UsbPacket(
|
|
985
|
+
path: string,
|
|
986
|
+
frame: Uint8Array,
|
|
987
|
+
_context: ProtocolV2CallContext
|
|
988
|
+
): Promise<void> {
|
|
989
|
+
if (this.cancelled) {
|
|
990
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
991
|
+
}
|
|
992
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
protected async readProtocolV2UsbPacket(
|
|
996
|
+
path: string,
|
|
997
|
+
_context: ProtocolV2CallContext
|
|
998
|
+
): Promise<Uint8Array> {
|
|
999
|
+
for (;;) {
|
|
904
1000
|
if (this.cancelled) {
|
|
905
1001
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
906
1002
|
}
|
|
907
1003
|
try {
|
|
908
|
-
await
|
|
909
|
-
|
|
1004
|
+
const packet = await this.transferInOnce(
|
|
1005
|
+
path,
|
|
1006
|
+
this.getOpenDevice(path).epIn,
|
|
1007
|
+
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
1008
|
+
);
|
|
1009
|
+
return new Uint8Array(
|
|
1010
|
+
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
1011
|
+
);
|
|
910
1012
|
} catch (error) {
|
|
911
|
-
|
|
912
|
-
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
913
|
-
if (!shouldRetry) {
|
|
1013
|
+
if (!this.isUsbTransferTimeout(error)) {
|
|
914
1014
|
throw error;
|
|
915
1015
|
}
|
|
916
|
-
try {
|
|
917
|
-
await this.reconnectForRetry(path, 'out', attempt, error);
|
|
918
|
-
} catch (reconnectError) {
|
|
919
|
-
lastError = reconnectError;
|
|
920
|
-
this.Log?.debug(
|
|
921
|
-
`[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
922
|
-
reconnectError
|
|
923
|
-
)}`
|
|
924
|
-
);
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
1016
|
}
|
|
928
1017
|
}
|
|
929
|
-
throw lastError;
|
|
930
1018
|
}
|
|
931
1019
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
937
|
-
}
|
|
1020
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1021
|
+
await this.cancelActiveTransfers(path);
|
|
1022
|
+
await this.closeOpenDevice(path);
|
|
1023
|
+
}
|
|
938
1024
|
|
|
939
|
-
|
|
940
|
-
|
|
1025
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
1026
|
+
this.deviceProtocol.delete(path);
|
|
1027
|
+
this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
1028
|
+
}
|
|
941
1029
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
);
|
|
948
|
-
const packet = deadline
|
|
949
|
-
? await this.withProtocolReadTimeout(
|
|
950
|
-
path,
|
|
951
|
-
transferIn,
|
|
952
|
-
Math.max(deadline - Date.now(), 1),
|
|
953
|
-
'V2'
|
|
954
|
-
)
|
|
955
|
-
: await transferIn;
|
|
956
|
-
const bytes = new Uint8Array(
|
|
957
|
-
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
958
|
-
);
|
|
959
|
-
try {
|
|
960
|
-
frame = assembler.push(bytes);
|
|
961
|
-
} catch (error) {
|
|
962
|
-
throw ERRORS.TypedError(
|
|
963
|
-
HardwareErrorCode.NetworkError,
|
|
964
|
-
error instanceof Error ? error.message : String(error)
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return frame;
|
|
1030
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
1031
|
+
return new ProtocolV2LinkError(
|
|
1032
|
+
'response-timeout',
|
|
1033
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1034
|
+
);
|
|
969
1035
|
}
|
|
970
1036
|
|
|
971
1037
|
private async callProtocolV2(
|
|
@@ -974,42 +1040,7 @@ export default class NodeUsbTransport {
|
|
|
974
1040
|
data: Record<string, unknown>,
|
|
975
1041
|
options?: TransportCallOptions
|
|
976
1042
|
) {
|
|
977
|
-
|
|
978
|
-
if (!this.messagesV2) {
|
|
979
|
-
throw ERRORS.TypedError(
|
|
980
|
-
HardwareErrorCode.TransportNotConfigured,
|
|
981
|
-
'Protocol V2 schema not configured'
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
if (!protocolV1Messages) {
|
|
985
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
let session = this.protocolV2Sessions.get(path);
|
|
989
|
-
if (!session) {
|
|
990
|
-
session = new ProtocolV2Session({
|
|
991
|
-
schemas: {
|
|
992
|
-
protocolV1: protocolV1Messages,
|
|
993
|
-
protocolV2: this.messagesV2,
|
|
994
|
-
},
|
|
995
|
-
router: PROTOCOL_V2_CHANNEL_USB,
|
|
996
|
-
writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(path, frame),
|
|
997
|
-
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
998
|
-
logger: this.Log,
|
|
999
|
-
logPrefix: 'ProtocolV2 NodeUSB',
|
|
1000
|
-
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
1001
|
-
new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
1002
|
-
});
|
|
1003
|
-
this.protocolV2Sessions.set(path, session);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
|
|
1007
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
1008
|
-
try {
|
|
1009
|
-
return await session.call(name, data, options);
|
|
1010
|
-
} finally {
|
|
1011
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
1012
|
-
}
|
|
1043
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
1013
1044
|
}
|
|
1014
1045
|
|
|
1015
1046
|
/**
|