UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

280 lines (244 loc) 7.76 kB
/** * * This plugin summons many typical communication protocols. It allows to connect create different types of connections and to address them in levels. * It adds a send action to cue creation that allows to send messages to all previously created devices. * It allows to switch or listen based on the state of the created devices. * * * @requires device * @requires plugin * * @module devices/devices * @copyright Lasse Marburg 2021 * @license MIT */ const deviceModule = require("./device.js") const { Plugin } = require("../plugin.js") var schema = require("./schema.json") class Devices extends Plugin { constructor() { super(schema) } async setup(config, game) { this.update(config.settings) await super.setup(config, game, { udp: deviceModule.UDPdevice, osc: deviceModule.OSCdevice, tcp: deviceModule.TCPdevice, http: deviceModule.Webdevice, serial: deviceModule.Serialdevice }) this.schema.actions.send.test = "Hallo" this.addTemplate("send", (payload, action) => { let subtitle = `To device ${payload.to}` let body = [ { text: `${payload.method || ""} ${payload.path || ""} ${payload.message || ""}` } ] return { subtitle: subtitle, body: body } }) this.addConditionTemplate("onDeviceMessage", { value_name: "message", subtitle_insertion: "`Wait for message from ${payload.device}`" }) await this.load({}) deviceModule.setVirtualMidiPort(this.name) return this.schema } /** * Loads the setup for the devices plugin. * * This function retrieves the distinct levels from the game database and updates the schema definitions and collections. * It also attempts to retrieve serial and MIDI ports, updating the schema accordingly if successful. * * @param {Object} setup - The setup object to be loaded. * @returns {Promise<Object>} The updated setup object with the schema. */ async load(setup) { let level = await this.game.db.levels.distinct("name", {}) this.schema.definitions.init.properties.level.enum = level for (let collection in this.schema.collections) { this.schema.collections[collection].properties.init = this.schema.definitions.init } try { let serialports = await deviceModule.getSerialPorts() this.schema.collections.serial.properties.settings.properties.port.enum = serialports } catch (error) { log.error(this.name, "Error when finding serial ports.") log.error(this.name, error) delete this.schema.collections.serial } try { let midiports = await deviceModule.getMidiPorts() } catch (error) { log.error(this.name, error) delete this.schema.collections.midi } await this.schemaUpdate(this.schema) setup.schema = this.schema return setup } update(settings) { if (settings.hasOwnProperty("midi")) { if (settings.midi.active) { deviceModule.activateMidi() } else { deviceModule.deactivateMidi() } } } /** * Send payload to device * * if device send() returns a value, its stored in session document. * */ async send(data, session) { const device = this.getItem(data.to) data.message = session.variables.parseJSON(data.message) let result = await device.send(data.message, { path: data.path, is_float: data.is_float, method: data.method }) if (result) { let store = {} store[device.name] = result await session.variables.store(store) } } /** * Handles incoming device messages by setting up a listener for the specified device. * * @param {Object} data - The data object containing listener action information. * @param {Session} session - The current session object. * @returns {Function} Function that cancels the listener when called. */ async onDeviceMessage(data, session) { data.device = await session.variables.review(data.device) const device = this.getItem(data.device) let listener = new DeviceListener(data, session, device, this.game) await listener.init() return listener.cancel.bind(listener) } command(input) { switch (input[0]) { case "ports": deviceModule .getPorts() .then((ports) => { log.info(this.name, ports) }) .catch( function (error) { log.error(this.name, error) }.bind(this) ) break case "midi": if (input[1] == "on") { deviceModule.activateMidi() log.info(this.name, "please restart adaptor:ex to reactivate midi") } else if (input[1] == "off") { deviceModule.deactivateMidi() } break default: super.command(input) break } } } /** * Listener Action */ class DeviceListener { constructor(data, session, device, game) { Object.assign(this, data) /** @type {Session} */ this.session = session /** @type {Game} */ this.game = game /** @type {Device} */ this.device = device } /** * Start listening for incoming messages */ async init() { this.listenerCallback = this.session.getCallback(this.onEvent.bind(this)) this.device.event.on("incomingMessage", this.listenerCallback) this.session.log.info( `Listening on incoming messages from ${this.device.name}` ) } /** * Check for condition matches with the event payload * * If event listener condition has field property, match condition against this field in payload. * * dispatch next state without condition check wherever 'if' property is missing * * @param {*} payload - event payload message provided with the event. Might be anything (including undefined/null) * * @returns {Promise<boolean>} - true if next state was triggered by else option */ async onEvent(payload) { await this.session.variables.store({ message: payload }) if (this.hasOwnProperty("if") && Array.isArray(this["if"])) { for (let if_condition of this["if"]) { if (if_condition["field"]) { if (typeof payload !== "object") { this.session.log(`Incoming message data is not of type object`) continue } if_condition["value"] = adaptor.getPath(payload, if_condition.field) if (typeof if_condition.value === "undefined") { this.session.log( `Incoming message data has no field ${if_condition.field}` ) continue } } else { if_condition["value"] = payload } let condition = new adaptor.logic.Condition( if_condition, this.session, this.game ) let match = await condition.match() if (match) { this.session.next(match.next) return } } } if (this.hasOwnProperty("else")) { if (this.else.next) { this.session.next(this.else.next) return true } else { this.session.log.warn( `'next' missing in else to handle incoming message from ${this.device.name}` ) } } this.session.log( `no match on incoming message from ${this.device.name} and no else or next was defined` ) } /** * Stop listening for incoming messages */ cancel() { this.device.event.off("incomingMessage", this.listenerCallback) this.session.log( `Stop waiting for incoming messages from ${this.device.name}. ${this.device.event.listenerCount(this.event)} listeners left open.` ) } } module.exports.Plugin = Devices