adaptorex
Version:
Connect all your live interactive storytelling devices and software
280 lines (244 loc) • 7.76 kB
JavaScript
/**
*
* 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