@onekeyfe/hd-transport-usb 1.1.25 → 1.1.26-alpha.0
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/dist/constants.d.ts +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +23 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -82
- package/package.json +4 -4
- package/src/constants.ts +3 -0
- package/src/index.ts +284 -117
package/dist/constants.d.ts
CHANGED
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW,KAAK,CAAC;AAG9B,eAAO,MAAM,SAAS,KAAO,CAAC;AAG9B,eAAO,MAAM,aAAa,IAAI,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,WAAW,KAAK,CAAC;AAG9B,eAAO,MAAM,SAAS,KAAO,CAAC;AAG9B,eAAO,MAAM,YAAY,QAAkB,CAAC;AAG5C,eAAO,MAAM,aAAa,IAAI,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as transport from '@onekeyfe/hd-transport';
|
|
2
|
+
import transport__default, { OneKeyDeviceInfo, AcquireInput } from '@onekeyfe/hd-transport';
|
|
3
|
+
import EventEmitter from 'events';
|
|
2
4
|
|
|
3
5
|
declare const PACKET_SIZE = 64;
|
|
4
6
|
|
|
5
|
-
declare
|
|
7
|
+
declare class NodeUsbTransport {
|
|
8
|
+
messages: ReturnType<typeof transport__default.parseConfigure> | undefined;
|
|
9
|
+
name: string;
|
|
10
|
+
configured: boolean;
|
|
11
|
+
Log?: any;
|
|
12
|
+
emitter?: EventEmitter;
|
|
13
|
+
private serialToBusId;
|
|
14
|
+
private openDevices;
|
|
15
|
+
init(logger: any, emitter?: EventEmitter): void;
|
|
16
|
+
configure(signedData: any): void;
|
|
17
|
+
listen(): void;
|
|
18
|
+
enumerate(): Promise<OneKeyDeviceInfo[]>;
|
|
19
|
+
acquire(input: AcquireInput): Promise<string>;
|
|
20
|
+
release(path: string, _onclose?: boolean): Promise<void>;
|
|
21
|
+
call(path: string, name: string, data: Record<string, unknown>): Promise<transport.MessageFromOneKey>;
|
|
22
|
+
cancel(): void;
|
|
23
|
+
private openDevice;
|
|
24
|
+
private receiveData;
|
|
25
|
+
}
|
|
6
26
|
|
|
7
|
-
export { PACKET_SIZE,
|
|
27
|
+
export { PACKET_SIZE, NodeUsbTransport as default };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,SAA8B,MAAM,wBAAwB,CAAC;AAKpE,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA6K7E,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,QAAQ,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;IAElE,IAAI,SAAsB;IAE1B,UAAU,UAAS;IAEnB,GAAG,CAAC,EAAE,GAAG,CAAC;IAEV,OAAO,CAAC,EAAE,YAAY,CAAC;IAGvB,OAAO,CAAC,aAAa,CAA6B;IAGlD,OAAO,CAAC,WAAW,CAAiC;IAMpD,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,YAAY;IAKxC,SAAS,CAAC,UAAU,EAAE,GAAG;IAMzB,MAAM;IASA,SAAS,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA8BxC,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAkB7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BxD,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAqCpE,MAAM;YAUQ,UAAU;YAuDV,WAAW;CAiC1B;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -63,19 +63,59 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
63
63
|
|
|
64
64
|
const PACKET_SIZE = 64;
|
|
65
65
|
const REPORT_ID = 0x3f;
|
|
66
|
+
const PAYLOAD_SIZE = PACKET_SIZE - 1;
|
|
66
67
|
const HEADER_LENGTH = 6;
|
|
67
68
|
|
|
68
|
-
const { decodeProtocol } = transport__default["default"];
|
|
69
|
+
const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport__default["default"];
|
|
69
70
|
const INTERFACE_NUMBER = 0;
|
|
70
71
|
const ENDPOINT_IN = 0x81;
|
|
71
72
|
const ENDPOINT_OUT = 0x01;
|
|
72
73
|
const TRANSFER_TIMEOUT_MS = 30000;
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
function
|
|
74
|
+
const PACKET_IO_MAX_RETRIES = 3;
|
|
75
|
+
const PACKET_IO_RETRY_DELAY = 300;
|
|
76
|
+
function getBusId(dev) {
|
|
76
77
|
return `usb:${dev.busNumber}:${dev.deviceAddress}`;
|
|
77
78
|
}
|
|
78
|
-
function
|
|
79
|
+
function readSerialNumber(dev, openDevices) {
|
|
80
|
+
const { iSerialNumber } = dev.deviceDescriptor;
|
|
81
|
+
if (!iSerialNumber)
|
|
82
|
+
return Promise.resolve(getBusId(dev));
|
|
83
|
+
const busId = getBusId(dev);
|
|
84
|
+
if (openDevices) {
|
|
85
|
+
for (const [serial, od] of openDevices) {
|
|
86
|
+
if (od.device === dev || getBusId(od.device) === busId) {
|
|
87
|
+
return Promise.resolve(serial);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new Promise(resolve => {
|
|
92
|
+
try {
|
|
93
|
+
dev.open();
|
|
94
|
+
try {
|
|
95
|
+
dev.getStringDescriptor(iSerialNumber, (_err, data) => {
|
|
96
|
+
try {
|
|
97
|
+
dev.close();
|
|
98
|
+
}
|
|
99
|
+
catch (_a) {
|
|
100
|
+
}
|
|
101
|
+
resolve(data || busId);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (_a) {
|
|
105
|
+
try {
|
|
106
|
+
dev.close();
|
|
107
|
+
}
|
|
108
|
+
catch (_b) {
|
|
109
|
+
}
|
|
110
|
+
resolve(busId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (_c) {
|
|
114
|
+
resolve(busId);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function transferInOnce(ep, length) {
|
|
79
119
|
return new Promise((resolve, reject) => {
|
|
80
120
|
ep.transfer(length, (err, data) => {
|
|
81
121
|
if (err)
|
|
@@ -86,7 +126,7 @@ function transferIn(ep, length) {
|
|
|
86
126
|
});
|
|
87
127
|
});
|
|
88
128
|
}
|
|
89
|
-
function
|
|
129
|
+
function transferOutOnce(ep, data) {
|
|
90
130
|
return new Promise((resolve, reject) => {
|
|
91
131
|
ep.transfer(data, (err) => {
|
|
92
132
|
if (err)
|
|
@@ -95,9 +135,43 @@ function transferOut(ep, data) {
|
|
|
95
135
|
});
|
|
96
136
|
});
|
|
97
137
|
}
|
|
98
|
-
function
|
|
138
|
+
function wait(ms) {
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
setTimeout(resolve, ms);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function transferIn(ep, length) {
|
|
99
144
|
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
-
|
|
145
|
+
let lastError;
|
|
146
|
+
for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
|
|
147
|
+
try {
|
|
148
|
+
return yield transferInOnce(ep, length);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
lastError = err;
|
|
152
|
+
if (attempt < PACKET_IO_MAX_RETRIES) {
|
|
153
|
+
yield wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw lastError;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function transferOut(ep, data) {
|
|
161
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
let lastError;
|
|
163
|
+
for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
|
|
164
|
+
try {
|
|
165
|
+
return yield transferOutOnce(ep, data);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
lastError = err;
|
|
169
|
+
if (attempt < PACKET_IO_MAX_RETRIES) {
|
|
170
|
+
yield wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
throw lastError;
|
|
101
175
|
});
|
|
102
176
|
}
|
|
103
177
|
function skipReportByte(packet) {
|
|
@@ -109,12 +183,24 @@ function skipReportByte(packet) {
|
|
|
109
183
|
function toArrayBuffer(buf) {
|
|
110
184
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
111
185
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
186
|
+
class NodeUsbTransport {
|
|
187
|
+
constructor() {
|
|
188
|
+
this.name = 'NodeUsbTransport';
|
|
189
|
+
this.configured = false;
|
|
190
|
+
this.serialToBusId = new Map();
|
|
191
|
+
this.openDevices = new Map();
|
|
192
|
+
}
|
|
193
|
+
init(logger, emitter) {
|
|
194
|
+
this.Log = logger;
|
|
195
|
+
this.emitter = emitter;
|
|
196
|
+
}
|
|
197
|
+
configure(signedData) {
|
|
198
|
+
const messages = parseConfigure(signedData);
|
|
199
|
+
this.configured = true;
|
|
200
|
+
this.messages = messages;
|
|
201
|
+
}
|
|
202
|
+
listen() {
|
|
203
|
+
}
|
|
118
204
|
enumerate() {
|
|
119
205
|
return __awaiter(this, void 0, void 0, function* () {
|
|
120
206
|
const allDevices = usb__namespace.getDeviceList();
|
|
@@ -122,24 +208,115 @@ const UsbPlugin = {
|
|
|
122
208
|
const { idVendor, idProduct } = d.deviceDescriptor;
|
|
123
209
|
return hdShared.ONEKEY_WEBUSB_FILTER.some(f => idVendor === f.vendorId && idProduct === f.productId);
|
|
124
210
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
211
|
+
const newSerialToBusId = new Map();
|
|
212
|
+
const results = [];
|
|
213
|
+
for (const d of onekeyDevices) {
|
|
214
|
+
const busId = getBusId(d);
|
|
215
|
+
const serial = yield readSerialNumber(d, this.openDevices);
|
|
216
|
+
newSerialToBusId.set(serial, busId);
|
|
217
|
+
results.push({
|
|
218
|
+
path: serial,
|
|
219
|
+
id: serial,
|
|
220
|
+
name: 'OneKey',
|
|
221
|
+
commType: 'usb',
|
|
222
|
+
debug: false,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
this.serialToBusId = newSerialToBusId;
|
|
226
|
+
return results;
|
|
130
227
|
});
|
|
131
|
-
}
|
|
132
|
-
|
|
228
|
+
}
|
|
229
|
+
acquire(input) {
|
|
230
|
+
var _a, _b, _c;
|
|
231
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
const path = (_a = input.path) !== null && _a !== void 0 ? _a : '';
|
|
233
|
+
if (!path) {
|
|
234
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'No device path provided');
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
yield this.openDevice(path);
|
|
238
|
+
return path;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport acquire error: ', error);
|
|
242
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, (_c = error.message) !== null && _c !== void 0 ? _c : String(error));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
release(path, _onclose) {
|
|
133
247
|
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
activeDevice = existing;
|
|
248
|
+
const openDev = this.openDevices.get(path);
|
|
249
|
+
if (!openDev)
|
|
137
250
|
return;
|
|
251
|
+
try {
|
|
252
|
+
yield new Promise(resolve => {
|
|
253
|
+
openDev.iface.release(() => {
|
|
254
|
+
try {
|
|
255
|
+
openDev.device.close();
|
|
256
|
+
}
|
|
257
|
+
catch (_a) {
|
|
258
|
+
}
|
|
259
|
+
resolve();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (_a) {
|
|
264
|
+
try {
|
|
265
|
+
openDev.device.close();
|
|
266
|
+
}
|
|
267
|
+
catch (_b) {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.openDevices.delete(path);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
call(path, name, data) {
|
|
274
|
+
var _a, _b;
|
|
275
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
276
|
+
if (!this.messages) {
|
|
277
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
|
|
278
|
+
}
|
|
279
|
+
const openDev = this.openDevices.get(path);
|
|
280
|
+
if (!openDev) {
|
|
281
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
|
|
282
|
+
}
|
|
283
|
+
const { messages } = this;
|
|
284
|
+
if (transport.LogBlockCommand.has(name)) {
|
|
285
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport call-', ' name: ', name);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
(_b = this.Log) === null || _b === void 0 ? void 0 : _b.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
|
|
138
289
|
}
|
|
290
|
+
const encodeBuffers = buildEncodeBuffers(messages, name, data);
|
|
291
|
+
for (const buffer of encodeBuffers) {
|
|
292
|
+
const packet = new Uint8Array(PACKET_SIZE);
|
|
293
|
+
packet[0] = REPORT_ID;
|
|
294
|
+
packet.set(new Uint8Array(buffer), 1);
|
|
295
|
+
yield transferOut(openDev.epOut, Buffer.from(packet));
|
|
296
|
+
}
|
|
297
|
+
const resData = yield this.receiveData(openDev);
|
|
298
|
+
if (typeof resData !== 'string') {
|
|
299
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.NetworkError, 'Returning data is not string.');
|
|
300
|
+
}
|
|
301
|
+
const jsonData = receiveOne(messages, resData);
|
|
302
|
+
return check.call(jsonData);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
cancel() {
|
|
306
|
+
var _a;
|
|
307
|
+
(_a = this.Log) === null || _a === void 0 ? void 0 : _a.debug('NodeUsbTransport cancel');
|
|
308
|
+
}
|
|
309
|
+
openDevice(path) {
|
|
310
|
+
var _a;
|
|
311
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
312
|
+
const existing = this.openDevices.get(path);
|
|
313
|
+
if (existing)
|
|
314
|
+
return;
|
|
315
|
+
const busId = (_a = this.serialToBusId.get(path)) !== null && _a !== void 0 ? _a : path;
|
|
139
316
|
const allDevices = usb__namespace.getDeviceList();
|
|
140
|
-
const dev = allDevices.find(d =>
|
|
317
|
+
const dev = allDevices.find(d => getBusId(d) === busId);
|
|
141
318
|
if (!dev) {
|
|
142
|
-
throw
|
|
319
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
|
|
143
320
|
}
|
|
144
321
|
dev.open();
|
|
145
322
|
dev.timeout = TRANSFER_TIMEOUT_MS;
|
|
@@ -150,7 +327,7 @@ const UsbPlugin = {
|
|
|
150
327
|
iface.detachKernelDriver();
|
|
151
328
|
}
|
|
152
329
|
}
|
|
153
|
-
catch (
|
|
330
|
+
catch (_b) {
|
|
154
331
|
}
|
|
155
332
|
}
|
|
156
333
|
iface.claim();
|
|
@@ -158,60 +335,16 @@ const UsbPlugin = {
|
|
|
158
335
|
const epOut = iface.endpoints.find((e) => e.direction === 'out' && e.address === ENDPOINT_OUT);
|
|
159
336
|
if (!epIn || !epOut) {
|
|
160
337
|
dev.close();
|
|
161
|
-
throw
|
|
338
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, 'USB endpoints not found (expected IN 0x81, OUT 0x01)');
|
|
162
339
|
}
|
|
163
340
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
164
341
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
165
|
-
|
|
166
|
-
openDevices.set(uuid, openDev);
|
|
167
|
-
activeDevice = openDev;
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
disconnect(uuid) {
|
|
171
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
172
|
-
const openDev = openDevices.get(uuid);
|
|
173
|
-
if (openDev) {
|
|
174
|
-
try {
|
|
175
|
-
openDev.iface.release(() => {
|
|
176
|
-
try {
|
|
177
|
-
openDev.device.close();
|
|
178
|
-
}
|
|
179
|
-
catch (_a) {
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
catch (_a) {
|
|
184
|
-
try {
|
|
185
|
-
openDev.device.close();
|
|
186
|
-
}
|
|
187
|
-
catch (_b) {
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
openDevices.delete(uuid);
|
|
191
|
-
if (activeDevice === openDev) {
|
|
192
|
-
activeDevice = null;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
},
|
|
197
|
-
send(uuid, data) {
|
|
198
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
199
|
-
const openDev = openDevices.get(uuid);
|
|
200
|
-
if (!openDev) {
|
|
201
|
-
throw new Error(`Device not connected: ${uuid}`);
|
|
202
|
-
}
|
|
203
|
-
activeDevice = openDev;
|
|
204
|
-
const dataBuffer = Buffer.from(data, 'hex');
|
|
205
|
-
yield transferOut(openDev.epOut, dataBuffer);
|
|
342
|
+
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
206
343
|
});
|
|
207
|
-
}
|
|
208
|
-
|
|
344
|
+
}
|
|
345
|
+
receiveData(dev) {
|
|
209
346
|
return __awaiter(this, void 0, void 0, function* () {
|
|
210
|
-
|
|
211
|
-
throw new Error('No active device for receive');
|
|
212
|
-
}
|
|
213
|
-
const dev = activeDevice;
|
|
214
|
-
const firstPacket = yield readPacket(dev);
|
|
347
|
+
const firstPacket = yield transferIn(dev.epIn, PACKET_SIZE);
|
|
215
348
|
const firstData = skipReportByte(firstPacket);
|
|
216
349
|
const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
|
|
217
350
|
const lengthWithHeader = Number(length) + HEADER_LENGTH;
|
|
@@ -222,10 +355,10 @@ const UsbPlugin = {
|
|
|
222
355
|
decoded.append(restBuffer);
|
|
223
356
|
}
|
|
224
357
|
while (decoded.offset < lengthWithHeader) {
|
|
225
|
-
const packet = yield
|
|
358
|
+
const packet = yield transferIn(dev.epIn, PACKET_SIZE);
|
|
226
359
|
const pktData = skipReportByte(packet);
|
|
227
360
|
const buf = toArrayBuffer(pktData);
|
|
228
|
-
if (lengthWithHeader - decoded.offset >=
|
|
361
|
+
if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
|
|
229
362
|
decoded.append(buf);
|
|
230
363
|
}
|
|
231
364
|
else {
|
|
@@ -236,9 +369,8 @@ const UsbPlugin = {
|
|
|
236
369
|
const result = decoded.toBuffer();
|
|
237
370
|
return Buffer.from(result).toString('hex');
|
|
238
371
|
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
241
374
|
|
|
242
375
|
exports.PACKET_SIZE = PACKET_SIZE;
|
|
243
|
-
exports
|
|
244
|
-
exports["default"] = UsbPlugin;
|
|
376
|
+
exports["default"] = NodeUsbTransport;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-usb",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.26-alpha.0",
|
|
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.1.
|
|
24
|
-
"@onekeyfe/hd-transport": "1.1.
|
|
23
|
+
"@onekeyfe/hd-shared": "1.1.26-alpha.0",
|
|
24
|
+
"@onekeyfe/hd-transport": "1.1.26-alpha.0",
|
|
25
25
|
"bytebuffer": "^5.0.1",
|
|
26
26
|
"usb": "^2.14.0"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "8d3072a406b0672c9d20e2f58e25cef94a9759fb"
|
|
29
29
|
}
|
package/src/constants.ts
CHANGED
|
@@ -4,5 +4,8 @@ export const PACKET_SIZE = 64;
|
|
|
4
4
|
/** Protocol marker byte (0x3F = '?') — first byte of every packet */
|
|
5
5
|
export const REPORT_ID = 0x3f;
|
|
6
6
|
|
|
7
|
+
/** Usable payload per packet after stripping the 0x3F report byte */
|
|
8
|
+
export const PAYLOAD_SIZE = PACKET_SIZE - 1;
|
|
9
|
+
|
|
7
10
|
/** Protocol header length: typeId (2 bytes) + length (4 bytes) */
|
|
8
11
|
export const HEADER_LENGTH = 6;
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import ByteBuffer from 'bytebuffer';
|
|
2
2
|
import * as usb from 'usb';
|
|
3
|
-
import transport from '@onekeyfe/hd-transport';
|
|
4
|
-
import { ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
|
|
3
|
+
import transport, { LogBlockCommand } from '@onekeyfe/hd-transport';
|
|
4
|
+
import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER } from '@onekeyfe/hd-shared';
|
|
5
5
|
|
|
6
|
-
import { HEADER_LENGTH, PACKET_SIZE, REPORT_ID } from './constants';
|
|
6
|
+
import { HEADER_LENGTH, PACKET_SIZE, PAYLOAD_SIZE, REPORT_ID } from './constants';
|
|
7
7
|
|
|
8
|
-
import type
|
|
8
|
+
import type EventEmitter from 'events';
|
|
9
|
+
import type { AcquireInput, OneKeyDeviceInfo } from '@onekeyfe/hd-transport';
|
|
9
10
|
|
|
10
|
-
const { decodeProtocol } = transport;
|
|
11
|
+
const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport;
|
|
11
12
|
|
|
12
13
|
/** USB interface number for vendor-specific communication */
|
|
13
14
|
const INTERFACE_NUMBER = 0;
|
|
@@ -18,6 +19,10 @@ const ENDPOINT_OUT = 0x01;
|
|
|
18
19
|
/** Transfer timeout in milliseconds */
|
|
19
20
|
const TRANSFER_TIMEOUT_MS = 30000;
|
|
20
21
|
|
|
22
|
+
/** Packet I/O retry configuration (matches WebUsbTransport) */
|
|
23
|
+
const PACKET_IO_MAX_RETRIES = 3;
|
|
24
|
+
const PACKET_IO_RETRY_DELAY = 300;
|
|
25
|
+
|
|
21
26
|
/**
|
|
22
27
|
* Opened device state — holds the USB device, claimed interface, and endpoints.
|
|
23
28
|
*/
|
|
@@ -29,26 +34,62 @@ interface OpenDevice {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
*
|
|
33
|
-
* LowlevelTransportSharedPlugin.receive() takes no uuid parameter.
|
|
37
|
+
* Fallback identifier using bus topology (unstable across re-plugs).
|
|
34
38
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const openDevices = new Map<string, OpenDevice>();
|
|
39
|
+
function getBusId(dev: usb.Device): string {
|
|
40
|
+
return `usb:${dev.busNumber}:${dev.deviceAddress}`;
|
|
41
|
+
}
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
44
|
+
* Read USB string descriptor serial number from a device.
|
|
45
|
+
* Opens device briefly, reads serial, then closes.
|
|
46
|
+
* Falls back to bus path if serial cannot be read.
|
|
43
47
|
*/
|
|
44
|
-
function
|
|
45
|
-
|
|
48
|
+
function readSerialNumber(dev: usb.Device, openDevices?: Map<string, OpenDevice>): Promise<string> {
|
|
49
|
+
const { iSerialNumber } = dev.deviceDescriptor;
|
|
50
|
+
if (!iSerialNumber) return Promise.resolve(getBusId(dev));
|
|
51
|
+
|
|
52
|
+
// If the device is already open (acquired), read serial without open/close
|
|
53
|
+
const busId = getBusId(dev);
|
|
54
|
+
if (openDevices) {
|
|
55
|
+
for (const [serial, od] of openDevices) {
|
|
56
|
+
if (od.device === dev || getBusId(od.device) === busId) {
|
|
57
|
+
return Promise.resolve(serial);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Promise(resolve => {
|
|
63
|
+
try {
|
|
64
|
+
dev.open();
|
|
65
|
+
try {
|
|
66
|
+
dev.getStringDescriptor(iSerialNumber, (_err: Error | undefined, data?: string) => {
|
|
67
|
+
try {
|
|
68
|
+
dev.close();
|
|
69
|
+
} catch {
|
|
70
|
+
/* ignore */
|
|
71
|
+
}
|
|
72
|
+
resolve(data || busId);
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
try {
|
|
76
|
+
dev.close();
|
|
77
|
+
} catch {
|
|
78
|
+
/* ignore */
|
|
79
|
+
}
|
|
80
|
+
resolve(busId);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// dev.open() failed (e.g. LIBUSB_ERROR_BUSY if already open elsewhere)
|
|
84
|
+
resolve(busId);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
46
87
|
}
|
|
47
88
|
|
|
48
89
|
/**
|
|
49
|
-
* Promisified USB IN transfer.
|
|
90
|
+
* Promisified USB IN transfer (single attempt).
|
|
50
91
|
*/
|
|
51
|
-
function
|
|
92
|
+
function transferInOnce(ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
52
93
|
return new Promise((resolve, reject) => {
|
|
53
94
|
ep.transfer(length, (err: Error | undefined, data: Buffer | undefined) => {
|
|
54
95
|
if (err) return reject(err);
|
|
@@ -59,9 +100,9 @@ function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
|
59
100
|
}
|
|
60
101
|
|
|
61
102
|
/**
|
|
62
|
-
* Promisified USB OUT transfer.
|
|
103
|
+
* Promisified USB OUT transfer (single attempt).
|
|
63
104
|
*/
|
|
64
|
-
function
|
|
105
|
+
function transferOutOnce(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
65
106
|
return new Promise((resolve, reject) => {
|
|
66
107
|
ep.transfer(data, (err: Error | undefined) => {
|
|
67
108
|
if (err) return reject(err);
|
|
@@ -70,16 +111,50 @@ function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
|
70
111
|
});
|
|
71
112
|
}
|
|
72
113
|
|
|
114
|
+
function wait(ms: number): Promise<void> {
|
|
115
|
+
return new Promise(resolve => {
|
|
116
|
+
setTimeout(resolve, ms);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
73
120
|
/**
|
|
74
|
-
*
|
|
121
|
+
* USB IN transfer with retry.
|
|
75
122
|
*/
|
|
76
|
-
async function
|
|
77
|
-
|
|
123
|
+
async function transferIn(ep: usb.InEndpoint, length: number): Promise<Buffer> {
|
|
124
|
+
let lastError: unknown;
|
|
125
|
+
for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
|
|
126
|
+
try {
|
|
127
|
+
return await transferInOnce(ep, length);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
lastError = err;
|
|
130
|
+
if (attempt < PACKET_IO_MAX_RETRIES) {
|
|
131
|
+
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
throw lastError;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* USB OUT transfer with retry.
|
|
140
|
+
*/
|
|
141
|
+
async function transferOut(ep: usb.OutEndpoint, data: Buffer): Promise<void> {
|
|
142
|
+
let lastError: unknown;
|
|
143
|
+
for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt++) {
|
|
144
|
+
try {
|
|
145
|
+
return await transferOutOnce(ep, data);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
lastError = err;
|
|
148
|
+
if (attempt < PACKET_IO_MAX_RETRIES) {
|
|
149
|
+
await wait(attempt * PACKET_IO_RETRY_DELAY);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw lastError;
|
|
78
154
|
}
|
|
79
155
|
|
|
80
156
|
/**
|
|
81
157
|
* Skip the 0x3F protocol marker byte from a USB packet.
|
|
82
|
-
* The device sends 0x3F as the first byte of every response packet.
|
|
83
158
|
*/
|
|
84
159
|
function skipReportByte(packet: Buffer): Buffer {
|
|
85
160
|
if (packet[0] === REPORT_ID) {
|
|
@@ -96,24 +171,56 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer {
|
|
|
96
171
|
}
|
|
97
172
|
|
|
98
173
|
/**
|
|
99
|
-
* Node.js USB
|
|
174
|
+
* Node.js USB Transport — complete transport implementation using libusb.
|
|
100
175
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
176
|
+
* Unlike the old UsbPlugin (which was a LowlevelTransportSharedPlugin piped
|
|
177
|
+
* through LowlevelTransport), this class is a standalone transport that handles
|
|
178
|
+
* both protocol encoding/decoding and USB I/O directly.
|
|
104
179
|
*
|
|
105
|
-
*
|
|
106
|
-
* which is NOT visible to the HID framework on macOS.
|
|
180
|
+
* Modeled after WebUsbTransport.
|
|
107
181
|
*/
|
|
108
|
-
export
|
|
109
|
-
|
|
182
|
+
export default class NodeUsbTransport {
|
|
183
|
+
messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
110
184
|
|
|
111
|
-
|
|
112
|
-
// libusb requires no global initialization
|
|
113
|
-
},
|
|
185
|
+
name = 'NodeUsbTransport';
|
|
114
186
|
|
|
115
|
-
|
|
116
|
-
|
|
187
|
+
configured = false;
|
|
188
|
+
|
|
189
|
+
Log?: any;
|
|
190
|
+
|
|
191
|
+
emitter?: EventEmitter;
|
|
192
|
+
|
|
193
|
+
/** serial → bus id, built during enumerate */
|
|
194
|
+
private serialToBusId = new Map<string, string>();
|
|
195
|
+
|
|
196
|
+
/** path → opened device state */
|
|
197
|
+
private openDevices = new Map<string, OpenDevice>();
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Initialize transport.
|
|
201
|
+
* Signature matches the Transport.init interface (logger, emitter).
|
|
202
|
+
*/
|
|
203
|
+
init(logger: any, emitter?: EventEmitter) {
|
|
204
|
+
this.Log = logger;
|
|
205
|
+
this.emitter = emitter;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
configure(signedData: any) {
|
|
209
|
+
const messages = parseConfigure(signedData);
|
|
210
|
+
this.configured = true;
|
|
211
|
+
this.messages = messages;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
listen() {
|
|
215
|
+
// empty — could add hotplug events via usb.on('attach'/'detach')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Enumerate connected OneKey USB devices.
|
|
220
|
+
* Opens each device briefly to read its serial number (used as `path`),
|
|
221
|
+
* then closes it. acquire() re-opens from a fresh getDeviceList().
|
|
222
|
+
*/
|
|
223
|
+
async enumerate(): Promise<OneKeyDeviceInfo[]> {
|
|
117
224
|
const allDevices = usb.getDeviceList();
|
|
118
225
|
|
|
119
226
|
const onekeyDevices = allDevices.filter(d => {
|
|
@@ -121,26 +228,132 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
|
|
|
121
228
|
return ONEKEY_WEBUSB_FILTER.some(f => idVendor === f.vendorId && idProduct === f.productId);
|
|
122
229
|
});
|
|
123
230
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
231
|
+
const newSerialToBusId = new Map<string, string>();
|
|
232
|
+
const results: OneKeyDeviceInfo[] = [];
|
|
233
|
+
for (const d of onekeyDevices) {
|
|
234
|
+
const busId = getBusId(d);
|
|
235
|
+
const serial = await readSerialNumber(d, this.openDevices);
|
|
236
|
+
newSerialToBusId.set(serial, busId);
|
|
237
|
+
results.push({
|
|
238
|
+
path: serial,
|
|
239
|
+
id: serial,
|
|
240
|
+
name: 'OneKey',
|
|
241
|
+
commType: 'usb',
|
|
242
|
+
debug: false,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Atomic swap — concurrent acquire() always sees a complete map
|
|
246
|
+
this.serialToBusId = newSerialToBusId;
|
|
247
|
+
return results;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Acquire device — open USB device, claim interface, return path (string).
|
|
252
|
+
*/
|
|
253
|
+
async acquire(input: AcquireInput): Promise<string> {
|
|
254
|
+
const path = input.path ?? '';
|
|
255
|
+
if (!path) {
|
|
256
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'No device path provided');
|
|
257
|
+
}
|
|
130
258
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
259
|
+
try {
|
|
260
|
+
await this.openDevice(path);
|
|
261
|
+
return path;
|
|
262
|
+
} catch (error: any) {
|
|
263
|
+
this.Log?.debug('NodeUsbTransport acquire error: ', error);
|
|
264
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Release device — release interface and close.
|
|
270
|
+
*/
|
|
271
|
+
async release(path: string, _onclose?: boolean): Promise<void> {
|
|
272
|
+
const openDev = this.openDevices.get(path);
|
|
273
|
+
if (!openDev) return;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
await new Promise<void>(resolve => {
|
|
277
|
+
openDev.iface.release(() => {
|
|
278
|
+
try {
|
|
279
|
+
openDev.device.close();
|
|
280
|
+
} catch {
|
|
281
|
+
/* ignore */
|
|
282
|
+
}
|
|
283
|
+
resolve();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
} catch {
|
|
287
|
+
try {
|
|
288
|
+
openDev.device.close();
|
|
289
|
+
} catch {
|
|
290
|
+
/* ignore */
|
|
291
|
+
}
|
|
137
292
|
}
|
|
293
|
+
this.openDevices.delete(path);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Call device method — encode protobuf, send packets, receive response.
|
|
298
|
+
* This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive.
|
|
299
|
+
*/
|
|
300
|
+
async call(path: string, name: string, data: Record<string, unknown>) {
|
|
301
|
+
if (!this.messages) {
|
|
302
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const openDev = this.openDevices.get(path);
|
|
306
|
+
if (!openDev) {
|
|
307
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { messages } = this;
|
|
311
|
+
if (LogBlockCommand.has(name)) {
|
|
312
|
+
this.Log?.debug('NodeUsbTransport call-', ' name: ', name);
|
|
313
|
+
} else {
|
|
314
|
+
this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Encode protobuf message into 63-byte chunks (same as WebUsbTransport)
|
|
318
|
+
const encodeBuffers = buildEncodeBuffers(messages, name, data);
|
|
319
|
+
|
|
320
|
+
// Send each chunk with 0x3F report ID prefix
|
|
321
|
+
for (const buffer of encodeBuffers) {
|
|
322
|
+
const packet = new Uint8Array(PACKET_SIZE);
|
|
323
|
+
packet[0] = REPORT_ID;
|
|
324
|
+
packet.set(new Uint8Array(buffer), 1);
|
|
325
|
+
await transferOut(openDev.epOut, Buffer.from(packet));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Receive response
|
|
329
|
+
const resData = await this.receiveData(openDev);
|
|
330
|
+
if (typeof resData !== 'string') {
|
|
331
|
+
throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.');
|
|
332
|
+
}
|
|
333
|
+
const jsonData = receiveOne(messages, resData);
|
|
334
|
+
return check.call(jsonData);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
cancel() {
|
|
338
|
+
this.Log?.debug('NodeUsbTransport cancel');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- Private helpers ---
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Open a USB device by path (serial number), claim interface, cache endpoints.
|
|
345
|
+
*/
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
347
|
+
private async openDevice(path: string): Promise<void> {
|
|
348
|
+
const existing = this.openDevices.get(path);
|
|
349
|
+
if (existing) return;
|
|
138
350
|
|
|
139
|
-
//
|
|
351
|
+
// Resolve serial → bus id, then find a fresh device object
|
|
352
|
+
const busId = this.serialToBusId.get(path) ?? path;
|
|
140
353
|
const allDevices = usb.getDeviceList();
|
|
141
|
-
const dev = allDevices.find(d =>
|
|
354
|
+
const dev = allDevices.find(d => getBusId(d) === busId);
|
|
142
355
|
if (!dev) {
|
|
143
|
-
throw
|
|
356
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`);
|
|
144
357
|
}
|
|
145
358
|
|
|
146
359
|
dev.open();
|
|
@@ -161,7 +374,6 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
|
|
|
161
374
|
|
|
162
375
|
iface.claim();
|
|
163
376
|
|
|
164
|
-
// Find IN and OUT endpoints
|
|
165
377
|
const epIn = iface.endpoints.find(
|
|
166
378
|
(e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN
|
|
167
379
|
);
|
|
@@ -171,74 +383,31 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
|
|
|
171
383
|
|
|
172
384
|
if (!epIn || !epOut) {
|
|
173
385
|
dev.close();
|
|
174
|
-
throw
|
|
386
|
+
throw ERRORS.TypedError(
|
|
387
|
+
HardwareErrorCode.DeviceNotFound,
|
|
388
|
+
'USB endpoints not found (expected IN 0x81, OUT 0x01)'
|
|
389
|
+
);
|
|
175
390
|
}
|
|
176
391
|
|
|
177
392
|
epIn.timeout = TRANSFER_TIMEOUT_MS;
|
|
178
393
|
epOut.timeout = TRANSFER_TIMEOUT_MS;
|
|
179
394
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
activeDevice = openDev;
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
186
|
-
async disconnect(uuid: string): Promise<void> {
|
|
187
|
-
const openDev = openDevices.get(uuid);
|
|
188
|
-
if (openDev) {
|
|
189
|
-
try {
|
|
190
|
-
openDev.iface.release(() => {
|
|
191
|
-
try {
|
|
192
|
-
openDev.device.close();
|
|
193
|
-
} catch {
|
|
194
|
-
/* ignore */
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
} catch {
|
|
198
|
-
try {
|
|
199
|
-
openDev.device.close();
|
|
200
|
-
} catch {
|
|
201
|
-
/* ignore */
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
openDevices.delete(uuid);
|
|
205
|
-
if (activeDevice === openDev) {
|
|
206
|
-
activeDevice = null;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
async send(uuid: string, data: string): Promise<void> {
|
|
212
|
-
const openDev = openDevices.get(uuid);
|
|
213
|
-
if (!openDev) {
|
|
214
|
-
throw new Error(`Device not connected: ${uuid}`);
|
|
215
|
-
}
|
|
216
|
-
activeDevice = openDev;
|
|
217
|
-
|
|
218
|
-
// data is a hex string of a 64-byte packet (0x3F + 63 bytes payload),
|
|
219
|
-
// already framed by LowlevelTransport's buildBuffers().
|
|
220
|
-
const dataBuffer = Buffer.from(data, 'hex');
|
|
221
|
-
|
|
222
|
-
// libusb transfers the raw packet directly — no Report ID prepend needed
|
|
223
|
-
// (Report ID is a HID concept; libusb operates at USB level)
|
|
224
|
-
await transferOut(openDev.epOut, dataBuffer);
|
|
225
|
-
},
|
|
226
|
-
|
|
227
|
-
async receive(): Promise<string> {
|
|
228
|
-
if (!activeDevice) {
|
|
229
|
-
throw new Error('No active device for receive');
|
|
230
|
-
}
|
|
231
|
-
const dev = activeDevice;
|
|
395
|
+
this.openDevices.set(path, { device: dev, iface, epIn, epOut });
|
|
396
|
+
}
|
|
232
397
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Receive a complete protobuf response from the device.
|
|
400
|
+
* Reads 64-byte packets, strips 0x3F marker, reassembles into hex string.
|
|
401
|
+
*/
|
|
402
|
+
private async receiveData(dev: OpenDevice): Promise<string> {
|
|
403
|
+
// Read first packet, skip report byte
|
|
404
|
+
const firstPacket = await transferIn(dev.epIn, PACKET_SIZE);
|
|
236
405
|
const firstData = skipReportByte(firstPacket);
|
|
237
406
|
|
|
238
|
-
//
|
|
407
|
+
// Decode header: ## marker → { typeId, length, restBuffer }
|
|
239
408
|
const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData));
|
|
240
409
|
|
|
241
|
-
//
|
|
410
|
+
// Allocate result: typeId(2) + length(4) + payload(length)
|
|
242
411
|
const lengthWithHeader = Number(length) + HEADER_LENGTH;
|
|
243
412
|
const decoded = new ByteBuffer(lengthWithHeader);
|
|
244
413
|
decoded.writeUint16(typeId);
|
|
@@ -247,24 +416,22 @@ export const UsbPlugin: LowlevelTransportSharedPlugin = {
|
|
|
247
416
|
decoded.append(restBuffer);
|
|
248
417
|
}
|
|
249
418
|
|
|
250
|
-
//
|
|
419
|
+
// Read subsequent packets until complete
|
|
251
420
|
while (decoded.offset < lengthWithHeader) {
|
|
252
|
-
const packet = await
|
|
421
|
+
const packet = await transferIn(dev.epIn, PACKET_SIZE);
|
|
253
422
|
const pktData = skipReportByte(packet);
|
|
254
423
|
const buf = toArrayBuffer(pktData);
|
|
255
|
-
if (lengthWithHeader - decoded.offset >=
|
|
424
|
+
if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) {
|
|
256
425
|
decoded.append(buf);
|
|
257
426
|
} else {
|
|
258
427
|
decoded.append(buf.slice(0, lengthWithHeader - decoded.offset));
|
|
259
428
|
}
|
|
260
429
|
}
|
|
261
430
|
|
|
262
|
-
// 5. Return as hex string
|
|
263
431
|
decoded.reset();
|
|
264
432
|
const result = decoded.toBuffer();
|
|
265
433
|
return Buffer.from(result as unknown as ArrayBuffer).toString('hex');
|
|
266
|
-
}
|
|
267
|
-
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
268
436
|
|
|
269
|
-
export default UsbPlugin;
|
|
270
437
|
export { PACKET_SIZE } from './constants';
|