Skip to content
This repository was archived by the owner on Jul 24, 2025. It is now read-only.

Commit a596e40

Browse files
committed
Initial commit
0 parents  commit a596e40

File tree

5 files changed

+621
-0
lines changed

5 files changed

+621
-0
lines changed

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "usb-stream",
3+
"version": "0.1.0",
4+
"description": "Get Stream instances from USB devices",
5+
"main": "src/usb-cdc-acm.js",
6+
"author": "Iván Sánchez Ortega <ivan@sanchezortega.es>",
7+
"license": "TBD",
8+
"private": true,
9+
"peerDependencies": {
10+
"usb": "^1.3.1"
11+
},
12+
"dependencies": {
13+
"debug": "^3.1.0"
14+
}
15+
}

src/split-descriptors.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Quasi-trivial utility to parse USB descriptors from a Uint8Array
2+
// The first byte in the descriptor is the descriptor length, and they are just
3+
// concatenated together, so something like:
4+
// 5 X X X X 4 X X X 9 X X X X X X X X
5+
// should be splitted into
6+
// 5 X X X X | 4 X X X | 9 X X X X X X X X
7+
8+
9+
// Given a Uint8Array, returns an Array of Uint8Array
10+
// Each element of the resulting array is a subarray of the original Uint8Array.
11+
// export default function splitDescriptors(bytes) {
12+
function splitDescriptors(bytes) {
13+
let len = bytes.length;
14+
let descs = [];
15+
let pointer = 0;
16+
17+
while(len > 0) {
18+
descLen = bytes[pointer];
19+
descs.push(bytes.subarray(pointer, pointer + descLen));
20+
len -= descLen;
21+
pointer += descLen;
22+
}
23+
24+
return descs;
25+
}
26+
27+
28+
29+
/*
30+
let test = Uint8Array.from([5, 36, 0, 16, 1, 5, 36, 1, 3, 1, 4, 36, 2, 6, 5, 36, 6, 0, 1]);
31+
32+
console.log(splitDescriptors(test));
33+
*/
34+
35+
/*
36+
The previous code should output:
37+
38+
[ Uint8Array [ 5, 36, 0, 16, 1 ],
39+
Uint8Array [ 5, 36, 1, 3, 1 ],
40+
Uint8Array [ 4, 36, 2, 6 ],
41+
Uint8Array [ 5, 36, 6, 0, 1 ] ]
42+
*/
43+
44+
45+
module.exports = splitDescriptors;

src/usb-cdc-acm.js

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
2+
// import EventEmitter from 'events';
3+
// import { Duplex } from 'stream';
4+
// import Debug from 'debug';
5+
// import usb from 'usb';
6+
// import util from 'util';
7+
// import splitDescriptors from ('./split-descriptors');
8+
const Duplex = require('stream').Duplex;
9+
const Debug = require('debug');
10+
const usb = require('usb');
11+
const util = require('util');
12+
13+
const debug = Debug('usb-stream');
14+
15+
const splitDescriptors = require('./split-descriptors');
16+
17+
18+
// Utility function.
19+
// Given an interface, assert that it looks like a CDC management interface
20+
// Specifically, the interface must have only one
21+
// "out" interrupt endpoint, and a CDC Union descriptor.
22+
// Will return boolean `false` if the interface is not valid,
23+
// or an integer number (corresponding to the associated data interface)
24+
function assertCdcInterface(iface) {
25+
const endpoints = iface.endpoints;
26+
const descriptor = iface.descriptor;
27+
28+
if (descriptor.bInterfaceClass !== usb.LIBUSB_CLASS_COMM || // 2, CDC
29+
descriptor.bInterfaceSubClass !== 2) // ACM
30+
{
31+
return false;
32+
}
33+
34+
// Check it has only one endpoint, and of the right kind
35+
if (endpoints.length !== 1 ||
36+
endpoints[0].transferType !== usb.LIBUSB_TRANSFER_TYPE_INTERRUPT ||
37+
endpoints[0].direction !== "in")
38+
{
39+
return false;
40+
}
41+
42+
// node-usb doesn't parse the CDC Union descriptor inside the interface
43+
// descriptor, so parse and find it manually here.
44+
const additionalDescriptors = splitDescriptors(descriptor.extra);
45+
let slaveInterfaceId = false;
46+
47+
for (let i=0, l=additionalDescriptors.length; i<l; i++) {
48+
const desc = additionalDescriptors[i];
49+
50+
// 0x24 = class-specific descriptor. 0x06 = CDC Union descriptor
51+
if (desc[1] === 0x24 && desc[2] === 6) {
52+
if (desc[3] !== iface.id) {
53+
// Master interface should be the current one!!
54+
return false;
55+
}
56+
slaveInterfaceId = desc[4];
57+
}
58+
}
59+
60+
if (slaveInterfaceId === false) {
61+
// CDC Union descriptor not found, this is not a well-formed USB CDC ACM interface
62+
return false;
63+
}
64+
65+
return (slaveInterfaceId);
66+
}
67+
68+
69+
// Utility function.
70+
// Given an interface, assert that it looks like a CDC data interface
71+
// Specifically, the interface must have only one
72+
// "in" bulk endpoint and one "out" bulk endpoint.
73+
function assertDataInterface(iface) {
74+
const endpoints = iface.endpoints;
75+
76+
return (
77+
// Right class (0x0A)
78+
iface.descriptor.bInterfaceClass === usb.LIBUSB_CLASS_DATA &&
79+
// Only two endpoints, and
80+
endpoints.length === 2 &&
81+
// both are bulk transfer, and
82+
endpoints[0].transferType === usb.LIBUSB_TRANSFER_TYPE_BULK &&
83+
endpoints[1].transferType === usb.LIBUSB_TRANSFER_TYPE_BULK &&
84+
// their direction (in/out) is different
85+
endpoints[0].direction !== endpoints[1].direction
86+
);
87+
}
88+
89+
90+
// export class UsbCdcAcm extends Duplex {
91+
class UsbCdcAcm extends Duplex {
92+
constructor(ifaceCdc) {
93+
94+
const ifaceDataId = assertCdcInterface(ifaceCdc);
95+
if (ifaceDataId === false){
96+
throw new Error('CDC interface is not valid');
97+
}
98+
99+
100+
const ifaceData = ifaceCdc.device.interfaces[ifaceDataId];
101+
102+
if (!assertDataInterface(ifaceData) ){
103+
throw new Error('Data interface is not valid');
104+
}
105+
106+
super();
107+
108+
this.ifaceCdc = ifaceCdc;
109+
this.ifaceData = ifaceData;
110+
this.device = ifaceCdc.device;
111+
112+
this.ctr = ifaceCdc.endpoints[0];
113+
114+
if (ifaceData.endpoints[0].direction === 'in') {
115+
this.in = ifaceData.endpoints[0];
116+
this.out = ifaceData.endpoints[1];
117+
} else {
118+
this.in = ifaceData.endpoints[1];
119+
this.out = ifaceData.endpoints[0];
120+
}
121+
122+
debug('claiming interfaces');
123+
124+
// if (ifaceCdc.isKernelDriverActive()) {
125+
// ifaceCdc.detachKernelDriver();
126+
// }
127+
ifaceCdc.claim();
128+
129+
// if (ifaceData.isKernelDriverActive()) {
130+
// ifaceData.detachKernelDriver();
131+
// }
132+
ifaceData.claim();
133+
134+
this.ctr.on('data', this._onStatus.bind(this));
135+
this.ctr.on('error', this._onError.bind(this));
136+
137+
// // Perform a SET_LINE_CODING request
138+
139+
// Perform a USB_CDC_REQ_SET_CONTROL_LINE_STATE (0x22) control transfer
140+
// This is documented in the PSTN doc of the USB spec, section 6.3.12
141+
this._controlTransfer(
142+
0x21, // bmRequestType: [host-to-device, type: class, recipient: iface]
143+
0x22, // SET_CONTROL_LINE_STATE
144+
0x03, // 0x02 "Activate carrier" & 0x01 "DTE is present"
145+
ifaceCdc.id, // interface index ???
146+
new Buffer([]) // No data expected back
147+
// ).then(()=>{
148+
// return this._controlTransfer(
149+
// 0x21, // bmRequestType: [host-to-device, type: class, recipient: iface]
150+
// 0x20, // SET_LINE_CODING
151+
// 0x01, // value 0x0
152+
// 0x00, // index 0
153+
// new Uint8Array([0x80, 0x25, 0, 0, 0, 0, 0x08])
154+
// )
155+
// })
156+
).then( ()=>{
157+
debug('Control transfer 0x21 0x20 performed');
158+
159+
this.in.on('data', this._onData.bind(this));
160+
this.in.on('error', (err)=>this.emit('error', err));
161+
this.out.on('error', (err)=>this.emit('error', err));
162+
163+
this.in.timeout = 1000;
164+
this.out.timeout = 1000;
165+
});
166+
167+
168+
debug(this.device.descriptor);
169+
170+
// if (!desc || desc.
171+
}
172+
173+
_read(){
174+
debug('_read');
175+
if (!this.polling) {
176+
debug('starting polling');
177+
this.in.startPoll();
178+
this.polling = true;
179+
}
180+
}
181+
182+
_onData(data) {
183+
debug('_onData ' + data);
184+
const keepReading = this.push(data);
185+
if (!keepReading) {
186+
this._stopPolling();
187+
}
188+
}
189+
190+
_onError(err) {
191+
debug('Error: ', err)
192+
this.emit('error', err)
193+
// throw err;
194+
}
195+
196+
_onStatus(sts) {
197+
debug('Status: ', sts)
198+
}
199+
200+
_stopPolling() {
201+
debug('_stopPolling');
202+
if (this.polling) {
203+
debug('stopping polling');
204+
this.in.stopPoll();
205+
this.polling = false;
206+
}
207+
}
208+
209+
_write(data){
210+
debug('_write ' + data.toString());
211+
212+
this.out.transfer(data, (err)=>{
213+
if (err) {
214+
debug('Out transfer error: ', err);
215+
this.emit(err)
216+
} else {
217+
debug('Out transfer OK');
218+
}
219+
});
220+
}
221+
222+
// _writev(){}
223+
224+
_final(){
225+
debug('_final');
226+
// this._stopPolling();
227+
228+
229+
// Close all resources, waiting for everything,
230+
// then emit a 'close' event.
231+
232+
this.ctr.removeAllListeners();
233+
this.in.removeAllListeners();
234+
this.out.removeAllListeners();
235+
236+
this.ifaceCdc.release(true, (err)=>{
237+
if (err) { throw err }
238+
this.ifaceData.release(true, (err2)=>{
239+
if (err2) { throw err2 }
240+
241+
debug('All resources released');
242+
this.emit('close');
243+
});
244+
});
245+
246+
// util.promisify(this.ctr.removeAllListeners.bind(this))()
247+
// .then(util.promisify(this.in.removeAllListeners.bind(this)))
248+
// .then(util.promisify(this.out.removeAllListeners.bind(this)))
249+
// .then(util.promisify(this.ifaceCdc.release.bind(this)))
250+
// .then(util.promisify(this.ifaceData.release.bind(this)))
251+
// .then(()=>{
252+
// debug('All resources released');
253+
// this.emit('close');
254+
// });
255+
256+
}
257+
258+
// The device's controlTransfer, wrapped as a Promise
259+
_controlTransfer(bmRequestType, bRequest, wValue, wIndex, data_or_length) {
260+
return Promise.resolve();
261+
// return new Promise((res, rej)=>{
262+
// this.device.controlTransfer(
263+
// bmRequestType,
264+
// bRequest,
265+
// wValue,
266+
// wIndex,
267+
// data_or_length,
268+
// (err, data)=>{ err ? rej(err) : res(data); })
269+
// });
270+
}
271+
272+
273+
// Given an instance of Device (from the 'usb' library), opens it, looks through
274+
// its interfaces, and creates an instance of UsbStream per interface which
275+
// looks like a CDC ACM control interface (having the right descriptor and endpoints).
276+
//
277+
// The given Device must be already open()ed. Conversely, it has to be close()d
278+
// when the stream is no longer used, or if this method throws an error.
279+
//
280+
// Returns an array of instances of UsbCdcAcm.
281+
static fromUsbDevice(device){
282+
283+
let ifaces = device.interfaces;
284+
285+
for(let i=0,l=ifaces.length; i<l; i++) {
286+
const iface = ifaces[i];
287+
const endpoints = ifaces[i].endpoints;
288+
289+
// debug(iface.descriptor);
290+
// debug(iface.endpoints);
291+
// debug(iface.endpoints.map(e=>e.direction));
292+
// debug(endpoints);
293+
294+
if (assertCdcInterface(iface) !== false) {
295+
return new UsbCdcAcm(iface);
296+
}
297+
}
298+
299+
throw new Error('No valid interfaces found in USB device (they do not have one "in" bulk endpoint and one "out" bulk endpoint)');
300+
}
301+
302+
}
303+
304+
305+
306+
307+
module.exports = UsbCdcAcm;

0 commit comments

Comments
 (0)