@onekeyfe/hd-transport-usb 1.2.0-alpha.7 → 1.2.0-alpha.71
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 +439 -0
- package/dist/index.d.ts +86 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +203 -160
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/jest.config.js +5 -0
- package/package.json +5 -4
- package/src/index.ts +262 -199
- package/src/transportLog.ts +1 -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
|
|
|
@@ -129,31 +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
135
|
/**
|
|
158
136
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
159
137
|
*/
|
|
@@ -180,12 +158,14 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
|
180
158
|
*
|
|
181
159
|
* Modeled after WebUsbTransport.
|
|
182
160
|
*/
|
|
183
|
-
export default class NodeUsbTransport {
|
|
161
|
+
export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
184
162
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
185
163
|
|
|
186
164
|
/** Protobuf schema for Protocol V2 transports. */
|
|
187
165
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
188
166
|
|
|
167
|
+
private protocolV2SchemaSource: string | undefined;
|
|
168
|
+
|
|
189
169
|
name = 'NodeUsbTransport';
|
|
190
170
|
|
|
191
171
|
version = '';
|
|
@@ -207,21 +187,29 @@ export default class NodeUsbTransport {
|
|
|
207
187
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
208
188
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
209
189
|
|
|
210
|
-
/** Per-path Protocol V2 frame assembler, preserving buffered frames during reads. */
|
|
211
|
-
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
212
|
-
|
|
213
|
-
/** Per-path Protocol V2 session, preserving seq across API calls on the same device path. */
|
|
214
|
-
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
215
|
-
|
|
216
|
-
/** Current Protocol V2 read timeout, consumed by cached session readFrame closures. */
|
|
217
|
-
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
218
|
-
|
|
219
190
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
220
191
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
221
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
|
+
|
|
222
202
|
/** set to true when cancel() is called; checked by retry loops */
|
|
223
203
|
private cancelled = false;
|
|
224
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
|
+
|
|
225
213
|
/**
|
|
226
214
|
* Initialize transport.
|
|
227
215
|
* Signature matches the Transport.init interface (logger, emitter).
|
|
@@ -240,18 +228,47 @@ export default class NodeUsbTransport {
|
|
|
240
228
|
}
|
|
241
229
|
|
|
242
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;
|
|
243
238
|
this.messagesV2 = parseConfigure(signedData);
|
|
244
|
-
this.
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
);
|
|
247
245
|
}
|
|
248
246
|
|
|
249
247
|
listen() {
|
|
250
248
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
251
249
|
}
|
|
252
250
|
|
|
253
|
-
stop() {
|
|
254
|
-
|
|
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();
|
|
255
272
|
}
|
|
256
273
|
|
|
257
274
|
/**
|
|
@@ -326,12 +343,18 @@ export default class NodeUsbTransport {
|
|
|
326
343
|
}
|
|
327
344
|
|
|
328
345
|
try {
|
|
346
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
329
347
|
await this.closeOpenDevice(path);
|
|
330
348
|
await this.openDevice(path);
|
|
331
|
-
await this.detectProtocol(path, input.expectedProtocol);
|
|
349
|
+
await this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
|
|
332
350
|
return path;
|
|
333
351
|
} catch (error: any) {
|
|
334
352
|
this.Log?.debug('NodeUsbTransport acquire error: ', error);
|
|
353
|
+
try {
|
|
354
|
+
await this.release(path);
|
|
355
|
+
} catch (cleanupError) {
|
|
356
|
+
this.Log?.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
357
|
+
}
|
|
335
358
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
336
359
|
}
|
|
337
360
|
}
|
|
@@ -340,9 +363,91 @@ export default class NodeUsbTransport {
|
|
|
340
363
|
* Release device — release interface and close.
|
|
341
364
|
*/
|
|
342
365
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
366
|
+
await this.cancelActiveTransfers(path);
|
|
367
|
+
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
343
368
|
await this.closeOpenDevice(path);
|
|
344
369
|
this.deviceProtocol.delete(path);
|
|
345
|
-
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
373
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
374
|
+
([, active]) => path === undefined || active.path === path
|
|
375
|
+
);
|
|
376
|
+
transfers.forEach(([transfer]) => {
|
|
377
|
+
try {
|
|
378
|
+
transfer.cancel();
|
|
379
|
+
} catch {
|
|
380
|
+
// A transfer may finish between the snapshot and cancel().
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private createTrackedTransfer(
|
|
387
|
+
path: string,
|
|
388
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
389
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
390
|
+
): usb.Transfer {
|
|
391
|
+
let resolveSettled: () => void = () => undefined;
|
|
392
|
+
const settled = new Promise<void>(resolve => {
|
|
393
|
+
resolveSettled = resolve;
|
|
394
|
+
});
|
|
395
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
396
|
+
this.activeTransfers.delete(transfer);
|
|
397
|
+
resolveSettled();
|
|
398
|
+
callback(error, buffer, actualLength);
|
|
399
|
+
});
|
|
400
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
401
|
+
return transfer;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
405
|
+
return new Promise((resolve, reject) => {
|
|
406
|
+
const buffer = Buffer.alloc(length);
|
|
407
|
+
const transfer = this.createTrackedTransfer(
|
|
408
|
+
path,
|
|
409
|
+
ep,
|
|
410
|
+
(error, _submittedBuffer, actualLength) => {
|
|
411
|
+
if (error) {
|
|
412
|
+
reject(error);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (actualLength <= 0) {
|
|
416
|
+
reject(new Error('Empty USB transfer'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
resolve(buffer.subarray(0, actualLength));
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
try {
|
|
423
|
+
transfer.submit(buffer);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
const active = this.activeTransfers.get(transfer);
|
|
426
|
+
this.activeTransfers.delete(transfer);
|
|
427
|
+
active?.resolveSettled();
|
|
428
|
+
reject(error);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
436
|
+
if (error) {
|
|
437
|
+
reject(error);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
resolve();
|
|
441
|
+
});
|
|
442
|
+
try {
|
|
443
|
+
transfer.submit(data);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
const active = this.activeTransfers.get(transfer);
|
|
446
|
+
this.activeTransfers.delete(transfer);
|
|
447
|
+
active?.resolveSettled();
|
|
448
|
+
reject(error);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
346
451
|
}
|
|
347
452
|
|
|
348
453
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
@@ -397,18 +502,8 @@ export default class NodeUsbTransport {
|
|
|
397
502
|
`Device protocol has not been detected for ${path}`
|
|
398
503
|
);
|
|
399
504
|
}
|
|
400
|
-
if (
|
|
401
|
-
this.Log?.debug('
|
|
402
|
-
} else {
|
|
403
|
-
this.Log?.debug(
|
|
404
|
-
'NodeUsbTransport call-',
|
|
405
|
-
' name: ',
|
|
406
|
-
name,
|
|
407
|
-
' data: ',
|
|
408
|
-
data,
|
|
409
|
-
' protocol: ',
|
|
410
|
-
protocol
|
|
411
|
-
);
|
|
505
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
506
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
412
507
|
}
|
|
413
508
|
|
|
414
509
|
if (protocol === 'V2') {
|
|
@@ -445,7 +540,6 @@ export default class NodeUsbTransport {
|
|
|
445
540
|
}
|
|
446
541
|
|
|
447
542
|
cancel() {
|
|
448
|
-
this.Log?.debug('NodeUsbTransport cancel');
|
|
449
543
|
this.cancelled = true;
|
|
450
544
|
}
|
|
451
545
|
|
|
@@ -475,6 +569,12 @@ export default class NodeUsbTransport {
|
|
|
475
569
|
|
|
476
570
|
private isRetryableError(error: unknown): boolean {
|
|
477
571
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
572
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
573
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
574
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
575
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
478
578
|
return (
|
|
479
579
|
message.includes('libusb') ||
|
|
480
580
|
message.includes('transfer') ||
|
|
@@ -531,6 +631,10 @@ export default class NodeUsbTransport {
|
|
|
531
631
|
);
|
|
532
632
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
533
633
|
|
|
634
|
+
await this.rotateProtocolV2UsbGeneration(
|
|
635
|
+
path,
|
|
636
|
+
`Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
|
|
637
|
+
);
|
|
534
638
|
// Close the existing device without clearing the detected protocol cache.
|
|
535
639
|
try {
|
|
536
640
|
await this.closeOpenDevice(path);
|
|
@@ -575,7 +679,7 @@ export default class NodeUsbTransport {
|
|
|
575
679
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
576
680
|
packet[0] = REPORT_ID;
|
|
577
681
|
packet.set(new Uint8Array(buffer), 1);
|
|
578
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
682
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
579
683
|
}
|
|
580
684
|
return; // all chunks sent successfully
|
|
581
685
|
} catch (error) {
|
|
@@ -618,27 +722,27 @@ export default class NodeUsbTransport {
|
|
|
618
722
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
619
723
|
}
|
|
620
724
|
try {
|
|
621
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
725
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
622
726
|
} catch (error) {
|
|
623
727
|
lastError = error;
|
|
624
728
|
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
625
729
|
attempt -= 1;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
|
|
730
|
+
} else {
|
|
731
|
+
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
732
|
+
if (!shouldRetry) {
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
|
|
737
|
+
} catch (reconnectError) {
|
|
738
|
+
lastError = reconnectError;
|
|
739
|
+
this.Log?.debug(
|
|
740
|
+
`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
741
|
+
reconnectError
|
|
742
|
+
)}`
|
|
743
|
+
);
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
642
746
|
}
|
|
643
747
|
}
|
|
644
748
|
}
|
|
@@ -698,7 +802,7 @@ export default class NodeUsbTransport {
|
|
|
698
802
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
699
803
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
700
804
|
|
|
701
|
-
await this.drainStaleInput(epIn);
|
|
805
|
+
await this.drainStaleInput(path, epIn);
|
|
702
806
|
|
|
703
807
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
704
808
|
} catch (err) {
|
|
@@ -711,14 +815,14 @@ export default class NodeUsbTransport {
|
|
|
711
815
|
}
|
|
712
816
|
}
|
|
713
817
|
|
|
714
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
818
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
715
819
|
const originalTimeout = epIn.timeout;
|
|
716
820
|
epIn.timeout = 50;
|
|
717
821
|
try {
|
|
718
822
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
719
823
|
for (let index = 0; index < 16; index += 1) {
|
|
720
824
|
try {
|
|
721
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
825
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
722
826
|
} catch {
|
|
723
827
|
break;
|
|
724
828
|
}
|
|
@@ -744,17 +848,9 @@ export default class NodeUsbTransport {
|
|
|
744
848
|
|
|
745
849
|
private async detectProtocol(
|
|
746
850
|
path: string,
|
|
747
|
-
expectedProtocol?: ProtocolType
|
|
851
|
+
expectedProtocol?: ProtocolType,
|
|
852
|
+
protocolHint?: ProtocolType
|
|
748
853
|
): Promise<ProtocolType> {
|
|
749
|
-
if (expectedProtocol === 'V1') {
|
|
750
|
-
if (await this.probeProtocolV1(path)) {
|
|
751
|
-
this.deviceProtocol.set(path, 'V1');
|
|
752
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
753
|
-
return 'V1';
|
|
754
|
-
}
|
|
755
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
854
|
if (expectedProtocol === 'V2') {
|
|
759
855
|
if (await this.probeProtocolV2(path)) {
|
|
760
856
|
this.deviceProtocol.set(path, 'V2');
|
|
@@ -764,22 +860,27 @@ export default class NodeUsbTransport {
|
|
|
764
860
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
765
861
|
}
|
|
766
862
|
|
|
767
|
-
if (
|
|
768
|
-
this.
|
|
769
|
-
|
|
770
|
-
|
|
863
|
+
if (expectedProtocol === 'V1') {
|
|
864
|
+
if (await this.probeProtocolV1(path)) {
|
|
865
|
+
this.deviceProtocol.set(path, 'V1');
|
|
866
|
+
return 'V1';
|
|
867
|
+
}
|
|
868
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
771
869
|
}
|
|
772
870
|
|
|
773
|
-
|
|
774
|
-
this.deviceProtocol.
|
|
775
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
|
|
776
|
-
return 'V1';
|
|
777
|
-
}
|
|
871
|
+
const probeOrder: ProtocolType[] =
|
|
872
|
+
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
778
873
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
874
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
875
|
+
if (index > 0) {
|
|
876
|
+
await this.resetConnectionAfterProbe(path);
|
|
877
|
+
}
|
|
878
|
+
const detected =
|
|
879
|
+
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
880
|
+
if (detected) {
|
|
881
|
+
this.deviceProtocol.set(path, protocol);
|
|
882
|
+
return protocol;
|
|
883
|
+
}
|
|
783
884
|
}
|
|
784
885
|
|
|
785
886
|
this.deviceProtocol.delete(path);
|
|
@@ -787,11 +888,13 @@ export default class NodeUsbTransport {
|
|
|
787
888
|
}
|
|
788
889
|
|
|
789
890
|
private async resetConnectionAfterProbe(path: string) {
|
|
790
|
-
this.
|
|
791
|
-
this.protocolV2Sessions.delete(path);
|
|
792
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
891
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
793
892
|
|
|
794
893
|
try {
|
|
894
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
895
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
896
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
897
|
+
await this.cancelActiveTransfers(path);
|
|
795
898
|
await this.closeOpenDevice(path);
|
|
796
899
|
} catch (error) {
|
|
797
900
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -851,8 +954,7 @@ export default class NodeUsbTransport {
|
|
|
851
954
|
try {
|
|
852
955
|
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
853
956
|
return true;
|
|
854
|
-
} catch (
|
|
855
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
957
|
+
} catch (_error) {
|
|
856
958
|
return false;
|
|
857
959
|
}
|
|
858
960
|
}
|
|
@@ -871,75 +973,71 @@ export default class NodeUsbTransport {
|
|
|
871
973
|
});
|
|
872
974
|
}
|
|
873
975
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
976
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
977
|
+
if (!this.messages || !this.messagesV2) {
|
|
978
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
protocolV1: this.messages,
|
|
982
|
+
protocolV2: this.messagesV2,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
protected getProtocolV2UsbLogger() {
|
|
987
|
+
return this.Log;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
protected async writeProtocolV2UsbPacket(
|
|
991
|
+
path: string,
|
|
992
|
+
frame: Uint8Array,
|
|
993
|
+
_context: ProtocolV2CallContext
|
|
994
|
+
): Promise<void> {
|
|
995
|
+
if (this.cancelled) {
|
|
996
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
997
|
+
}
|
|
998
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
protected async readProtocolV2UsbPacket(
|
|
1002
|
+
path: string,
|
|
1003
|
+
_context: ProtocolV2CallContext
|
|
1004
|
+
): Promise<Uint8Array> {
|
|
1005
|
+
for (;;) {
|
|
877
1006
|
if (this.cancelled) {
|
|
878
1007
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
879
1008
|
}
|
|
880
1009
|
try {
|
|
881
|
-
await
|
|
882
|
-
|
|
1010
|
+
const packet = await this.transferInOnce(
|
|
1011
|
+
path,
|
|
1012
|
+
this.getOpenDevice(path).epIn,
|
|
1013
|
+
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
1014
|
+
);
|
|
1015
|
+
return new Uint8Array(
|
|
1016
|
+
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
1017
|
+
);
|
|
883
1018
|
} catch (error) {
|
|
884
|
-
|
|
885
|
-
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
886
|
-
if (!shouldRetry) {
|
|
1019
|
+
if (!this.isUsbTransferTimeout(error)) {
|
|
887
1020
|
throw error;
|
|
888
1021
|
}
|
|
889
|
-
try {
|
|
890
|
-
await this.reconnectForRetry(path, 'out', attempt, error);
|
|
891
|
-
} catch (reconnectError) {
|
|
892
|
-
lastError = reconnectError;
|
|
893
|
-
this.Log?.debug(
|
|
894
|
-
`[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
895
|
-
reconnectError
|
|
896
|
-
)}`
|
|
897
|
-
);
|
|
898
|
-
break;
|
|
899
|
-
}
|
|
900
1022
|
}
|
|
901
1023
|
}
|
|
902
|
-
throw lastError;
|
|
903
1024
|
}
|
|
904
1025
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
910
|
-
}
|
|
1026
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1027
|
+
await this.cancelActiveTransfers(path);
|
|
1028
|
+
await this.closeOpenDevice(path);
|
|
1029
|
+
}
|
|
911
1030
|
|
|
912
|
-
|
|
913
|
-
|
|
1031
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
1032
|
+
this.deviceProtocol.delete(path);
|
|
1033
|
+
this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
1034
|
+
}
|
|
914
1035
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
{ waitIndefinitelyOnTimeout: !deadline }
|
|
921
|
-
);
|
|
922
|
-
const packet = deadline
|
|
923
|
-
? await this.withProtocolReadTimeout(
|
|
924
|
-
path,
|
|
925
|
-
transferIn,
|
|
926
|
-
Math.max(deadline - Date.now(), 1),
|
|
927
|
-
'V2'
|
|
928
|
-
)
|
|
929
|
-
: await transferIn;
|
|
930
|
-
const bytes = new Uint8Array(
|
|
931
|
-
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
932
|
-
);
|
|
933
|
-
try {
|
|
934
|
-
frame = assembler.push(bytes);
|
|
935
|
-
} catch (error) {
|
|
936
|
-
throw ERRORS.TypedError(
|
|
937
|
-
HardwareErrorCode.NetworkError,
|
|
938
|
-
error instanceof Error ? error.message : String(error)
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return frame;
|
|
1036
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
1037
|
+
return new ProtocolV2LinkError(
|
|
1038
|
+
'response-timeout',
|
|
1039
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1040
|
+
);
|
|
943
1041
|
}
|
|
944
1042
|
|
|
945
1043
|
private async callProtocolV2(
|
|
@@ -948,42 +1046,7 @@ export default class NodeUsbTransport {
|
|
|
948
1046
|
data: Record<string, unknown>,
|
|
949
1047
|
options?: TransportCallOptions
|
|
950
1048
|
) {
|
|
951
|
-
|
|
952
|
-
if (!this.messagesV2) {
|
|
953
|
-
throw ERRORS.TypedError(
|
|
954
|
-
HardwareErrorCode.TransportNotConfigured,
|
|
955
|
-
'Protocol V2 schema not configured'
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
if (!protocolV1Messages) {
|
|
959
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
let session = this.protocolV2Sessions.get(path);
|
|
963
|
-
if (!session) {
|
|
964
|
-
session = new ProtocolV2Session({
|
|
965
|
-
schemas: {
|
|
966
|
-
protocolV1: protocolV1Messages,
|
|
967
|
-
protocolV2: this.messagesV2,
|
|
968
|
-
},
|
|
969
|
-
router: PROTOCOL_V2_CHANNEL_USB,
|
|
970
|
-
writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(path, frame),
|
|
971
|
-
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
972
|
-
logger: this.Log,
|
|
973
|
-
logPrefix: 'ProtocolV2 NodeUSB',
|
|
974
|
-
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
975
|
-
new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
976
|
-
});
|
|
977
|
-
this.protocolV2Sessions.set(path, session);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
|
|
981
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
982
|
-
try {
|
|
983
|
-
return await session.call(name, data, options);
|
|
984
|
-
} finally {
|
|
985
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
986
|
-
}
|
|
1049
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
987
1050
|
}
|
|
988
1051
|
|
|
989
1052
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|