timeline-state-resolver
Version:
Have timeline, control stuff
345 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LawoConnection = void 0;
const emberplus_connection_1 = require("emberplus-connection");
const eventemitter3_1 = require("eventemitter3");
const lib_1 = require("../../lib");
const timeline_state_resolver_types_1 = require("timeline-state-resolver-types");
const _ = require("underscore");
class LawoConnection extends eventemitter3_1.EventEmitter {
constructor(options, getCurrentTime) {
super();
this.getCurrentTime = getCurrentTime;
this._lastSentValue = {};
this._connected = false;
this._sourcesPath = '';
this._rampMotorFunctionPath = '';
this._dbPropertyName = '';
this._faderIntervalTime = 0;
this._faderThreshold = -60;
this._sourceNameToNodeName = new Map();
this.transitions = {};
if (options.faderInterval) {
this._faderIntervalTime = options.faderInterval;
}
switch (options.deviceMode) {
case timeline_state_resolver_types_1.LawoDeviceMode.Ruby:
this._sourcesPath = 'Ruby.Sources';
this._dbPropertyName = 'Fader.Motor dB Value';
this._rampMotorFunctionPath = 'Ruby.Functions.RampMotorFader';
break;
case timeline_state_resolver_types_1.LawoDeviceMode.RubyManualRamp:
this._sourcesPath = 'Ruby.Sources';
this._dbPropertyName = 'Fader.Motor dB Value';
this._faderThreshold = -60;
break;
case timeline_state_resolver_types_1.LawoDeviceMode.MC2:
this._sourcesPath = 'Channels.Inputs';
this._dbPropertyName = 'Fader.Fader Level';
this._faderThreshold = -90;
this._sourceNamePath = 'General.Inherited Label';
break;
case timeline_state_resolver_types_1.LawoDeviceMode.R3lay:
this._sourcesPath = 'R3LAYVRX4.Ex.Sources';
this._dbPropertyName = 'Active.Amplification';
this._faderThreshold = -60;
break;
case timeline_state_resolver_types_1.LawoDeviceMode.Manual:
default:
this._sourcesPath = options.sourcesPath || '';
this._dbPropertyName = options.dbPropertyName || '';
this._rampMotorFunctionPath = options.dbPropertyName || '';
this._faderThreshold = options.faderThreshold || -60;
}
const host = options.host ? options.host : undefined;
const port = options.port ? options.port : undefined;
this._lawo = new emberplus_connection_1.EmberClient(host || '', port);
this._lawo.on('error', (e) => {
if ((e.message + '').match(/econnrefused/i) || (e.message + '').match(/disconnected/i)) {
this._connected = false;
}
else {
this.emit('error', 'Lawo.Emberplus', e);
}
});
// this._lawo.on('warn', (w) => {
// this.emitDebug('Warning: Lawo.Emberplus', w)
// })
let firstConnection = true;
this._lawo.on('connected', () => {
this._connected = true;
if (firstConnection) {
(0, lib_1.deferAsync)(async () => {
const req = await this._lawo.getDirectory(this._lawo.tree);
await req.response;
await this._mapSourcesToNodeNames();
this.emit('connected', firstConnection);
}, (e) => {
if (e instanceof Error) {
this.emit('error', 'Error while expanding root', e);
}
});
}
else {
this.emit('connected', firstConnection);
}
firstConnection = false;
});
this._lawo.on('disconnected', () => {
this._connected = false;
this.emit('disconnected');
});
this._lawo
.connect()
.then((err) => {
if (err)
this.emit('error', 'Lawo initialization', err);
})
.catch((err) => {
this.emit('error', 'Lawo initialization', err);
});
}
/**
* Safely disconnect from physical device such that this instance of the class
* can be garbage collected.
*/
async terminate() {
if (this.transitionInterval)
clearInterval(this.transitionInterval);
// @todo: Implement lawo dispose function upstream
try {
this._lawo
.disconnect()
.then(() => {
this._lawo.discard();
})
.catch(() => null); // fail silently
this._lawo.removeAllListeners('error');
this._lawo.removeAllListeners('connected');
this._lawo.removeAllListeners('disconnected');
}
catch (e) {
this.emit('error', 'Lawo.terminate', e);
}
}
get connected() {
return this._connected;
}
/**
* Gets an ember node based on its path
* @param path
*/
async _getParameterNodeByPath(path) {
const node = await this._lawo.getElementByPath(path);
return node;
}
_identifierToNodeName(identifier) {
if (this._sourceNamePath) {
const s = this._sourceNameToNodeName.get(identifier);
if (!s)
this.emit('warning', `Source identifier "${identifier}" could not be found`);
return s || identifier;
}
else {
return identifier;
}
}
/**
* Returns an attribute path
* @param identifier
* @param attributePath
*/
sourceNodeAttributePath(identifier, attributePath) {
return _.compact([this._sourcesPath, this._identifierToNodeName(identifier), attributePath]).join('.');
}
async rampFader(command, timelineObjId) {
const path = this.sourceNodeAttributePath(command.identifier, this._dbPropertyName);
const transitionDuration = command.transitionDuration ?? 0;
// save start time of command
const startSend = this.getCurrentTime();
this._lastSentValue[path] = startSend;
// TODO - Lawo result 6 code is based on time - difference ratio, certain ratios we may want to run a manual fade?
if (!this._rampMotorFunctionPath ||
(transitionDuration > 0 && transitionDuration < 500 && this._faderIntervalTime < 250)) {
// add the fade to the fade object, such that we can fade the signal using the fader
if (!command.from) {
// @todo: see if we can query the lawo first always?
const node = await this._getParameterNodeByPath(path);
if (node) {
if (node.contents.factor) {
command.from = node.contents.value / (node.contents.factor || 1);
}
else {
command.from = node.contents.value;
}
if (command.from === command.value)
return;
}
else {
throw new Error('Node ' + path + ' was not found');
}
}
this.transitions[path] = {
...command,
timelineObjId,
started: this.getCurrentTime(),
};
if (!this.transitionInterval)
this.transitionInterval = setInterval(() => this.runAnimation(), this._faderIntervalTime || 75);
}
else if (transitionDuration >= 500) {
// Motor Ramp in Lawo cannot handle too short durations
const fn = await this._lawo.getElementByPath(this._rampMotorFunctionPath);
if (!fn)
throw new Error('Function path not found');
if (fn.contents.type !== emberplus_connection_1.Model.ElementType.Function)
throw new Error('Node at specified path for function is not a function');
const req = await this._lawo.invoke(fn, { type: emberplus_connection_1.Model.ParameterType.String, value: command.identifier }, { type: emberplus_connection_1.Model.ParameterType.Real, value: command.value }, { type: emberplus_connection_1.Model.ParameterType.Real, value: transitionDuration / 1000 });
this.emit('debug', `Ember function invoked (${timelineObjId}, ${command.identifier}, ${command.value})`);
const res = await req.response;
if (res && res.success === false) {
const reasons = {
1: 'Incorrect number of parameters',
2: 'Incorrect datatype',
3: 'Input value out of range',
4: 'Source / sum not found',
5: 'Source / sum not assigned to fader',
6: 'Combination of values not allowed',
7: 'Touch active',
};
const result = res.result[0].value;
if (res.result && (result === 6 || result === 5) && this._lastSentValue[path] <= startSend) {
// result 5 / 6 and no new command fired for this path in meantime
// Lawo rejected the command, so ensure the value gets set
this.emit('info', `Ember function result (${timelineObjId}, ${command.identifier}) was ${result}, running a direct setValue now`);
await this.setValueWrapper(path, command.value, timelineObjId, false); // result 6 is quite likely to cause a timeout
}
else {
this.emit('error', `Lawo: Ember function success false (${timelineObjId}, ${command.identifier}), result ${res.result[0].value}`, new Error('Lawo Result ' + res.result[0].value));
}
this.emit('debug', `Lawo: Ember fn error ${command.identifier}): result ${result}: ${reasons[result]}`, {
...res,
source: command.identifier,
});
}
else {
this.emit('debug', `Ember function result (${timelineObjId}, ${command.identifier}): ${JSON.stringify(res)}`, res);
}
}
else {
// withouth timed fader movement
await this.setValueWrapper(path, command.value, timelineObjId);
}
}
async setValue(command, timelineObjId) {
// save start time of command
const startSend = this.getCurrentTime();
this._lastSentValue[command.identifier] = startSend;
return this.setValueWrapper(command.identifier, command.value, timelineObjId);
}
async setValueWrapper(path, value, timelineObjId, logResult = true) {
try {
const node = await this._getParameterNodeByPath(path);
if (!node)
throw new Error(`Unable to setValue for node "${path}", node not found!`);
const scaledValue = node.contents.factor ? value * node.contents.factor : value;
if (node.contents.value === scaledValue)
return; // no need to do another setValue
const req = await this._lawo.setValue(node, scaledValue, logResult);
if (logResult) {
const res = await req.response;
this.emit('debug', `Ember result (${timelineObjId}): ${res && res.contents.value}`, {
path,
value,
res: res && res.contents,
});
}
else if (!req.sentOk) {
this.emit('error', 'SetValue no logResult', new Error(`Ember req (${timelineObjId}) for "${path}" to "${scaledValue}" failed`));
}
}
catch (e) {
this.emit('error', `Lawo: Error in setValue (${timelineObjId})`, e);
throw e;
}
}
runAnimation() {
for (const addr in this.transitions) {
const transition = this.transitions[addr];
// delete old transitions
if (transition.started + transition.transitionDuration < this.getCurrentTime()) {
delete this.transitions[addr];
// assert correct finished value:
this.setValueWrapper(addr, transition.value, transition.timelineObjId).catch(() => null);
}
}
for (const addr in this.transitions) {
const transition = this.transitions[addr];
const from = this._faderThreshold
? Math.max(this._faderThreshold, transition.from)
: transition.from;
const to = this._faderThreshold
? Math.max(this._faderThreshold, transition.value)
: transition.value;
const p = (this.getCurrentTime() - transition.started) / transition.transitionDuration;
const v = from + p * (to - from); // should this have easing?
this.setValueWrapper(addr, v, transition.timelineObjId, false).catch(() => null);
}
if (Object.keys(this.transitions).length === 0) {
if (this.transitionInterval) {
clearInterval(this.transitionInterval);
}
this.transitionInterval = undefined;
}
}
async _mapSourcesToNodeNames() {
if (!this._sourceNamePath)
return;
this.emit('info', 'Start mapping source identifiers to channel node identifiers');
// get the node that contains the sources
const sourceNode = await this._lawo.getElementByPath(this._sourcesPath);
if (!sourceNode) {
this.emit('warning', 'Could not map source names to node names because source node could not be found!');
return;
}
// get the sources
const req = await this._lawo.getDirectory(sourceNode);
const sources = (await req.response);
if (!sources)
return;
for (const child of Object.values(sources.children || {})) {
if (child.contents.type === emberplus_connection_1.Model.ElementType.Node) {
try {
// get the identifier
let previousNode = undefined;
const node = await this._lawo.getElementByPath(this._sourcesPath + '.' + child.number + '.' + this._sourceNamePath, (node0) => {
const node = node0;
// identifier changed
if (!node)
return;
const sourceId = child.contents.identifier || child.number + '';
// remove old mapping if it hasn't changed
if (previousNode && this._sourceNameToNodeName.get(previousNode) === sourceId) {
this.emit('info', `removing mapping ${previousNode}`);
this._sourceNameToNodeName.delete(previousNode);
}
// set new mapping
this._sourceNameToNodeName.set(node.contents.value, sourceId);
previousNode = node.contents.value;
this.emit('info', `mapping ${node.contents.value} to channel ${sourceId}`);
});
if (!node)
continue;
this._sourceNameToNodeName.set(node.contents.value, child.contents.identifier || child.number + '');
previousNode = node.contents.value;
}
catch (e) {
this.emit('error', 'lawo: map sources to node names', e);
}
}
}
this.emit('info', 'Mapped source identifiers to channel node identifiers');
}
}
exports.LawoConnection = LawoConnection;
//# sourceMappingURL=connection.js.map