@onekeyfe/hd-transport-usb 1.2.0-alpha.2 → 1.2.0-alpha.21
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 +387 -0
- package/dist/index.d.ts +86 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +190 -178
- package/dist/transportLog.d.ts +7 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +239 -224
- package/src/transportLog.ts +11 -0
package/src/index.ts
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
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
|
-
ProtocolV2Session,
|
|
10
|
+
ProtocolV2UsbTransportBase,
|
|
13
11
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
14
12
|
} from '@onekeyfe/hd-transport';
|
|
15
13
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
16
14
|
|
|
15
|
+
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
16
|
+
|
|
17
17
|
import type EventEmitter from 'events';
|
|
18
18
|
import type {
|
|
19
19
|
AcquireInput,
|
|
20
20
|
OneKeyDeviceInfo,
|
|
21
21
|
ProtocolType,
|
|
22
|
+
ProtocolV2CallContext,
|
|
23
|
+
ProtocolV2Schemas,
|
|
22
24
|
TransportCallOptions,
|
|
23
25
|
} from '@onekeyfe/hd-transport';
|
|
24
26
|
|
|
@@ -44,7 +46,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
|
|
|
44
46
|
/** Packet I/O retry configuration (matches WebUsbTransport) */
|
|
45
47
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
46
48
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
47
|
-
const PROTOCOL_PROBE_TIMEOUT =
|
|
49
|
+
const PROTOCOL_PROBE_TIMEOUT = 5000;
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Opened device state — holds the USB device, claimed interface, and endpoints.
|
|
@@ -129,57 +131,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
133
|
|
|
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
134
|
/**
|
|
184
135
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
185
136
|
*/
|
|
@@ -206,7 +157,7 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
|
206
157
|
*
|
|
207
158
|
* Modeled after WebUsbTransport.
|
|
208
159
|
*/
|
|
209
|
-
export default class NodeUsbTransport {
|
|
160
|
+
export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
210
161
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
211
162
|
|
|
212
163
|
/** Protobuf schema for Protocol V2 transports. */
|
|
@@ -233,21 +184,29 @@ export default class NodeUsbTransport {
|
|
|
233
184
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
234
185
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
235
186
|
|
|
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
187
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
246
188
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
247
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
192
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
193
|
+
*/
|
|
194
|
+
private activeTransfers = new Map<
|
|
195
|
+
usb.Transfer,
|
|
196
|
+
{ path: string; settled: Promise<void>; resolveSettled: () => void }
|
|
197
|
+
>();
|
|
198
|
+
|
|
248
199
|
/** set to true when cancel() is called; checked by retry loops */
|
|
249
200
|
private cancelled = false;
|
|
250
201
|
|
|
202
|
+
constructor() {
|
|
203
|
+
super({
|
|
204
|
+
router: PROTOCOL_V2_CHANNEL_USB,
|
|
205
|
+
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
206
|
+
logPrefix: 'ProtocolV2 NodeUSB',
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
251
210
|
/**
|
|
252
211
|
* Initialize transport.
|
|
253
212
|
* Signature matches the Transport.init interface (logger, emitter).
|
|
@@ -267,17 +226,36 @@ export default class NodeUsbTransport {
|
|
|
267
226
|
|
|
268
227
|
configureProtocolV2(signedData: any) {
|
|
269
228
|
this.messagesV2 = parseConfigure(signedData);
|
|
270
|
-
this.
|
|
271
|
-
|
|
272
|
-
|
|
229
|
+
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
230
|
+
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
231
|
+
);
|
|
273
232
|
}
|
|
274
233
|
|
|
275
234
|
listen() {
|
|
276
235
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
277
236
|
}
|
|
278
237
|
|
|
279
|
-
stop() {
|
|
280
|
-
|
|
238
|
+
async stop(): Promise<void> {
|
|
239
|
+
this.cancelled = true;
|
|
240
|
+
|
|
241
|
+
await this.cancelActiveTransfers();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Reconnect may already be in flight when dispose starts. Wait for it to
|
|
250
|
+
// settle, then close every remaining handle so it cannot reopen USB after
|
|
251
|
+
// the first cleanup pass.
|
|
252
|
+
await Promise.allSettled(this.reconnectLocks.values());
|
|
253
|
+
await this.cancelActiveTransfers();
|
|
254
|
+
await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
255
|
+
|
|
256
|
+
this.reconnectLocks.clear();
|
|
257
|
+
this.deviceProtocol.clear();
|
|
258
|
+
this.serialToBusId.clear();
|
|
281
259
|
}
|
|
282
260
|
|
|
283
261
|
/**
|
|
@@ -352,6 +330,7 @@ export default class NodeUsbTransport {
|
|
|
352
330
|
}
|
|
353
331
|
|
|
354
332
|
try {
|
|
333
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
355
334
|
await this.closeOpenDevice(path);
|
|
356
335
|
await this.openDevice(path);
|
|
357
336
|
await this.detectProtocol(path, input.expectedProtocol);
|
|
@@ -366,9 +345,91 @@ export default class NodeUsbTransport {
|
|
|
366
345
|
* Release device — release interface and close.
|
|
367
346
|
*/
|
|
368
347
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
348
|
+
await this.cancelActiveTransfers(path);
|
|
349
|
+
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
369
350
|
await this.closeOpenDevice(path);
|
|
370
351
|
this.deviceProtocol.delete(path);
|
|
371
|
-
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
355
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
356
|
+
([, active]) => path === undefined || active.path === path
|
|
357
|
+
);
|
|
358
|
+
transfers.forEach(([transfer]) => {
|
|
359
|
+
try {
|
|
360
|
+
transfer.cancel();
|
|
361
|
+
} catch {
|
|
362
|
+
// A transfer may finish between the snapshot and cancel().
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private createTrackedTransfer(
|
|
369
|
+
path: string,
|
|
370
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
371
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
372
|
+
): usb.Transfer {
|
|
373
|
+
let resolveSettled: () => void = () => undefined;
|
|
374
|
+
const settled = new Promise<void>(resolve => {
|
|
375
|
+
resolveSettled = resolve;
|
|
376
|
+
});
|
|
377
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
378
|
+
this.activeTransfers.delete(transfer);
|
|
379
|
+
resolveSettled();
|
|
380
|
+
callback(error, buffer, actualLength);
|
|
381
|
+
});
|
|
382
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
383
|
+
return transfer;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
387
|
+
return new Promise((resolve, reject) => {
|
|
388
|
+
const buffer = Buffer.alloc(length);
|
|
389
|
+
const transfer = this.createTrackedTransfer(
|
|
390
|
+
path,
|
|
391
|
+
ep,
|
|
392
|
+
(error, _submittedBuffer, actualLength) => {
|
|
393
|
+
if (error) {
|
|
394
|
+
reject(error);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (actualLength <= 0) {
|
|
398
|
+
reject(new Error('Empty USB transfer'));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
resolve(buffer.subarray(0, actualLength));
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
try {
|
|
405
|
+
transfer.submit(buffer);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const active = this.activeTransfers.get(transfer);
|
|
408
|
+
this.activeTransfers.delete(transfer);
|
|
409
|
+
active?.resolveSettled();
|
|
410
|
+
reject(error);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
418
|
+
if (error) {
|
|
419
|
+
reject(error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
resolve();
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
transfer.submit(data);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
const active = this.activeTransfers.get(transfer);
|
|
428
|
+
this.activeTransfers.delete(transfer);
|
|
429
|
+
active?.resolveSettled();
|
|
430
|
+
reject(error);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
372
433
|
}
|
|
373
434
|
|
|
374
435
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
@@ -423,18 +484,8 @@ export default class NodeUsbTransport {
|
|
|
423
484
|
`Device protocol has not been detected for ${path}`
|
|
424
485
|
);
|
|
425
486
|
}
|
|
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
|
-
);
|
|
487
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
488
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol));
|
|
438
489
|
}
|
|
439
490
|
|
|
440
491
|
if (protocol === 'V2') {
|
|
@@ -471,7 +522,6 @@ export default class NodeUsbTransport {
|
|
|
471
522
|
}
|
|
472
523
|
|
|
473
524
|
cancel() {
|
|
474
|
-
this.Log?.debug('NodeUsbTransport cancel');
|
|
475
525
|
this.cancelled = true;
|
|
476
526
|
}
|
|
477
527
|
|
|
@@ -501,6 +551,12 @@ export default class NodeUsbTransport {
|
|
|
501
551
|
|
|
502
552
|
private isRetryableError(error: unknown): boolean {
|
|
503
553
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
554
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
555
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
556
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
557
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
504
560
|
return (
|
|
505
561
|
message.includes('libusb') ||
|
|
506
562
|
message.includes('transfer') ||
|
|
@@ -515,6 +571,11 @@ export default class NodeUsbTransport {
|
|
|
515
571
|
);
|
|
516
572
|
}
|
|
517
573
|
|
|
574
|
+
private isUsbTransferTimeout(error: unknown): boolean {
|
|
575
|
+
const message = this.getErrorMessage(error).toLowerCase();
|
|
576
|
+
return message.includes('timeout') || message.includes('timed_out');
|
|
577
|
+
}
|
|
578
|
+
|
|
518
579
|
private getDeviceInterface(dev: usb.Device): usb.Interface {
|
|
519
580
|
const { interfaces } = dev;
|
|
520
581
|
if (!interfaces?.length) {
|
|
@@ -552,6 +613,10 @@ export default class NodeUsbTransport {
|
|
|
552
613
|
);
|
|
553
614
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
554
615
|
|
|
616
|
+
await this.rotateProtocolV2UsbGeneration(
|
|
617
|
+
path,
|
|
618
|
+
`Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
|
|
619
|
+
);
|
|
555
620
|
// Close the existing device without clearing the detected protocol cache.
|
|
556
621
|
try {
|
|
557
622
|
await this.closeOpenDevice(path);
|
|
@@ -596,7 +661,7 @@ export default class NodeUsbTransport {
|
|
|
596
661
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
597
662
|
packet[0] = REPORT_ID;
|
|
598
663
|
packet.set(new Uint8Array(buffer), 1);
|
|
599
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
664
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
600
665
|
}
|
|
601
666
|
return; // all chunks sent successfully
|
|
602
667
|
} catch (error) {
|
|
@@ -629,7 +694,8 @@ export default class NodeUsbTransport {
|
|
|
629
694
|
private async transferInWithRetry(
|
|
630
695
|
path: string,
|
|
631
696
|
openDev: OpenDevice,
|
|
632
|
-
length: number
|
|
697
|
+
length: number,
|
|
698
|
+
options?: { waitIndefinitelyOnTimeout?: boolean }
|
|
633
699
|
): Promise<Buffer> {
|
|
634
700
|
let lastError: unknown;
|
|
635
701
|
let currentDev = openDev;
|
|
@@ -638,23 +704,27 @@ export default class NodeUsbTransport {
|
|
|
638
704
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
639
705
|
}
|
|
640
706
|
try {
|
|
641
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
707
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
642
708
|
} catch (error) {
|
|
643
709
|
lastError = error;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
710
|
+
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
711
|
+
attempt -= 1;
|
|
712
|
+
} else {
|
|
713
|
+
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
714
|
+
if (!shouldRetry) {
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
|
|
719
|
+
} catch (reconnectError) {
|
|
720
|
+
lastError = reconnectError;
|
|
721
|
+
this.Log?.debug(
|
|
722
|
+
`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
723
|
+
reconnectError
|
|
724
|
+
)}`
|
|
725
|
+
);
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
658
728
|
}
|
|
659
729
|
}
|
|
660
730
|
}
|
|
@@ -680,15 +750,6 @@ export default class NodeUsbTransport {
|
|
|
680
750
|
dev.open();
|
|
681
751
|
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
682
752
|
|
|
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
753
|
const iface = this.getDeviceInterface(dev);
|
|
693
754
|
|
|
694
755
|
// On Linux, detach kernel driver if active
|
|
@@ -723,9 +784,7 @@ export default class NodeUsbTransport {
|
|
|
723
784
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
724
785
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
725
786
|
|
|
726
|
-
await
|
|
727
|
-
await clearEndpointHalt(epOut);
|
|
728
|
-
await this.drainStaleInput(epIn);
|
|
787
|
+
await this.drainStaleInput(path, epIn);
|
|
729
788
|
|
|
730
789
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
731
790
|
} catch (err) {
|
|
@@ -738,14 +797,14 @@ export default class NodeUsbTransport {
|
|
|
738
797
|
}
|
|
739
798
|
}
|
|
740
799
|
|
|
741
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
800
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
742
801
|
const originalTimeout = epIn.timeout;
|
|
743
802
|
epIn.timeout = 50;
|
|
744
803
|
try {
|
|
745
804
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
746
805
|
for (let index = 0; index < 16; index += 1) {
|
|
747
806
|
try {
|
|
748
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
807
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
749
808
|
} catch {
|
|
750
809
|
break;
|
|
751
810
|
}
|
|
@@ -773,39 +832,35 @@ export default class NodeUsbTransport {
|
|
|
773
832
|
path: string,
|
|
774
833
|
expectedProtocol?: ProtocolType
|
|
775
834
|
): Promise<ProtocolType> {
|
|
835
|
+
if (expectedProtocol === 'V2') {
|
|
836
|
+
// Bootloader may not answer Ping after an update reboot. An explicit V2 hint means
|
|
837
|
+
// the protocol was confirmed earlier; establish the link and let the first command
|
|
838
|
+
// verify that USB initialization completed.
|
|
839
|
+
this.deviceProtocol.set(path, 'V2');
|
|
840
|
+
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
841
|
+
return 'V2';
|
|
842
|
+
}
|
|
843
|
+
|
|
776
844
|
if (expectedProtocol === 'V1') {
|
|
777
845
|
if (await this.probeProtocolV1(path)) {
|
|
778
846
|
this.deviceProtocol.set(path, 'V1');
|
|
779
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
780
847
|
return 'V1';
|
|
781
848
|
}
|
|
782
849
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
783
850
|
}
|
|
784
851
|
|
|
785
|
-
if (expectedProtocol === 'V2') {
|
|
786
|
-
if (await this.probeProtocolV2(path)) {
|
|
787
|
-
this.deviceProtocol.set(path, 'V2');
|
|
788
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
789
|
-
return 'V2';
|
|
790
|
-
}
|
|
791
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
852
|
if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) {
|
|
795
853
|
this.deviceProtocol.set(path, 'V2');
|
|
796
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (cached)`);
|
|
797
854
|
return 'V2';
|
|
798
855
|
}
|
|
799
856
|
|
|
800
857
|
if (await this.probeProtocolV1(path)) {
|
|
801
858
|
this.deviceProtocol.set(path, 'V1');
|
|
802
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
|
|
803
859
|
return 'V1';
|
|
804
860
|
}
|
|
805
861
|
|
|
806
862
|
if (await this.probeProtocolV2(path)) {
|
|
807
863
|
this.deviceProtocol.set(path, 'V2');
|
|
808
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2`);
|
|
809
864
|
return 'V2';
|
|
810
865
|
}
|
|
811
866
|
|
|
@@ -814,11 +869,13 @@ export default class NodeUsbTransport {
|
|
|
814
869
|
}
|
|
815
870
|
|
|
816
871
|
private async resetConnectionAfterProbe(path: string) {
|
|
817
|
-
this.
|
|
818
|
-
this.protocolV2Sessions.delete(path);
|
|
819
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
872
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
820
873
|
|
|
821
874
|
try {
|
|
875
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
876
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
877
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
878
|
+
await this.cancelActiveTransfers(path);
|
|
822
879
|
await this.closeOpenDevice(path);
|
|
823
880
|
} catch (error) {
|
|
824
881
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -878,8 +935,7 @@ export default class NodeUsbTransport {
|
|
|
878
935
|
try {
|
|
879
936
|
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
880
937
|
return true;
|
|
881
|
-
} catch (
|
|
882
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
938
|
+
} catch (_error) {
|
|
883
939
|
return false;
|
|
884
940
|
}
|
|
885
941
|
}
|
|
@@ -898,74 +954,68 @@ export default class NodeUsbTransport {
|
|
|
898
954
|
});
|
|
899
955
|
}
|
|
900
956
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
957
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
958
|
+
if (!this.messages || !this.messagesV2) {
|
|
959
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
960
|
+
}
|
|
961
|
+
return {
|
|
962
|
+
protocolV1: this.messages,
|
|
963
|
+
protocolV2: this.messagesV2,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
protected getProtocolV2UsbLogger() {
|
|
968
|
+
return this.Log;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
protected async writeProtocolV2UsbPacket(
|
|
972
|
+
path: string,
|
|
973
|
+
frame: Uint8Array,
|
|
974
|
+
_context: ProtocolV2CallContext
|
|
975
|
+
): Promise<void> {
|
|
976
|
+
if (this.cancelled) {
|
|
977
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
978
|
+
}
|
|
979
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
protected async readProtocolV2UsbPacket(
|
|
983
|
+
path: string,
|
|
984
|
+
_context: ProtocolV2CallContext
|
|
985
|
+
): Promise<Uint8Array> {
|
|
986
|
+
for (;;) {
|
|
904
987
|
if (this.cancelled) {
|
|
905
988
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
906
989
|
}
|
|
907
990
|
try {
|
|
908
|
-
await
|
|
909
|
-
|
|
991
|
+
const packet = await this.transferInOnce(
|
|
992
|
+
path,
|
|
993
|
+
this.getOpenDevice(path).epIn,
|
|
994
|
+
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
995
|
+
);
|
|
996
|
+
return new Uint8Array(
|
|
997
|
+
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
998
|
+
);
|
|
910
999
|
} catch (error) {
|
|
911
|
-
|
|
912
|
-
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
913
|
-
if (!shouldRetry) {
|
|
1000
|
+
if (!this.isUsbTransferTimeout(error)) {
|
|
914
1001
|
throw error;
|
|
915
1002
|
}
|
|
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
1003
|
}
|
|
928
1004
|
}
|
|
929
|
-
throw lastError;
|
|
930
1005
|
}
|
|
931
1006
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
937
|
-
}
|
|
1007
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1008
|
+
await this.cancelActiveTransfers(path);
|
|
1009
|
+
await this.closeOpenDevice(path);
|
|
1010
|
+
}
|
|
938
1011
|
|
|
939
|
-
|
|
940
|
-
|
|
1012
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
1013
|
+
this.deviceProtocol.delete(path);
|
|
1014
|
+
this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
1015
|
+
}
|
|
941
1016
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
path,
|
|
945
|
-
this.getOpenDevice(path),
|
|
946
|
-
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
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;
|
|
1017
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
1018
|
+
return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
969
1019
|
}
|
|
970
1020
|
|
|
971
1021
|
private async callProtocolV2(
|
|
@@ -974,42 +1024,7 @@ export default class NodeUsbTransport {
|
|
|
974
1024
|
data: Record<string, unknown>,
|
|
975
1025
|
options?: TransportCallOptions
|
|
976
1026
|
) {
|
|
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
|
-
}
|
|
1027
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
1013
1028
|
}
|
|
1014
1029
|
|
|
1015
1030
|
/**
|