adaptorex
Version:
Connect all your live interactive storytelling devices and software
1,365 lines (1,225 loc) • 36.5 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) =>
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
}