UNPKG

shuttle-control-usb

Version:

NodeJS Interface for Contour ShuttleXpress, ShuttlePro V1, and ShuttlePro V2

230 lines (209 loc) 6.5 kB
'use strict' const hid = require('node-hid') const { usb } = require('usb') const crypto = require('crypto') const shuttleDevices = require('./ShuttleDefs') const { vids, pids } = require('./ShuttlePIDs') const EventEmitter = require('events').EventEmitter const defaultState = { buttons: [], shuttle: 0, jog: 0 } class Shuttle extends EventEmitter { constructor () { super() this._hid = []; } // Expose VIDs and PIDs vids = vids pids = pids // Methods start (watchUsb = true) { if (watchUsb) { usb.on('attach', this._listener) // Find already connected devices this._search() } } stop () { usb.off('attach', this._listener) if (this._hid.length > 0) { this._hid.forEach((device) => { device.hid.close() }) } } getDeviceList () { return this._hid.map((device) => { return { id: device.id, path: device.path, name: device.def.name, hasShuttle: device.def.rules.shuttle !== undefined, hasJog: device.def.rules.jog !== undefined, numButtons: device.def.buttonMasks.length } }) } getDeviceById (id) { const device = this.getDeviceList().find(ele => ele.id === id) return device ? device : null } getDeviceByPath (path) { const device = this.getDeviceList().find(ele => ele.path === path) return device ? device : null } getRawHidDevice (id) { const device = this._hid.find(ele => ele.id === id) return device ? device.hid : null } _search () { 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) => { this._connect(device, deviceDef) }) }) } connect (path) { const devices = hid.devices() shuttleDevices.forEach((deviceDef) => { const connectedPaths = this._hid.map(h => h.path) if (!connectedPaths.includes(path)) { const device = devices.find(d => d.path === path && d.vendorId === deviceDef.vid && d.productId === deviceDef.pid) if (device) { this._connect(device, deviceDef) } } }) } _connect (device, deviceDef) { 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)) }) const deviceIdx = this._hid.findIndex(ele => ele.id === newId) if (deviceIdx > -1) { const device = this._hid[deviceIdx] this.emit('connected', { id: device.id, path: device.path, name: device.def.name, hasShuttle: device.def.rules.shuttle !== undefined, hasJog: device.def.rules.jog !== undefined, numButtons: device.def.buttonMasks.length }) device.def.buttonMasks.forEach((ele) => { device.state.buttons.push(false) }) device.hid.on('data', (data) => { this._updateData(data, device) }) device.hid.on('error', (error) => { device.hid.close() const index = this._hid.findIndex(ele => ele.id === device.id) this._hid.splice(index, 1) this.emit('disconnected', device.id) }) } } catch (err) { // Ignore } } _updateData (data, device) { 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 !== 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.def.buttonMasks.forEach((mask, index) => { const button = (buttonsRaw & mask) 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) } device.state.buttons[index] = button }) } } _read (data, offset, type) { let value = null switch (type) { case "uint8": value = data.readUInt8(offset) break case "int8": value = data.readInt8(offset) break case "uint16le": value = data.readUInt16LE(offset) break case "int16le": value = data.readInt16LE(offset) break case "uint16be": value = data.readUInt16BE(offset) break case "int16be": value = data.readInt16BE(offset) break case "uint32le": value = data.readUInt32LE(offset) break case "int32le": value = data.readInt32LE(offset) break case "uint32be": value = data.readUInt32BE(offset) break case "int32be": value = data.readInt32BE(offset) break case "uint64le": value = data.readBigUInt64LE(offset) break case "int64le": value = data.readBigInt64LE(offset) break case "uint64be": value = data.readUBigInt64BE(offset) break case "int64be": value = data.readBigInt64BE(offset) break } return value } _listener = (d) => { // Delay connection by 1 second because // it takes a second to load on macOS and Linux setTimeout(() => { this._search() }, 1000) } } module.exports = new Shuttle()