├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── package.json ├── readme.md ├── rollup.config.js ├── spec ├── split-descriptors-spec.js └── support │ └── jasmine.json ├── src ├── split-descriptors.js └── usb-cdc-acm.js └── test └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "airbnb-base" 5 | ], 6 | "rules": { 7 | "indent": [ 8 | "error", 9 | 4, 10 | { 11 | "SwitchCase": 1 12 | } 13 | ], 14 | "quotes": [ 15 | "error", 16 | "single" 17 | ], 18 | "linebreak-style": "off", 19 | "arrow-parens": [ 20 | "error", 21 | "as-needed" 22 | ], 23 | "arrow-body-style": [ 24 | "error", 25 | "as-needed" 26 | ], 27 | "strict": "off", 28 | "no-console": "off", 29 | "valid-jsdoc": 2, 30 | "import/no-extraneous-dependencies": 0, 31 | "import/no-unresolved": [ 32 | "error", 33 | { 34 | "ignore": [ "nrfconnect/.*", "pc-ble-driver-js", "pc-nrfjprog-js", "serialport", "electron" ] 35 | } 36 | ], 37 | "import/extensions": ["off"], 38 | "no-undef": 1, 39 | "no-unused-vars": 1, 40 | "comma-dangle": ["error", { 41 | "arrays": "always-multiline", 42 | "objects": "always-multiline", 43 | "imports": "always-multiline", 44 | "exports": "always-multiline", 45 | "functions": "ignore", 46 | }], 47 | "no-underscore-dangle": 0 48 | }, 49 | "plugins": [ 50 | "import" 51 | ], 52 | "env": { 53 | "es6": true, 54 | "browser": true, 55 | "node": true, 56 | "jasmine": true, 57 | "jest": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: objective-c 3 | 4 | env: 5 | matrix: 6 | - NODE_VERSION="8" 7 | - NODE_VERSION="9" 8 | 9 | before_install: 10 | - brew update 11 | - brew install nvm 12 | - export NVM_DIR=~/.nvm 13 | - source $(brew --prefix nvm)/nvm.sh 14 | - nvm install $NODE_VERSION 15 | - node --version 16 | - npm --version 17 | 18 | script: 19 | - npm install 20 | - npm run rollup 21 | 22 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2 | # v0.1.1 (2018-01-24) 3 | 4 | * Worked around calling `isKernelDriverAttached()` in win32 platforms, where it's unsupported. 5 | 6 | # v0.1.0 (2018-01-23) 7 | 8 | * Initial release 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 - 2018, Nordic Semiconductor ASA 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Nordic Semiconductor ASA nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usb-cdc-acm", 3 | "version": "0.1.1", 4 | "description": "Userspace javascript implementation of a USB CDC ACM driver, on top of libusb", 5 | "module": "src/usb-cdc-acm.js", 6 | "main": "dist/usb-cdc-acm.cjs.js", 7 | "author": "Iván Sánchez Ortega ", 8 | "license": "BSD", 9 | "scripts": { 10 | "rollup": "rollup -c rollup.config.js", 11 | "lint": "eslint src/", 12 | "lintfix": "eslint src/ --fix", 13 | "test": "rollup -c rollup.config.js && jasmine" 14 | }, 15 | "dependencies": { 16 | "debug": "^3.1.0", 17 | "usb": "^1.3.1" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^4.16.0", 21 | "eslint-config-airbnb-base": "^12.1.0", 22 | "eslint-plugin-import": "^2.8.0", 23 | "jasmine": "^2.8.0", 24 | "rollup": "^0.55.0", 25 | "rollup-plugin-buble": "^0.18.0", 26 | "rollup-plugin-eslint": "^4.0.0" 27 | }, 28 | "repository": "https://github.com/NordicPlayground/node-usb-cdc-acm.git" 29 | } 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # usb-cdc-acm 3 | 4 | Userspace javascript implementation of a USB CDC ACM driver, on top of libusb. 5 | 6 | [![Build Status](https://travis-ci.org/NordicPlayground/node-usb-cdc-acm.svg?branch=master)](https://travis-ci.org/NordicPlayground/node-usb-cdc-acm) 7 | 8 | This is part of [Nordic Semiconductor](http://www.nordicsemi.com/)'s javascript tools to 9 | interface with nRF SoCs and development kits. Although USB CDC ACM is part of the USB specifications 10 | and a *de facto* standard when it comes to emulating a serial port connection on an embedded device, 11 | Nordic Semiconductor cannot offer support for other hardware platforms. This software is provided "as is". 12 | 13 | ## Motivation 14 | 15 | Sometimes you want to fetch info from the USB descriptors via the 16 | [NodeJS `usb` module](https://github.com/tessel/node-usb), and at the same time use the CDC ACM 17 | interface to send and receive data. But this is not possible with some host configurations 18 | (notably, win32/win64 platforms and their need to manually switch the driver via Zadig or the like). 19 | 20 | ## API 21 | 22 | `usb-cdc-acm` provides a [duplex `Stream` interface](https://nodejs.org/api/stream.html). Please 23 | refer to NodeJS's documentation about `Stream`s for a full API spec. 24 | 25 | Other than that, `usb-cdc-acm` provides two entry points. Either of those need you to use `usb` to 26 | fetch a reference to the `Device` or to a CDC `Interface`. 27 | 28 | Use the factory method when your USB device only has one CDC ACM interface: 29 | ```js 30 | var usb = require('usb'); 31 | const UsbCdcAcm = require('usb-cdc-acm'); 32 | 33 | var device = usb.findByIds( 0x1915, 0x520f ); // VID/PID for Nordic Semi / USB CDC demo 34 | 35 | // The device MUST be open before instantiating the UsbCdcAcm stream! 36 | device.open(); 37 | 38 | // An options object with the baud rate is optional. 39 | let stream = UsbCdcAcm.fromUsbDevice(device, { baudRate: 1000000}); 40 | 41 | // Then, use it as any other Stream 42 | stream.on('data', function(data) { console.log('recv: ', data); }); 43 | stream.write('Hello world!'); 44 | 45 | // Remember to destroy the stream and close the device when finished! 46 | // Failure to do so might leave the USB device in an ususable state for other applications. 47 | setTimeout(function(){ 48 | stream.destroy(); 49 | device.close(); 50 | }, 5000); 51 | ``` 52 | 53 | If the device has several CDC ACM interfaces and finer control is needed, 54 | ```js 55 | // An options object with the baud rate is optional. 56 | let stream = new UsbCdcAcm(device.interfaces[0], { baudRate: 1000000}); 57 | ``` 58 | 59 | For a more complete example, check the `test/test.js` file. 60 | 61 | 62 | ## Legal 63 | 64 | Distributed under a BSD-3 license. See the `LICENSE` file for details. 65 | 66 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | 2 | import buble from 'rollup-plugin-buble'; 3 | import eslint from 'rollup-plugin-eslint'; 4 | import pkg from './package.json'; 5 | 6 | export default [ 7 | { 8 | input: pkg.module, 9 | output: [ 10 | { file: pkg.main, format: 'cjs', sourcemap: true }, 11 | ], 12 | external: ['stream', 'debug', 'usb'], 13 | plugins: [ 14 | eslint(), 15 | buble(), 16 | ] 17 | }, 18 | { 19 | input: 'src/split-descriptors.js', 20 | output: [ 21 | { file: 'dist/split-descriptors.cjs.js', format: 'cjs', sourcemap: true }, 22 | ], 23 | plugins: [ 24 | eslint(), 25 | buble(), 26 | ] 27 | } 28 | ]; 29 | -------------------------------------------------------------------------------- /spec/split-descriptors-spec.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2010 - 2018, Nordic Semiconductor ASA 2 | * 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of Nordic Semiconductor ASA nor the names of its 16 | * contributors may be used to endorse or promote products derived from this 17 | * software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | const splitDescriptors = require('../dist/split-descriptors.cjs.js'); 33 | 34 | describe('splitDescriptors', () => { 35 | it('should return empty array for undefined input', () => { 36 | expect(splitDescriptors()).toEqual([]); 37 | }); 38 | 39 | it('should return empty array for null input', () => { 40 | expect(splitDescriptors(null)).toEqual([]); 41 | }); 42 | 43 | it('should return empty array for non-Uint8Array', () => { 44 | expect(splitDescriptors([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([]); 45 | }); 46 | 47 | it('should split input to subarrays', () => { 48 | const bytes = new Uint8Array([ 49 | 5, 36, 0, 16, 1, 5, 36, 1, 3, 1, 4, 36, 2, 6, 5, 36, 6, 0, 1, 50 | ]); 51 | expect(splitDescriptors(bytes)) 52 | .toEqual([ 53 | new Uint8Array([5, 36, 0, 16, 1]), 54 | new Uint8Array([5, 36, 1, 3, 1]), 55 | new Uint8Array([4, 36, 2, 6]), 56 | new Uint8Array([5, 36, 6, 0, 1]), 57 | ]); 58 | }); 59 | 60 | it('should silently ignore insufficient data', () => { 61 | const bytes = new Uint8Array([15, 36, 0, 16, 1]); 62 | expect(splitDescriptors(bytes)).toEqual([bytes]); 63 | }); 64 | 65 | it('should throw exception on 0 length descriptor', () => { 66 | const bytes = new Uint8Array([0, 36, 0, 16, 1]); 67 | expect(() => { 68 | splitDescriptors(bytes); 69 | }).toThrowError(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /src/split-descriptors.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2010 - 2018, Nordic Semiconductor ASA 2 | * 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of Nordic Semiconductor ASA nor the names of its 16 | * contributors may be used to endorse or promote products derived from this 17 | * software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | // Quasi-trivial utility to parse USB descriptors from a Uint8Array 33 | // The first byte in the descriptor is the descriptor length, and they are just 34 | // concatenated together, so something like: 35 | // 5 X X X X 4 X X X 9 X X X X X X X X 36 | // should be splitted into 37 | // 5 X X X X | 4 X X X | 9 X X X X X X X X 38 | 39 | // Given a Uint8Array, returns an Array of Uint8Array 40 | // Each element of the resulting array is a subarray of the original Uint8Array. 41 | export default function splitDescriptors(bytes) { 42 | const descs = []; 43 | if (!(bytes instanceof Uint8Array)) { 44 | return descs; 45 | } 46 | let len = bytes.length; 47 | let pointer = 0; 48 | 49 | while (len > 0) { 50 | const descLen = bytes[pointer]; 51 | if (descLen < 1) { 52 | throw new Error('invalid descriptor length'); 53 | } 54 | descs.push(bytes.subarray(pointer, pointer + descLen)); 55 | len -= descLen; 56 | pointer += descLen; 57 | } 58 | 59 | // TODO: Consider handling if len !== 0 at this point. 60 | 61 | return descs; 62 | } 63 | -------------------------------------------------------------------------------- /src/usb-cdc-acm.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2010 - 2018, Nordic Semiconductor ASA 2 | * 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of Nordic Semiconductor ASA nor the names of its 16 | * contributors may be used to endorse or promote products derived from this 17 | * software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | import { Duplex } from 'stream'; 33 | import Debug from 'debug'; 34 | import usb from 'usb'; 35 | import splitDescriptors from './split-descriptors'; 36 | 37 | // Two debug levels: one for initialization/teardown messages, and one 38 | // for logging all data being sent/recv around 39 | const debugInfo = Debug('usb-cdc-acm:info'); 40 | const debugData = Debug('usb-cdc-acm:data'); 41 | 42 | 43 | // Utility function. 44 | // Given an interface, assert that it looks like a CDC management interface 45 | // Specifically, the interface must have only one 46 | // "out" interrupt endpoint, and a CDC Union descriptor. 47 | // Will return boolean `false` if the interface is not valid, 48 | // or an integer number (corresponding to the associated data interface) 49 | function assertCdcInterface(iface) { 50 | const { endpoints, descriptor } = iface; 51 | 52 | if (descriptor.bInterfaceClass !== usb.LIBUSB_CLASS_COMM || // 2, CDC 53 | descriptor.bInterfaceSubClass !== 2) { // ACM 54 | return false; 55 | } 56 | 57 | // Check it has only one endpoint, and of the right kind 58 | if (endpoints.length !== 1 || 59 | endpoints[0].transferType !== usb.LIBUSB_TRANSFER_TYPE_INTERRUPT || 60 | endpoints[0].direction !== 'in') { 61 | return false; 62 | } 63 | 64 | // node-usb doesn't parse the CDC Union descriptor inside the interface 65 | // descriptor, so parse and find it manually here. 66 | const additionalDescriptors = splitDescriptors(descriptor.extra); 67 | let slaveInterfaceId = false; 68 | 69 | for (let i = 0, l = additionalDescriptors.length; i < l; i += 1) { 70 | const desc = additionalDescriptors[i]; 71 | 72 | // 0x24 = class-specific descriptor. 0x06 = CDC Union descriptor 73 | if (desc[1] === 0x24 && desc[2] === 6) { 74 | if (desc[3] !== iface.id) { 75 | // Master interface should be the current one!! 76 | return false; 77 | } 78 | [,,,, slaveInterfaceId] = desc; // slaveInterfaceId = desc[4]; 79 | } 80 | } 81 | 82 | if (slaveInterfaceId === false) { 83 | // CDC Union descriptor not found, this is not a well-formed USB CDC ACM interface 84 | return false; 85 | } 86 | 87 | return (slaveInterfaceId); 88 | } 89 | 90 | 91 | // Utility function. 92 | // Given an interface, assert that it looks like a CDC data interface 93 | // Specifically, the interface must have only one 94 | // "in" bulk endpoint and one "out" bulk endpoint. 95 | function assertDataInterface(iface) { 96 | const { endpoints } = iface; 97 | 98 | return ( 99 | // Right class (0x0A) 100 | iface.descriptor.bInterfaceClass === usb.LIBUSB_CLASS_DATA && 101 | // Only two endpoints, and 102 | endpoints.length === 2 && 103 | // both are bulk transfer, and 104 | endpoints[0].transferType === usb.LIBUSB_TRANSFER_TYPE_BULK && 105 | endpoints[1].transferType === usb.LIBUSB_TRANSFER_TYPE_BULK && 106 | // their direction (in/out) is different 107 | endpoints[0].direction !== endpoints[1].direction 108 | ); 109 | } 110 | 111 | 112 | export default class UsbCdcAcm extends Duplex { 113 | constructor(ifaceCdc, options = {}) { 114 | const ifaceDataId = assertCdcInterface(ifaceCdc); 115 | if (ifaceDataId === false) { 116 | throw new Error('CDC interface is not valid'); 117 | } 118 | 119 | const ifaceData = ifaceCdc.device.interfaces[ifaceDataId]; 120 | if (!assertDataInterface(ifaceData)) { 121 | throw new Error('Data interface is not valid'); 122 | } 123 | 124 | super(options); 125 | 126 | this.ifaceCdc = ifaceCdc; 127 | this.ifaceData = ifaceData; 128 | this.device = ifaceCdc.device; 129 | 130 | [this.ctr] = ifaceCdc.endpoints; 131 | 132 | if (ifaceData.endpoints[0].direction === 'in') { 133 | [this.in, this.out] = ifaceData.endpoints; 134 | } else { 135 | [this.out, this.in] = ifaceData.endpoints; 136 | } 137 | 138 | debugInfo('claiming interfaces'); 139 | 140 | this._reattachCdcDriverAtFinal = false; 141 | this._reattachDataDriverAtFinal = false; 142 | // Linux/mac need to detach the cdc-acm kernel driver, but 143 | // windows users did that manually, and libusb-win just throws 144 | // errors when detaching/attaching kernel drivers. 145 | if (process.platform !== 'win32') { 146 | if (ifaceCdc.isKernelDriverActive()) { 147 | ifaceCdc.detachKernelDriver(); 148 | this._reattachCdcDriverAtFinal = true; 149 | } 150 | 151 | if (ifaceData.isKernelDriverActive()) { 152 | ifaceData.detachKernelDriver(); 153 | this._reattachDataDriverAtFinal = true; 154 | } 155 | } 156 | ifaceCdc.claim(); 157 | ifaceData.claim(); 158 | 159 | this.ctr.on('data', this._onStatus.bind(this)); 160 | this.ctr.on('error', this._onError.bind(this)); 161 | this.ctr.startPoll(); 162 | 163 | 164 | // Set baud rate and serial line params, 165 | // then set the line as active 166 | this._controlSetLineCoding(options.baudRate || 9600) 167 | .then(() => { this._controlLineState(true); }) 168 | .then(() => { this._controlGetLineCoding(); }) 169 | .then(() => { 170 | this.in.on('data', data => this._onData(data)); 171 | this.in.on('error', err => this.emit('error', err)); 172 | this.out.on('error', err => this.emit('error', err)); 173 | 174 | this.in.timeout = 1000; 175 | this.out.timeout = 1000; 176 | }); 177 | } 178 | 179 | _read() { 180 | debugData('_read'); 181 | if (!this.polling) { 182 | debugInfo('starting polling'); 183 | this.in.startPoll(); 184 | this.polling = true; 185 | } 186 | } 187 | 188 | _onData(data) { 189 | debugData('_onData ', data); 190 | const keepReading = this.push(data); 191 | if (!keepReading) { 192 | this._stopPolling(); 193 | } 194 | } 195 | 196 | _onError(err) { 197 | debugInfo('Error: ', err); 198 | this.emit('error', err); 199 | // throw err; 200 | } 201 | 202 | _onStatus(sts) { // eslint-disable-line class-methods-use-this 203 | debugInfo('Status: ', sts); 204 | } 205 | 206 | _stopPolling() { 207 | debugInfo('_stopPolling'); 208 | if (this.polling) { 209 | debugInfo('stopping polling'); 210 | this.in.stopPoll(); 211 | this.polling = false; 212 | } 213 | } 214 | 215 | _write(data, encoding, callback) { 216 | debugData(`_write ${data.toString()}`); 217 | 218 | this.out.transfer(data, callback); 219 | } 220 | 221 | _destroy() { 222 | debugInfo('_destroy'); 223 | 224 | // Set line state as unused, close all resources, release interfaces 225 | // (waiting until they are released), reattach kernel drivers if they 226 | // were attached before, then emit a 'close' event. 227 | 228 | this._controlLineState(false) 229 | .then(() => { 230 | this._stopPolling(); 231 | this.ctr.stopPoll(); 232 | 233 | this.ctr.removeAllListeners(); 234 | this.in.removeAllListeners(); 235 | this.out.removeAllListeners(); 236 | 237 | this.ifaceCdc.release(true, err => { 238 | if (err) { throw err; } 239 | this.ifaceData.release(true, err2 => { 240 | if (err2) { throw err2; } 241 | 242 | if (this._reattachCdcDriverAtFinal) { 243 | this.ifaceCdc.attachKernelDriver(); 244 | } 245 | if (this._reattachDataDriverAtFinal) { 246 | this.ifaceData.attachKernelDriver(); 247 | } 248 | 249 | debugInfo('All resources released'); 250 | this.emit('close'); 251 | }); 252 | }); 253 | }); 254 | } 255 | 256 | 257 | // Performs a _controlTransfer() to set the line state. 258 | // Set active to a truthy value to indicate there is something connected to the line, 259 | // falsy otherwise. 260 | // Returns a Promise. 261 | _controlLineState(active) { 262 | // This is documented in the PSTN doc of the USB spec, section 6.3.12 263 | return this._controlTransfer( 264 | 0x21, // bmRequestType: [host-to-device, type: class, recipient: iface] 265 | 0x22, // SET_CONTROL_LINE_STATE 266 | active ? 0x03 : 0x00, // 0x02 "Activate carrier" & 0x01 "DTE is present" 267 | this.ifaceCdc.id, // interface index 268 | Buffer.from([]), // No data expected back 269 | ); 270 | } 271 | 272 | // Performs a _controlTransfer to set the line coding. 273 | // This includes bitrate, stop bits, parity, and data bits. 274 | _controlSetLineCoding(baudRate = 9600) { 275 | // This is documented in the PSTN doc of the USB spec, section 6.3.10, 276 | // values for the data structure at the table in 6.3.11. 277 | const data = Buffer.from([ 278 | 0, 0, 0, 0, // Four bytes for the bitrate, will be filled in later. 279 | 0, // Stop bits. 0 means "1 stop bit" 280 | 0, // Parity. 0 means "no parity" 281 | 8, // Number of data bits 282 | ]); 283 | 284 | data.writeInt32LE(baudRate, 0); 285 | 286 | debugInfo('Setting baud rate to ', baudRate); 287 | 288 | return this._controlTransfer( 289 | 0x21, // bmRequestType: [host-to-device, type: class, recipient: iface] 290 | 0x20, // SET_LINE_CODING 291 | 0x00, // Always zero 292 | this.ifaceCdc.id, // interface index 293 | data, 294 | ); 295 | } 296 | 297 | // Performs a _controlTransfer to get the line coding. 298 | // This includes bitrate, stop bits, parity, and data bits. 299 | _controlGetLineCoding() { 300 | // This is documented in the PSTN doc of the USB spec, section 6.3.11, 301 | debugInfo('Requesting actual line coding values'); 302 | 303 | return this._controlTransfer( 304 | 0xA1, // bmRequestType: [device-to-host, type: class, recipient: iface] 305 | 0x21, // GET_LINE_CODING 306 | 0x00, // Always zero 307 | this.ifaceCdc.id, // interface index 308 | 7 // Length of data expected back 309 | ).then(data => { 310 | const baudRate = data.readInt32LE(0); 311 | const rawStopBits = data.readInt8(4); 312 | const rawParity = data.readInt8(5); 313 | const dataBits = data.readInt8(6); 314 | 315 | let stopBits; 316 | let parity; 317 | switch (rawStopBits) { 318 | case 0: stopBits = 1; break; 319 | case 1: stopBits = 1.5; break; 320 | case 2: stopBits = 2; break; 321 | default: throw new Error('Invalid value for stop bits received (during a GET_LINE_CODING request)'); 322 | } 323 | switch (rawParity) { 324 | case 0: parity = 'none'; break; 325 | case 1: parity = 'odd'; break; 326 | case 2: parity = 'even'; break; 327 | case 3: parity = 'mark'; break; 328 | case 4: parity = 'space'; break; 329 | default: throw new Error('Invalid value for parity received (during a GET_LINE_CODING request)'); 330 | } 331 | 332 | debugInfo('Got line coding: ', data); 333 | debugInfo('Reported baud rate: ', baudRate); 334 | debugInfo('Reported stop bits: ', stopBits); 335 | debugInfo('Reported parity: ', parity); 336 | debugInfo('Reported data bits: ', dataBits); 337 | 338 | return data; 339 | }); 340 | } 341 | 342 | // The device's controlTransfer, wrapped as a Promise 343 | _controlTransfer(bmRequestType, bRequest, wValue, wIndex, dataOrLength) { 344 | return new Promise((res, rej) => { 345 | this.device.controlTransfer( 346 | bmRequestType, 347 | bRequest, 348 | wValue, 349 | wIndex, 350 | dataOrLength, 351 | ((err, data) => (err ? rej(err) : res(data))), 352 | ); 353 | }); 354 | } 355 | 356 | 357 | // Given an instance of Device (from the 'usb' library), opens it, looks through 358 | // its interfaces, and creates an instance of UsbStream per interface which 359 | // looks like a CDC ACM control interface (having the right descriptor and endpoints). 360 | // 361 | // The given Device must be already open()ed. Conversely, it has to be close()d 362 | // when the stream is no longer used, or if this method throws an error. 363 | // 364 | // Returns an array of instances of UsbCdcAcm. 365 | static fromUsbDevice(device, options = {}) { 366 | const ifaces = device.interfaces; 367 | 368 | for (let i = 0, l = ifaces.length; i < l; i += 1) { 369 | const iface = ifaces[i]; 370 | 371 | if (assertCdcInterface(iface) !== false) { 372 | return new UsbCdcAcm(iface, options); 373 | } 374 | } 375 | 376 | throw new Error('No valid CDC interfaces found in USB device'); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2010 - 2018, Nordic Semiconductor ASA 2 | * 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * 15 | * 3. Neither the name of Nordic Semiconductor ASA nor the names of its 16 | * contributors may be used to endorse or promote products derived from this 17 | * software without specific prior written permission. 18 | * 19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | * IMPLIED WARRANTIES OF MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE 22 | * ARE DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE 23 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | * POSSIBILITY OF SUCH DAMAGE. 30 | */ 31 | 32 | /** 33 | * 34 | * This demo shows how to get an instance of an usb-cdc-acm Stream, 35 | * and send/receive data from it. 36 | * 37 | * It's quite important to both destroy the stream and close the device 38 | * when done. 39 | * 40 | */ 41 | 42 | const usb = require('usb'); 43 | const UsbCdcAcm = require('..'); 44 | const Debug = require('debug'); 45 | 46 | const debug = Debug('main'); 47 | 48 | Debug.enable('*'); 49 | 50 | // const device = usb.findByIds(0x1915, 0x521f); // VID/PID for Nordic semi / USB SDFU 51 | // const device = usb.findByIds(0x1915, 0x520f); // VID/PID for Nordic semi / USB CDC demo 52 | // const device = usb.findByIds(0x1366, 0x1015); // VID/PID for a Segger IMCU (with USB storage) 53 | // const device = usb.findByIds(0x1366, 0x0105); // VID/PID for a Segger IMCU (without USB storage) 54 | 55 | const [,, vid, pid] = process.argv; 56 | const vendorId = parseInt(vid || '1915', 16); 57 | const productId = parseInt(pid || '520f', 16); 58 | 59 | debug(`Looking for VID/PID: 0x${vendorId.toString(16)}/0x${productId.toString(16)}`); 60 | const device = usb.findByIds(vendorId, productId); 61 | 62 | if (!device) { 63 | console.log('Use this script with nRF USB device'); 64 | process.exit(); 65 | } 66 | 67 | device.timeout = 100; 68 | debug('Opening device'); 69 | device.open(); 70 | 71 | // usb.setDebugLevel(4); // Uncomment for extra USB verbosiness 72 | 73 | // const stream = UsbCdcAcm.fromUsbDevice(device, { baudRate: 115200 }); 74 | const stream = UsbCdcAcm.fromUsbDevice(device, { baudRate: 1000000 }); 75 | 76 | // Display all data received 77 | stream.on('data', data => { debug('data', data.toString()); }); 78 | 79 | // Log other events from the Stream, just in case 80 | stream.on('error', err => { debug('error', err); }); 81 | stream.on('status', sts => { debug('status', sts); }); 82 | stream.on('close', () => { debug('Stream is now closed'); }); 83 | stream.on('drain', () => { debug('Stream can be drained now'); }); 84 | 85 | let i = 0; 86 | 87 | const timer = setInterval(() => { 88 | stream.write(`foobar ${i} ${Date()}\n`); 89 | i += 1; 90 | debug('Sent a write'); 91 | }, 2500); 92 | 93 | 94 | setTimeout(() => { 95 | clearInterval(timer); 96 | 97 | debug('Closing the stream'); 98 | stream.destroy(); 99 | 100 | setTimeout(() => { 101 | debug('Closing device'); 102 | // device.close(); 103 | }, 5000); 104 | }, 50000); 105 | --------------------------------------------------------------------------------