@onekeyfe/hd-transport-usb 1.2.0-alpha.1 → 1.2.0-alpha.11
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 +260 -0
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -127
- package/package.json +4 -4
- package/src/index.ts +99 -163
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import transportPackage, { PROTOCOL_V2_CHANNEL_USB, ProtocolV2 } from '@onekeyfe/hd-transport';
|
|
2
|
+
|
|
3
|
+
import NodeUsbTransport from '../src';
|
|
4
|
+
|
|
5
|
+
let mockUsbDevices: any[] = [];
|
|
6
|
+
|
|
7
|
+
jest.mock('usb', () => ({
|
|
8
|
+
getDeviceList: jest.fn(() => mockUsbDevices),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { parseConfigure } = transportPackage;
|
|
12
|
+
|
|
13
|
+
const protocolV1Schema = {
|
|
14
|
+
nested: {
|
|
15
|
+
Initialize: { fields: {} },
|
|
16
|
+
Success: {
|
|
17
|
+
fields: {
|
|
18
|
+
message: { type: 'string', id: 1 },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
MessageType: {
|
|
22
|
+
values: {
|
|
23
|
+
MessageType_Initialize: 1,
|
|
24
|
+
MessageType_Success: 2,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const protocolV2Schema = {
|
|
31
|
+
nested: {
|
|
32
|
+
Ping: {
|
|
33
|
+
fields: {
|
|
34
|
+
message: { type: 'string', id: 1 },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
Success: {
|
|
38
|
+
fields: {
|
|
39
|
+
message: { type: 'string', id: 1 },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
MessageType: {
|
|
43
|
+
values: {
|
|
44
|
+
MessageType_Ping: 60206,
|
|
45
|
+
MessageType_Success: 60207,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const schemas = {
|
|
52
|
+
protocolV1: parseConfigure(protocolV1Schema),
|
|
53
|
+
protocolV2: parseConfigure(protocolV2Schema),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type PendingRead = {
|
|
57
|
+
started: Promise<void>;
|
|
58
|
+
fail: (error?: Error) => void;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createHarness = () => {
|
|
62
|
+
const path = '6136';
|
|
63
|
+
const responseQueue: Buffer[] = [];
|
|
64
|
+
const sentSeqs: number[] = [];
|
|
65
|
+
let writeError: Error | undefined;
|
|
66
|
+
let holdNextRead:
|
|
67
|
+
| {
|
|
68
|
+
markStarted: () => void;
|
|
69
|
+
started: Promise<void>;
|
|
70
|
+
callback?: (error?: Error, data?: Buffer) => void;
|
|
71
|
+
}
|
|
72
|
+
| undefined;
|
|
73
|
+
|
|
74
|
+
const epIn = {
|
|
75
|
+
direction: 'in',
|
|
76
|
+
address: 0x81,
|
|
77
|
+
timeout: 30_000,
|
|
78
|
+
transfer: jest.fn((_length: number, callback: (error?: Error, data?: Buffer) => void) => {
|
|
79
|
+
if (epIn.timeout === 50) {
|
|
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);
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const epOut = {
|
|
100
|
+
direction: 'out',
|
|
101
|
+
address: 0x01,
|
|
102
|
+
timeout: 30_000,
|
|
103
|
+
transfer: jest.fn((data: Buffer, callback: (error?: Error) => void) => {
|
|
104
|
+
const seq = data[6];
|
|
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();
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const iface = {
|
|
127
|
+
descriptor: { bInterfaceClass: 0xff, bInterfaceNumber: 0 },
|
|
128
|
+
endpoints: [epIn, epOut],
|
|
129
|
+
claim: jest.fn(),
|
|
130
|
+
release: jest.fn((callback: () => void) => callback()),
|
|
131
|
+
isKernelDriverActive: jest.fn(() => false),
|
|
132
|
+
detachKernelDriver: jest.fn(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const device = {
|
|
136
|
+
busNumber: 1,
|
|
137
|
+
deviceAddress: 2,
|
|
138
|
+
timeout: 30_000,
|
|
139
|
+
deviceDescriptor: {
|
|
140
|
+
idVendor: 0x1209,
|
|
141
|
+
idProduct: 0x4f4a,
|
|
142
|
+
iSerialNumber: 1,
|
|
143
|
+
},
|
|
144
|
+
interfaces: [iface],
|
|
145
|
+
open: jest.fn(),
|
|
146
|
+
close: jest.fn(),
|
|
147
|
+
getStringDescriptor: jest.fn(
|
|
148
|
+
(_index: number, callback: (error?: Error, value?: string) => void) =>
|
|
149
|
+
callback(undefined, path)
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
mockUsbDevices = [device];
|
|
154
|
+
const transport = new NodeUsbTransport();
|
|
155
|
+
transport.init({ debug: jest.fn(), error: jest.fn() });
|
|
156
|
+
transport.configure(protocolV1Schema);
|
|
157
|
+
transport.configureProtocolV2(protocolV2Schema);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
transport,
|
|
161
|
+
path,
|
|
162
|
+
device,
|
|
163
|
+
iface,
|
|
164
|
+
epIn,
|
|
165
|
+
epOut,
|
|
166
|
+
sentSeqs,
|
|
167
|
+
async acquire() {
|
|
168
|
+
await transport.enumerate();
|
|
169
|
+
await transport.acquire({ path, expectedProtocol: 'V2' });
|
|
170
|
+
},
|
|
171
|
+
failNextWrite(error: Error) {
|
|
172
|
+
writeError = error;
|
|
173
|
+
},
|
|
174
|
+
holdRead(): PendingRead {
|
|
175
|
+
let markStarted: () => void = () => undefined;
|
|
176
|
+
const started = new Promise<void>(resolve => {
|
|
177
|
+
markStarted = resolve;
|
|
178
|
+
});
|
|
179
|
+
const pending = { markStarted, started, callback: undefined };
|
|
180
|
+
holdNextRead = pending;
|
|
181
|
+
return {
|
|
182
|
+
started,
|
|
183
|
+
fail(error = new Error('read released after test')) {
|
|
184
|
+
pending.callback?.(error);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
describe('NodeUsbTransport Protocol V2 link lifecycle', () => {
|
|
192
|
+
test('keeps seq across probe, call and reacquire', async () => {
|
|
193
|
+
const harness = createHarness();
|
|
194
|
+
const { transport, path, sentSeqs } = harness;
|
|
195
|
+
|
|
196
|
+
await harness.acquire();
|
|
197
|
+
await transport.call(path, 'Ping', { message: 'first' });
|
|
198
|
+
await transport.release(path);
|
|
199
|
+
await harness.acquire();
|
|
200
|
+
await transport.call(path, 'Ping', { message: 'second' });
|
|
201
|
+
|
|
202
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
203
|
+
await transport.release(path);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('does not resend a Protocol V2 frame after transferOut fails', async () => {
|
|
207
|
+
const harness = createHarness();
|
|
208
|
+
const { transport, path, epOut } = harness;
|
|
209
|
+
await harness.acquire();
|
|
210
|
+
epOut.transfer.mockClear();
|
|
211
|
+
harness.failNextWrite(new Error('LIBUSB_ERROR_IO'));
|
|
212
|
+
|
|
213
|
+
await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow(
|
|
214
|
+
'LIBUSB_ERROR_IO'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(epOut.transfer).toHaveBeenCalledTimes(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('rejects a pending read when release invalidates the link', async () => {
|
|
221
|
+
const harness = createHarness();
|
|
222
|
+
const { transport, path } = harness;
|
|
223
|
+
await harness.acquire();
|
|
224
|
+
const pendingRead = harness.holdRead();
|
|
225
|
+
|
|
226
|
+
const call = transport.call(path, 'Ping', { message: 'pending' }, { timeoutMs: 5000 });
|
|
227
|
+
const outcome = call.then(
|
|
228
|
+
() => 'resolved',
|
|
229
|
+
error => error.message
|
|
230
|
+
);
|
|
231
|
+
await pendingRead.started;
|
|
232
|
+
await transport.release(path);
|
|
233
|
+
const settled = await Promise.race([
|
|
234
|
+
outcome,
|
|
235
|
+
new Promise<string>(resolve => {
|
|
236
|
+
setTimeout(() => resolve('still pending'), 50);
|
|
237
|
+
}),
|
|
238
|
+
]);
|
|
239
|
+
pendingRead.fail();
|
|
240
|
+
|
|
241
|
+
expect(settled).not.toBe('still pending');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('keeps the cursor after a response timeout rebuilds the USB connection', async () => {
|
|
245
|
+
const harness = createHarness();
|
|
246
|
+
const { transport, path, sentSeqs } = harness;
|
|
247
|
+
await harness.acquire();
|
|
248
|
+
const pendingRead = harness.holdRead();
|
|
249
|
+
|
|
250
|
+
await expect(
|
|
251
|
+
transport.call(path, 'Ping', { message: 'timeout' }, { timeoutMs: 20 })
|
|
252
|
+
).rejects.toThrow('20ms');
|
|
253
|
+
pendingRead.fail();
|
|
254
|
+
await harness.acquire();
|
|
255
|
+
await transport.call(path, 'Ping', { message: 'after-timeout' });
|
|
256
|
+
|
|
257
|
+
expect(sentSeqs).toEqual([1, 2, 3, 4]);
|
|
258
|
+
await transport.release(path);
|
|
259
|
+
});
|
|
260
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as transport from '@onekeyfe/hd-transport';
|
|
2
|
-
import transport__default, { OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolType } from '@onekeyfe/hd-transport';
|
|
2
|
+
import transport__default, { ProtocolV2UsbTransportBase, OneKeyDeviceInfo, AcquireInput, TransportCallOptions, ProtocolV2Schemas, ProtocolV2CallContext, ProtocolType } from '@onekeyfe/hd-transport';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
4
|
|
|
5
|
-
declare class NodeUsbTransport {
|
|
5
|
+
declare class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
6
6
|
messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
7
7
|
messagesV2: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
8
8
|
name: string;
|
|
@@ -14,11 +14,9 @@ declare class NodeUsbTransport {
|
|
|
14
14
|
private serialToBusId;
|
|
15
15
|
private openDevices;
|
|
16
16
|
private deviceProtocol;
|
|
17
|
-
private protocolV2Assemblers;
|
|
18
|
-
private protocolV2Sessions;
|
|
19
|
-
private protocolV2ReadTimeouts;
|
|
20
17
|
private reconnectLocks;
|
|
21
18
|
private cancelled;
|
|
19
|
+
constructor();
|
|
22
20
|
init(logger: any, emitter?: EventEmitter): Promise<string>;
|
|
23
21
|
configure(signedData: any): Promise<void>;
|
|
24
22
|
configureProtocolV2(signedData: any): void;
|
|
@@ -41,6 +39,7 @@ declare class NodeUsbTransport {
|
|
|
41
39
|
private getOpenDevice;
|
|
42
40
|
private getErrorMessage;
|
|
43
41
|
private isRetryableError;
|
|
42
|
+
private isUsbTransferTimeout;
|
|
44
43
|
private getDeviceInterface;
|
|
45
44
|
private reconnectForRetry;
|
|
46
45
|
private sendAllChunksWithRetry;
|
|
@@ -54,8 +53,13 @@ declare class NodeUsbTransport {
|
|
|
54
53
|
private withProtocolReadTimeout;
|
|
55
54
|
private probeProtocolV1;
|
|
56
55
|
private probeProtocolV2;
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas;
|
|
57
|
+
protected getProtocolV2UsbLogger(): any;
|
|
58
|
+
protected writeProtocolV2UsbPacket(path: string, frame: Uint8Array, _context: ProtocolV2CallContext): Promise<void>;
|
|
59
|
+
protected readProtocolV2UsbPacket(path: string, _context: ProtocolV2CallContext): Promise<Uint8Array>;
|
|
60
|
+
protected resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void>;
|
|
61
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string): void;
|
|
62
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
|
|
59
63
|
private callProtocolV2;
|
|
60
64
|
private receiveData;
|
|
61
65
|
getProtocolType(path: string): ProtocolType | undefined;
|
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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ const TRANSFER_TIMEOUT_MS = 30000;
|
|
|
71
71
|
const SERIAL_READ_TIMEOUT_MS = 5000;
|
|
72
72
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
73
73
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
74
|
-
const PROTOCOL_PROBE_TIMEOUT =
|
|
74
|
+
const PROTOCOL_PROBE_TIMEOUT = 5000;
|
|
75
75
|
function getBusId(dev) {
|
|
76
76
|
return `usb:${dev.busNumber}:${dev.deviceAddress}`;
|
|
77
77
|
}
|
|
@@ -152,26 +152,6 @@ function transferOutOnce(ep, data) {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
-
function resetUsbDevice(dev) {
|
|
156
|
-
return new Promise(resolve => {
|
|
157
|
-
const reset = dev.reset;
|
|
158
|
-
if (typeof reset !== 'function') {
|
|
159
|
-
resolve();
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
reset.call(dev, () => resolve());
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
function clearEndpointHalt(ep) {
|
|
166
|
-
return new Promise(resolve => {
|
|
167
|
-
const clearHalt = ep.clearHalt;
|
|
168
|
-
if (typeof clearHalt !== 'function') {
|
|
169
|
-
resolve();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
clearHalt.call(ep, () => resolve());
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
155
|
function skipReportByte(packet) {
|
|
176
156
|
if (packet[0] === REPORT_ID) {
|
|
177
157
|
return packet.subarray(1);
|
|
@@ -181,8 +161,13 @@ function skipReportByte(packet) {
|
|
|
181
161
|
function toArrayBuffer(buf) {
|
|
182
162
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
183
163
|
}
|
|
184
|
-
class NodeUsbTransport {
|
|
164
|
+
class NodeUsbTransport extends transport.ProtocolV2UsbTransportBase {
|
|
185
165
|
constructor() {
|
|
166
|
+
super({
|
|
167
|
+
router: transport.PROTOCOL_V2_CHANNEL_USB,
|
|
168
|
+
maxFrameBytes: transport.PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
169
|
+
logPrefix: 'ProtocolV2 NodeUSB',
|
|
170
|
+
});
|
|
186
171
|
this.name = 'NodeUsbTransport';
|
|
187
172
|
this.version = '';
|
|
188
173
|
this.configured = false;
|
|
@@ -190,9 +175,6 @@ class NodeUsbTransport {
|
|
|
190
175
|
this.serialToBusId = new Map();
|
|
191
176
|
this.openDevices = new Map();
|
|
192
177
|
this.deviceProtocol = new Map();
|
|
193
|
-
this.protocolV2Assemblers = new Map();
|
|
194
|
-
this.protocolV2Sessions = new Map();
|
|
195
|
-
this.protocolV2ReadTimeouts = new Map();
|
|
196
178
|
this.reconnectLocks = new Map();
|
|
197
179
|
this.cancelled = false;
|
|
198
180
|
}
|
|
@@ -210,13 +192,13 @@ class NodeUsbTransport {
|
|
|
210
192
|
configureProtocolV2(signedData) {
|
|
211
193
|
var _a;
|
|
212
194
|
this.messagesV2 = parseConfigure(signedData);
|
|
213
|
-
this.
|
|
214
|
-
this.protocolV2ReadTimeouts.clear();
|
|
195
|
+
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); });
|
|
215
196
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] Protocol V2 schema configured');
|
|
216
197
|
}
|
|
217
198
|
listen() {
|
|
218
199
|
}
|
|
219
200
|
stop() {
|
|
201
|
+
this.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error => { var _a; return (_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] stop link cleanup failed:', error); });
|
|
220
202
|
}
|
|
221
203
|
post(path, name, data) {
|
|
222
204
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -274,6 +256,7 @@ class NodeUsbTransport {
|
|
|
274
256
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
|
|
275
257
|
}
|
|
276
258
|
try {
|
|
259
|
+
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
277
260
|
yield this.closeOpenDevice(path);
|
|
278
261
|
yield this.openDevice(path);
|
|
279
262
|
yield this.detectProtocol(path, input.expectedProtocol);
|
|
@@ -287,9 +270,9 @@ class NodeUsbTransport {
|
|
|
287
270
|
}
|
|
288
271
|
release(path, _onclose) {
|
|
289
272
|
return __awaiter(this, void 0, void 0, function* () {
|
|
273
|
+
yield this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
290
274
|
yield this.closeOpenDevice(path);
|
|
291
275
|
this.deviceProtocol.delete(path);
|
|
292
|
-
this.protocolV2Assemblers.delete(path);
|
|
293
276
|
});
|
|
294
277
|
}
|
|
295
278
|
closeOpenDevice(path) {
|
|
@@ -397,6 +380,10 @@ class NodeUsbTransport {
|
|
|
397
380
|
message.includes('timeout') ||
|
|
398
381
|
message.includes('interrupt'));
|
|
399
382
|
}
|
|
383
|
+
isUsbTransferTimeout(error) {
|
|
384
|
+
const message = this.getErrorMessage(error).toLowerCase();
|
|
385
|
+
return message.includes('timeout') || message.includes('timed_out');
|
|
386
|
+
}
|
|
400
387
|
getDeviceInterface(dev) {
|
|
401
388
|
var _a;
|
|
402
389
|
const { interfaces } = dev;
|
|
@@ -416,6 +403,7 @@ class NodeUsbTransport {
|
|
|
416
403
|
var _a, _b;
|
|
417
404
|
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
|
|
418
405
|
yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
406
|
+
yield this.rotateProtocolV2UsbGeneration(path, `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`);
|
|
419
407
|
try {
|
|
420
408
|
yield this.closeOpenDevice(path);
|
|
421
409
|
}
|
|
@@ -472,7 +460,7 @@ class NodeUsbTransport {
|
|
|
472
460
|
throw lastError;
|
|
473
461
|
});
|
|
474
462
|
}
|
|
475
|
-
transferInWithRetry(path, openDev, length) {
|
|
463
|
+
transferInWithRetry(path, openDev, length, options) {
|
|
476
464
|
var _a;
|
|
477
465
|
return __awaiter(this, void 0, void 0, function* () {
|
|
478
466
|
let lastError;
|
|
@@ -486,17 +474,22 @@ class NodeUsbTransport {
|
|
|
486
474
|
}
|
|
487
475
|
catch (error) {
|
|
488
476
|
lastError = error;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
currentDev = yield this.reconnectForRetry(path, 'in', attempt, error);
|
|
477
|
+
if ((options === null || options === void 0 ? void 0 : options.waitIndefinitelyOnTimeout) && this.isUsbTransferTimeout(error)) {
|
|
478
|
+
attempt -= 1;
|
|
495
479
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
(
|
|
499
|
-
|
|
480
|
+
else {
|
|
481
|
+
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
482
|
+
if (!shouldRetry) {
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
currentDev = yield this.reconnectForRetry(path, 'in', attempt, error);
|
|
487
|
+
}
|
|
488
|
+
catch (reconnectError) {
|
|
489
|
+
lastError = reconnectError;
|
|
490
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
500
493
|
}
|
|
501
494
|
}
|
|
502
495
|
}
|
|
@@ -518,13 +511,6 @@ class NodeUsbTransport {
|
|
|
518
511
|
try {
|
|
519
512
|
dev.open();
|
|
520
513
|
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
521
|
-
yield resetUsbDevice(dev);
|
|
522
|
-
try {
|
|
523
|
-
dev.open();
|
|
524
|
-
}
|
|
525
|
-
catch (_d) {
|
|
526
|
-
}
|
|
527
|
-
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
528
514
|
const iface = this.getDeviceInterface(dev);
|
|
529
515
|
if (process.platform === 'linux') {
|
|
530
516
|
try {
|
|
@@ -532,7 +518,7 @@ class NodeUsbTransport {
|
|
|
532
518
|
iface.detachKernelDriver();
|
|
533
519
|
}
|
|
534
520
|
}
|
|
535
|
-
catch (
|
|
521
|
+
catch (_d) {
|
|
536
522
|
}
|
|
537
523
|
}
|
|
538
524
|
iface.claim();
|
|
@@ -543,8 +529,6 @@ class NodeUsbTransport {
|
|
|
543
529
|
}
|
|
544
530
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
545
531
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
546
|
-
yield clearEndpointHalt(epIn);
|
|
547
|
-
yield clearEndpointHalt(epOut);
|
|
548
532
|
yield this.drainStaleInput(epIn);
|
|
549
533
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
550
534
|
}
|
|
@@ -552,7 +536,7 @@ class NodeUsbTransport {
|
|
|
552
536
|
try {
|
|
553
537
|
dev.close();
|
|
554
538
|
}
|
|
555
|
-
catch (
|
|
539
|
+
catch (_e) {
|
|
556
540
|
}
|
|
557
541
|
throw err;
|
|
558
542
|
}
|
|
@@ -622,16 +606,14 @@ class NodeUsbTransport {
|
|
|
622
606
|
});
|
|
623
607
|
}
|
|
624
608
|
resetConnectionAfterProbe(path) {
|
|
625
|
-
var _a
|
|
609
|
+
var _a;
|
|
626
610
|
return __awaiter(this, void 0, void 0, function* () {
|
|
627
|
-
|
|
628
|
-
this.protocolV2Sessions.delete(path);
|
|
629
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
611
|
+
yield this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
630
612
|
try {
|
|
631
613
|
yield this.closeOpenDevice(path);
|
|
632
614
|
}
|
|
633
615
|
catch (error) {
|
|
634
|
-
(
|
|
616
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('[NodeUsbTransport] close after protocol probe error:', error);
|
|
635
617
|
}
|
|
636
618
|
yield this.enumerate();
|
|
637
619
|
yield this.openDevice(path);
|
|
@@ -704,96 +686,60 @@ class NodeUsbTransport {
|
|
|
704
686
|
});
|
|
705
687
|
});
|
|
706
688
|
}
|
|
707
|
-
|
|
708
|
-
|
|
689
|
+
getProtocolV2UsbSchemas() {
|
|
690
|
+
if (!this.messages || !this.messagesV2) {
|
|
691
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
protocolV1: this.messages,
|
|
695
|
+
protocolV2: this.messagesV2,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
getProtocolV2UsbLogger() {
|
|
699
|
+
return this.Log;
|
|
700
|
+
}
|
|
701
|
+
writeProtocolV2UsbPacket(path, frame, _context) {
|
|
709
702
|
return __awaiter(this, void 0, void 0, function* () {
|
|
710
|
-
|
|
711
|
-
|
|
703
|
+
if (this.cancelled) {
|
|
704
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
705
|
+
}
|
|
706
|
+
yield transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
readProtocolV2UsbPacket(path, _context) {
|
|
710
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
711
|
+
for (;;) {
|
|
712
712
|
if (this.cancelled) {
|
|
713
713
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
714
714
|
}
|
|
715
715
|
try {
|
|
716
|
-
yield
|
|
717
|
-
return;
|
|
716
|
+
const packet = yield transferInOnce(this.getOpenDevice(path).epIn, transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
717
|
+
return new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
718
718
|
}
|
|
719
719
|
catch (error) {
|
|
720
|
-
|
|
721
|
-
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
722
|
-
if (!shouldRetry) {
|
|
720
|
+
if (!this.isUsbTransferTimeout(error)) {
|
|
723
721
|
throw error;
|
|
724
722
|
}
|
|
725
|
-
try {
|
|
726
|
-
yield this.reconnectForRetry(path, 'out', attempt, error);
|
|
727
|
-
}
|
|
728
|
-
catch (reconnectError) {
|
|
729
|
-
lastError = reconnectError;
|
|
730
|
-
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(reconnectError)}`);
|
|
731
|
-
break;
|
|
732
|
-
}
|
|
733
723
|
}
|
|
734
724
|
}
|
|
735
|
-
throw lastError;
|
|
736
725
|
});
|
|
737
726
|
}
|
|
738
|
-
|
|
727
|
+
resetProtocolV2UsbNativeLink(path, _reason) {
|
|
739
728
|
return __awaiter(this, void 0, void 0, function* () {
|
|
740
|
-
|
|
741
|
-
if (!assembler) {
|
|
742
|
-
assembler = new transport.ProtocolV2FrameAssembler(transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
743
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
744
|
-
}
|
|
745
|
-
let frame = assembler.push(new Uint8Array(0));
|
|
746
|
-
const deadline = timeoutMs ? Date.now() + timeoutMs : undefined;
|
|
747
|
-
while (!frame) {
|
|
748
|
-
const transferIn = this.transferInWithRetry(path, this.getOpenDevice(path), transport.PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
749
|
-
const packet = deadline
|
|
750
|
-
? yield this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V2')
|
|
751
|
-
: yield transferIn;
|
|
752
|
-
const bytes = new Uint8Array(packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength));
|
|
753
|
-
try {
|
|
754
|
-
frame = assembler.push(bytes);
|
|
755
|
-
}
|
|
756
|
-
catch (error) {
|
|
757
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, error instanceof Error ? error.message : String(error));
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
return frame;
|
|
729
|
+
yield this.closeOpenDevice(path);
|
|
761
730
|
});
|
|
762
731
|
}
|
|
763
|
-
|
|
732
|
+
onProtocolV2UsbLinkInvalidated(path, reason) {
|
|
764
733
|
var _a;
|
|
734
|
+
this.deviceProtocol.delete(path);
|
|
735
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
736
|
+
}
|
|
737
|
+
createProtocolV2UsbTimeoutError(name, timeoutMs) {
|
|
738
|
+
return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
739
|
+
}
|
|
740
|
+
callProtocolV2(path, name, data, options) {
|
|
765
741
|
return __awaiter(this, void 0, void 0, function* () {
|
|
766
|
-
|
|
767
|
-
if (!this.messagesV2) {
|
|
768
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured, 'Protocol V2 schema not configured');
|
|
769
|
-
}
|
|
770
|
-
if (!protocolV1Messages) {
|
|
771
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
772
|
-
}
|
|
773
|
-
let session = this.protocolV2Sessions.get(path);
|
|
774
|
-
if (!session) {
|
|
775
|
-
session = new transport.ProtocolV2Session({
|
|
776
|
-
schemas: {
|
|
777
|
-
protocolV1: protocolV1Messages,
|
|
778
|
-
protocolV2: this.messagesV2,
|
|
779
|
-
},
|
|
780
|
-
router: transport.PROTOCOL_V2_CHANNEL_USB,
|
|
781
|
-
writeFrame: (frame) => this.writeProtocolV2Frame(path, frame),
|
|
782
|
-
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
783
|
-
logger: this.Log,
|
|
784
|
-
logPrefix: 'ProtocolV2 NodeUSB',
|
|
785
|
-
createTimeoutError: (messageName, timeoutMs) => new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
786
|
-
});
|
|
787
|
-
this.protocolV2Sessions.set(path, session);
|
|
788
|
-
}
|
|
789
|
-
this.protocolV2ReadTimeouts.set(path, options === null || options === void 0 ? void 0 : options.timeoutMs);
|
|
790
|
-
(_a = this.protocolV2Assemblers.get(path)) === null || _a === void 0 ? void 0 : _a.reset();
|
|
791
|
-
try {
|
|
792
|
-
return yield session.call(name, data, options);
|
|
793
|
-
}
|
|
794
|
-
finally {
|
|
795
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
796
|
-
}
|
|
742
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
797
743
|
});
|
|
798
744
|
}
|
|
799
745
|
receiveData(path, dev, timeoutMs) {
|
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.11",
|
|
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",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
23
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.11",
|
|
24
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.11",
|
|
25
25
|
"bytebuffer": "^5.0.1",
|
|
26
26
|
"usb": "^2.14.0"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "05d3421ec00e07e99f6665e6436a612503e32dfc"
|
|
29
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,7 @@ import transport, {
|
|
|
8
8
|
PROTOCOL_V1_USB_PACKET_SIZE,
|
|
9
9
|
PROTOCOL_V2_CHANNEL_USB,
|
|
10
10
|
PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
11
|
-
|
|
12
|
-
ProtocolV2Session,
|
|
11
|
+
ProtocolV2UsbTransportBase,
|
|
13
12
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
14
13
|
} from '@onekeyfe/hd-transport';
|
|
15
14
|
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared';
|
|
@@ -19,6 +18,8 @@ import type {
|
|
|
19
18
|
AcquireInput,
|
|
20
19
|
OneKeyDeviceInfo,
|
|
21
20
|
ProtocolType,
|
|
21
|
+
ProtocolV2CallContext,
|
|
22
|
+
ProtocolV2Schemas,
|
|
22
23
|
TransportCallOptions,
|
|
23
24
|
} from '@onekeyfe/hd-transport';
|
|
24
25
|
|
|
@@ -44,7 +45,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000;
|
|
|
44
45
|
/** Packet I/O retry configuration (matches WebUsbTransport) */
|
|
45
46
|
const PACKET_IO_MAX_RETRIES = 3;
|
|
46
47
|
const PACKET_IO_RETRY_DELAY = 300;
|
|
47
|
-
const PROTOCOL_PROBE_TIMEOUT =
|
|
48
|
+
const PROTOCOL_PROBE_TIMEOUT = 5000;
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Opened device state — holds the USB device, claimed interface, and endpoints.
|
|
@@ -154,32 +155,6 @@ function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
|
154
155
|
});
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
function resetUsbDevice(dev: usb.Device): Promise<void> {
|
|
158
|
-
return new Promise(resolve => {
|
|
159
|
-
const reset = (dev as usb.Device & { reset?: (callback: (err?: Error) => void) => void }).reset;
|
|
160
|
-
if (typeof reset !== 'function') {
|
|
161
|
-
resolve();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
reset.call(dev, () => resolve());
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function clearEndpointHalt(ep: usb.InEndpoint | usb.OutEndpoint): Promise<void> {
|
|
169
|
-
return new Promise(resolve => {
|
|
170
|
-
const clearHalt = (
|
|
171
|
-
ep as (usb.InEndpoint | usb.OutEndpoint) & {
|
|
172
|
-
clearHalt?: (callback: (err?: Error) => void) => void;
|
|
173
|
-
}
|
|
174
|
-
).clearHalt;
|
|
175
|
-
if (typeof clearHalt !== 'function') {
|
|
176
|
-
resolve();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
clearHalt.call(ep, () => resolve());
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
158
|
/**
|
|
184
159
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
185
160
|
*/
|
|
@@ -206,7 +181,7 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
|
206
181
|
*
|
|
207
182
|
* Modeled after WebUsbTransport.
|
|
208
183
|
*/
|
|
209
|
-
export default class NodeUsbTransport {
|
|
184
|
+
export default class NodeUsbTransport extends ProtocolV2UsbTransportBase<string> {
|
|
210
185
|
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
211
186
|
|
|
212
187
|
/** Protobuf schema for Protocol V2 transports. */
|
|
@@ -233,21 +208,20 @@ export default class NodeUsbTransport {
|
|
|
233
208
|
/** Per-path protocol type detected by active wire-level probe. */
|
|
234
209
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
235
210
|
|
|
236
|
-
/** Per-path Protocol V2 frame assembler, preserving buffered frames during reads. */
|
|
237
|
-
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
238
|
-
|
|
239
|
-
/** Per-path Protocol V2 session, preserving seq across API calls on the same device path. */
|
|
240
|
-
private protocolV2Sessions: Map<string, ProtocolV2Session> = new Map();
|
|
241
|
-
|
|
242
|
-
/** Current Protocol V2 read timeout, consumed by cached session readFrame closures. */
|
|
243
|
-
private protocolV2ReadTimeouts: Map<string, number | undefined> = new Map();
|
|
244
|
-
|
|
245
211
|
/** per-path reconnect lock to prevent concurrent reconnects */
|
|
246
212
|
private reconnectLocks = new Map<string, Promise<OpenDevice>>();
|
|
247
213
|
|
|
248
214
|
/** set to true when cancel() is called; checked by retry loops */
|
|
249
215
|
private cancelled = false;
|
|
250
216
|
|
|
217
|
+
constructor() {
|
|
218
|
+
super({
|
|
219
|
+
router: PROTOCOL_V2_CHANNEL_USB,
|
|
220
|
+
maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES,
|
|
221
|
+
logPrefix: 'ProtocolV2 NodeUSB',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
251
225
|
/**
|
|
252
226
|
* Initialize transport.
|
|
253
227
|
* Signature matches the Transport.init interface (logger, emitter).
|
|
@@ -267,8 +241,9 @@ export default class NodeUsbTransport {
|
|
|
267
241
|
|
|
268
242
|
configureProtocolV2(signedData: any) {
|
|
269
243
|
this.messagesV2 = parseConfigure(signedData);
|
|
270
|
-
this.
|
|
271
|
-
|
|
244
|
+
this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error =>
|
|
245
|
+
this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error)
|
|
246
|
+
);
|
|
272
247
|
this.Log?.debug('[NodeUsbTransport] Protocol V2 schema configured');
|
|
273
248
|
}
|
|
274
249
|
|
|
@@ -277,7 +252,9 @@ export default class NodeUsbTransport {
|
|
|
277
252
|
}
|
|
278
253
|
|
|
279
254
|
stop() {
|
|
280
|
-
|
|
255
|
+
this.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error =>
|
|
256
|
+
this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error)
|
|
257
|
+
);
|
|
281
258
|
}
|
|
282
259
|
|
|
283
260
|
/**
|
|
@@ -352,6 +329,7 @@ export default class NodeUsbTransport {
|
|
|
352
329
|
}
|
|
353
330
|
|
|
354
331
|
try {
|
|
332
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired');
|
|
355
333
|
await this.closeOpenDevice(path);
|
|
356
334
|
await this.openDevice(path);
|
|
357
335
|
await this.detectProtocol(path, input.expectedProtocol);
|
|
@@ -366,9 +344,9 @@ export default class NodeUsbTransport {
|
|
|
366
344
|
* Release device — release interface and close.
|
|
367
345
|
*/
|
|
368
346
|
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
347
|
+
await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released');
|
|
369
348
|
await this.closeOpenDevice(path);
|
|
370
349
|
this.deviceProtocol.delete(path);
|
|
371
|
-
this.protocolV2Assemblers.delete(path);
|
|
372
350
|
}
|
|
373
351
|
|
|
374
352
|
private async closeOpenDevice(path: string): Promise<void> {
|
|
@@ -515,6 +493,11 @@ export default class NodeUsbTransport {
|
|
|
515
493
|
);
|
|
516
494
|
}
|
|
517
495
|
|
|
496
|
+
private isUsbTransferTimeout(error: unknown): boolean {
|
|
497
|
+
const message = this.getErrorMessage(error).toLowerCase();
|
|
498
|
+
return message.includes('timeout') || message.includes('timed_out');
|
|
499
|
+
}
|
|
500
|
+
|
|
518
501
|
private getDeviceInterface(dev: usb.Device): usb.Interface {
|
|
519
502
|
const { interfaces } = dev;
|
|
520
503
|
if (!interfaces?.length) {
|
|
@@ -552,6 +535,10 @@ export default class NodeUsbTransport {
|
|
|
552
535
|
);
|
|
553
536
|
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
554
537
|
|
|
538
|
+
await this.rotateProtocolV2UsbGeneration(
|
|
539
|
+
path,
|
|
540
|
+
`Node USB Protocol V1 ${direction} reconnect attempt ${attempt}`
|
|
541
|
+
);
|
|
555
542
|
// Close the existing device without clearing the detected protocol cache.
|
|
556
543
|
try {
|
|
557
544
|
await this.closeOpenDevice(path);
|
|
@@ -629,7 +616,8 @@ export default class NodeUsbTransport {
|
|
|
629
616
|
private async transferInWithRetry(
|
|
630
617
|
path: string,
|
|
631
618
|
openDev: OpenDevice,
|
|
632
|
-
length: number
|
|
619
|
+
length: number,
|
|
620
|
+
options?: { waitIndefinitelyOnTimeout?: boolean }
|
|
633
621
|
): Promise<Buffer> {
|
|
634
622
|
let lastError: unknown;
|
|
635
623
|
let currentDev = openDev;
|
|
@@ -641,20 +629,24 @@ export default class NodeUsbTransport {
|
|
|
641
629
|
return await transferInOnce(currentDev.epIn, length);
|
|
642
630
|
} catch (error) {
|
|
643
631
|
lastError = error;
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
632
|
+
if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) {
|
|
633
|
+
attempt -= 1;
|
|
634
|
+
} else {
|
|
635
|
+
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
636
|
+
if (!shouldRetry) {
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
currentDev = await this.reconnectForRetry(path, 'in', attempt, error);
|
|
641
|
+
} catch (reconnectError) {
|
|
642
|
+
lastError = reconnectError;
|
|
643
|
+
this.Log?.debug(
|
|
644
|
+
`[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
645
|
+
reconnectError
|
|
646
|
+
)}`
|
|
647
|
+
);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
658
650
|
}
|
|
659
651
|
}
|
|
660
652
|
}
|
|
@@ -680,15 +672,6 @@ export default class NodeUsbTransport {
|
|
|
680
672
|
dev.open();
|
|
681
673
|
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
682
674
|
|
|
683
|
-
await resetUsbDevice(dev);
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
dev.open();
|
|
687
|
-
} catch {
|
|
688
|
-
// libusb keeps some devices open across reset; continue with the current handle.
|
|
689
|
-
}
|
|
690
|
-
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
691
|
-
|
|
692
675
|
const iface = this.getDeviceInterface(dev);
|
|
693
676
|
|
|
694
677
|
// On Linux, detach kernel driver if active
|
|
@@ -723,8 +706,6 @@ export default class NodeUsbTransport {
|
|
|
723
706
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
724
707
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
725
708
|
|
|
726
|
-
await clearEndpointHalt(epIn);
|
|
727
|
-
await clearEndpointHalt(epOut);
|
|
728
709
|
await this.drainStaleInput(epIn);
|
|
729
710
|
|
|
730
711
|
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
@@ -814,9 +795,7 @@ export default class NodeUsbTransport {
|
|
|
814
795
|
}
|
|
815
796
|
|
|
816
797
|
private async resetConnectionAfterProbe(path: string) {
|
|
817
|
-
this.
|
|
818
|
-
this.protocolV2Sessions.delete(path);
|
|
819
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
798
|
+
await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset');
|
|
820
799
|
|
|
821
800
|
try {
|
|
822
801
|
await this.closeOpenDevice(path);
|
|
@@ -898,74 +877,66 @@ export default class NodeUsbTransport {
|
|
|
898
877
|
});
|
|
899
878
|
}
|
|
900
879
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
880
|
+
protected getProtocolV2UsbSchemas(): ProtocolV2Schemas {
|
|
881
|
+
if (!this.messages || !this.messagesV2) {
|
|
882
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
protocolV1: this.messages,
|
|
886
|
+
protocolV2: this.messagesV2,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
protected getProtocolV2UsbLogger() {
|
|
891
|
+
return this.Log;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
protected async writeProtocolV2UsbPacket(
|
|
895
|
+
path: string,
|
|
896
|
+
frame: Uint8Array,
|
|
897
|
+
_context: ProtocolV2CallContext
|
|
898
|
+
): Promise<void> {
|
|
899
|
+
if (this.cancelled) {
|
|
900
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
901
|
+
}
|
|
902
|
+
await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
protected async readProtocolV2UsbPacket(
|
|
906
|
+
path: string,
|
|
907
|
+
_context: ProtocolV2CallContext
|
|
908
|
+
): Promise<Uint8Array> {
|
|
909
|
+
for (;;) {
|
|
904
910
|
if (this.cancelled) {
|
|
905
911
|
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled');
|
|
906
912
|
}
|
|
907
913
|
try {
|
|
908
|
-
await
|
|
909
|
-
|
|
914
|
+
const packet = await transferInOnce(
|
|
915
|
+
this.getOpenDevice(path).epIn,
|
|
916
|
+
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
917
|
+
);
|
|
918
|
+
return new Uint8Array(
|
|
919
|
+
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
920
|
+
);
|
|
910
921
|
} catch (error) {
|
|
911
|
-
|
|
912
|
-
const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error);
|
|
913
|
-
if (!shouldRetry) {
|
|
922
|
+
if (!this.isUsbTransferTimeout(error)) {
|
|
914
923
|
throw error;
|
|
915
924
|
}
|
|
916
|
-
try {
|
|
917
|
-
await this.reconnectForRetry(path, 'out', attempt, error);
|
|
918
|
-
} catch (reconnectError) {
|
|
919
|
-
lastError = reconnectError;
|
|
920
|
-
this.Log?.debug(
|
|
921
|
-
`[NodeUsbTransport] Protocol V2 write reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(
|
|
922
|
-
reconnectError
|
|
923
|
-
)}`
|
|
924
|
-
);
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
925
|
}
|
|
928
926
|
}
|
|
929
|
-
throw lastError;
|
|
930
927
|
}
|
|
931
928
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES);
|
|
936
|
-
this.protocolV2Assemblers.set(path, assembler);
|
|
937
|
-
}
|
|
929
|
+
protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise<void> {
|
|
930
|
+
await this.closeOpenDevice(path);
|
|
931
|
+
}
|
|
938
932
|
|
|
939
|
-
|
|
940
|
-
|
|
933
|
+
protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) {
|
|
934
|
+
this.deviceProtocol.delete(path);
|
|
935
|
+
this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason);
|
|
936
|
+
}
|
|
941
937
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
path,
|
|
945
|
-
this.getOpenDevice(path),
|
|
946
|
-
PROTOCOL_V2_FRAME_MAX_BYTES
|
|
947
|
-
);
|
|
948
|
-
const packet = deadline
|
|
949
|
-
? await this.withProtocolReadTimeout(
|
|
950
|
-
path,
|
|
951
|
-
transferIn,
|
|
952
|
-
Math.max(deadline - Date.now(), 1),
|
|
953
|
-
'V2'
|
|
954
|
-
)
|
|
955
|
-
: await transferIn;
|
|
956
|
-
const bytes = new Uint8Array(
|
|
957
|
-
packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength)
|
|
958
|
-
);
|
|
959
|
-
try {
|
|
960
|
-
frame = assembler.push(bytes);
|
|
961
|
-
} catch (error) {
|
|
962
|
-
throw ERRORS.TypedError(
|
|
963
|
-
HardwareErrorCode.NetworkError,
|
|
964
|
-
error instanceof Error ? error.message : String(error)
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return frame;
|
|
938
|
+
protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error {
|
|
939
|
+
return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
|
|
969
940
|
}
|
|
970
941
|
|
|
971
942
|
private async callProtocolV2(
|
|
@@ -974,42 +945,7 @@ export default class NodeUsbTransport {
|
|
|
974
945
|
data: Record<string, unknown>,
|
|
975
946
|
options?: TransportCallOptions
|
|
976
947
|
) {
|
|
977
|
-
|
|
978
|
-
if (!this.messagesV2) {
|
|
979
|
-
throw ERRORS.TypedError(
|
|
980
|
-
HardwareErrorCode.TransportNotConfigured,
|
|
981
|
-
'Protocol V2 schema not configured'
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
if (!protocolV1Messages) {
|
|
985
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
let session = this.protocolV2Sessions.get(path);
|
|
989
|
-
if (!session) {
|
|
990
|
-
session = new ProtocolV2Session({
|
|
991
|
-
schemas: {
|
|
992
|
-
protocolV1: protocolV1Messages,
|
|
993
|
-
protocolV2: this.messagesV2,
|
|
994
|
-
},
|
|
995
|
-
router: PROTOCOL_V2_CHANNEL_USB,
|
|
996
|
-
writeFrame: (frame: Uint8Array) => this.writeProtocolV2Frame(path, frame),
|
|
997
|
-
readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)),
|
|
998
|
-
logger: this.Log,
|
|
999
|
-
logPrefix: 'ProtocolV2 NodeUSB',
|
|
1000
|
-
createTimeoutError: (messageName: string, timeoutMs: number) =>
|
|
1001
|
-
new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`),
|
|
1002
|
-
});
|
|
1003
|
-
this.protocolV2Sessions.set(path, session);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
this.protocolV2ReadTimeouts.set(path, options?.timeoutMs);
|
|
1007
|
-
this.protocolV2Assemblers.get(path)?.reset();
|
|
1008
|
-
try {
|
|
1009
|
-
return await session.call(name, data, options);
|
|
1010
|
-
} finally {
|
|
1011
|
-
this.protocolV2ReadTimeouts.delete(path);
|
|
1012
|
-
}
|
|
948
|
+
return this.callProtocolV2Usb(path, name, data, options);
|
|
1013
949
|
}
|
|
1014
950
|
|
|
1015
951
|
/**
|