@onekeyfe/hd-transport-usb 1.2.0-alpha.9 → 1.2.0-alpha.90
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 +219 -40
- package/dist/index.d.ts +76 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -68
- 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 +179 -78
- package/src/transportLog.ts +1 -0
|
@@ -62,6 +62,7 @@ const createHarness = () => {
|
|
|
62
62
|
const path = '6136';
|
|
63
63
|
const responseQueue: Buffer[] = [];
|
|
64
64
|
const sentSeqs: number[] = [];
|
|
65
|
+
let cancelledTransferCount = 0;
|
|
65
66
|
let writeError: Error | undefined;
|
|
66
67
|
let holdNextRead:
|
|
67
68
|
| {
|
|
@@ -71,29 +72,84 @@ const createHarness = () => {
|
|
|
71
72
|
}
|
|
72
73
|
| undefined;
|
|
73
74
|
|
|
75
|
+
const performInTransfer = (callback: (error?: Error, data?: Buffer) => void) => {
|
|
76
|
+
if (epIn.timeout === 50) {
|
|
77
|
+
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (holdNextRead) {
|
|
81
|
+
const pending = holdNextRead;
|
|
82
|
+
holdNextRead = undefined;
|
|
83
|
+
pending.callback = callback;
|
|
84
|
+
pending.markStarted();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const response = responseQueue.shift();
|
|
88
|
+
if (!response) {
|
|
89
|
+
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
callback(undefined, response);
|
|
93
|
+
};
|
|
94
|
+
|
|
74
95
|
const epIn = {
|
|
75
96
|
direction: 'in',
|
|
76
97
|
address: 0x81,
|
|
77
98
|
timeout: 30_000,
|
|
78
99
|
transfer: jest.fn((_length: number, callback: (error?: Error, data?: Buffer) => void) => {
|
|
79
|
-
|
|
80
|
-
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (holdNextRead) {
|
|
84
|
-
const pending = holdNextRead;
|
|
85
|
-
holdNextRead = undefined;
|
|
86
|
-
pending.callback = callback;
|
|
87
|
-
pending.markStarted();
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const response = responseQueue.shift();
|
|
91
|
-
if (!response) {
|
|
92
|
-
callback(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
callback(undefined, response);
|
|
100
|
+
performInTransfer(callback);
|
|
96
101
|
}),
|
|
102
|
+
makeTransfer: jest.fn(
|
|
103
|
+
(
|
|
104
|
+
_timeout: number,
|
|
105
|
+
callback: (error: Error | undefined, data: Buffer, actualLength: number) => void
|
|
106
|
+
) => {
|
|
107
|
+
let settled = false;
|
|
108
|
+
const finish = (error?: Error, data = Buffer.alloc(0)) => {
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
callback(error, data, data.length);
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
submit: jest.fn((buffer: Buffer) => {
|
|
115
|
+
performInTransfer((error, data) => {
|
|
116
|
+
if (error) {
|
|
117
|
+
finish(error);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
data?.copy(buffer);
|
|
121
|
+
finish(undefined, buffer.subarray(0, data?.length ?? 0));
|
|
122
|
+
});
|
|
123
|
+
}),
|
|
124
|
+
cancel: jest.fn(() => {
|
|
125
|
+
cancelledTransferCount += 1;
|
|
126
|
+
finish(new Error('LIBUSB_TRANSFER_CANCELLED'));
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const performOutTransfer = (data: Buffer, callback: (error?: Error) => void) => {
|
|
134
|
+
const seq = data[6];
|
|
135
|
+
sentSeqs.push(seq);
|
|
136
|
+
if (writeError) {
|
|
137
|
+
const error = writeError;
|
|
138
|
+
writeError = undefined;
|
|
139
|
+
callback(error);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
responseQueue.push(
|
|
143
|
+
Buffer.from(
|
|
144
|
+
ProtocolV2.encodeFrame(
|
|
145
|
+
schemas,
|
|
146
|
+
'Success',
|
|
147
|
+
{ message: 'ok' },
|
|
148
|
+
{ router: PROTOCOL_V2_CHANNEL_USB, seq }
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
callback();
|
|
97
153
|
};
|
|
98
154
|
|
|
99
155
|
const epOut = {
|
|
@@ -101,33 +157,40 @@ const createHarness = () => {
|
|
|
101
157
|
address: 0x01,
|
|
102
158
|
timeout: 30_000,
|
|
103
159
|
transfer: jest.fn((data: Buffer, callback: (error?: Error) => void) => {
|
|
104
|
-
|
|
105
|
-
sentSeqs.push(seq);
|
|
106
|
-
if (writeError) {
|
|
107
|
-
const error = writeError;
|
|
108
|
-
writeError = undefined;
|
|
109
|
-
callback(error);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
responseQueue.push(
|
|
113
|
-
Buffer.from(
|
|
114
|
-
ProtocolV2.encodeFrame(
|
|
115
|
-
schemas,
|
|
116
|
-
'Success',
|
|
117
|
-
{ message: 'ok' },
|
|
118
|
-
{ router: PROTOCOL_V2_CHANNEL_USB, seq }
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
|
-
);
|
|
122
|
-
callback();
|
|
160
|
+
performOutTransfer(data, callback);
|
|
123
161
|
}),
|
|
162
|
+
makeTransfer: jest.fn(
|
|
163
|
+
(
|
|
164
|
+
_timeout: number,
|
|
165
|
+
callback: (error: Error | undefined, data: Buffer, actualLength: number) => void
|
|
166
|
+
) => {
|
|
167
|
+
let settled = false;
|
|
168
|
+
const finish = (error: Error | undefined, data: Buffer) => {
|
|
169
|
+
if (settled) return;
|
|
170
|
+
settled = true;
|
|
171
|
+
callback(error, data, data.length);
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
submit: jest.fn((data: Buffer) => {
|
|
175
|
+
performOutTransfer(data, error => finish(error, data));
|
|
176
|
+
}),
|
|
177
|
+
cancel: jest.fn(() => {
|
|
178
|
+
cancelledTransferCount += 1;
|
|
179
|
+
finish(new Error('LIBUSB_TRANSFER_CANCELLED'), Buffer.alloc(0));
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
),
|
|
124
184
|
};
|
|
125
185
|
|
|
126
186
|
const iface = {
|
|
127
187
|
descriptor: { bInterfaceClass: 0xff, bInterfaceNumber: 0 },
|
|
128
188
|
endpoints: [epIn, epOut],
|
|
129
189
|
claim: jest.fn(),
|
|
130
|
-
release: jest.fn((
|
|
190
|
+
release: jest.fn((closeEndpointsOrCallback: boolean | (() => void), callback?: () => void) => {
|
|
191
|
+
if (typeof closeEndpointsOrCallback === 'function') closeEndpointsOrCallback();
|
|
192
|
+
else callback?.();
|
|
193
|
+
}),
|
|
131
194
|
isKernelDriverActive: jest.fn(() => false),
|
|
132
195
|
detachKernelDriver: jest.fn(),
|
|
133
196
|
};
|
|
@@ -164,6 +227,7 @@ const createHarness = () => {
|
|
|
164
227
|
epIn,
|
|
165
228
|
epOut,
|
|
166
229
|
sentSeqs,
|
|
230
|
+
getCancelledTransferCount: () => cancelledTransferCount,
|
|
167
231
|
async acquire() {
|
|
168
232
|
await transport.enumerate();
|
|
169
233
|
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
@@ -189,7 +253,108 @@ const createHarness = () => {
|
|
|
189
253
|
};
|
|
190
254
|
|
|
191
255
|
describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
192
|
-
test('
|
|
256
|
+
test('falls back to Protocol V2 when a cached V1 hint is stale', async () => {
|
|
257
|
+
const transport = new NodeUsbTransport() as any;
|
|
258
|
+
const events: string[] = [];
|
|
259
|
+
transport.probeProtocolV1 = jest.fn().mockImplementation(() => {
|
|
260
|
+
events.push('probe-v1');
|
|
261
|
+
return Promise.resolve(false);
|
|
262
|
+
});
|
|
263
|
+
transport.resetConnectionAfterProbe = jest.fn().mockImplementation(() => {
|
|
264
|
+
events.push('reset');
|
|
265
|
+
return Promise.resolve();
|
|
266
|
+
});
|
|
267
|
+
transport.probeProtocolV2 = jest.fn().mockImplementation(() => {
|
|
268
|
+
events.push('probe-v2');
|
|
269
|
+
return Promise.resolve(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await expect(transport.detectProtocol('pro-usb', undefined, 'V1')).resolves.toBe('V2');
|
|
273
|
+
|
|
274
|
+
expect(events).toEqual(['probe-v1', 'reset', 'probe-v2']);
|
|
275
|
+
expect(transport.getProtocolType('pro-usb')).toBe('V2');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('keeps active links when the Protocol V2 schema is configured repeatedly', () => {
|
|
279
|
+
const transport = new NodeUsbTransport() as any;
|
|
280
|
+
transport.invalidateAllProtocolV2UsbLinks = jest.fn().mockResolvedValue(undefined);
|
|
281
|
+
const schemaSource = JSON.stringify(protocolV2Schema);
|
|
282
|
+
|
|
283
|
+
transport.configureProtocolV2(schemaSource);
|
|
284
|
+
transport.configureProtocolV2(schemaSource);
|
|
285
|
+
|
|
286
|
+
expect(transport.invalidateAllProtocolV2UsbLinks).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('does not retry a native transfer cancelled by probe cleanup', () => {
|
|
290
|
+
const { transport } = createHarness();
|
|
291
|
+
|
|
292
|
+
expect((transport as any).isRetryableError(new Error('LIBUSB_TRANSFER_CANCELLED'))).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('cancels pending native transfers before closing a timed-out protocol probe', async () => {
|
|
296
|
+
const harness = createHarness();
|
|
297
|
+
const { transport, path } = harness;
|
|
298
|
+
await harness.acquire();
|
|
299
|
+
const cancelActiveTransfers = jest.spyOn(transport as any, 'cancelActiveTransfers');
|
|
300
|
+
const closeOpenDevice = jest.spyOn(transport as any, 'closeOpenDevice');
|
|
301
|
+
|
|
302
|
+
await (transport as any).resetConnectionAfterProbe(path);
|
|
303
|
+
|
|
304
|
+
expect(cancelActiveTransfers).toHaveBeenCalledWith(path);
|
|
305
|
+
expect(closeOpenDevice).toHaveBeenCalledWith(path);
|
|
306
|
+
expect(cancelActiveTransfers.mock.invocationCallOrder[0]).toBeLessThan(
|
|
307
|
+
closeOpenDevice.mock.invocationCallOrder[0]
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('releases the USB interface when protocol detection rejects acquire', async () => {
|
|
312
|
+
const harness = createHarness();
|
|
313
|
+
const { transport, path, device, iface } = harness;
|
|
314
|
+
await transport.enumerate();
|
|
315
|
+
device.close.mockClear();
|
|
316
|
+
iface.release.mockClear();
|
|
317
|
+
jest
|
|
318
|
+
.spyOn(transport as any, 'detectProtocol')
|
|
319
|
+
.mockRejectedValueOnce(new Error('terminal protocol probe failure'));
|
|
320
|
+
|
|
321
|
+
await expect(transport.acquire({ path })).rejects.toMatchObject({
|
|
322
|
+
errorCode: expect.any(Number),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(iface.release).toHaveBeenCalledTimes(1);
|
|
326
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
327
|
+
expect((transport as any).openDevices.has(path)).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('stop releases an acquired USB interface even before a Protocol V2 call', async () => {
|
|
331
|
+
const harness = createHarness();
|
|
332
|
+
const { transport, path, device, iface } = harness;
|
|
333
|
+
await transport.enumerate();
|
|
334
|
+
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
335
|
+
device.close.mockClear();
|
|
336
|
+
iface.release.mockClear();
|
|
337
|
+
|
|
338
|
+
await transport.stop();
|
|
339
|
+
|
|
340
|
+
expect(iface.release).toHaveBeenCalledTimes(1);
|
|
341
|
+
expect(device.close).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(transport.getProtocolType(path)).toBeUndefined();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('actively probes explicit Protocol V2 during bootloader reconnect', async () => {
|
|
346
|
+
const harness = createHarness();
|
|
347
|
+
const { transport, path, epOut } = harness;
|
|
348
|
+
|
|
349
|
+
await transport.enumerate();
|
|
350
|
+
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
351
|
+
|
|
352
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
353
|
+
expect(transport.getProtocolType(path)).toBe('V2');
|
|
354
|
+
await transport.release(path);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('keeps seq across calls and actively probed reacquire', async () => {
|
|
193
358
|
const harness = createHarness();
|
|
194
359
|
const { transport, path, sentSeqs } = harness;
|
|
195
360
|
|
|
@@ -207,14 +372,14 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
207
372
|
const harness = createHarness();
|
|
208
373
|
const { transport, path, epOut } = harness;
|
|
209
374
|
await harness.acquire();
|
|
210
|
-
epOut.
|
|
375
|
+
epOut.makeTransfer.mockClear();
|
|
211
376
|
harness.failNextWrite(new Error('LIBUSB_ERROR_IO'));
|
|
212
377
|
|
|
213
378
|
await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow(
|
|
214
379
|
'LIBUSB_ERROR_IO'
|
|
215
380
|
);
|
|
216
381
|
|
|
217
|
-
expect(epOut.
|
|
382
|
+
expect(epOut.makeTransfer).toHaveBeenCalledTimes(1);
|
|
218
383
|
});
|
|
219
384
|
|
|
220
385
|
test('rejects a pending read when release invalidates the link', async () => {
|
|
@@ -241,6 +406,20 @@ describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
|
241
406
|
expect(settled).not.toBe('still pending');
|
|
242
407
|
});
|
|
243
408
|
|
|
409
|
+
test('stop cancels an in-flight native USB transfer before releasing the interface', async () => {
|
|
410
|
+
const harness = createHarness();
|
|
411
|
+
const { transport, path } = harness;
|
|
412
|
+
await harness.acquire();
|
|
413
|
+
const pendingRead = harness.holdRead();
|
|
414
|
+
|
|
415
|
+
const call = transport.call(path, 'Ping', { message: 'pending' }, { timeoutMs: 5000 });
|
|
416
|
+
await pendingRead.started;
|
|
417
|
+
await transport.stop();
|
|
418
|
+
|
|
419
|
+
await expect(call).rejects.toThrow();
|
|
420
|
+
expect(harness.getCancelledTransferCount()).toBe(1);
|
|
421
|
+
});
|
|
422
|
+
|
|
244
423
|
test('keeps the cursor after a response timeout rebuilds the USB connection', async () => {
|
|
245
424
|
const harness = createHarness();
|
|
246
425
|
const { transport, path, sentSeqs } = harness;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,48 +2,119 @@ import * as transport from '@onekeyfe/hd-transport';
|
|
|
2
2
|
import transport__default, { ProtocolV2UsbTransportBase, OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolV2Schemas, ProtocolV2CallContext, ProtocolType } from '@onekeyfe/hd-transport';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Node.js USB Transport — complete transport implementation using libusb.
|
|
7
|
+
*
|
|
8
|
+
* Unlike the old UsbPlugin (which was a LowlevelTransportSharedPlugin piped
|
|
9
|
+
* through LowlevelTransport), this class is a standalone transport that handles
|
|
10
|
+
* both protocol encoding/decoding and USB I/O directly.
|
|
11
|
+
*
|
|
12
|
+
* Modeled after WebUsbTransport.
|
|
13
|
+
*/
|
|
5
14
|
declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
6
15
|
messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
16
|
+
/** Protobuf schema for Protocol V2 transports. */
|
|
7
17
|
messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
18
|
+
private protocolV2SchemaSource;
|
|
8
19
|
name: string;
|
|
9
20
|
version: string;
|
|
10
21
|
configured: boolean;
|
|
11
22
|
isOutdated: boolean;
|
|
12
23
|
Log?: any;
|
|
13
24
|
emitter?: EventEmitter;
|
|
25
|
+
/** serial → bus id, built during enumerate */
|
|
14
26
|
private serialToBusId;
|
|
27
|
+
/** path → opened device state */
|
|
15
28
|
private openDevices;
|
|
29
|
+
/** Per-path protocol type detected by active wire-level probe. */
|
|
16
30
|
private deviceProtocol;
|
|
31
|
+
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
17
32
|
private reconnectLocks;
|
|
33
|
+
/**
|
|
34
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
35
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
36
|
+
*/
|
|
37
|
+
private activeTransfers;
|
|
38
|
+
/** set to true when cancel() is called; checked by retry loops */
|
|
18
39
|
private cancelled;
|
|
19
40
|
constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Initialize transport.
|
|
43
|
+
* Signature matches the Transport.init interface (logger, emitter).
|
|
44
|
+
*/
|
|
20
45
|
init(logger: any, emitter?: EventEmitter): Promise<string>;
|
|
21
46
|
configure(signedData: any): Promise<void>;
|
|
22
47
|
configureProtocolV2(signedData: any): void;
|
|
23
48
|
listen(): void;
|
|
24
|
-
stop(): void
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Low-level post (send only, no response). Not used by NodeUsbTransport
|
|
52
|
+
* since call() handles the full send+receive cycle, but required by the Transport interface.
|
|
53
|
+
*/
|
|
25
54
|
post(path: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Low-level read (receive only). Not used by NodeUsbTransport
|
|
57
|
+
* since call() handles the full send+receive cycle, but required by the Transport interface.
|
|
58
|
+
*/
|
|
26
59
|
read(path: string): Promise<{
|
|
27
60
|
message: {
|
|
28
61
|
[key: string]: any;
|
|
29
62
|
};
|
|
30
63
|
type: string;
|
|
31
64
|
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Enumerate connected OneKey USB devices.
|
|
67
|
+
* Opens each device briefly to read its serial number (used as `path`),
|
|
68
|
+
* then closes it. acquire() re-opens from a fresh getDeviceList().
|
|
69
|
+
*/
|
|
32
70
|
enumerate(): Promise<OneKeyDeviceInfo[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Acquire device — open USB device, claim interface, return path (string).
|
|
73
|
+
*/
|
|
33
74
|
acquire(input: AcquireInput): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Release device — release interface and close.
|
|
77
|
+
*/
|
|
34
78
|
release(path: string, _onclose?: boolean): Promise<void>;
|
|
79
|
+
private cancelActiveTransfers;
|
|
80
|
+
private createTrackedTransfer;
|
|
81
|
+
private transferInOnce;
|
|
82
|
+
private transferOutOnce;
|
|
35
83
|
private closeOpenDevice;
|
|
84
|
+
/**
|
|
85
|
+
* Call device method — encode protobuf, send packets, receive response.
|
|
86
|
+
* This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
|
|
87
|
+
*/
|
|
36
88
|
call(path: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
|
|
37
89
|
private callProtocolV1;
|
|
38
90
|
cancel(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Get the current open device for a path, re-resolving from the map
|
|
93
|
+
* so callers always use a fresh reference after reconnect.
|
|
94
|
+
*/
|
|
39
95
|
private getOpenDevice;
|
|
40
96
|
private getErrorMessage;
|
|
41
97
|
private isRetryableError;
|
|
42
98
|
private isUsbTransferTimeout;
|
|
43
99
|
private getDeviceInterface;
|
|
100
|
+
/**
|
|
101
|
+
* Reconnect device before retrying a failed transfer (aligned with WebUsbTransport).
|
|
102
|
+
* Uses per-path lock to prevent concurrent reconnects to the same device.
|
|
103
|
+
*/
|
|
44
104
|
private reconnectForRetry;
|
|
105
|
+
/**
|
|
106
|
+
* Send all encoded chunks to the device with retry.
|
|
107
|
+
* If a chunk fails and triggers reconnect, the entire sequence restarts
|
|
108
|
+
* from chunk 0 because the device resets protocol state on reconnect.
|
|
109
|
+
*/
|
|
45
110
|
private sendAllChunksWithRetry;
|
|
111
|
+
/**
|
|
112
|
+
* USB IN transfer with retry and reconnect (aligned with WebUsbTransport).
|
|
113
|
+
*/
|
|
46
114
|
private transferInWithRetry;
|
|
115
|
+
/**
|
|
116
|
+
* Open a USB device by path (serial number), claim interface, cache endpoints.
|
|
117
|
+
*/
|
|
47
118
|
private openDevice;
|
|
48
119
|
private drainStaleInput;
|
|
49
120
|
private createProtocolMismatchError;
|
|
@@ -61,6 +132,10 @@ declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
|
61
132
|
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string): void;
|
|
62
133
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
|
|
63
134
|
private callProtocolV2;
|
|
135
|
+
/**
|
|
136
|
+
* Receive a complete protobuf response from the device.
|
|
137
|
+
* Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
|
|
138
|
+
*/
|
|
64
139
|
private receiveData;
|
|
65
140
|
getProtocolType(path: string): ProtocolType | undefined;
|
|
66
141
|
}
|
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,SAAS,EAAE,EAQhB,0BAA0B,EAE3B,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAE3B,MAAM,wBAAwB,CAAC;AAKhC,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAuIhC,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,0BAA0B,CAAC,MAAM,CAAC;IAC9E,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAGlE,UAAU,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAEpE,OAAO,CAAC,sBAAsB,CAAqB;IAEnD,IAAI,SAAsB;IAE1B,OAAO,SAAM;IAEb,UAAU,UAAS;IAEnB,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAGvB,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAiC;IAGpD,OAAO,CAAC,cAAc,CAAwC;IAG9D,OAAO,CAAC,cAAc,CAA0C;IAMhE,OAAO,CAAC,eAAe,CAGnB;IAGJ,OAAO,CAAC,SAAS,CAAS;;IAc1B,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAMxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAOzB,mBAAmB,CAAC,UAAU,EAAE,GAAG;IAiBnC,MAAM;IAIA,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2BrB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9E,IAAI,CAAC,IAAI,EAAE,MAAM;;;;;;IAiBjB,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA8BxC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA4B7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;YAOhD,qBAAqB;IAcnC,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,cAAc;IA6BtB,OAAO,CAAC,eAAe;YAoBT,eAAe;IA6BvB,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,oBAAoB;YA8BlB,cAAc;IA0B5B,MAAM;IAUN,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,kBAAkB;IAmB1B,OAAO,CAAC,iBAAiB;YAuDX,sBAAsB;YAyCtB,mBAAmB;YA2CnB,UAAU;YA+DV,eAAe;IAiB7B,OAAO,CAAC,2BAA2B;IAOnC,OAAO,CAAC,4BAA4B;YAOtB,cAAc;YAyCd,yBAAyB;YAiBzB,uBAAuB;YA0CvB,eAAe;YAaf,eAAe;IAc7B,SAAS,CAAC,uBAAuB,IAAI,iBAAiB;IAUtD,SAAS,CAAC,sBAAsB;cAIhB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC;cAOA,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC;cAsBN,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1F,SAAS,CAAC,8BAA8B,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrE,SAAS,CAAC,+BAA+B,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,KAAK;YAOnE,cAAc;YAad,WAAW;IA6CzB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;CAGxD"}
|
package/dist/index.js
CHANGED
|
@@ -132,26 +132,6 @@ function readSerialNumber(dev, openDevices) {
|
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
|
-
function transferInOnce(ep, length) {
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
ep.transfer(length, (err, data) => {
|
|
138
|
-
if (err)
|
|
139
|
-
return reject(err);
|
|
140
|
-
if (!data || data.length === 0)
|
|
141
|
-
return reject(new Error('Empty USB transfer'));
|
|
142
|
-
resolve(data);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
function transferOutOnce(ep, data) {
|
|
147
|
-
return new Promise((resolve, reject) => {
|
|
148
|
-
ep.transfer(data, (err) => {
|
|
149
|
-
if (err)
|
|
150
|
-
return reject(err);
|
|
151
|
-
resolve();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
135
|
function skipReportByte(packet) {
|
|
156
136
|
if (packet[0] === REPORT_ID) {
|
|
157
137
|
return packet.subarray(1);
|
|
@@ -176,6 +156,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
176
156
|
this.openDevices = new Map();
|
|
177
157
|
this.deviceProtocol = new Map();
|
|
178
158
|
this.reconnectLocks = new Map();
|
|
159
|
+
this.activeTransfers = new Map();
|
|
179
160
|
this.cancelled = false;
|
|
180
161
|
}
|
|
181
162
|
init(logger, emitter) {
|
|
@@ -191,14 +172,38 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
191
172
|
}
|
|
192
173
|
configureProtocolV2(signedData) {
|
|
193
174
|
var _a;
|
|
175
|
+
const schemaSource = typeof signedData === 'string'
|
|
176
|
+
? signedData
|
|
177
|
+
: (_a = JSON.stringify(signedData)) !== null && _a !== void 0 ? _a : String(signedData);
|
|
178
|
+
if (schemaSource === this.protocolV2SchemaSource)
|
|
179
|
+
return;
|
|
180
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
194
181
|
this.messagesV2 = parseConfigure(signedData);
|
|
182
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
183
|
+
if (!hadProtocolV2Schema)
|
|
184
|
+
return;
|
|
195
185
|
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] schema link cleanup failed:', error); });
|
|
196
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V2 schema configured');
|
|
197
186
|
}
|
|
198
187
|
listen() {
|
|
199
188
|
}
|
|
200
189
|
stop() {
|
|
201
|
-
|
|
190
|
+
var _a;
|
|
191
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
192
|
+
this.cancelled = true;
|
|
193
|
+
yield this.cancelActiveTransfers();
|
|
194
|
+
try {
|
|
195
|
+
yield this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
199
|
+
}
|
|
200
|
+
yield Promise.allSettled(this.reconnectLocks.values());
|
|
201
|
+
yield this.cancelActiveTransfers();
|
|
202
|
+
yield Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
203
|
+
this.reconnectLocks.clear();
|
|
204
|
+
this.deviceProtocol.clear();
|
|
205
|
+
this.serialToBusId.clear();
|
|
206
|
+
});
|
|
202
207
|
}
|
|
203
208
|
post(path, name, data) {
|
|
204
209
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -248,7 +253,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
248
253
|
});
|
|
249
254
|
}
|
|
250
255
|
acquire(input) {
|
|
251
|
-
var _a, _b, _c;
|
|
256
|
+
var _a, _b, _c, _d;
|
|
252
257
|
return __awaiter(this, void 0, void 0, function* () {
|
|
253
258
|
this.cancelled = false;
|
|
254
259
|
const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
|
|
@@ -259,22 +264,100 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
259
264
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
260
265
|
yield this.closeOpenDevice(path);
|
|
261
266
|
yield this.openDevice(path);
|
|
262
|
-
yield this.detectProtocol(path, input.expectedProtocol);
|
|
267
|
+
yield this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
|
|
263
268
|
return path;
|
|
264
269
|
}
|
|
265
270
|
catch (error) {
|
|
266
271
|
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
|
|
267
|
-
|
|
272
|
+
try {
|
|
273
|
+
yield this.release(path);
|
|
274
|
+
}
|
|
275
|
+
catch (cleanupError) {
|
|
276
|
+
(_c = this.Log) === null || _c === void 0 ? void 0 : _c.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
277
|
+
}
|
|
278
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_d = error.message) !== null && _d !== void 0 ? _d : String(error));
|
|
268
279
|
}
|
|
269
280
|
});
|
|
270
281
|
}
|
|
271
282
|
release(path, _onclose) {
|
|
272
283
|
return __awaiter(this, void 0, void 0, function* () {
|
|
284
|
+
yield this.cancelActiveTransfers(path);
|
|
273
285
|
yield this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
274
286
|
yield this.closeOpenDevice(path);
|
|
275
287
|
this.deviceProtocol.delete(path);
|
|
276
288
|
});
|
|
277
289
|
}
|
|
290
|
+
cancelActiveTransfers(path) {
|
|
291
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
292
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(([, active]) => path === undefined || active.path === path);
|
|
293
|
+
transfers.forEach(([transfer]) => {
|
|
294
|
+
try {
|
|
295
|
+
transfer.cancel();
|
|
296
|
+
}
|
|
297
|
+
catch (_a) {
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
yield Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
createTrackedTransfer(path, endpoint, callback) {
|
|
304
|
+
let resolveSettled = () => undefined;
|
|
305
|
+
const settled = new Promise(resolve => {
|
|
306
|
+
resolveSettled = resolve;
|
|
307
|
+
});
|
|
308
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
309
|
+
this.activeTransfers.delete(transfer);
|
|
310
|
+
resolveSettled();
|
|
311
|
+
callback(error, buffer, actualLength);
|
|
312
|
+
});
|
|
313
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
314
|
+
return transfer;
|
|
315
|
+
}
|
|
316
|
+
transferInOnce(path, ep, length) {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const buffer = Buffer.alloc(length);
|
|
319
|
+
const transfer = this.createTrackedTransfer(path, ep, (error, _submittedBuffer, actualLength) => {
|
|
320
|
+
if (error) {
|
|
321
|
+
reject(error);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (actualLength <= 0) {
|
|
325
|
+
reject(new Error('Empty USB transfer'));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
resolve(buffer.subarray(0, actualLength));
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
transfer.submit(buffer);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
const active = this.activeTransfers.get(transfer);
|
|
335
|
+
this.activeTransfers.delete(transfer);
|
|
336
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
337
|
+
reject(error);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
transferOutOnce(path, ep, data) {
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
344
|
+
if (error) {
|
|
345
|
+
reject(error);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
resolve();
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
transfer.submit(data);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const active = this.activeTransfers.get(transfer);
|
|
355
|
+
this.activeTransfers.delete(transfer);
|
|
356
|
+
active === null || active === void 0 ? void 0 : active.resolveSettled();
|
|
357
|
+
reject(error);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
278
361
|
closeOpenDevice(path) {
|
|
279
362
|
return __awaiter(this, void 0, void 0, function* () {
|
|
280
363
|
const openDev = this.openDevices.get(path);
|
|
@@ -303,7 +386,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
303
386
|
});
|
|
304
387
|
}
|
|
305
388
|
call(path, name, data, options) {
|
|
306
|
-
var _a
|
|
389
|
+
var _a;
|
|
307
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
308
391
|
this.cancelled = false;
|
|
309
392
|
if (!this.messages) {
|
|
@@ -316,11 +399,8 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
316
399
|
if (!protocol) {
|
|
317
400
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
|
|
318
401
|
}
|
|
319
|
-
if (transport.
|
|
320
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
|
|
402
|
+
if (!transport.shouldSuppressHighVolumeCallLog(name)) {
|
|
403
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('transport call', transport.createTransportCallLog(name, protocol, data));
|
|
324
404
|
}
|
|
325
405
|
if (protocol === 'V2') {
|
|
326
406
|
return this.callProtocolV2(path, name, data, options);
|
|
@@ -345,8 +425,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
345
425
|
});
|
|
346
426
|
}
|
|
347
427
|
cancel() {
|
|
348
|
-
var _a;
|
|
349
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport cancel');
|
|
350
428
|
this.cancelled = true;
|
|
351
429
|
}
|
|
352
430
|
getOpenDevice(path) {
|
|
@@ -369,6 +447,9 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
369
447
|
}
|
|
370
448
|
isRetryableError(error) {
|
|
371
449
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
450
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
372
453
|
return (message.includes('libusb') ||
|
|
373
454
|
message.includes('transfer') ||
|
|
374
455
|
message.includes('disconnected') ||
|
|
@@ -437,7 +518,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
437
518
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
438
519
|
packet[0] = REPORT_ID;
|
|
439
520
|
packet.set(new Uint8Array(buffer), 1);
|
|
440
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
521
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
441
522
|
}
|
|
442
523
|
return;
|
|
443
524
|
}
|
|
@@ -470,7 +551,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
470
551
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
471
552
|
}
|
|
472
553
|
try {
|
|
473
|
-
return yield transferInOnce(currentDev.epIn, length);
|
|
554
|
+
return yield this.transferInOnce(path, currentDev.epIn, length);
|
|
474
555
|
}
|
|
475
556
|
catch (error) {
|
|
476
557
|
lastError = error;
|
|
@@ -529,7 +610,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
529
610
|
}
|
|
530
611
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
531
612
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
532
|
-
yield this.drainStaleInput(epIn);
|
|
613
|
+
yield this.drainStaleInput(path, epIn);
|
|
533
614
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
534
615
|
}
|
|
535
616
|
catch (err) {
|
|
@@ -542,14 +623,14 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
542
623
|
}
|
|
543
624
|
});
|
|
544
625
|
}
|
|
545
|
-
drainStaleInput(epIn) {
|
|
626
|
+
drainStaleInput(path, epIn) {
|
|
546
627
|
return __awaiter(this, void 0, void 0, function* () {
|
|
547
628
|
const originalTimeout = epIn.timeout;
|
|
548
629
|
epIn.timeout = 50;
|
|
549
630
|
try {
|
|
550
631
|
for (let index = 0; index < 16; index += 1) {
|
|
551
632
|
try {
|
|
552
|
-
yield transferInOnce(epIn, PACKET_SIZE);
|
|
633
|
+
yield this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
553
634
|
}
|
|
554
635
|
catch (_a) {
|
|
555
636
|
break;
|
|
@@ -567,39 +648,34 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
567
648
|
createProtocolDetectionError() {
|
|
568
649
|
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping');
|
|
569
650
|
}
|
|
570
|
-
detectProtocol(path, expectedProtocol) {
|
|
571
|
-
var _a
|
|
651
|
+
detectProtocol(path, expectedProtocol, protocolHint) {
|
|
652
|
+
var _a;
|
|
572
653
|
return __awaiter(this, void 0, void 0, function* () {
|
|
573
|
-
if (expectedProtocol === 'V1') {
|
|
574
|
-
if (yield this.probeProtocolV1(path)) {
|
|
575
|
-
this.deviceProtocol.set(path, 'V1');
|
|
576
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
577
|
-
return 'V1';
|
|
578
|
-
}
|
|
579
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
580
|
-
}
|
|
581
654
|
if (expectedProtocol === 'V2') {
|
|
582
655
|
if (yield this.probeProtocolV2(path)) {
|
|
583
656
|
this.deviceProtocol.set(path, 'V2');
|
|
584
|
-
(
|
|
657
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
585
658
|
return 'V2';
|
|
586
659
|
}
|
|
587
660
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
588
661
|
}
|
|
589
|
-
if (
|
|
590
|
-
this.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
this.deviceProtocol.set(path, 'V1');
|
|
596
|
-
(_d = this.Log) === null || _d === void 0 ? void 0 : _d.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
|
|
597
|
-
return 'V1';
|
|
662
|
+
if (expectedProtocol === 'V1') {
|
|
663
|
+
if (yield this.probeProtocolV1(path)) {
|
|
664
|
+
this.deviceProtocol.set(path, 'V1');
|
|
665
|
+
return 'V1';
|
|
666
|
+
}
|
|
667
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
598
668
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
(
|
|
602
|
-
|
|
669
|
+
const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
670
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
671
|
+
if (index > 0) {
|
|
672
|
+
yield this.resetConnectionAfterProbe(path);
|
|
673
|
+
}
|
|
674
|
+
const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
|
|
675
|
+
if (detected) {
|
|
676
|
+
this.deviceProtocol.set(path, protocol);
|
|
677
|
+
return protocol;
|
|
678
|
+
}
|
|
603
679
|
}
|
|
604
680
|
this.deviceProtocol.delete(path);
|
|
605
681
|
throw this.createProtocolDetectionError();
|
|
@@ -610,6 +686,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
610
686
|
return __awaiter(this, void 0, void 0, function* () {
|
|
611
687
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
612
688
|
try {
|
|
689
|
+
yield this.cancelActiveTransfers(path);
|
|
613
690
|
yield this.closeOpenDevice(path);
|
|
614
691
|
}
|
|
615
692
|
catch (error) {
|
|
@@ -657,7 +734,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
657
734
|
});
|
|
658
735
|
}
|
|
659
736
|
probeProtocolV1(path) {
|
|
660
|
-
var _a;
|
|
661
737
|
return __awaiter(this, void 0, void 0, function* () {
|
|
662
738
|
if (!this.messages) {
|
|
663
739
|
return false;
|
|
@@ -666,8 +742,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
666
742
|
yield this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
667
743
|
return true;
|
|
668
744
|
}
|
|
669
|
-
catch (
|
|
670
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
745
|
+
catch (_error) {
|
|
671
746
|
return false;
|
|
672
747
|
}
|
|
673
748
|
});
|
|
@@ -703,7 +778,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
703
778
|
if (this.cancelled) {
|
|
704
779
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
705
780
|
}
|
|
706
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
781
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
707
782
|
});
|
|
708
783
|
}
|
|
709
784
|
readProtocolV2UsbPacket(path, _context) {
|
|
@@ -713,7 +788,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
713
788
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
714
789
|
}
|
|
715
790
|
try {
|
|
716
|
-
const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
791
|
+
const packet = yield this.transferInOnce(path, this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
717
792
|
return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
718
793
|
}
|
|
719
794
|
catch (error) {
|
|
@@ -726,6 +801,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
726
801
|
}
|
|
727
802
|
resetProtocolV2UsbNativeLink(path, _reason) {
|
|
728
803
|
return __awaiter(this, void 0, void 0, function* () {
|
|
804
|
+
yield this.cancelActiveTransfers(path);
|
|
729
805
|
yield this.closeOpenDevice(path);
|
|
730
806
|
});
|
|
731
807
|
}
|
|
@@ -735,7 +811,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
735
811
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
736
812
|
}
|
|
737
813
|
createProtocolV2UsbTimeoutError(name, timeoutMs) {
|
|
738
|
-
return new
|
|
814
|
+
return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
739
815
|
}
|
|
740
816
|
callProtocolV2(path, name, data, options) {
|
|
741
817
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transportLog.d.ts","sourceRoot":"","sources":["../src/transportLog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC"}
|
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-usb",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.90",
|
|
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",
|
|
@@ -16,14 +16,15 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"dev": "rimraf dist && rollup -c ../../build/rollup.config.js -w",
|
|
18
18
|
"build": "rimraf dist && rollup -c ../../build/rollup.config.js",
|
|
19
|
+
"test": "jest",
|
|
19
20
|
"lint": "eslint .",
|
|
20
21
|
"lint:fix": "eslint . --fix"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.90",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.90",
|
|
25
26
|
"bytebuffer": "^5.0.1",
|
|
26
27
|
"usb": "^2.14.0"
|
|
27
28
|
},
|
|
28
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "e8271225634f3058498af8beee06d9de705b6710"
|
|
29
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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,
|
|
10
|
+
ProtocolV2LinkError,
|
|
11
11
|
ProtocolV2UsbTransportBase,
|
|
12
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
13
13
|
} from '@onekeyfe/hd-transport';
|
|
14
14
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
15
15
|
|
|
16
|
+
import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog';
|
|
17
|
+
|
|
16
18
|
import type EventEmitter from 'events';
|
|
17
19
|
import type {
|
|
18
20
|
AcquireInput,
|
|
@@ -130,31 +132,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
130
132
|
});
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
/**
|
|
134
|
-
* Promisified USB IN transfer (single attempt).
|
|
135
|
-
*/
|
|
136
|
-
function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
137
|
-
return new Promise((resolve, reject) => {
|
|
138
|
-
ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
|
|
139
|
-
if (err) return reject(err);
|
|
140
|
-
if (!data || data.length === 0) return reject(new Error('Empty USB transfer'));
|
|
141
|
-
resolve(data);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Promisified USB OUT transfer (single attempt).
|
|
148
|
-
*/
|
|
149
|
-
function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
150
|
-
return new Promise((resolve, reject) => {
|
|
151
|
-
ep.transfer(data, (err: Error | undefined) => {
|
|
152
|
-
if (err) return reject(err);
|
|
153
|
-
resolve();
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
135
|
/**
|
|
159
136
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
160
137
|
*/
|
|
@@ -187,6 +164,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
187
164
|
/** Protobuf schema for Protocol V2 transports. */
|
|
188
165
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
189
166
|
|
|
167
|
+
private protocolV2SchemaSource: string | undefined;
|
|
168
|
+
|
|
190
169
|
name = 'NodeUsbTransport';
|
|
191
170
|
|
|
192
171
|
version = '';
|
|
@@ -211,6 +190,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
211
190
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
212
191
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
213
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
|
+
|
|
214
202
|
/** set to true when cancel() is called; checked by retry loops */
|
|
215
203
|
private cancelled = false;
|
|
216
204
|
|
|
@@ -240,21 +228,47 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
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);
|
|
239
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
240
|
+
if (!hadProtocolV2Schema) return;
|
|
241
|
+
|
|
244
242
|
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
245
243
|
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
246
244
|
);
|
|
247
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
|
|
248
245
|
}
|
|
249
246
|
|
|
250
247
|
listen() {
|
|
251
248
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
252
249
|
}
|
|
253
250
|
|
|
254
|
-
stop() {
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
);
|
|
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();
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
/**
|
|
@@ -332,10 +346,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
332
346
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
333
347
|
await this.closeOpenDevice(path);
|
|
334
348
|
await this.openDevice(path);
|
|
335
|
-
await this.detectProtocol(path, input.expectedProtocol);
|
|
349
|
+
await this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
|
|
336
350
|
return path;
|
|
337
351
|
} catch (error: any) {
|
|
338
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
|
+
}
|
|
339
358
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
340
359
|
}
|
|
341
360
|
}
|
|
@@ -344,11 +363,93 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
344
363
|
* Release device — release interface and close.
|
|
345
364
|
*/
|
|
346
365
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
366
|
+
await this.cancelActiveTransfers(path);
|
|
347
367
|
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
348
368
|
await this.closeOpenDevice(path);
|
|
349
369
|
this.deviceProtocol.delete(path);
|
|
350
370
|
}
|
|
351
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
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
352
453
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
353
454
|
const openDev = this.openDevices.get(path);
|
|
354
455
|
if (!openDev) return;
|
|
@@ -401,18 +502,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
401
502
|
`Device protocol has not been detected for ${path}`
|
|
402
503
|
);
|
|
403
504
|
}
|
|
404
|
-
if (
|
|
405
|
-
this.Log?.debug('
|
|
406
|
-
} else {
|
|
407
|
-
this.Log?.debug(
|
|
408
|
-
'NodeUsbTransport call-',
|
|
409
|
-
' name: ',
|
|
410
|
-
name,
|
|
411
|
-
' data: ',
|
|
412
|
-
data,
|
|
413
|
-
' protocol: ',
|
|
414
|
-
protocol
|
|
415
|
-
);
|
|
505
|
+
if (!shouldSuppressHighVolumeCallLog(name)) {
|
|
506
|
+
this.Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
416
507
|
}
|
|
417
508
|
|
|
418
509
|
if (protocol === 'V2') {
|
|
@@ -449,7 +540,6 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
449
540
|
}
|
|
450
541
|
|
|
451
542
|
cancel() {
|
|
452
|
-
this.Log?.debug('NodeUsbTransport cancel');
|
|
453
543
|
this.cancelled = true;
|
|
454
544
|
}
|
|
455
545
|
|
|
@@ -479,6 +569,12 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
479
569
|
|
|
480
570
|
private isRetryableError(error: unknown): boolean {
|
|
481
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
|
+
}
|
|
482
578
|
return (
|
|
483
579
|
message.includes('libusb') ||
|
|
484
580
|
message.includes('transfer') ||
|
|
@@ -583,7 +679,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
583
679
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
584
680
|
packet[0] = REPORT_ID;
|
|
585
681
|
packet.set(new Uint8Array(buffer), 1);
|
|
586
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
682
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
587
683
|
}
|
|
588
684
|
return; // all chunks sent successfully
|
|
589
685
|
} catch (error) {
|
|
@@ -626,7 +722,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
626
722
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
627
723
|
}
|
|
628
724
|
try {
|
|
629
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
725
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
630
726
|
} catch (error) {
|
|
631
727
|
lastError = error;
|
|
632
728
|
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
@@ -706,7 +802,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
706
802
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
707
803
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
708
804
|
|
|
709
|
-
await this.drainStaleInput(epIn);
|
|
805
|
+
await this.drainStaleInput(path, epIn);
|
|
710
806
|
|
|
711
807
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
712
808
|
} catch (err) {
|
|
@@ -719,14 +815,14 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
719
815
|
}
|
|
720
816
|
}
|
|
721
817
|
|
|
722
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
818
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
723
819
|
const originalTimeout = epIn.timeout;
|
|
724
820
|
epIn.timeout = 50;
|
|
725
821
|
try {
|
|
726
822
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
727
823
|
for (let index = 0; index < 16; index += 1) {
|
|
728
824
|
try {
|
|
729
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
825
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
730
826
|
} catch {
|
|
731
827
|
break;
|
|
732
828
|
}
|
|
@@ -752,17 +848,9 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
752
848
|
|
|
753
849
|
private async detectProtocol(
|
|
754
850
|
path: string,
|
|
755
|
-
expectedProtocol?: ProtocolType
|
|
851
|
+
expectedProtocol?: ProtocolType,
|
|
852
|
+
protocolHint?: ProtocolType
|
|
756
853
|
): Promise<ProtocolType> {
|
|
757
|
-
if (expectedProtocol === 'V1') {
|
|
758
|
-
if (await this.probeProtocolV1(path)) {
|
|
759
|
-
this.deviceProtocol.set(path, 'V1');
|
|
760
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1 (expected)`);
|
|
761
|
-
return 'V1';
|
|
762
|
-
}
|
|
763
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
854
|
if (expectedProtocol === 'V2') {
|
|
767
855
|
if (await this.probeProtocolV2(path)) {
|
|
768
856
|
this.deviceProtocol.set(path, 'V2');
|
|
@@ -772,22 +860,27 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
772
860
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
773
861
|
}
|
|
774
862
|
|
|
775
|
-
if (
|
|
776
|
-
this.
|
|
777
|
-
|
|
778
|
-
|
|
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);
|
|
779
869
|
}
|
|
780
870
|
|
|
781
|
-
|
|
782
|
-
this.deviceProtocol.
|
|
783
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
|
|
784
|
-
return 'V1';
|
|
785
|
-
}
|
|
871
|
+
const probeOrder: ProtocolType[] =
|
|
872
|
+
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
786
873
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
+
}
|
|
791
884
|
}
|
|
792
885
|
|
|
793
886
|
this.deviceProtocol.delete(path);
|
|
@@ -798,6 +891,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
798
891
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
799
892
|
|
|
800
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);
|
|
801
898
|
await this.closeOpenDevice(path);
|
|
802
899
|
} catch (error) {
|
|
803
900
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -857,8 +954,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
857
954
|
try {
|
|
858
955
|
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
859
956
|
return true;
|
|
860
|
-
} catch (
|
|
861
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
957
|
+
} catch (_error) {
|
|
862
958
|
return false;
|
|
863
959
|
}
|
|
864
960
|
}
|
|
@@ -899,7 +995,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
899
995
|
if (this.cancelled) {
|
|
900
996
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
901
997
|
}
|
|
902
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
998
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
903
999
|
}
|
|
904
1000
|
|
|
905
1001
|
protected async readProtocolV2UsbPacket(
|
|
@@ -911,7 +1007,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
911
1007
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
912
1008
|
}
|
|
913
1009
|
try {
|
|
914
|
-
const packet = await transferInOnce(
|
|
1010
|
+
const packet = await this.transferInOnce(
|
|
1011
|
+
path,
|
|
915
1012
|
this.getOpenDevice(path).epIn,
|
|
916
1013
|
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
917
1014
|
);
|
|
@@ -927,6 +1024,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
927
1024
|
}
|
|
928
1025
|
|
|
929
1026
|
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1027
|
+
await this.cancelActiveTransfers(path);
|
|
930
1028
|
await this.closeOpenDevice(path);
|
|
931
1029
|
}
|
|
932
1030
|
|
|
@@ -936,7 +1034,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
936
1034
|
}
|
|
937
1035
|
|
|
938
1036
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
939
|
-
return new
|
|
1037
|
+
return new ProtocolV2LinkError(
|
|
1038
|
+
'response-timeout',
|
|
1039
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1040
|
+
);
|
|
940
1041
|
}
|
|
941
1042
|
|
|
942
1043
|
private async callProtocolV2(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|