@bannsaenger/node-red-contrib-artnet-controller
Version:
Node-RED node that controls lights via Art-Net. Acts as a Art-Net controller.
867 lines (786 loc) • 62.9 kB
JavaScript
const dmxlib = require('@bannsaenger/dmxnet');
const utils = require('./utils/arc-transition-utils');
const transitions = require('./transitioncurves/transitioncurves')
const artnetutils = require('./utils/artnet-utils');
const { networkInterfaces } = require('os');
const DEFAULT_MOVING_HEAD_CONFIG = {
pan_channel: 1,
tilt_channel: 3,
pan_angle: 540,
tilt_angle: 255
};
// compare two arrays, especially two received universes of dmx data
const equals = (a, b) => JSON.stringify(a) === JSON.stringify(b);
module.exports = function (RED) {
/*************************************************
* Config node for the Art-Net Controller
*/
function ArtNetController(config) {
RED.nodes.createNode(this, config);
this.name = config.name || '';
this.bind = config.bind || '0.0.0.0';
this.port = parseInt(config.port) || 6454;
this.sname = config.sname || 'dmxnet';
this.lname = config.lname || 'dmxnet - OpenSource ArtNet Transceiver';
this.oemcode = config.oemcode || '0x2908';
this.estacode = config.estacode || '0x0000';
this.loglevel = config.loglevel || 'warn';
this.senders = {};
/**
* create controller instance
*/
this.dmxnet = new dmxlib.dmxnet({
hosts: this.bind === '0.0.0.0' ? undefined : [this.bind],
listen: this.port,
oem: this.oemcode,
esta: this.estacode,
sName: this.sname,
lName: this.lname,
log: {
level: this.loglevel
},
errFunc: function(err) {
this.error(`Art-Net Controller (dmxlib) error: ${err.message}, stack: ${err.stack}`);
}.bind(this)
});
/**
* add the sender to local library so that other out nodes can search for existing universes
* @param {string} sender
* @param {string} address
* @param {number} port
* @param {number} net
* @param {number} subnet
* @param {number} universe
*/
this.registerSender = function(sender, address, port, net, subnet, universe) {
const uninet = `${net}:${subnet}:${universe}`;
const adport = `${address}:${port}`;
if (!this.senders.hasOwnProperty(uninet)) this.senders[uninet] = {};
this.senders[uninet][adport] = sender;
this.debug(`Added sender ${sender}: new senders: ${JSON.stringify(this.senders, null, 1)}`);
};
/**
* search for a sender in the local library for existing universes
* @param {string} address
* @param {number} port
* @param {number} net
* @param {number} subnet
* @param {number} universe
* @returns {string} sender
*/
this.getSender = function(address, port, net, subnet, universe) {
const uninet = `${net}:${subnet}:${universe}`;
const adport = `${address}:${port}`;
if (this.senders.hasOwnProperty(uninet)) {
// try to find the best address and port combination
if (this.senders[uninet].hasOwnProperty(adport)) {
this.debug(`Found exact match in senders for uninet ${uninet} - ${adport}. Sender: ${this.senders[uninet][adport]}`);
return this.senders[uninet][adport];
}
// now try to find the next best combination
for (var key in this.senders[uninet]) {
const locAddr = key.split(':')[0];
if (locAddr === address) {
this.debug(`Found match with same address in senders for uninet ${uninet} - ${key}. Sender: ${this.senders[uninet][key]}`);
return this.senders[uninet][key];
}
}
}
return '';
};
/**
* is called whenn node is unloaded
*/
this.on('close', function() {
// put this into the dmxnet lib
this.dmxnet.listener4.close();
delete this.dmxnet;
});
}
RED.nodes.registerType("Art-Net Controller", ArtNetController);
/*************************************************
* Config node for holding a universe and having the functionality
* of automatic and timed value transformation
*/
function ArtNetSender(config) {
RED.nodes.createNode(this, config);
this.name = config.name || '';
this.artnetcontroller = config.artnetcontroller;
this.address = config.address || '255.255.255.255';
this.port = config.port || 6454;
this.net = config.net || 0;
this.subnet = config.subnet || 0;
this.universe = config.universe || 0;
this.maxrate = config.maxrate || 10;
this.refresh = config.refresh || 1000;
this.savevalues = typeof config.savevalues === 'undefined' ? true : config.savevalues;
this.controllerObj = RED.nodes.getNode(this.artnetcontroller);
this.dataDirty = false; // set to true if dmxData is changed
this.instSending = false; // set to true if spontaneous sending is active when maxRate > 0
this.sendActive = false; // true while sending of dmxdata has to be delayed
// calculate required resolution (intervaltime of main worker, senderClock)
this.senderClock = 100; // default 10 per second
if (this.maxrate == 0) {
this.senderClock = this.refresh;
this.instSending = false;
} else {
this.senderClock = Math.round((1000 / this.maxrate) < this.refresh ? Math.round((1000 / this.maxrate)) : this.refresh);
this.instSending = true;
}
/**
* create sender instance
*/
this.log(`[ArtNetSender] Creating sender on controller: ${this.controllerObj.name} parameters: IP: ${this.address}:${this.port}, SubNetUni: ${this.net}:${this.subnet}:${this.universe}, refresh interval: ${this.refresh} ms, sender clock: ${this.senderClock} ms`);
this.sender = this.controllerObj.dmxnet.newSender({
ip: this.address,
port: this.port,
net: this.net,
subnet: this.subnet,
universe: this.universe,
base_refresh_interval: this.refresh
});
// register this sender in the global library
this.controllerObj.registerSender(this.id, this.address, this.port, this.net, this.subnet, this.universe);
this.nodeContext = this.context().global;
this.contextData = this.nodeContext.get(this.id) || {};
//this.closeCallbacksMap = {};
this.transitionsMap = {};
// get the saved values depending on the switch savevalues
this.dmxData = this.savevalues ? this.contextData.dmxData || [] : [];
this.debug(`[ArtNetSender] read dmx-data: (${this.dmxData.length}) -> ${JSON.stringify(this.dmxData)}`);
if (this.dmxData.length !== 512) {
this.dmxData = new Array(512).fill(0);
this.trace('[ArtNetSender] filling, now: ' + this.dmxData.length);
}
this.nodeContext.set(this.id, {'dmxData': this.dmxData});
// transfer the dmx values to the sender instance
let i = 0;
for (i = 0; i < 512; i++) {
this.sender.prepChannel(i, this.dmxData[i]);
}
// initial transmission
this.sender.transmit();
this.log(`[ArtNetSender] initial transmit starting mainWorker with senderClock ${this.senderClock} ms, ${this.instSending ? 'spontaneous sending = on' : 'spontaneous sending = off (send only on refresh)'}`);
/** ----------------------------------------------
* functions following
*/
/**
* The main system clock timer to handle sending and transitions
*/
this.mainWorker = setTimeout((function() {
this.workerFunc(); // execute the main time function
}.bind(this)), this.senderClock);
/**
* The corresponding function for the main system clock timer. Can be called separately for immediate execution.
*/
this.workerFunc = function() {
// states for state machine:
// state = TRANSITION default, transition in progress
// HOLD hold time active
// MIRROR mirrored transition in progress
// GAP gap time active
// start with transition handling
for (const currentChannel in this.transitionsMap) {
// pick the next transaction to handle
const curTrans = this.transitionsMap[currentChannel];
const currentTime = Date.now();
// handle TRANSITION (is the default state when transition is added)
if (curTrans.state === 'TRANSITION') {
if (curTrans.currentStep <= curTrans.stepsToGo) { // proceed next step
switch (curTrans.type) {
case 'sine':
case 'quadratic':
case 'gamma':
case 'linear':
let actValue = 0;
if (curTrans.type === 'gamma') {
actValue = transitions.TransitionFactory(curTrans.type).computeValue(curTrans.startValue, curTrans.targetValue, curTrans.stepsToGo, curTrans.currentStep, curTrans.gamma);
} else {
actValue = transitions.TransitionFactory(curTrans.type).computeValue(curTrans.startValue, curTrans.targetValue, curTrans.stepsToGo, curTrans.currentStep);
}
this.trace(`[mainWorker] (${curTrans.type}) doing step [${curTrans.currentStep}] for channel: ${currentChannel}, value: ${actValue}`);
this.set(currentChannel, actValue);
this.dataDirty = true;
break;
case 'arc':
// get point in spherical coordinates
var iterationPoint = utils.getIterationPoint(curTrans.steps[curTrans.currentStep].value, curTrans.radius, curTrans.backVector, curTrans.movement_point);
var tilt = artnetutils.validateChannelValue(artnetutils.radToChannelValue(iterationPoint.theta, curTrans.arcConfig.tilt_angle));
var pan = artnetutils.validateChannelValue(artnetutils.radToChannelValue(iterationPoint.phi, curTrans.arcConfig.pan_angle));
this.debug(`[mainWorker] (arc) doing step [${curTrans.currentStep}] for channel: ${currentChannel}, sphericalP -> r: ${parseFloat(iterationPoint.r).toFixed(4)}, theta: ${parseFloat(iterationPoint.theta).toFixed(4)}, phi: ${parseFloat(iterationPoint.phi).toFixed(4)}, T: ${parseFloat(curTrans.steps[curTrans.currentStep].value).toFixed(4)}, pan: ${parseFloat(pan).toFixed(4)}, tilt: ${parseFloat(tilt).toFixed(4)}`);
this.set(curTrans.arcConfig.pan_channel, pan);
this.set(curTrans.arcConfig.tilt_channel, tilt);
this.dataDirty = true;
break;
default:
this.warn(`[mainWorker] unknown transition: ${curTrans.type}`);
}
if (curTrans.currentStep === curTrans.stepsToGo) {
// only at the end of the transition
curTrans.overallTimeToGo += curTrans.timeToGo;
const overallTimeRequired = currentTime - curTrans.startTime - curTrans.overallTimeToGo;
const performText = `difference between target and actual time : ${overallTimeRequired} ms, ${((overallTimeRequired * 100) / curTrans.overallTimeToGo).toFixed(1)} % (positive = took too long, negative = to fast)`;
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, curTrans.state, curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. Go to state HOLD`);
curTrans.state = 'HOLD';
curTrans.currentStep = 0; // because this will be the first HOLD step
} else {
curTrans.currentStep++;
}
}
}
// handle HOLD
if (curTrans.state === 'HOLD') {
if ((curTrans.holdSteps > 0) && (curTrans.currentStep <= curTrans.holdSteps)) {
curTrans.currentStep++;
} else {
curTrans.overallTimeToGo += (curTrans.holdSteps > 0 ? curTrans.holdSteps + 1 : 0) * this.senderClock;
const overallTimeRequired = currentTime - curTrans.startTime - curTrans.overallTimeToGo
const performText = `difference between target and actual time : ${overallTimeRequired} ms, ${((overallTimeRequired * 100) / curTrans.overallTimeToGo).toFixed(1)} % (positive = took too long, negative = to fast)`;
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, curTrans.state, curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. Go to state MIRROR`);
curTrans.state = 'MIRROR';
curTrans.currentStep = 0; // because this will be the first MIRROR step
}
}
// handle MIRROR
if (curTrans.state === 'MIRROR') {
if (curTrans.mirror) {
if (curTrans.currentStep <= curTrans.stepsToGo) { // proceed next step
switch (curTrans.type) {
case 'sine':
case 'quadratic':
case 'gamma':
case 'linear':
// do the transition the other way round from target to start
let actValue = 0;
if (curTrans.type === 'gamma') {
actValue = transitions.TransitionFactory(curTrans.type).computeValue(curTrans.targetValue, curTrans.startValue, curTrans.stepsToGo, curTrans.currentStep, curTrans.gamma);
} else {
actValue = transitions.TransitionFactory(curTrans.type).computeValue(curTrans.targetValue, curTrans.startValue, curTrans.stepsToGo, curTrans.currentStep);
}
this.trace(`[mainWorker] (${curTrans.type}) doing step [${curTrans.currentStep}] for channel: ${currentChannel}, value: ${actValue}`);
this.set(currentChannel, actValue);
this.dataDirty = true;
break;
case 'arc':
// get point in spherical coordinates
var iterationPoint = utils.getIterationPoint(curTrans.steps[curTrans.currentStep].value, curTrans.radius, curTrans.backVector, curTrans.movement_point);
var tilt = artnetutils.validateChannelValue(artnetutils.radToChannelValue(iterationPoint.theta, curTrans.arcConfig.tilt_angle));
var pan = artnetutils.validateChannelValue(artnetutils.radToChannelValue(iterationPoint.phi, curTrans.arcConfig.pan_angle));
this.debug(`[mainWorker] (arc, MIRROR) doing step [${curTrans.currentStep}] for channel: ${currentChannel}, sphericalP -> r: ${parseFloat(iterationPoint.r).toFixed(4)}, theta: ${parseFloat(iterationPoint.theta).toFixed(4)}, phi: ${parseFloat(iterationPoint.phi).toFixed(4)}, T: ${parseFloat(curTrans.steps[curTrans.stepsToGo - curTrans.currentStep].value).toFixed(4)}, pan: ${parseFloat(pan).toFixed(4)}, tilt: ${parseFloat(tilt).toFixed(4)}`);
this.set(curTrans.arcConfig.pan_channel, pan);
this.set(curTrans.arcConfig.tilt_channel, tilt);
this.dataDirty = true;
break;
default:
this.warn(`[mainWorker] unknown MIRROR transition: ${curTrans.type}`);
}
if (curTrans.currentStep === curTrans.stepsToGo) {
// only at the end of the transition
this.set(currentChannel, curTrans.startValue);
this.dataDirty = true;
curTrans.overallTimeToGo += curTrans.timeToGo;
const overallTimeRequired = currentTime - curTrans.startTime - curTrans.overallTimeToGo
this.debug(`[mainWorker] channel: ${currentChannel}, difference between target and actual time : ${overallTimeRequired} ms, ${((overallTimeRequired * 100) / curTrans.overallTimeToGo).toFixed(1)} % (positive = took too long, negative = to fast). Go to state GAP`);
curTrans.state = 'GAP';
curTrans.currentStep = 0; // because this will be the first GAP step
} else {
curTrans.currentStep++;
}
}
} else {
this.trace(`[mainWorker] no mirror holdSteps: ${curTrans.holdSteps}, repeat: ${curTrans.repeat}, gapSteps: ${curTrans.gapSteps}, currentStep: ${curTrans.currentStep}`);
// now go back to the startValue depending on repeat and GAP and HOLD
if (curTrans.holdSteps > 0 || curTrans.repeat !== 0 || curTrans.gapSteps > 0) {
this.trace(`[mainWorker] bin hier: currentStep: ${curTrans.currentStep}`);
if (curTrans.currentStep < 0) { // then go back to startValue
this.trace(`[mainWorker] (${curTrans.type}) going back to StartValue: ${curTrans.startValue} for channel: ${currentChannel}. Go to state GAP`);
this.set(currentChannel, curTrans.startValue);
this.dataDirty = true;
// finished. Go to GAP
curTrans.overallTimeToGo += this.senderClock;
const overallTimeRequired = currentTime - curTrans.startTime - curTrans.overallTimeToGo
const performText = `difference between target and actual time : ${overallTimeRequired} ms, ${((overallTimeRequired * 100) / curTrans.overallTimeToGo).toFixed(1)} % (positive = took too long, negative = to fast)`;
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, curTrans.state, curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. Go to state GAP`);
curTrans.state = 'GAP';
curTrans.currentStep = 0; // because this will be the first GAP step
} else {
curTrans.currentStep = -1; // do one step and go back to startValue, stay in MIRROR state
this.trace(`[mainWorker] bin hier und mache: currentStep: ${curTrans.currentStep}`);
}
} else {
// nothing to do. Go to GAP
curTrans.overallTimeToGo += 0; // no action in this step done
const overallTimeRequired = currentTime - curTrans.startTime - curTrans.overallTimeToGo
const performText = `difference between target and actual time : ${overallTimeRequired} ms, ${((overallTimeRequired * 100) / curTrans.overallTimeToGo).toFixed(1)} % (positive = took too long, negative = to fast)`;
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, curTrans.state, curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. Go to state GAP`);
curTrans.state = 'GAP';
curTrans.currentStep = 0; // because this will be the first GAP step
}
}
}
// handle GAP
if (curTrans.state === 'GAP') {
if ((curTrans.gapSteps > 0) && (curTrans.currentStep <= curTrans.gapSteps)) {
curTrans.currentStep++;
} else {
curTrans.overallTimeToGo += (curTrans.gapSteps > 0 ? curTrans.gapSteps + 1 : 0) * this.senderClock;
const overallTimeRequired = currentTime - curTrans.startTime - curTrans.overallTimeToGo
const performText = `difference between target and actual time : ${overallTimeRequired} ms, ${((overallTimeRequired * 100) / curTrans.overallTimeToGo).toFixed(1)} % (positive = took too long, negative = to fast)`;
if (curTrans.repeat != 0) {
if (curTrans.currentRepetition != (curTrans.repeat - 1)) {
// care about the repetition, return to the start values
curTrans.currentRepetition++;
curTrans.currentStep = 1;
if (curTrans.startPanValue) this.set(curTrans.arcConfig.pan_channel, curTrans.startPanValue);
if (curTrans.startTiltValue) this.set(curTrans.arcConfig.tilt_channel, curTrans.startTiltValue);
this.dataDirty = true;
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, 'END', curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. Go to state TRANSITION`);
curTrans.state = 'TRANSITION';
curTrans.currentStep = 0; // because this will be the first TRANSITION step
} else {
// remove the transition
this.clearTransition(currentChannel, true);
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, 'END', curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. End of transition`);
}
} else {
// remove the transition
this.clearTransition(currentChannel, true);
RED.nodes.getNode(curTrans.originator).sendFeedback(curTrans.id, currentChannel, 'END', curTrans.currentRepetition, performText);
this.debug(`[mainWorker] channel: ${currentChannel}, ${performText}. End of transition`);
}
}
}
}
// now take care of sending
if (this.dataDirty) {
this.dataDirty = false;
this.sendActive = true; // for security reasons
this.mainWorker.refresh(); // refresh before transitting because this takes about 2ms
this.sender.transmit();
this.trace(`[mainWorker] Transmitting on isDirty, reset dataDirty flag and retrigger mainWorker`);
} else {
// last call of timer. Reset send delay.
if (this.sendActive) {
if (Object.keys(this.transitionsMap).length == 0) {
this.trace(`[mainWorker] Called without dirty data. No transition in progress. Reset sendActive`);
this.sendActive = false;
} else {
this.trace(`[mainWorker] Called without dirty data, but with active transition. Keep sendActive and retrigger mainWorker`);
this.mainWorker.refresh();
}
}
}
return;
};
/**
* Stringify a object without circular references
* @param {*} object Value to stringify
* @param {number} space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read
* @returns {string} Cleanly stringified object
*/
this.cleanStringify = function (object, space = 0) {
if (object && typeof object === 'object') {
object = copyWithoutCircularReferences([object], object);
}
return JSON.stringify(object, null, space);
function copyWithoutCircularReferences(references, object) {
let cleanObject = {};
Object.keys(object).forEach(function(key) {
const value = object[key];
if (value && typeof value === 'object') {
if (references.indexOf(value) < 0) {
references.push(value);
cleanObject[key] = copyWithoutCircularReferences(references, value);
references.pop();
} else {
cleanObject[key] = '###_Circular_###';
}
} else if (typeof value !== 'function') {
cleanObject[key] = value;
}
});
return cleanObject;
}
};
/**
* save the dmx values to the node context
*/
this.saveDataToContext = function () {
this.nodeContext.set(this.id, {'dmxData': this.dmxData});
};
/**
* immediatly send out the dmx buffer or delay sending
*/
this.sendData = function () {
if (this.maxrate == 0) { // when spontaneous sending is disabled
if (!this.sendActive) { // start mainWorker if no active sending detected
this.sendActive = true;
this.mainWorker.refresh();
}
return;
}
if (this.sendActive) { // only set dirty and no direct transmission
this.dataDirty = true;
this.trace(`[sendData] Only setting dataDirty to true`);
} else { // first call with direct transmission
this.dataDirty = false;
this.sendActive = true;
this.mainWorker.refresh(); // refresh before transitting because this takes about 2ms
this.sender.transmit();
this.trace(`[sendData] Transmitting spontaneous, set sendActive and retrigger mainWorker`);
}
};
/**
* set a dmx vlaue and sendout the dmx buffer
* @param {number} channel dmx channel to set
* @param {number} value dmx value
*/
this.setChannelValue = function (channel, value) {
this.set(channel, value);
this.sendData();
};
/**
* set the first n values and send out the buffer
* @param {Array<number> | TypedArray} values dmx values starting with channel 1
*/
this.setAll = function (values) {
const end = Math.min(values.length, 512);
let i = 0;
for (i = 0; i != end; i++) {
this.set(i + 1, values[i]);
}
this.sendData();
};
/**
* set the value for a dmx channel in local and senders datastore
* @param {number} channel dmx channel to set
* @param {number} value dam value of selected channel
*/
this.set = function (channel, value) {
if ((channel >= 1) && (channel <= 512)) {
if (value < 0) {
this.error(`[set] invalid value: ${value}`);
return;
}
if (value > 255) {
this.error(`[set] invalid value: ${value}`);
return;
}
this.dmxData[channel - 1] = artnetutils.roundChannelValue(value);
this.sender.prepChannel(channel - 1, artnetutils.roundChannelValue(value));
} else {
this.error(`[set] invalid channel: ${channel}`);
}
};
/**
* get a specific dmx value
* @param {number} channel dmx channel to obtain
* @returns {number} dmx value
*/
this.get = function (channel) {
return parseInt(this.dmxData[channel - 1] || 0);
};
// ##############################################################
// region transition map logic
// ##############################################################
/**
* clear all active transitions. Is called on close node
*/
this.clearTransitions = function () {
for (var channel in this.transitionsMap) {
if (this.transitionsMap.hasOwnProperty(channel)) {
this.clearTransition(channel);
}
}
// reset maps
this.transitionsMap = {};
};
/**
* clear a single transition
* @param {number} channel dmx base channel of the transition
* @param {boolean} skipDataSending if true no dmx data will be sent after clerance
*/
this.clearTransition = function (channel, skipDataSending = false) {
const transition = this.transitionsMap[channel];
if (transition) {
this.log(`[clearTransition] Clear transition of channel: ${channel}, skipDataSending: ${skipDataSending}`);
// set end value immediately
if (transition.targetValue) {
// this.set(channel, transition.targetValue);
}
if (transition.targetPanValue) {
this.set(transition.arcConfig.pan_channel, transition.targetPanValue);
}
if (transition.targetTiltValue) {
this.set(transition.arcConfig.tilt_channel, transition.targetTiltValue);
}
// skip data sending if we have start_buckets in payload
if (!skipDataSending) {
this.sendData();
}
// remove transition from map
delete this.transitionsMap[channel];
}
};
/**
* add a transition to the transitionsMap
* @param {number} channel - dmx base channel of the transition
* @param {string} type - type of transition to add
* @param {number} startValue - startvalue of transition
* @param {number} targetValue - Value to go to
* @param {number} duration - Total time the transition (without hold and gap) should take
* @param {number} repeat - (optional) Number of repetitions to do
* @param {number} gap - (optional) Value in ms to wait between repetitions
* @param {number} hold - (optional) Value in ms to hold the new_value
* @param {boolean} mirror - (optional) Mirror the transition after the hold time (e.g. fade up and down)
* @param {string} originator - original OutNode, for sending back status information
* @param {string} id - identification of the transition, for sending back status information
* @param {number} gamma - gamma factor for gamma transition
*/
this.addTransition = function (channel, type, startValue, targetValue, duration, repeat, gap, hold, mirror, originator, id, gamma) {
this.debug(`[addTransition] Add transition, transition: ${type}, id: ${id}, channel: ${channel}, targetValue: ${targetValue}`);
const stepCount = Math.ceil(duration / this.senderClock);
const gapSteps = Math.ceil(gap / this.senderClock);
const holdSteps = Math.ceil(hold / this.senderClock);
if (!duration) {
this.warn(`duration not defioned or 0. Must be 1 - MAXINT`);
duration = 1;
}
this.debug(`[addTransition] called with channel: ${channel}, targetValue: ${targetValue}, duration: ${duration}, starting from value: ${startValue}, repeat: ${repeat}, gapSteps: ${gap}`);
this.trace(`[addTransition] Maps after addTransition: transitionsMap: ${this.cleanStringify(this.transitionsMap, 1)}`);
this.clearTransition(channel);
const transitionItem = {
'state': 'TRANSITION',
'type': type,
'channel': channel,
'originator': originator || '',
'id': id || '',
'overallTimeToGo': 0,
'startValue' : startValue || 0,
'targetValue' : targetValue || 0,
'mirror': mirror || false,
'repeat' : repeat || 0,
'gapSteps' : gapSteps || 0,
'holdSteps': holdSteps || 0,
'currentRepetition' : 0,
'currentStep' : 0,
'stepsToGo' : stepCount || 0,
'startTime' : Date.now(),
'timeToGo' : duration || 0,
'gamma' : gamma || 2.2
};
this.transitionsMap[channel] = transitionItem;
this.trace(`[addTransition] Maps after addTransition:\ntransitionsMap: ${this.cleanStringify(this.transitionsMap)}`);
};
// ##############################################################
// end region transition map logic
// ##############################################################
/**
* the input function. Called by the ArtNet-In node
* @param {any} msg the message object routed to this node
*/
this.input = function(msg) {
const payload = msg.payload;
const transition = payload.transition || '';
const duration = parseInt(payload.duration || 0);
const repeat = parseInt(payload.repeat || 0);
const hold = parseInt(payload.hold || 0);
const gap = parseInt(payload.gap || 0);
const mirror = payload.mirror ? true : false;
const gamma = payload.gamma || 2.2;
const originator = payload.originator || '';
const id = payload.id || '';
let i = 0;
this.debug(`[input] received input to sender, payload: ${JSON.stringify(payload)} `);
// expand buckets if array exists
if (payload.buckets && Array.isArray(payload.buckets)) {
this.debug(`[input] expanding buckets`);
payload.buckets = this.expandBuckets(payload.buckets);
}
// no transition, only channel processing
if (transition === '') {
if (payload.channel) { // channel processing
this.debug(`[input] now sending single value`);
this.clearTransition(payload.channel, true);
this.set(payload.channel, payload.value);
this.sendData();
} else if (Array.isArray(payload.buckets)) { // buckets processing
for (i = 0; i < payload.buckets.length; i++) {
this.clearTransition(payload.buckets[i].channel, true);
this.set(payload.buckets[i].channel, payload.buckets[i].value);
}
this.debug(`[input] now sending buckets`);
this.sendData();
} else if (msg.payload.constructor === Uint8Array || Array.isArray(msg.payload)) { // universe (UInt8Array) processing
this.setAll(msg.payload);
} else {
this.error(`[input] Invalid payload. No channel, no buckets`);
}
} else { // processing transitions
// processing start_buckets (only valid in transitions)
if (payload.start_buckets && Array.isArray(payload.start_buckets)) {
this.debug(`[input] processing start_buckets`);
payload.start_buckets = this.expandBuckets(payload.start_buckets);
}
// transition: arc (as done by gunnebo)
if (transition === "arc") {
try {
if (!payload.end || !payload.center) {
this.error(`[input] Invalid payload for transition "arc"`);
}
var arcConfig = payload.arc || DEFAULT_MOVING_HEAD_CONFIG;
var cv_phi = payload.start.pan;
var cv_theta = payload.start.tilt;
var interval = {start: 0, end: 1};
if (Array.isArray(payload.interval) && payload.interval.length > 1) {
interval.start = payload.interval[0];
interval.end = payload.interval[1];
}
//add transition without target value
//this.addTransition(arcConfig.tilt_channel, "arc"); only one transition on pan_channel
this.addTransition(arcConfig.pan_channel, "arc");
this.moveToPointOnArc(cv_theta, cv_phi,
payload.end.tilt, payload.end.pan,
payload.center.tilt, payload.center.pan,
duration, interval, arcConfig,
payload.repeat, payload.gap);
} catch (e) {
this.error("[input] ERROR " + e.message);
}
} else if (["linear", "quadratic", "gamma", "sine"].includes(transition)) { // all other transitions
if (payload.channel) {
// make a bucket out of the single channel, overwrite bucket if existes. Single channel takes precedence
payload.buckets = [{"channel": payload.channel, "value": payload.value}];
}
if (Array.isArray(payload.buckets)) {
this.debug(`[input] add transitions "${transition}" for some buckets`);
for (i = 0; i < payload.buckets.length; i++) {
this.clearTransition(payload.buckets[i].channel, false);
let startValue = payload.start_buckets.find(({ channel }) => channel === payload.buckets[i].channel);
if (typeof startValue !== 'undefined') startValue = startValue.value; // try to fetch the value
if (typeof startValue === 'undefined') startValue = this.get(payload.buckets[i].channel); // last hope to set the actual old value
this.addTransition(payload.buckets[i].channel, transition, startValue, payload.buckets[i].value,
duration, repeat, gap, hold, mirror, originator, id, gamma);
}
this.workerFunc(); // initial timer call. Will refresh himself
} else {
this.error(`[input] Invalid payload. No channel, no buckets in transition "${transition}"`);
}
} else { // unknown transition
this.error(`[input] Invalid payload. Unknown transition: "${transition}"`);
}
}
};
/**
* Add the steps for a arc transition to the transitionsMap and start the transition
*/
this.moveToPointOnArc = function (_cv_theta, _cv_phi, _tilt_nv, _pan_nv, _tilt_center, _pan_center, transition_time, interval, arcConfig, repeat = 0, gap = 0) {
var steps = transition_time / this.senderClock;
var time_per_step = this.senderClock;
var gapSteps = Math.ceil(gap / this.senderClock);
var transition = this.transitionsMap[arcConfig.pan_channel];
var oldPanValue = this.get(arcConfig.pan_channel);
var oldTiltValue = this.get(arcConfig.tilt_channel);
if (!transition) {
this.warn(`called with arcConfig: ${JSON.stringify(arcConfig)}. No transition(s) in progress !!`);
return;
}
this.trace(`[moveToPointOnArc] called with arcConfig: ${JSON.stringify(arcConfig)}, interval: ${interval}, transition_time: ${transition_time}, repeat: ${repeat}, gapSteps`);
// current value
var cv_theta = artnetutils.channelValueToRad(_cv_theta, arcConfig.tilt_angle); //tilt
var cv_phi = artnetutils.channelValueToRad(_cv_phi, arcConfig.pan_angle); // pan
// target value
var nv_theta = artnetutils.channelValueToRad(_tilt_nv, arcConfig.tilt_angle);
var nv_phi = artnetutils.channelValueToRad(_pan_nv, arcConfig.pan_angle);
// center value
var tilt_center = artnetutils.channelValueToRad(_tilt_center, arcConfig.tilt_angle); //tilt
var pan_center = artnetutils.channelValueToRad(_pan_center, arcConfig.pan_angle); // pan
this.debug(`[moveToPointOnArc] Input points:
curPoint: ${cv_theta}, ${cv_phi}
newPoint: ${nv_theta}, ${nv_phi}
newPoint2: ${utils.radiansToDegrees(nv_theta)}, ${utils.radiansToDegrees(nv_phi)}
centerPoint: ${tilt_center}, ${pan_center}`);
// convert points to Cartesian coordinate system
this.debug("[moveToPointOnArc] *************************************");
this.debug("[moveToPointOnArc] 1 -> convert points to cartesian");
var currentPoint = utils.toCartesian({phi: cv_phi, theta: cv_theta});
var newPoint = utils.toCartesian({phi: nv_phi, theta: nv_theta});
var centerPoint = utils.toCartesian({phi: pan_center, theta: tilt_center});
var vn = centerPoint;
vn = utils.normalizeVector(vn);
centerPoint = utils.calcCenterPoint(centerPoint, currentPoint);
this.tracePoint("currentPoint ", currentPoint);
this.tracePoint("newPoint ", newPoint);
this.tracePoint("centerPoint ", centerPoint);
this.debug("[moveToPointOnArc] *************************************");
var movement_point = centerPoint;
// move center of circle to center of coordinates
this.debug("[moveToPointOnArc] 2 -> move to O(0,0,0)");
currentPoint = utils.movePointInCartesian(currentPoint, centerPoint, -1);
newPoint = utils.movePointInCartesian(newPoint, centerPoint, -1);
centerPoint = utils.movePointInCartesian(centerPoint, centerPoint, -1);
this.tracePoint("currentPoint ", currentPoint);
this.tracePoint("newPoint ", newPoint);
this.tracePoint("centerPoint ", centerPoint);
this.debug("[moveToPointOnArc] *************************************");
// calculate normal vector (i,j,k) for circle plane (three points)
this.debug("[moveToPointOnArc] 3 -> normal vector calculation");
//var vn = getNormalVector(centerPoint,currentPoint,newPoint);
//vn = normalizeVector(vn);
this.tracePoint("normalVector ", vn);
var backVector = utils.rotatePoint_xy_quarterion(utils.OZ, vn);
this.tracePoint("BackVector", backVector);
this.debug("[moveToPointOnArc] *************************************");
this.debug("[moveToPointOnArc] 4 -> rotate coordinate system");
currentPoint = utils.rotatePoint_xy_quarterion(currentPoint, vn);
newPoint = utils.rotatePoint_xy_quarterion(newPoint, vn);
centerPoint = utils.rotatePoint_xy_quarterion(centerPoint, vn);
this.tracePoint("currentPoint ", currentPoint);
this.tracePoint("newPoint ", newPoint);
this.tracePoint("centerPoint ", centerPoint);
this.debug("[moveToPointOnArc] *************************************");
this.debug("[moveToPointOnArc] 4.1 -> rotate coordinate system back for check");
var currentPoint1 = utils.rotatePoint_xy_quarterion(currentPoint, backVector);
var newPoint1 = utils.rotatePoint_xy_quarterion(newPoint, backVector);
var centerPoint1 = utils.rotatePoint_xy_quarterion(centerPoint, backVector);
this.tracePoint("currentPoint1 ", currentPoint1);
this.tracePoint("newPoint1 ", newPoint1);
this.tracePoint("centerPoint1 ", centerPoint1);
this.debug("[moveToPointOnArc] *************************************");
var radius = utils.getDistanceBetweenPointsInCartesian(currentPoint, centerPoint);
var radius2 = utils.getDistanceBetweenPointsInCartesian(newPoint, centerPoint);
this.debug(`[moveToPointOnArc] radius: ${radius}, radius2: ${radius2}, Epsilon: ${utils.EPSILON}, diff: ${Math.abs(radius2 - radius)}`);
if (Math.abs(radius2 - radius) > utils.EPSILON) {
this.error("[moveToPointOnArc] Invalid center point");
return;
}
this.debug("[moveToPointOnArc] 5 -> parametric equation startT and endT calculation");
//find t parameter for start and end point
var currentT = (Math.acos(currentPoint.x / radius) + 2 * Math.PI) % (2 * Math.PI);
var newT = (Math.acos(newPoint.x / radius) + 2 * Math.PI) % (2 * Math.PI);
this.debug(`[moveToPointOnArc] T parameters rad: ${radius}, ${currentT}, ${newT}`);
this.debug("[moveToPointOnArc] T parameters degree", utils.radiansToDegrees(currentT), utils.radiansToDegrees(newT));
this.debug("[moveToPointOnArc] *************************************");
var actualAngle = newT - currentT;
var angleDelta = Math.abs(actualAngle) <= Math.PI ? actualAngle : -(actualAngle - Math.PI);
var angleStep = angleDelta / steps;
// limit steps for interval
var startStep = parseInt(steps * interval.start);
var endStep = parseInt(steps * interval.end);
this.debug(`[moveToPointOnArc] angleStep: ${angleDelta}, ${angleStep}, ${utils.radiansToDegrees(angleDelta)}`);
this.debug(`[moveToPointOnArc] angleStep: ${steps}, ${startStep}, ${endStep}`);
// set basic values for this transition
if (endStep == 1) {
transition.targetPanValue = artnetutils.validateChannelValue(artnetutils.radToChannelValue(nv_phi, arcConfig.pan_angle));
transition.targetTiltValue = artnetutils.validateChannelValue(artnetutils.radToChannelValue(nv_phi, arcConfig.tilt_angle));
}
transition.repeat = repeat;
transition.gapSteps = gapSteps;
transition.startPanValue = oldPanValue;
transition.startTiltValue = oldTiltValue;