@onekeyfe/hd-transport-usb 1.2.0-alpha.11 → 1.2.0-alpha.110
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 +141 -69
- 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 +175 -80
- 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;AAGhC,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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAAS,EAAE,EAQhB,0BAA0B,EAE3B,MAAM,wBAAwB,CAAC;AAGhC,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;YA0BlB,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,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
303
386
|
});
|
|
304
387
|
}
|
|
305
388
|
call(path, name, data, options) {
|
|
306
|
-
var _a, _b;
|
|
307
389
|
return __awaiter(this, void 0, void 0, function* () {
|
|
308
390
|
this.cancelled = false;
|
|
309
391
|
if (!this.messages) {
|
|
@@ -316,12 +398,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
316
398
|
if (!protocol) {
|
|
317
399
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${path}`);
|
|
318
400
|
}
|
|
319
|
-
if (transport.LogBlockCommand.has(name)) {
|
|
320
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
|
|
321
|
-
}
|
|
322
|
-
else {
|
|
323
|
-
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data, ' protocol: ', protocol);
|
|
324
|
-
}
|
|
325
401
|
if (protocol === 'V2') {
|
|
326
402
|
return this.callProtocolV2(path, name, data, options);
|
|
327
403
|
}
|
|
@@ -345,8 +421,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
345
421
|
});
|
|
346
422
|
}
|
|
347
423
|
cancel() {
|
|
348
|
-
var _a;
|
|
349
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport cancel');
|
|
350
424
|
this.cancelled = true;
|
|
351
425
|
}
|
|
352
426
|
getOpenDevice(path) {
|
|
@@ -369,6 +443,9 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
369
443
|
}
|
|
370
444
|
isRetryableError(error) {
|
|
371
445
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
446
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
372
449
|
return (message.includes('libusb') ||
|
|
373
450
|
message.includes('transfer') ||
|
|
374
451
|
message.includes('disconnected') ||
|
|
@@ -437,7 +514,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
437
514
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
438
515
|
packet[0] = REPORT_ID;
|
|
439
516
|
packet.set(new Uint8Array(buffer), 1);
|
|
440
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
517
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
441
518
|
}
|
|
442
519
|
return;
|
|
443
520
|
}
|
|
@@ -470,7 +547,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
470
547
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
471
548
|
}
|
|
472
549
|
try {
|
|
473
|
-
return yield transferInOnce(currentDev.epIn, length);
|
|
550
|
+
return yield this.transferInOnce(path, currentDev.epIn, length);
|
|
474
551
|
}
|
|
475
552
|
catch (error) {
|
|
476
553
|
lastError = error;
|
|
@@ -529,7 +606,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
529
606
|
}
|
|
530
607
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
531
608
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
532
|
-
yield this.drainStaleInput(epIn);
|
|
609
|
+
yield this.drainStaleInput(path, epIn);
|
|
533
610
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
534
611
|
}
|
|
535
612
|
catch (err) {
|
|
@@ -542,14 +619,14 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
542
619
|
}
|
|
543
620
|
});
|
|
544
621
|
}
|
|
545
|
-
drainStaleInput(epIn) {
|
|
622
|
+
drainStaleInput(path, epIn) {
|
|
546
623
|
return __awaiter(this, void 0, void 0, function* () {
|
|
547
624
|
const originalTimeout = epIn.timeout;
|
|
548
625
|
epIn.timeout = 50;
|
|
549
626
|
try {
|
|
550
627
|
for (let index = 0; index < 16; index += 1) {
|
|
551
628
|
try {
|
|
552
|
-
yield transferInOnce(epIn, PACKET_SIZE);
|
|
629
|
+
yield this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
553
630
|
}
|
|
554
631
|
catch (_a) {
|
|
555
632
|
break;
|
|
@@ -567,39 +644,34 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
567
644
|
createProtocolDetectionError() {
|
|
568
645
|
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
646
|
}
|
|
570
|
-
detectProtocol(path, expectedProtocol) {
|
|
571
|
-
var _a
|
|
647
|
+
detectProtocol(path, expectedProtocol, protocolHint) {
|
|
648
|
+
var _a;
|
|
572
649
|
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
650
|
if (expectedProtocol === 'V2') {
|
|
582
651
|
if (yield this.probeProtocolV2(path)) {
|
|
583
652
|
this.deviceProtocol.set(path, 'V2');
|
|
584
|
-
(
|
|
653
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V2 (expected)`);
|
|
585
654
|
return 'V2';
|
|
586
655
|
}
|
|
587
656
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
588
657
|
}
|
|
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';
|
|
658
|
+
if (expectedProtocol === 'V1') {
|
|
659
|
+
if (yield this.probeProtocolV1(path)) {
|
|
660
|
+
this.deviceProtocol.set(path, 'V1');
|
|
661
|
+
return 'V1';
|
|
662
|
+
}
|
|
663
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
598
664
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
(
|
|
602
|
-
|
|
665
|
+
const probeOrder = protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
666
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
667
|
+
if (index > 0) {
|
|
668
|
+
yield this.resetConnectionAfterProbe(path);
|
|
669
|
+
}
|
|
670
|
+
const detected = protocol === 'V1' ? yield this.probeProtocolV1(path) : yield this.probeProtocolV2(path);
|
|
671
|
+
if (detected) {
|
|
672
|
+
this.deviceProtocol.set(path, protocol);
|
|
673
|
+
return protocol;
|
|
674
|
+
}
|
|
603
675
|
}
|
|
604
676
|
this.deviceProtocol.delete(path);
|
|
605
677
|
throw this.createProtocolDetectionError();
|
|
@@ -610,6 +682,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
610
682
|
return __awaiter(this, void 0, void 0, function* () {
|
|
611
683
|
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
612
684
|
try {
|
|
685
|
+
yield this.cancelActiveTransfers(path);
|
|
613
686
|
yield this.closeOpenDevice(path);
|
|
614
687
|
}
|
|
615
688
|
catch (error) {
|
|
@@ -657,7 +730,6 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
657
730
|
});
|
|
658
731
|
}
|
|
659
732
|
probeProtocolV1(path) {
|
|
660
|
-
var _a;
|
|
661
733
|
return __awaiter(this, void 0, void 0, function* () {
|
|
662
734
|
if (!this.messages) {
|
|
663
735
|
return false;
|
|
@@ -666,8 +738,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
666
738
|
yield this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
667
739
|
return true;
|
|
668
740
|
}
|
|
669
|
-
catch (
|
|
670
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
741
|
+
catch (_error) {
|
|
671
742
|
return false;
|
|
672
743
|
}
|
|
673
744
|
});
|
|
@@ -703,7 +774,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
703
774
|
if (this.cancelled) {
|
|
704
775
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
705
776
|
}
|
|
706
|
-
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
777
|
+
yield this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
707
778
|
});
|
|
708
779
|
}
|
|
709
780
|
readProtocolV2UsbPacket(path, _context) {
|
|
@@ -713,7 +784,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
713
784
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
714
785
|
}
|
|
715
786
|
try {
|
|
716
|
-
const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
787
|
+
const packet = yield this.transferInOnce(path, this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
717
788
|
return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
718
789
|
}
|
|
719
790
|
catch (error) {
|
|
@@ -726,6 +797,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
726
797
|
}
|
|
727
798
|
resetProtocolV2UsbNativeLink(path, _reason) {
|
|
728
799
|
return __awaiter(this, void 0, void 0, function* () {
|
|
800
|
+
yield this.cancelActiveTransfers(path);
|
|
729
801
|
yield this.closeOpenDevice(path);
|
|
730
802
|
});
|
|
731
803
|
}
|
|
@@ -735,7 +807,7 @@ class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
|
735
807
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
736
808
|
}
|
|
737
809
|
createProtocolV2UsbTimeoutError(name, timeoutMs) {
|
|
738
|
-
return new
|
|
810
|
+
return new transport.ProtocolV2LinkError('response-timeout', `Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
739
811
|
}
|
|
740
812
|
callProtocolV2(path, name, data, options) {
|
|
741
813
|
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.110",
|
|
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.110",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.110",
|
|
25
26
|
"bytebuffer": "^5.0.1",
|
|
26
27
|
"usb": "^2.14.0"
|
|
27
28
|
},
|
|
28
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "6b487cbc8186019cf5d659cbe2fa26299e4e1aa8"
|
|
29
30
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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';
|
|
@@ -130,31 +130,6 @@ function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
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
133
|
/**
|
|
159
134
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
160
135
|
*/
|
|
@@ -187,6 +162,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
187
162
|
/** Protobuf schema for Protocol V2 transports. */
|
|
188
163
|
messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
189
164
|
|
|
165
|
+
private protocolV2SchemaSource: string | undefined;
|
|
166
|
+
|
|
190
167
|
name = 'NodeUsbTransport';
|
|
191
168
|
|
|
192
169
|
version = '';
|
|
@@ -211,6 +188,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
211
188
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
212
189
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
213
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Retain the low-level Transfer so release()/stop() can cancel native pending reads.
|
|
193
|
+
* Endpoint.transfer() hides it and may keep the CLI alive after output completes.
|
|
194
|
+
*/
|
|
195
|
+
private activeTransfers = new Map<
|
|
196
|
+
usb.Transfer,
|
|
197
|
+
{ path: string; settled: Promise<void>; resolveSettled: () => void }
|
|
198
|
+
>();
|
|
199
|
+
|
|
214
200
|
/** set to true when cancel() is called; checked by retry loops */
|
|
215
201
|
private cancelled = false;
|
|
216
202
|
|
|
@@ -240,21 +226,47 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
240
226
|
}
|
|
241
227
|
|
|
242
228
|
configureProtocolV2(signedData: any) {
|
|
229
|
+
const schemaSource =
|
|
230
|
+
typeof signedData === 'string'
|
|
231
|
+
? signedData
|
|
232
|
+
: JSON.stringify(signedData) ?? String(signedData);
|
|
233
|
+
if (schemaSource === this.protocolV2SchemaSource) return;
|
|
234
|
+
|
|
235
|
+
const hadProtocolV2Schema = this.protocolV2SchemaSource !== undefined;
|
|
243
236
|
this.messagesV2 = parseConfigure(signedData);
|
|
237
|
+
this.protocolV2SchemaSource = schemaSource;
|
|
238
|
+
if (!hadProtocolV2Schema) return;
|
|
239
|
+
|
|
244
240
|
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
245
241
|
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
246
242
|
);
|
|
247
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
|
|
248
243
|
}
|
|
249
244
|
|
|
250
245
|
listen() {
|
|
251
246
|
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
252
247
|
}
|
|
253
248
|
|
|
254
|
-
stop() {
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
);
|
|
249
|
+
async stop(): Promise<void> {
|
|
250
|
+
this.cancelled = true;
|
|
251
|
+
|
|
252
|
+
await this.cancelActiveTransfers();
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await this.disposeProtocolV2UsbLinks('Node USB transport stopped');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Reconnect may already be in flight when dispose starts. Wait for it to
|
|
261
|
+
// settle, then close every remaining handle so it cannot reopen USB after
|
|
262
|
+
// the first cleanup pass.
|
|
263
|
+
await Promise.allSettled(this.reconnectLocks.values());
|
|
264
|
+
await this.cancelActiveTransfers();
|
|
265
|
+
await Promise.all(Array.from(this.openDevices.keys(), path => this.closeOpenDevice(path)));
|
|
266
|
+
|
|
267
|
+
this.reconnectLocks.clear();
|
|
268
|
+
this.deviceProtocol.clear();
|
|
269
|
+
this.serialToBusId.clear();
|
|
258
270
|
}
|
|
259
271
|
|
|
260
272
|
/**
|
|
@@ -332,10 +344,15 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
332
344
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
333
345
|
await this.closeOpenDevice(path);
|
|
334
346
|
await this.openDevice(path);
|
|
335
|
-
await this.detectProtocol(path, input.expectedProtocol);
|
|
347
|
+
await this.detectProtocol(path, input.expectedProtocol, input.protocolHint);
|
|
336
348
|
return path;
|
|
337
349
|
} catch (error: any) {
|
|
338
350
|
this.Log?.debug('NodeUsbTransport acquire error: ', error);
|
|
351
|
+
try {
|
|
352
|
+
await this.release(path);
|
|
353
|
+
} catch (cleanupError) {
|
|
354
|
+
this.Log?.debug('NodeUsbTransport acquire cleanup error: ', cleanupError);
|
|
355
|
+
}
|
|
339
356
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
340
357
|
}
|
|
341
358
|
}
|
|
@@ -344,11 +361,93 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
344
361
|
* Release device — release interface and close.
|
|
345
362
|
*/
|
|
346
363
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
364
|
+
await this.cancelActiveTransfers(path);
|
|
347
365
|
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
348
366
|
await this.closeOpenDevice(path);
|
|
349
367
|
this.deviceProtocol.delete(path);
|
|
350
368
|
}
|
|
351
369
|
|
|
370
|
+
private async cancelActiveTransfers(path?: string): Promise<void> {
|
|
371
|
+
const transfers = Array.from(this.activeTransfers.entries()).filter(
|
|
372
|
+
([, active]) => path === undefined || active.path === path
|
|
373
|
+
);
|
|
374
|
+
transfers.forEach(([transfer]) => {
|
|
375
|
+
try {
|
|
376
|
+
transfer.cancel();
|
|
377
|
+
} catch {
|
|
378
|
+
// A transfer may finish between the snapshot and cancel().
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
await Promise.allSettled(transfers.map(([, active]) => active.settled));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private createTrackedTransfer(
|
|
385
|
+
path: string,
|
|
386
|
+
endpoint: usb.InEndpoint | usb.OutEndpoint,
|
|
387
|
+
callback: (error: Error | undefined, buffer: Buffer, actualLength: number) => void
|
|
388
|
+
): usb.Transfer {
|
|
389
|
+
let resolveSettled: () => void = () => undefined;
|
|
390
|
+
const settled = new Promise<void>(resolve => {
|
|
391
|
+
resolveSettled = resolve;
|
|
392
|
+
});
|
|
393
|
+
const transfer = endpoint.makeTransfer(endpoint.timeout, (error, buffer, actualLength) => {
|
|
394
|
+
this.activeTransfers.delete(transfer);
|
|
395
|
+
resolveSettled();
|
|
396
|
+
callback(error, buffer, actualLength);
|
|
397
|
+
});
|
|
398
|
+
this.activeTransfers.set(transfer, { path, settled, resolveSettled });
|
|
399
|
+
return transfer;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private transferInOnce(path: string, ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
403
|
+
return new Promise((resolve, reject) => {
|
|
404
|
+
const buffer = Buffer.alloc(length);
|
|
405
|
+
const transfer = this.createTrackedTransfer(
|
|
406
|
+
path,
|
|
407
|
+
ep,
|
|
408
|
+
(error, _submittedBuffer, actualLength) => {
|
|
409
|
+
if (error) {
|
|
410
|
+
reject(error);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (actualLength <= 0) {
|
|
414
|
+
reject(new Error('Empty USB transfer'));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
resolve(buffer.subarray(0, actualLength));
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
try {
|
|
421
|
+
transfer.submit(buffer);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
const active = this.activeTransfers.get(transfer);
|
|
424
|
+
this.activeTransfers.delete(transfer);
|
|
425
|
+
active?.resolveSettled();
|
|
426
|
+
reject(error);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private transferOutOnce(path: string, ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const transfer = this.createTrackedTransfer(path, ep, error => {
|
|
434
|
+
if (error) {
|
|
435
|
+
reject(error);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
resolve();
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
transfer.submit(data);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const active = this.activeTransfers.get(transfer);
|
|
444
|
+
this.activeTransfers.delete(transfer);
|
|
445
|
+
active?.resolveSettled();
|
|
446
|
+
reject(error);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
352
451
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
353
452
|
const openDev = this.openDevices.get(path);
|
|
354
453
|
if (!openDev) return;
|
|
@@ -401,20 +500,6 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
401
500
|
`Device protocol has not been detected for ${path}`
|
|
402
501
|
);
|
|
403
502
|
}
|
|
404
|
-
if (LogBlockCommand.has(name)) {
|
|
405
|
-
this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' protocol: ', protocol);
|
|
406
|
-
} else {
|
|
407
|
-
this.Log?.debug(
|
|
408
|
-
'NodeUsbTransport call-',
|
|
409
|
-
' name: ',
|
|
410
|
-
name,
|
|
411
|
-
' data: ',
|
|
412
|
-
data,
|
|
413
|
-
' protocol: ',
|
|
414
|
-
protocol
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
503
|
if (protocol === 'V2') {
|
|
419
504
|
return this.callProtocolV2(path, name, data, options);
|
|
420
505
|
}
|
|
@@ -449,7 +534,6 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
449
534
|
}
|
|
450
535
|
|
|
451
536
|
cancel() {
|
|
452
|
-
this.Log?.debug('NodeUsbTransport cancel');
|
|
453
537
|
this.cancelled = true;
|
|
454
538
|
}
|
|
455
539
|
|
|
@@ -479,6 +563,12 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
479
563
|
|
|
480
564
|
private isRetryableError(error: unknown): boolean {
|
|
481
565
|
const message = this.getErrorMessage(error).toLowerCase();
|
|
566
|
+
// cancelActiveTransfers() terminates timed-out or releasing native requests.
|
|
567
|
+
// LIBUSB_TRANSFER_CANCELLED is not transient. Retrying would start another pending
|
|
568
|
+
// read on the rebuilt interface and race protocol probing against release.
|
|
569
|
+
if (message.includes('cancelled') || message.includes('canceled')) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
482
572
|
return (
|
|
483
573
|
message.includes('libusb') ||
|
|
484
574
|
message.includes('transfer') ||
|
|
@@ -583,7 +673,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
583
673
|
const packet = new Uint8Array(PACKET_SIZE);
|
|
584
674
|
packet[0] = REPORT_ID;
|
|
585
675
|
packet.set(new Uint8Array(buffer), 1);
|
|
586
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
676
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(packet));
|
|
587
677
|
}
|
|
588
678
|
return; // all chunks sent successfully
|
|
589
679
|
} catch (error) {
|
|
@@ -626,7 +716,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
626
716
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
627
717
|
}
|
|
628
718
|
try {
|
|
629
|
-
return await transferInOnce(currentDev.epIn, length);
|
|
719
|
+
return await this.transferInOnce(path, currentDev.epIn, length);
|
|
630
720
|
} catch (error) {
|
|
631
721
|
lastError = error;
|
|
632
722
|
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
@@ -706,7 +796,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
706
796
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
707
797
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
708
798
|
|
|
709
|
-
await this.drainStaleInput(epIn);
|
|
799
|
+
await this.drainStaleInput(path, epIn);
|
|
710
800
|
|
|
711
801
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
712
802
|
} catch (err) {
|
|
@@ -719,14 +809,14 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
719
809
|
}
|
|
720
810
|
}
|
|
721
811
|
|
|
722
|
-
private async drainStaleInput(epIn: usb.InEndpoint): Promise<void> {
|
|
812
|
+
private async drainStaleInput(path: string, epIn: usb.InEndpoint): Promise<void> {
|
|
723
813
|
const originalTimeout = epIn.timeout;
|
|
724
814
|
epIn.timeout = 50;
|
|
725
815
|
try {
|
|
726
816
|
// Drain a small bounded number of packets left by the previous USB session.
|
|
727
817
|
for (let index = 0; index < 16; index += 1) {
|
|
728
818
|
try {
|
|
729
|
-
await transferInOnce(epIn, PACKET_SIZE);
|
|
819
|
+
await this.transferInOnce(path, epIn, PACKET_SIZE);
|
|
730
820
|
} catch {
|
|
731
821
|
break;
|
|
732
822
|
}
|
|
@@ -752,17 +842,9 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
752
842
|
|
|
753
843
|
private async detectProtocol(
|
|
754
844
|
path: string,
|
|
755
|
-
expectedProtocol?: ProtocolType
|
|
845
|
+
expectedProtocol?: ProtocolType,
|
|
846
|
+
protocolHint?: ProtocolType
|
|
756
847
|
): 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
848
|
if (expectedProtocol === 'V2') {
|
|
767
849
|
if (await this.probeProtocolV2(path)) {
|
|
768
850
|
this.deviceProtocol.set(path, 'V2');
|
|
@@ -772,22 +854,27 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
772
854
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
773
855
|
}
|
|
774
856
|
|
|
775
|
-
if (
|
|
776
|
-
this.
|
|
777
|
-
|
|
778
|
-
|
|
857
|
+
if (expectedProtocol === 'V1') {
|
|
858
|
+
if (await this.probeProtocolV1(path)) {
|
|
859
|
+
this.deviceProtocol.set(path, 'V1');
|
|
860
|
+
return 'V1';
|
|
861
|
+
}
|
|
862
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
779
863
|
}
|
|
780
864
|
|
|
781
|
-
|
|
782
|
-
this.deviceProtocol.
|
|
783
|
-
this.Log?.debug(`[NodeUsbTransport] detectProtocol: path=${path} -> V1`);
|
|
784
|
-
return 'V1';
|
|
785
|
-
}
|
|
865
|
+
const probeOrder: ProtocolType[] =
|
|
866
|
+
protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
786
867
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
868
|
+
for (const [index, protocol] of probeOrder.entries()) {
|
|
869
|
+
if (index > 0) {
|
|
870
|
+
await this.resetConnectionAfterProbe(path);
|
|
871
|
+
}
|
|
872
|
+
const detected =
|
|
873
|
+
protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path);
|
|
874
|
+
if (detected) {
|
|
875
|
+
this.deviceProtocol.set(path, protocol);
|
|
876
|
+
return protocol;
|
|
877
|
+
}
|
|
791
878
|
}
|
|
792
879
|
|
|
793
880
|
this.deviceProtocol.delete(path);
|
|
@@ -798,6 +885,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
798
885
|
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
799
886
|
|
|
800
887
|
try {
|
|
888
|
+
// A timed-out probe may leave an IN transfer pending. Cancel and await it before
|
|
889
|
+
// closing the interface, or libusb may never callback and release()/stop() will
|
|
890
|
+
// wait forever, surfacing as Polling timeout (809).
|
|
891
|
+
await this.cancelActiveTransfers(path);
|
|
801
892
|
await this.closeOpenDevice(path);
|
|
802
893
|
} catch (error) {
|
|
803
894
|
this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
@@ -857,8 +948,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
857
948
|
try {
|
|
858
949
|
await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT });
|
|
859
950
|
return true;
|
|
860
|
-
} catch (
|
|
861
|
-
this.Log?.debug('[NodeUsbTransport] Protocol V1 Initialize probe failed:', error);
|
|
951
|
+
} catch (_error) {
|
|
862
952
|
return false;
|
|
863
953
|
}
|
|
864
954
|
}
|
|
@@ -899,7 +989,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
899
989
|
if (this.cancelled) {
|
|
900
990
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
901
991
|
}
|
|
902
|
-
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
992
|
+
await this.transferOutOnce(path, this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
903
993
|
}
|
|
904
994
|
|
|
905
995
|
protected async readProtocolV2UsbPacket(
|
|
@@ -911,7 +1001,8 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
911
1001
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
912
1002
|
}
|
|
913
1003
|
try {
|
|
914
|
-
const packet = await transferInOnce(
|
|
1004
|
+
const packet = await this.transferInOnce(
|
|
1005
|
+
path,
|
|
915
1006
|
this.getOpenDevice(path).epIn,
|
|
916
1007
|
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
917
1008
|
);
|
|
@@ -927,6 +1018,7 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
927
1018
|
}
|
|
928
1019
|
|
|
929
1020
|
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
1021
|
+
await this.cancelActiveTransfers(path);
|
|
930
1022
|
await this.closeOpenDevice(path);
|
|
931
1023
|
}
|
|
932
1024
|
|
|
@@ -936,7 +1028,10 @@ export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string>
|
|
|
936
1028
|
}
|
|
937
1029
|
|
|
938
1030
|
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
939
|
-
return new
|
|
1031
|
+
return new ProtocolV2LinkError(
|
|
1032
|
+
'response-timeout',
|
|
1033
|
+
`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`
|
|
1034
|
+
);
|
|
940
1035
|
}
|
|
941
1036
|
|
|
942
1037
|
private async callProtocolV2(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTransportCallLog, shouldSuppressHighVolumeCallLog } from '@onekeyfe/hd-transport';
|