UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,225 lines (1,077 loc) 34.3 kB
/** * * Diverse Types of Device connectors * * @requires plugin * @requires node-midi * @requires serialport * @requires net * @requires axios * @requires udp * @requires json5 * * @module devices/device * @copyright Lasse Marburg 2021 * @license MIT */ try { var osc = require('osc-min') } catch (err) { var osc = null console.log("running without osc module") } try { var { SerialPort } = require('serialport') var { ReadlineParser } = require('@serialport/parser-readline') } catch (err) { if(err.code == 'MODULE_NOT_FOUND') { var SerialPort = null console.log("serialport module missing. Can not use serial device") } else { throw err } } const dgram = require('udp') const net = require('net') const axios = require('axios') const JSON5 = require('json5') const { PluginItem } = require('../plugin_items.js') var midi = null var virtual_midi_port = '' /** * midi makes trouble on some machines so it needs to be activated manually * */ function activateMidi() { try { midi = require('midi') /** global var that holds name of virtual midi port. Keeps it to one virtual midi port per game */ virtual_midi_port = '' log.debug("devices", "Midi is active.") } catch (err) { midi = null log.debug("devices", "Midi is inactive.") } } function deactivateMidi() { midi = null log.debug("devices", "Midi is inactive.") } /** * define a name for the virtual midi in and out ports * * @param {string} vmp - name for virtual midi port */ function setVirtualMidiPort(vmp) { virtual_midi_port = vmp } /** * enlist available Midi port names. * prepend a possible virtual midi port based on game name * * @returns {Promise<array>} array with midi portnames. Array empty [] if no valid pors are found */ function getMidiPorts() { return new Promise(function (resolve, reject) { var ports = {} if (midi) { input = new midi.input() ports["midi_input"] = [virtual_midi_port] for (let i = 0; i < input.getPortCount(); i++) { if (input.getPortName(i) != virtual_midi_port) { ports.midi_input.push(input.getPortName(i)) } } output = new midi.output() ports["midi_output"] = [virtual_midi_port] for (let i = 0; i < output.getPortCount(); i++) { if (output.getPortName(i) != virtual_midi_port) { ports.midi_output.push(output.getPortName(i)) } } } return resolve(ports) }) } /** * enlist available Serial port names. * * @returns {Promise<array>} array with serial portnames. Array empty [] if no valid pors are found */ function getSerialPorts() { return new Promise(function (resolve, reject) { if (SerialPort) { let ports = [] SerialPort.list() .then(serial_ports => { if (Array.isArray(serial_ports)) { for (let port of serial_ports) { if (Object.keys(port).some(key => key !== 'path' && port[key] !== undefined)) { ports.push(port.path) } } } return resolve(ports) }) .catch(err => { return reject(err) }) } else { resolve([]) } }) } /** * create and return device based on interface as specified in device settings * defaults to device collection for data storage * * @param {Object} data - data set the device will be based on * @param {Object} app - express app to allow http requests and routing for webdevice * @param {EventEmitter} game.event - event emitter that originated in game class instance * * @returns {Device} instance of one of the device classes */ function getDevice(data, collection, plugin, game) { switch (collection.name.toUpperCase()) { case "UDP": return new UDPdevice(data, collection, plugin, game) case "OSC": return new OSCdevice(data, collection, plugin, game) case "TCP": return new TCPdevice(data, collection, plugin, game) case "HTTP": return new Webdevice(data, collection, plugin, game) case "SERIAL": return new Serialdevice(data, collection, plugin, game) case "MIDI": return new Mididevice(data, collection, plugin, game) default: log.error("getDevice", "no such interface " + collection.name) return undefined } } /** * Has Access to write incoming messages to DB. * Devices should define their own [send]{@link Device#send} function and redirect incoming * messages as JSON or JSON formatted string to the [status update]{@link Device#incomingMessage} function. * * @param {Object} config - contains all device specifications * @param {string} config.name - name identifier for Device * @param {string} config.interface - connection protocol that is used for communication * @param {Object} config.settings - specification of interface parameters * @param {Object} config.properties - properties the device can send and/or receive * @param {Collection} db_coll - instance of Database Collection * @param {EventEmitter} game.event - to dispatch events for the whole game */ class Device extends PluginItem { constructor(config, db_coll, plugin, game) { super(config, db_coll, plugin, game) this.log("Device created") } /** * validate incoming message and update device status in DB. Incoming message is stored in 'incoming' field. * * objects are appended to the incoming variable in dot notation ('key1.key2') so they * don't overwrite existing variables * * single values (string, number) will overwrite all existing values! * * Fire game event where event name is the device name and message is the event load * * @todo rewrite default level launch maybe using keyword. Always run default level with device document as first argument. * * @param {Object|string} message - json formatted message */ async incomingMessage(message) { if (!message) { return } if(typeof message === 'string') { if(message[0] == '{') { try { message = JSON5.parse(message) } catch(err) { if(err instanceof SyntaxError) { this.session.log.error('Could not parse JSON. Syntax error in message "' + message) } else { throw new Error(err) } } } } if (this.hasOwnProperty("init")) { if (this.init.hasOwnProperty("reference") && this.init.hasOwnProperty("level")) { for (let key in message) { if (this.init.reference == key) { await this.initialContact(this.init.reference, message[key], this.init.level, "Player") } } } else { log.warn(this.name, "tried starting default level but reference or level is missing.") } } await this.document.set({'incoming':message}) this.event.emit('incomingMessage', message) } /** * change connection properties and restart server/connection if needed * all device types that can change connection properties (meaning all, I guess) have to have this * @abstract * @param {Object} settings - (Updated) device settings */ connect() { log.error(this.name, 'No reconnect function defined for "' + this.constructor.name + '"!') } /** * send data to device. * * @abstract * @param {Object} data - data to send. */ async send(data) { log.error(this.name, 'No send function defined for "' + this.constructor.name + '"! Tried to send: ') log.error(this.name, data) return } /** * on changes in this devices document * @abstract */ update(updt) { // } /** * @todo add reconnect command */ command(input) { switch (input[0]) { case "send": try { this.send(JSON5.parse(input[1])) } catch (err) { log.warn(this.name, 'Can not send message. Error when parsing string to json.') log.warn(this.name, input[1]) } break case "settings": log.info(this.name, this.settings) break case "reconnect": this.reconnect(this.settings) break default: log.warn(this.name, 'no such command: ' + input[0]) break } } } /** * UDP network interface * send and receive UDP messages * * based on dgram module * best understood with help of: * {@link https://www.hacksparrow.com/node-js-udp-server-and-client-example.html} * * @param {string} config.name - name identifier for this device * @param {number} config.settings.port - port to communicate with device. Device and framework port are identical if framework_port is not provided. * @param {number} [config.settings.framework_port] - distinct port to listen for messages from this UDP device * @param {string} config.settings.ip - ip of device. Where to send messages to from adaptor:ex */ class UDPdevice extends Device { constructor(config, db_coll, plugin, game) { super(config, db_coll, plugin, game) this.update(config) this.connect() } /** * start udp server to listen for messages * if adaptor_port is not defined, device object listens on port for incomming messages from device * * @param {Object} settings - new setting properties. See class doc for setting properties. */ connect() { this.setConnected(false) if (this.hasOwnProperty('server')) { this.server.close(this.startServer.bind(this)) } else { this.startServer() } } disconnect() { if (this.hasOwnProperty('server')) { this.server.close() delete this.server log.info(this.name, "Disconnected") this.setConnected(false) } } update({settings, name}) { if (!settings.hasOwnProperty('adaptor_port')) { settings['adaptor_port'] = settings.port } if(settings.adaptor_port != this.settings.adaptor_port) { this.connect() } this.settings = settings this.name = name } startServer() { this.server = dgram.createSocket('udp4') this.server.on('listening', () => { var address = this.server.address() log.info(this.name, 'adaptor:ex listening on ' + address.address + ':' + address.port) this.setConnected(true) }) this.server.on('message', (message, remote) => { this.handle(message, remote) }) this.server.on('error', err => { if (err.code == 'EADDRINUSE') { log.error(this.name, 'Adaptor port already in use') log.error(this.name, err.message) } else { log.error(err) } this.setConnected(false) }) this.server.bind(this.settings.adaptor_port, '0.0.0.0') } handle(message, remote) { log.debug(this.name, remote.address + ':' + remote.port + ' - ' + message) this.incomingMessage(message.toString()) //log.debug(this.name, remote.address + ':' + remote.port +' - ' + message) } async send(msg) { if (this.settings.hasOwnProperty('ip')) { let json_msg = msg if (!Buffer.isBuffer(msg)) { msg = new Buffer.from(JSON.stringify(msg)) } var client = dgram.createSocket('udp4') client.send(msg, 0, msg.length, this.settings.port, this.settings.ip, (err, bytes) => { if (err) { log.error(this.name, err) } log.debug(this.name, 'send ' + JSON.stringify(json_msg) + ' to ' + this.settings.ip + ':' + this.settings.port) client.close() return }) } else { throw new adaptor.InvalidError(`Try to send message to ${this.name} but no ip was defined.`) } } close() { this.disconnect() } } /** * OSC/UDP network interface * send and receive OSC messages over UDP connection * * based on osc-min package * {@link https://www.npmjs.com/package/osc-min} * * @param {string} config.name - name identifier for this device * @param {number} config.settings.port - port to communicate with device. Device and adaptor port are identical if adaptor_port is not provided. * @param {number} [config.settings.adaptor_port] - distinct port to listen for messages from this UDP device * @param {string} config.settings.ip - ip of device. Where to send messages from adaptor * @param {string} config.settings.path - device OSC path that is prepended to message OSC path */ class OSCdevice extends UDPdevice { constructor(config, db_coll, plugin, game) { super(config, db_coll, plugin, game) } /** * send OSC formatted message. For every (nested) property a separate message is send. */ async send(msg, {path,is_float}={}) { var osc_msg = [] if(typeof msg === "string") { osc_msg = [{"addr":"","args":msg}] } else if (typeof msg === "number") { if(is_float || !Number.isInteger(msg)) { osc_msg = [{"addr":"","args":{"value":parseFloat(msg),"type":"float"}}] } else { osc_msg = [{"addr":"","args":{"value":parseInt(msg),"type":"integer"}}] } } else { osc_msg = [...this.toOSC(msg)] } for (let o of osc_msg) { if (o.hasOwnProperty('addr') && o.hasOwnProperty('args')) { if(path) { o.addr = path + o.addr } if (this.settings.hasOwnProperty("path")) { o.addr = this.settings.path + o.addr } if(o.addr == "") { o.addr = "/" } log.debug("to " + this.name, o) let buf = osc.toBuffer({ address: o.addr, args: o.args }) await super.send(buf) } } return } /** * iterate through object and convert it to osc compatible message * e.g.: {one:{two:{three:4}}} becomes {address:"/one/two/three", args 4} * * created with help of Jonas Wilms: * https://stackoverflow.com/questions/54634869/convert-json-to-osc-address-and-arguments/54635002#54635002 */ * toOSC(obj, previous = "") { for (const [key, value] of Object.entries(obj)) { if (typeof value !== "object" || Array.isArray(value)) { yield { addr: previous + "/" + key, args: value }; } else { yield* this.toOSC(value, previous + "/" + key); } } } route(osc_msg) { let osc_data = {} let path = osc_msg.address.replace(/\//g, '.') path = path.slice(1) if (osc_msg.args.length > 1) { let values = [] for (let arg of osc_msg.args) { values.push(arg.value) } osc_data[path] = values } else { let value = osc_msg.args[0].value osc_data[path] = value } return osc_data } /** * cast incoming OSC messages to a json format and update Database with incoming * function is called by UDP event handler and overwrites UDPDevice.handle() function */ handle(message, remote) { if (Buffer.isBuffer(message)) { try { message = osc.fromBuffer(message) let data = this.route(message) log.debug("from " + this.name, data) this.incomingMessage(data) } catch (error) { log.warn(this.name, "invalid OSC packet") log.warn(this.name, error) return } } else { log.warn(this.name, "Incomming msg is not a Buffer. Type: " + typeof (message)) } } } /** * TCP network interface * send and receive TCP messages * * based on net module * best understood with help of: * {@link https://www.hacksparrow.com/tcp-socket-programming-in-node-js.html} * * @param {string} config.name - name identifier for this device * @param {number} config.settings.port - port to communicate with device. Device and adaptor port are identical if adaptor_port is not provided. * @param {number} [config.settings.adaptor_port] - distinct port to listen for messages from this TCP device * @param {string} config.settings.ip - ip of device. Where to send messages to from adaptor:ex */ class TCPdevice extends Device { constructor(config, db_coll, plugin, game) { super(config, db_coll, plugin, game) this.connect(config.settings) } /** * start tcp server to listen for messages * if adaptor_port is not defined, device object listens on port for incomming messages from device * * @param {Object} settings - new setting properties. See class doc for setting properties. */ connect() { this.setConnected(false) if (this.hasOwnProperty('server')) { if (this.server.listening) { this.server.close(this.startServer.bind(this)) } else { log.warn(this.name, "tcp server was not running properly. Try to Reconnect none the less.") this.startServer() } } else { this.startServer() } } disconnect() { if (this.hasOwnProperty('server')) { if (this.server.listening) { this.server.close() this.setConnected(false) log.info(this.name, "Disconnected") } } } update({settings, name}) { if (!settings.hasOwnProperty('adaptor_port')) { settings['adaptor_port'] = settings.port } if(settings.adaptor_port != this.settings.adaptor_port) { this.connect() } this.settings = settings this.name = name } startServer() { this.server = net.createServer() this.server.listen({ port: this.settings.adaptor_port, host: '0.0.0.0' }, () => { log.info(this.name, 'adaptor:ex listening on ' + this.server.address().address + ':' + this.server.address().port) this.setConnected(true) }) this.server.on('error', (err) => { switch (err.code) { case 'EADDRINUSE': log.error(this.name, 'Port ' + err.port + ' already in use!') break default: log.error(this.name, err) } this.setConnected(false) }) this.server.on('connection', (sock) => { log.debug(this.name, 'connected ' + sock.remoteAddress + ':' + sock.remotePort) sock.on('data', (data) => { log.debug(this.name, 'incomming TCP message ' + sock.remoteAddress + ': ' + data) this.incomingMessage(data.toString(), sock) }) sock.on('close', (data) => { log.debug(this.name, 'closed ' + sock.remoteAddress + ':' + sock.remotePort) this.setConnected(false) }) }) } /** * default handle function. Overwrite if you want to deal with incomming messages from child class * */ handle(message, socket) { log.debug(this.name, 'incomming ' + socket.remoteAddress + ': ' + message) this.incomingMessage(message) } send(msg) { if (this.settings.hasOwnProperty('ip')) { var client = new net.Socket() client.connect(this.settings.port, this.settings.ip, () => { log.debug(this.name + ' send', msg) client.write(JSON.stringify(msg)) client.destroy() }) client.on('data', (data) => { log.debug(this.name + ' sent', data) client.destroy() }) client.on('close', () => { log.debug(this.name, 'closed') return }) client.on('error', (err) => { log.error(this.name, 'couldn`t send tcp message to ' + err.address + ':' + err.port + ': ') log.error(this.name, msg) switch (err.code) { case 'ECONNREFUSED': log.error(this.name, 'Connection was REFUSED') break case 'EHOSTUNREACH': log.error(this.name, 'unable to REACH HOST') break case 'EHOSTDOWN': log.error(this.name, 'HOST is DOWN') break case 'ETIMEDOUT': log.error(this.name, 'connection TIMED OUT') break default: log.error(this.name, err) } return }) } else { log.error(this.name, 'try to send message but no ip was defined.') return } } close() { this.disconnect() } } /** * http request interface. * Handles requests to the device and allows to make remote requests * * @param {string} config.name - name identifier for this device * @param {string} config.settings.url - url or ip of device with port and path * @param {string} config.settings.host - url or ip of device * @param {string} config.settings.path - path to device on host * @param {string} [config.settings.route=config.name] - route path for requests from device * @param {string} [config.settings.method] - POST or GET (default) * @param {number} [config.settings.port] - path to device on host if not 8080 * @param {Object} express_app - access to express app object */ class Webdevice extends Device { constructor(config, db_coll, plugin, game) { super(config, db_coll, plugin, game) this.connect() } /** * connect http device * set outgoing request properties * open routing on new route path * close previous route path. With help from: * https://stackoverflow.com/questions/10378690/remove-route-mappings-in-nodejs-express/28369539#28369539 * * @param {Object} settings - new setting properties. See class doc for setting properties. */ async connect() { this.setConnected(false) if(this.settings.from_device) { this.removeRoutes(this.app) let {router, urls} = this.getRouter() this.app = router if (this.settings.from_device.method && this.settings.from_device.method.toUpperCase() == 'GET') { this.app.get('/', this.handle.bind(this)) } else { this.settings.from_device.method = "POST" this.app.post('/', this.handle.bind(this)) } for(let url of urls) { this.settings.from_device.webhook[url.name] = url.url } log.debug(this.name, `${this.settings.from_device.method} webhook at ${JSON.stringify(this.settings.from_device.webhook)}`) await this.storeSettings() } this.setConnected(true) } load(data) { return data } async update({name, settings}) { if(name != this.name) { this.name = name return await this.connect() } if(settings.from_device) { if(this.settings.from_device && this.settings.from_device.method != settings.from_device.method) { this.settings = settings return await this.connect() } if(!this.settings.from_device) { this.settings = settings return await this.connect() } } this.settings = settings } /** * deal with requests to this http device */ handle(req, res) { try { if (!adaptor.isEmpty(req.body)) { this.log.info(req.body) this.incomingMessage(req.body) } else if (!adaptor.isEmpty(req.query)) { if (req.query.hasOwnProperty("data")) { this.log.info(req.query.data) this.incomingMessage(req.query.data) } else { this.log.info(req.query) this.incomingMessage(req.query) } } else { this.log.warn("Request with no body or query data. Headers:") this.log.warn(req.headers) this.log.warn(req.query) this.log.warn(req.body) //this.log.warn(req) } res.status(200) res.send("OK") } catch (e) { this.log.error(this.name, new Error(e)) } } /** * make http request to device. * * If there is data in the response, dispatch an incoming message event. * * @returns {*} if request returns with data, data is returned. If there is more than one request, latest requests data is returned. */ async send(data,{path,method,header}) { let return_value = {} let response = await this.request(data,path,method,header) let response_data = this.getData(response) if(response_data) { this.incomingMessage(response_data) return_value = response_data } return return_value } /** * make request to device * * @param {Object} [data] - request payload, params or data * @param {string} [path] - subaddress to make request to * @param {string} [method=GET] - Request method GET or POST */ request(data, path="", method, header=null) { if (!method && this.settings.to_device.method) { method = this.settings.to_device.method } if(!method) { method = "GET" } return new Promise((resolve, reject) => { let options = { method:method, url:new URL(path, this.settings.to_device.url).href, headers: this.settings.to_device.headers || {} } if(typeof data !== "object") { data = {"value": data} } if(method.toUpperCase() == "GET") { options["params"] = data } else { options["data"] = data } log.debug(this.name, `${options.method} request to ${options.url} with payload:`) log.debug(this.name, data) axios(options) .then(response => { if (response.data.error) { return reject(response.data.error) } return resolve(response) }) .catch(err => { this.logRequestError(err) return reject(err.message) }) }) } logRequestError(error) { if (error.response) { // Request made and server responded log.error(this.name, error.response.data) // log.error(this.name, "status code: " + error.response.status) log.trace(this.name, error.response.headers) } else if (error.request) { // The request was made but no response was received log.trace(this.name, error.request) } else { log.error(this.name, error) } } /** * look into request to find any json data in body or query * * @param {Object} req - http request object * @returns {Object} - data that was found in body or query part. undefined if none was found. */ getData(req) { if(!adaptor.isEmpty(req.data)) { log.info(this.name, req.data) return(req.data) } else if (!adaptor.isEmpty(req.body)) { log.info(this.name, req.body) return(req.body) } else if (!adaptor.isEmpty(req.query)) { if (req.query.hasOwnProperty("data")) { log.info(this.name, req.query.data) return(req.query.data) } else { log.info(this.name, req.query) return(req.query) } } else { log.warn(this.name, "Request with no body or query data. Headers:") log.warn(this.name, req.headers) // log.warn(this.name, req.query) // log.warn(this.name, req.body) //log.warn(this.name, req) return } } /** * stop routing */ disconnect() { this.removeRoutes(this.app) this.setConnected(false) } /** * stop routing */ close() { this.removeRoutes(this.app) } } /** * serial interface * serialport module API: * https://serialport.io/docs/api-stream * * @param {Object} config - configuration properties for this serial device * @param {Object} config.settings - Serial connection properties * @param {Object} config.settings.port - serial port * @param {Object} [config.settings.baud=115200] - Serial connection baud rate */ class Serialdevice extends Device { constructor(config, db_coll, plugin, game) { if (SerialPort) { super(config, db_coll, plugin, game) this.connect(config.settings) .catch(err => { this.log.error(err) }) } else { super(config, db_coll, plugin, game) log.error(0, "Can't create Serial Device. serialport module not installed.") } } /** * close (if open) and open Serial connection. * @param {Object} settings - new setting properties. See class doc for setting properties. * */ connect(settings) { this.disconnect() if(!settings.port) { this.setConnected(false) throw new adaptor.InvalidError("No Serial port selected. Can not connect Serial Device.") } return new Promise((resolve, reject) => { this.conn = new SerialPort({ path: settings.port, baudRate: settings.baud }, err => { if (err) { log.error(this.name, 'tried connecting ' + settings.port) log.error(this.name, err.message) this.setConnected(false) this.conn.on('open', () => { this.setConnected(true) log.info(this.name, 'Serial port is now open') }) } else { this.setConnected(true) this.log.info("Connected") const parser = this.conn.pipe(new ReadlineParser({ delimiter: '\n' })) this.conn.on('close', ()=> { log.error(this.name, "Serial Port was closed.") this.setConnected(false) }) parser.on('data', message => { log.debug('from ' + this.name, message.toString()) this.incomingMessage(message) }) } return resolve({connected:this.connected}) }) }) } disconnect() { if (this.hasOwnProperty('conn')) { if (this.conn.isOpen) { this.setConnected(false) this.conn.close() this.log.info("Disconnected") } } return ({connected:this.connected}) } load(data) { if (this.hasOwnProperty('conn')) { this.setConnected(this.conn.isOpen) } else { this.setConnected(false) } } async update({settings, name}) { if (!this.settings.hasOwnProperty('baud')) { this.settings.baud = 115200 } if(this.settings.port != settings.port) { this.settings = settings await this.connect(this.settings) } else { this.settings = settings; // update Schema data } this.name = name } async send(message) { if(!this.conn.isOpen) { log.error(this.name, "Can not send. Serial port is disconnected.") return } if(typeof message === "object") { message = JSON5.stringify(message) } this.conn.write(message + "\n", (err) => { if (err) { log.error(this.name, 'tried to send ' + message) log.error(this.name, err.message) } else { log.debug('to ' + this.name, message) } return }) } close() { this.disconnect() } } /** * midi interface * uses: * https://www.npmjs.com/package/midi * */ class Mididevice extends Device { constructor(config, db_coll, plugin, game) { if (midi) { super(config, db_coll, plugin, game) this.connect(config.settings) } else { super(config, db_coll, plugin, game) log.error(0, "Can't create Midi Device. node-midi module not installed.") } } /** * close (if open) and open Midi input and output * route incomming midi by channel except its 0 (any) * create virtual Port if input or output port is the vitual port name dafined by game * * @param {Object} settings - new setting properties. See class doc for setting properties. * */ connect(settings) { if (this.hasOwnProperty('input')) { this.input.closePort() } this.settings = settings this.input = new midi.input() this.input.on('message', function (deltaTime, message) { let status = {} if (message[0] < 144) { status.channel = message[0] - 127 log.debug(this.name, 'note off ' + message + ' time: ' + deltaTime) } else if (message[0] < 160) { status.channel = message[0] - 143 log.debug(this.name, 'note on ' + message + ' time: ' + deltaTime) } else { log.warn(this.name, 'unknown midi status: ' + message[0]) return } status.pitch = message[1] status.velocity = message[2] status.note = message[1] + ' ' + message[2] if (this.settings.channel == status.channel || this.settings.channel == 0) { this.incomingMessage(status) } }.bind(this)) // Connect Input let connected = false if (this.settings.input_port == virtual_midi_port) { this.input.openVirtualPort(this.settings.input_port) log.info(this.name, 'connected midi input on virtual port: ' + this.settings.input_port) connected = true } else { for (let i = 0; i < this.input.getPortCount(); i++) { if (this.settings.input_port == this.input.getPortName(i)) { this.input.openPort(i) log.info(this.name, 'connected midi input on port: ' + this.input.getPortName(i)) connected = true } } } if (!connected) { log.info(this.name, 'could not connect. no such midi input port: ' + this.settings.input_port) } // Connect Output if (this.hasOwnProperty('output')) { this.output.closePort() } this.output = new midi.output() connected = false if (this.settings.output_port == virtual_midi_port) { this.output.openVirtualPort(this.settings.output_port) log.info(this.name, 'connected midi output on virtual port: ' + this.settings.output_port) connected = true } else { for (let i = 0; i < this.output.getPortCount(); i++) { if (this.settings.output_port == this.output.getPortName(i)) { this.output.openPort(i) log.info(this.name, 'connected midi output on port: ' + this.output.getPortName(i)) connected = true } } } if (!connected) { log.info(this.name, 'could not connect. no such midi output port: ' + this.settings.output_port) } } /** * send note on midi message based on either: * a blank (' ') seperated string with two numbers (pitch and velocity) * a number for each pitch and velocity */ async send(msg) { let channel = this.settings.channel + 143 if (msg.hasOwnProperty("channel")) { channel = msg.channel + 143 } let snd = [channel, 0, 0] if (msg.hasOwnProperty("note")) { msg = msg.note.split(" ") snd[1] = msg[0] if (msg.length == 2) { snd[2] = msg[1] } } if (msg.hasOwnProperty("pitch")) { snd[1] = msg.pitch } if (msg.hasOwnProperty("velocity")) { snd[2] = msg.velocity } this.output.sendMessage(snd) log.debug(this.name, 'send', snd[0], snd[1], snd[2]) return } close() { if (this.hasOwnProperty('output')) { this.output.closePort() } if (this.hasOwnProperty('input')) { this.input.closePort() } } } module.exports = { getDevice: getDevice, midi: midi, activateMidi:activateMidi, deactivateMidi:deactivateMidi, getSerialPorts: getSerialPorts, getMidiPorts: getMidiPorts, Device: Device, UDPdevice: UDPdevice, OSCdevice: OSCdevice, osc: osc, TCPdevice: TCPdevice, Webdevice: Webdevice, Serialdevice: Serialdevice, Mididevice: Mididevice, setVirtualMidiPort: setVirtualMidiPort }