@bannsaenger/node-red-contrib-artnet-controller
Version:
Node-RED node that controls lights via Art-Net. Acts as a Art-Net controller.
922 lines (838 loc) • 45.5 kB
JavaScript
const dmxlib = require('dmxnet');
const utils = require('./utils/arc-transition-utils');
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) {
var uninet = `${net}:${subnet}:${universe}`;
var 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) {
var uninet = `${net}:${subnet}:${universe}`;
var 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]) {
var 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
for (var 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() {
var currentTime = Date.now();
// start with transition handling
for (const currentChannel in this.transitionsMap) {
var currentTransaction = this.transitionsMap[currentChannel];
if (currentTransaction.currentStep == 1) currentTransaction.startTime = currentTime;
if (currentTransaction.currentStep <= currentTransaction.stepsToGo) { // proceed next step
switch (currentTransaction.type) {
case 'linear':
this.trace(`[mainWorker] (linear) doing step [${currentTransaction.currentStep}] for channel: ${currentChannel}, value: ${currentTransaction.steps[currentTransaction.currentStep].value}`);
this.set(currentChannel, currentTransaction.steps[currentTransaction.currentStep].value);
this.dataDirty = true;
break;
case 'arc':
// get point in spherical coordinates
var iterationPoint = utils.getIterationPoint(currentTransaction.steps[currentTransaction.currentStep].value, currentTransaction.radius, currentTransaction.backVector, currentTransaction.movement_point);
var tilt = artnetutils.validateChannelValue(artnetutils.radToChannelValue(iterationPoint.theta, currentTransaction.arcConfig.tilt_angle));
var pan = artnetutils.validateChannelValue(artnetutils.radToChannelValue(iterationPoint.phi, currentTransaction.arcConfig.pan_angle));
this.debug(`[mainWorker] (arc) doing step [${currentTransaction.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(currentTransaction.steps[currentTransaction.currentStep].value).toFixed(4)}, pan: ${parseFloat(pan).toFixed(4)}, tilt: ${parseFloat(tilt).toFixed(4)}`);
this.set(currentTransaction.arcConfig.pan_channel, pan);
this.set(currentTransaction.arcConfig.tilt_channel, tilt);
this.dataDirty = true;
break;
default:
this.warn(`[mainWorker] unknown transition: ${currentTransaction.type}`);
}
if (currentTransaction.currentStep == currentTransaction.stepsToGo) {
// only at the end of the transition
this.debug(`[mainWorker] difference between target and actual time : ${currentTime - currentTransaction.startTime - currentTransaction.timeToGo} ms, ${(((currentTime - currentTransaction.startTime - currentTransaction.timeToGo) * 100) / currentTransaction.timeToGo).toFixed(1)} % (positive = took too long, negative = to fast)`);
}
currentTransaction.currentStep++;
} else { // check if transition is to repeat
if (currentTransaction.repeat != 0) {
if (currentTransaction.currentRepetition != (currentTransaction.repeat - 1)) {
// if equal the last repetition has been done, if endless repetions .repeat = -1
if (currentTransaction.gapSteps > currentTransaction.currentGapStep) {
// care about the break beween repetitions
currentTransaction.currentGapStep++;
} else {
// care about the repetition, return to the start values
currentTransaction.currentRepetition++;
currentTransaction.currentGapStep = 0;
currentTransaction.currentStep = 1;
if (currentTransaction.startValue) this.set(currentChannel, currentTransaction.startValue);
if (currentTransaction.startPanValue) this.set(currentTransaction.arcConfig.pan_channel, currentTransaction.startPanValue);
if (currentTransaction.startTiltValue) this.set(currentTransaction.arcConfig.tilt_channel, currentTransaction.startTiltValue);
this.dataDirty = true;
}
} else {
// remove the transition
this.clearTransition(currentChannel, true);
}
} else {
// remove the transition
this.clearTransition(currentChannel, true);
}
}
}
// now take care of sending
if (this.dataDirty) {
this.dataDirty = false;
this.sendActive = true; // for security reasons
this.sender.transmit();
this.mainWorker.refresh();
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;
}.bind(this)), this.senderClock);
/**
* 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) {
var cleanObject = {};
Object.keys(object).forEach(function(key) {
var 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.sender.transmit();
this.mainWorker.refresh();
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 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) {
var 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
* @param {number} channel dmx base channel of the transition
* @param {string} type type of transition to add
* @param {number} value startvalue of transition
*/
this.addTransition = function (channel, type, value) {
this.debug(`[addTransition] Add transition, transition: ${type} channel: ${channel}, value: ${value}`);
this.clearTransition(channel);
var transitionItem = {
'type': type,
'channel': channel,
'steps': [],
'targetValue' : 0,
'repeat' : 0,
'gapSteps' : 0,
'currentGapStep' : 0,
'currentRepetition' : 0,
'currentStep' : 1,
'startValue' : 0,
'stepsToGo' : 0,
'startTime' : 0,
'timeToGo' : 0
};
if (value) {
transitionItem.value = parseInt(value);
}
this.transitionsMap[channel] = transitionItem;
// this.trace(`[addTransition] Maps after addTransition:\ntransitionsMap: ${this.cleanStringify(this.transitionsMap)}\ncloseCallbacksMap: ${this.cleanStringify(this.closeCallbacksMap)}`);
};
// ##############################################################
// 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) {
var payload = msg.payload;
var transition = payload.transition;
var duration = parseInt(payload.duration || 0);
var i = 0;
this.debug(`[input] received input to sender, payload: ${JSON.stringify(payload)} `);
// processing start_buckets
if (payload.start_buckets && Array.isArray(payload.start_buckets)) {
this.debug(`[input] processing start_buckets`);
for (i = 0; i < payload.start_buckets.length; i++) {
this.clearTransition(payload.start_buckets[i].channel, true);
// skip data sending to device
this.set(payload.start_buckets[i].channel, payload.start_buckets[i].value);
}
this.sendData();
}
// processing transitions
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 (transition === "linear") {
if (payload.channel) {
this.debug(`[input] add a transition "linear" for single value`);
this.addTransition(payload.channel, "linear");
this.fadeToValue(payload.channel, payload.value, duration, payload.repeat);
} else if (Array.isArray(payload.buckets)) {
this.debug(`[input] add transitions "linear" for some buckets`);
for (i = 0; i < payload.buckets.length; i++) {
this.addTransition(payload.buckets[i].channel, "linear");
this.fadeToValue(payload.buckets[i].channel, payload.buckets[i].value, duration, payload.repeat, payload.gap);
}
} else {
this.error(`[input] Invalid payload. No channel, no buckets in transition "linear"`);
}
} else { // no transition
if (payload.channel) {
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)) {
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 {
this.error(`[input] Invalid payload. No channel, no buckets`);
}
}
};
/**
* Add the steps for a linear transition to the transitionsMap and start the transition
* @param {number} channel Channel on which this trnsition is done
* @param {number} new_value Value to go to
* @param {number} transition_time Total time the transition should take
* @param {number} repeat (optional) Number of repetitions to do
* @param {number} gap (optional) Value in ms to wait between repetitions
*/
this.fadeToValue = function (channel, new_value, transition_time, repeat = 0, gap = 0) {
var oldValue = this.get(channel);
var steps = Math.ceil(transition_time / this.senderClock);
var gapSteps = Math.ceil(gap / this.senderClock);
var transition = this.transitionsMap[channel];
if (!transition) {
this.warn(`[fadeToValue] called with channel: ${channel}. No transition in progress !!`);
return;
}
this.trace(`[fadeToValue] called with channel: ${channel}, new_value: ${new_value}, transition_time: ${transition_time}, starting from value: ${oldValue}, repeat: ${repeat}, gapSteps: ${gap}`);
// calculate difference between new and old values
var diff = Math.abs(oldValue - new_value);
// should we fade up or down?
var direction = (new_value > oldValue);
var value_per_step = diff / steps;
var time_per_step = transition_time / steps;
var valueStep = direction === true ? value_per_step : -value_per_step;
// set basic values for this transition
transition.targetValue = new_value;
transition.repeat = repeat;
transition.gapSteps = gapSteps;
transition.startValue = oldValue;
transition.timeToGo = time_per_step * steps;
for (var i = 1; i <= steps; i++) {
var iterationValue = oldValue + i * valueStep;
this.trace(`iterationValue: ${iterationValue} in step: ${i}`);
// create step
transition.steps[i] = {
'value' : Math.round(iterationValue),
'time' : i * time_per_step,
'step' : i
};
transition.stepsToGo = i;
}
// start transition
this.sendData();
this.trace(`[fadeToValue] Maps after fadeToValue: transitionsMap: ${this.cleanStringify(this.transitionsMap, 1)}`);
};
/**
* 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;
transition.timeToGo = time_per_step * (endStep - startStep);
transition.radius = radius;
transition.backVector = backVector;
transition.movement_point = movement_point;
transition.arcConfig = arcConfig;
var counter = 1;
for (var i = startStep; i <= endStep; i++) {
var t = currentT + i * angleStep;
// create step
transition.steps[counter] = {
'value' : t,
'time' : i * time_per_step,
'step' : i
};
transition.stepsToGo = counter;
counter++;
}
// start transition
this.sendData();
this.trace(`[moveToPointOnArc] Maps after fadeToValue: transitionsMap: ${this.cleanStringify(this.transitionsMap, 1)}`);
};
/**
* Trace and log a point to the console
* @param {*} tag
* @param {*} point
*/
this.tracePoint = function (tag,point){
this.trace(tag + " : " + parseFloat(point.x).toFixed(4) + " : " + parseFloat(point.y).toFixed(4) + " : " + parseFloat(point.z).toFixed(4));
};
/**
* called when node is destroyed
*/
this.on('close', function() {
clearTimeout(this.mainWorker);
this.clearTransitions();
this.saveDataToContext();
this.sender.stop();
delete this.sender;
});
}
RED.nodes.registerType("Art-Net Sender", ArtNetSender);
/*************************************************
* The out node for sending data from a flow
*/
function ArtNetOutNode(config) {
RED.nodes.createNode(this, config);
this.name = config.name || '';
this.artnetsender = config.artnetsender;
this.ignoreaddress = typeof config.ignoreaddress === 'undefined' ? false : config.ignoreaddress;
this.senderObject = RED.nodes.getNode(this.artnetsender);
this.controllerObj = RED.nodes.getNode(this.senderObject.artnetcontroller);
this.log(`[ArtNetOutNode] senderObject: ${this.senderObject.type}:${this.senderObject.id}, controllerObj: ${this.controllerObj.type}:${this.controllerObj.id}`);
this.on('input', function (msg) {
this.trace(`get payload: ${JSON.stringify(msg.payload)}`);
var payload = msg.payload;
var locNet, locSubnet, locUniverse;
// check if ignore address is set
if (this.ignoreaddress) {
this.debug(`[input] ignore address is set, routing to default sender`);
this.senderObject.input(msg);
return;
}
// check if address information in payload
if ((payload.net === undefined) && (payload.subnet === undefined) && (payload.universe === undefined)) {
this.debug(`[input] no address information found, routing to default sender`);
this.senderObject.input(msg);
return;
}
// ok there if address information, now check each value
if (payload.net !== undefined) {
if ((parseInt(payload.net) < 0) || (parseInt(payload.net) > 15)) {
this.warn(`[input] invalid net in payload: ${payload.net}`);
locNet = this.senderObject.net;
} else {
locNet = parseInt(payload.net);
}
} else {
locNet = this.senderObject.net;
}
if (payload.subnet !== undefined) {
if ((parseInt(payload.subnet) < 0) || (parseInt(payload.subnet) > 15)) {
this.warn(`[input] invalid net in payload: ${payload.subnet}`);
locSubnet = this.senderObject.subnet;
} else {
locSubnet = parseInt(payload.subnet);
}
} else {
locSubnet = this.senderObject.net;
}
if (payload.universe !== undefined) {
if ((parseInt(payload.universe) < 0) || (parseInt(payload.universe) > 15)) {
this.warn(`[input] invalid universe in payload: ${payload.universe}`);
locUniverse = this.senderObject.universe;
} else {
locUniverse = parseInt(payload.universe);
}
} else {
locUniverse = this.senderObject.net;
}
// now look for a matching sender
var locSender = this.controllerObj.getSender(this.senderObject.address, this.senderObject.port, locNet, locSubnet, locUniverse);
if (locSender) {
this.debug(`[input] ${locNet}:${locSubnet}:${locUniverse}, routing to sender: ${locSender}`);
RED.nodes.getNode(locSender).input(msg);
} else {
this.warn(`[input] no sender found for specified address information: ${locNet}:${locSubnet}:${locUniverse}, routing to default sender`);
this.senderObject.input(msg);
}
});
}
RED.nodes.registerType("Art-Net Out", ArtNetOutNode);
/*************************************************
* ArtNetInNode is like ArtNetReceiver (in dmxnet speech)
*/
function ArtNetInNode(config) {
RED.nodes.createNode(this, config);
this.name = config.name || '';
this.artnetcontroller = config.artnetcontroller;
this.net = config.net || 0;
this.subnet = config.subnet || 0;
this.universe = config.universe || 0;
this.outformat = config.outformat || 'buckets';
this.sendonchange = typeof config.sendonchange === 'undefined' ? true : config.sendonchange;
this.controllerObj = RED.nodes.getNode(this.artnetcontroller);
this.dmxData = [];
/**
* create receiver instance
*/
this.log(`[ArtNetInNode] Creating receiver on controller: ${this.controllerObj.name} parameters: SubNetUni: ${this.net}:${this.subnet}:${this.universe}, format: ${this.outformat}, ${this.sendonchange ? 'send data only when dmx data changes' : 'send on every art-dmx packet'}`);
this.receiver = this.controllerObj.dmxnet.newReceiver({
net: this.net,
subnet: this.subnet,
universe: this.universe,
});
// Dump data if DMX Data is received
this.receiver.on('data', function(data) {
//console.log('DMX data:', data); // eslint-disable-line no-console
var msg = {};
var buckets = [];
var i = 0;
if (!this.sendonchange || !equals(this.dmxData, data)) { // sendonchange = false OR data changed
if (!this.sendonchange) { // send in any case
if (this.outformat == 'buckets') {
for (i = 0; i < data.length; i++) {
buckets.push({'channel': i + 1, 'value': data[i]});
}
msg = {'payload': {'buckets': buckets}};
} else {
msg = {'payload': data};
}
} else { // send only changes
if (this.outformat == 'buckets') {
for (i = 0; i < data.length; i++) {
if (this.dmxData[i] != data[i]) { // only if this single value changed
buckets.push({'channel': i + 1, 'value': data[i]});
}
}
msg = {'payload': {'buckets': buckets}};
} else {
msg = {'payload': data};
}
}
this.send(msg);
}
this.dmxData = data; // save the actual universe for comparison
}.bind(this));
}
RED.nodes.registerType("Art-Net In", ArtNetInNode);
/*************************************************
* Get IP information and pass it to the configuration dialog
*/
RED.httpAdmin.get("/ips", RED.auth.needsPermission('artnet.read'), function(req,res) {
try {
const nets = networkInterfaces();
var IPs4 = [{name: '[IPv4] 0.0.0.0 - ' + RED._("artnet.names.allips"), address: '0.0.0.0', family: 'ipv4'}];
// IPv6 not implemented yet
//var IPs6 = [{name: '[IPv6] ::', address: '::', family: 'ipv6'}];
for (const name of Object.keys(nets)) { // Lookup all interfaces
for (const net of nets[name]) {
// Skip over non-IPv4
if (net.family === 'IPv4') { // First IPv4 address
IPs4.push({name: '[IPv4] ' + net.address + ' - ' + name, address: net.address, family: 'ipv4'});
}
}
}
res.json(IPs4);
} catch(err) {
res.json([RED._("artnet.errors.list")]);
}
});
};