UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,365 lines (1,225 loc) 36.5 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) => this.log.error(err.message) ) } 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) { const address = osc_msg.address || "" const path = address.split("/").filter(Boolean) let value if (osc_msg.args.length > 1) { value = osc_msg.args.map((arg) => arg.value) } else { value = osc_msg.args[0]?.value } let json = {} path.reduce((o, c, i) => { if (i === path.length - 1) { o[c] = value } else { o[c] = {} o = o[c] } return o }, json) return json } /** * 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, "Incoming 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.autoconnect = true } /** * 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 } = {}) { if (!this.settings.to_device) { // this.log.error("No settings to send data to device.") throw new adaptor.InvalidError("No settings to send data to device.") } 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.autoconnect = true /** 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() { this.disconnect() this.log.info(this.settings) if (!this.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: this.settings.port, baudRate: this.settings.baud }, (err) => { if (err) { log.error(this.name, "tried connecting " + this.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.replace(/\r$/, "")) }) } 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.baud != settings.baud ) { this.settings = settings await this.connect() } 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 }