diff --git a/README.md b/README.md index 639495f..092be3d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# ShuttleControlUSB +# ShuttleControlUSB [![npm](https://img.shields.io/npm/v/shuttle-control-usb.svg)](https://www.npmjs.com/package/shuttle-control-usb) -_A Library to use Contour Design ShuttleXpress and ShuttlePro (v1 and v2) in Node.js projects without the driver._ +A Library to use Contour Design ShuttleXpress and ShuttlePro (v1 and v2) in Node.js projects without the driver. In some markets, these devices are also known as Multimedia Controller Xpress and Multimedia Controller PRO v2. + +_This library now supports multiple devices connected at one time (since v1.1.0). This change is backwards compatible and simply includes the device connection UUID for each event as the final parameter in the callback._ ## Installation ```sh -npm install --save https://github.com/hopejr/ShuttleControlUSB.git +npm install shuttle-control-usb ``` ## Usage @@ -24,7 +26,7 @@ shuttle.start(); ### Methods `start()` -Starts the service and monitors USB device connections for ShuttleXPress or ShuttlePro (v1 and v2). It will find the first device connected. Only one device is supported at a time. +Starts the service and monitors USB device connections for ShuttleXPress or ShuttlePro (v1 and v2). It will find all connected devices. This should be called after the 'connect' event listener has been declared, otherwise already-connected devices will not be detected. @@ -41,6 +43,7 @@ Emitted when a device has been plugged into a USB port. Returns: - `deviceInfo` Object + - `id` String - either an MD5 hash of the serial number (if it exists) or the device path, used to distinguish between multiple devices that may be connected at once. - `name` String - name of the device ('ShuttleXpress', 'ShuttlePro v1', or 'ShuttlePro v2') - `hasShuttle` Boolean - `hasJog` Boolean @@ -49,11 +52,15 @@ Returns: #### Event: `disconnected` Emitted when the device has been unplugged or has failed. +Returns: +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. + #### Event: `shuttle` Emitted when shuttle data is available from the device. Returns: - `value` Integer - Range from -7 to 7 for ShuttleXpress and ShuttlePro (v1 and v2) +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. #### Event: `shuttle-trans` Emitted when shuttle data is available from the device. @@ -61,30 +68,35 @@ Emitted when shuttle data is available from the device. Returns: - `old` Integer - `new` Integer +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. #### Event: `jog` Emitted when jog data is available from the device. Returns: - `value` Integer - Range from 0 to 255 for ShuttleXpress and ShuttlePro (v1 and v2) +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. #### Event: `jog-dir` Emitted when jog data is available from the device. Returns: - `dir` Integer - Either 1 (clockwise) or -1 (counter-clockwise) +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. #### Event: `buttondown` Emitted when a button is pressed on the device. Returns: - `button` Integer - the button number +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. #### Event: `buttonup` Emitted when a button is released on the device. Returns: - `button` Integer - the button number +- `id` String - either an MD5 hash of the serial number (if it exists) or the device path. ## Linux Note @@ -95,10 +107,6 @@ By default, the udev system adds ShuttleXpress, ShuttlePro V1, and ShuttlePro V2 Then reboot your computer. -# Future Features -I'm looking at allowing multiple devices to be connected at once. Stay tuned for this feature. It will likely result in a change to the API and will be a breaking change. - - ## Licence MIT diff --git a/lib/Shuttle.js b/lib/Shuttle.js index 4a10dfd..bfdf1e0 100644 --- a/lib/Shuttle.js +++ b/lib/Shuttle.js @@ -2,6 +2,7 @@ const hid = require('node-hid') const { usb } = require('usb') +const crypto = require('crypto') const shuttleDevices = require('./ShuttleDefs') const EventEmitter = require('events').EventEmitter @@ -14,101 +15,120 @@ const defaultState = { class Shuttle extends EventEmitter { constructor () { super() - this._connected = false - this._hid = null; - this._state = JSON.parse(JSON.stringify(defaultState)) + this._hid = []; } start () { - if (!this._connected) { - usb.on('attach', (d) => { - // Delay connection by 1 second because - // it takes a second to load on macOS - setTimeout(() => { - this._connect() - }, process.platform === 'darwin' ? 1000 : 0) - }) - // Find already - this._connect() - } + usb.on('attach', (d) => { + // Delay connection by 1 second because + // it takes a second to load on macOS + setTimeout(() => { + this._connect() + }, process.platform === 'darwin' ? 1000 : 0) + }) + // Find already + this._connect() } stop () { usb.unrefHotplugEvents() - if (this._connected) { - if (this._hid !== null) { - this._hid.close() - this._hid = null - } - this._connected = false + if (this._hid.length > 0) { + this._hid.forEach((device) => { + device.hid.close() + }) } } + getDeviceList () { + return this._hid.map((device) => { + return { + id: device.id, + name: device.def.name, + hasShuttle: device.def.rules.shuttle !== undefined, + hasJog: device.def.rules.jog !== undefined, + numButtons: device.def.buttonMasks.length + } + }) + } + _connect () { - if (this._hid === null) { - let foundDevice = null - shuttleDevices.forEach((device) => { - if (foundDevice === null) { - try { - this._hid = new hid.HID(device.vid, device.did) - foundDevice = device - } catch (err) { - // Ignore - } + const foundDevices = [] + const devices = hid.devices() + shuttleDevices.forEach((deviceDef) => { + const connectedPaths = this._hid.map(h => h.path) + const filteredDevices = devices.filter((d) => { + return d.vendorId === deviceDef.vid && d.productId === deviceDef.pid + && !connectedPaths.includes(d.path) + }) + filteredDevices.forEach((device) => { + try { + const newHid = new hid.HID(device.path) + const newId = crypto.createHash('md5').update(device.serialNumber || device.path).digest('hex') + this._hid.push({ + id: newId, + hid: newHid, + def: deviceDef, + path: device.path, + state: JSON.parse(JSON.stringify(defaultState)) + }) + foundDevices.push(newId) + } catch (err) { + // Ignore } }) + }) - if (this._hid !== null) { - this._connected = true - + foundDevices.forEach((foundDevice) => { + const deviceIdx = this._hid.findIndex(ele => ele.id === foundDevice) + if (deviceIdx > -1) { + const device = this._hid[deviceIdx] this.emit('connected', { - name: foundDevice.name, - hasShuttle: foundDevice.rules.shuttle !== undefined, - hasJog: foundDevice.rules.jog !== undefined, - numButtons: foundDevice.buttonMasks.length + id: device.id, + name: device.def.name, + hasShuttle: device.def.rules.shuttle !== undefined, + hasJog: device.def.rules.jog !== undefined, + numButtons: device.def.buttonMasks.length }) - foundDevice.buttonMasks.forEach((ele) => { - this._state.buttons.push(false) + device.def.buttonMasks.forEach((ele) => { + device.state.buttons.push(false) }) - this._hid.on('data', (data) => { - this._updateData(data, foundDevice) + device.hid.on('data', (data) => { + this._updateData(data, device) }) - this._hid.on('error', (error) => { - this._hid.close() - this._hid = null - this.emit('disconnected') - this._state = JSON.parse(JSON.stringify(defaultState)) - this._connected = false + device.hid.on('error', (error) => { + device.hid.close() + this._hid.splice(deviceIdx, 1) + this.emit('disconnected', device.id) }) } - } + }) } _updateData (data, device) { - if (data.length === device.packetSize) { - let shuttle = this._read(data, device.rules.shuttle.offset, device.rules.shuttle.type) - let jog = this._read(data, device.rules.jog.offset, device.rules.jog.type) - let buttonsRaw = this._read(data, device.rules.buttons.offset, device.rules.buttons.type) - if (shuttle !== this._state.shuttle) { - this.emit('shuttle', shuttle) - this.emit('shuttle-trans', this._state.shuttle, shuttle) - this._state.shuttle = shuttle + if (data.length === device.def.packetSize) { + let shuttle = this._read(data, device.def.rules.shuttle.offset, device.def.rules.shuttle.type) + let jog = this._read(data, device.def.rules.jog.offset, device.def.rules.jog.type) + let buttonsRaw = this._read(data, device.def.rules.buttons.offset, device.def.rules.buttons.type) + if (shuttle !== device.state.shuttle) { + this.emit('shuttle', shuttle, device.id) + this.emit('shuttle-trans', device.state.shuttle, shuttle, device.id) + device.state.shuttle = shuttle } - if (jog !== this._state.jog) { - let dir = (this._state.jog === 0xff && jog === 0) || (!(this._state.jog === 0 && jog === 0xff) && this._state.jog < jog) ? 1 : -1 - this._state.jog = jog - this.emit('jog', jog) - this.emit('jog-dir', dir) + if (jog !== device.state.jog) { + let dir = (device.state.jog === 0xff && jog === 0) || (!(device.state.jog === 0 && jog === 0xff) && device.state.jog < jog) ? 1 : -1 + device.state.jog = jog + this.emit('jog', jog, device.id) + this.emit('jog-dir', dir, device.id) } // Treat buttons a little differently. Need to do button up and button down events - device.buttonMasks.forEach((mask, index) => { + device.def.buttonMasks.forEach((mask, index) => { const button = (buttonsRaw & mask) - if (button && !this._state.buttons[index]) { - this.emit('buttondown', index + 1) - } else if (!button && this._state.buttons[index]) { - this.emit('buttonup', index + 1) + if (button && !device.state.buttons[index]) { + this.emit('buttondown', index + 1, device.id) + } else if (!button && device.state.buttons[index]) { + this.emit('buttonup', index + 1, device.id) } - this._state.buttons[index] = button + device.state.buttons[index] = button }) } } diff --git a/lib/ShuttleDefs.js b/lib/ShuttleDefs.js index aec61f8..031e471 100644 --- a/lib/ShuttleDefs.js +++ b/lib/ShuttleDefs.js @@ -5,7 +5,7 @@ module.exports = [ name: 'ShuttleXpress', vendor: 'Contour Design, Inc.', vid: 0x0b33, - did: 0x0020, + pid: 0x0020, packetSize: 5, rules: { shuttle: { @@ -27,7 +27,7 @@ module.exports = [ name: 'ShuttlePro v1', vendor: 'Contour Design, Inc.', vid: 0x0b33, - did: 0x0010, + pid: 0x0010, packetSize: 5, rules: { shuttle: { @@ -49,7 +49,7 @@ module.exports = [ name: 'ShuttlePro v2', vendor: 'Contour Design, Inc.', vid: 0x0b33, - did: 0x0030, + pid: 0x0030, packetSize: 5, rules: { shuttle: { diff --git a/package-lock.json b/package-lock.json index a010b9c..b20f6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shuttle-control-usb", - "version": "1.0.6", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "shuttle-control-usb", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "dependencies": { "node-hid": "^2.1.2", diff --git a/package.json b/package.json index e10d97d..4025d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shuttle-control-usb", - "version": "1.0.6", + "version": "1.1.0", "description": "NodeJS Interface for Contour ShuttleXpress, ShuttlePro V1, and ShuttlePro V2", "main": "index.js", "scripts": { diff --git a/tests/test.js b/tests/test.js index dd11c22..5d85a78 100644 --- a/tests/test.js +++ b/tests/test.js @@ -3,6 +3,7 @@ const shuttle = require('../lib/Shuttle') shuttle.on('connected', (deviceInfo) => { console.log('Starting tests') + console.log('Connected', deviceInfo.id, deviceInfo.name) test('shuttle test', (t) => { t.plan(3) @@ -15,10 +16,24 @@ shuttle.on('connected', (deviceInfo) => { } else if (deviceInfo.name === 'ShuttlePro v2') { t.equal(deviceInfo.numButtons === 15) } - shuttle.stop() + console.log('Unplug device') }) }) +shuttle.on('disconnected', (id) => { + console.log('Disconnected', id) + if (shuttle.getDeviceList().length === 0) { + console.log('Testing complete') + shuttle.stop() + } +}) + +shuttle.on('buttonup', (button, id) => { + console.log('Shuttle button up', button, id) +}) + shuttle.start() -console.log('Plug device in') +if (shuttle.getDeviceList().length === 0) { + console.log('Plug device in') +}