adaptorex
Version:
Connect all your live interactive storytelling devices and software
1,225 lines (1,077 loc) • 34.3 kB
JavaScript
/**
*
* 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
}