Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion dist/nodes/sacn-in/locales/de/sacn-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"universe": "Universum",
"output": "Ausgabe",
"output_full": "Vollständiges Universum",
"output_changes": "Änderungen"
"output_changes": "Änderungen",
"trigger": "Ausgabe-Auslösung",
"trigger_changes": "Nur Änderungen",
"trigger_always": "Jedes Paket",
"trigger_interval": "Änderungen + zyklisch",
"interval": "Intervall (ms)",
"clearOnUniverseChange": "Bei Universumswechsel auf 0 setzen"
}
}
}
8 changes: 7 additions & 1 deletion dist/nodes/sacn-in/locales/en-US/sacn-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"universe": "universe",
"output": "output",
"output_full": "whole universe",
"output_changes": "changes"
"output_changes": "changes",
"trigger": "output trigger",
"trigger_changes": "changes only",
"trigger_always": "every packet",
"trigger_interval": "changes + cyclic",
"interval": "interval (ms)",
"clearOnUniverseChange": "blank on universe change"
}
}
}
78 changes: 77 additions & 1 deletion dist/nodes/sacn-in/sacn-in.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@
<option value="changes" data-i18n="sacn-in.label.output_changes"></option>
</select>
</div>
<div class="form-row">
<label for="node-input-trigger">
<i class="fa fa-share-square-o"></i>
<span data-i18n="sacn-in.label.trigger"></span>
</label>
<select id="node-input-trigger" style="width:70%;">
<option value="changes" data-i18n="sacn-in.label.trigger_changes"></option>
<option value="always" data-i18n="sacn-in.label.trigger_always"></option>
<option value="interval" data-i18n="sacn-in.label.trigger_interval"></option>
</select>
</div>
<div class="form-row" id="row-interval">
<label for="node-input-interval">
<i class="fa fa-clock-o"></i>
<span data-i18n="sacn-in.label.interval"></span>
</label>
<input type="number" id="node-input-interval" min="1" step="1" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-clearOnUniverseChange" style="width:auto;">
<i class="fa fa-eraser"></i>
<span data-i18n="sacn-in.label.clearOnUniverseChange"></span>
</label>
<input type="checkbox" id="node-input-clearOnUniverseChange" style="display:inline-block; width:auto; vertical-align:top; margin-left:4px;"/>
</div>
<div class="form-row">
<label for="node-input-interface">
<i class="fa fa-plug"></i>
Expand Down Expand Up @@ -70,11 +95,40 @@ <h4>Configuration</h4>
<dd>Whether to emit every received packet or merged values using HTP or LTP.</dd>
<dt>Output <span class="property-type">full | changes</span></dt>
<dd>Emit the whole universe on every update (<i>full</i>) or only the changed channels (<i>changes</i>).</dd>
<dt>Output trigger <span class="property-type">changes | always | interval</span></dt>
<dd>
Controls <b>when</b> a message is emitted, independently of the mode:
<ul>
<li><i>changes only</i> — emit only when the values change.</li>
<li><i>every packet</i> — emit on every received sACN packet, even without a change.</li>
<li>
<i>changes + cyclic</i> — emit on change and reset a keepalive timer; if no change arrives
within the interval, the current full universe is re-emitted.
</li>
</ul>
In the merging modes, <i>every packet</i> and the cyclic re-emit send the full merged universe.
</dd>
<dt class="optional">Interval <span class="property-type">number</span></dt>
<dd>Keepalive interval in milliseconds for the <i>changes + cyclic</i> trigger. Default <code>1000</code>.</dd>
<dt class="optional">Blank on universe change <span class="property-type">boolean</span></dt>
<dd>
When switching the universe via <code>msg.universe</code>, immediately emit a full universe of
zeros for the new universe until the first real data arrives. Default <code>false</code>.
</dd>
<dt class="optional">Interface <span class="property-type">string</span></dt>
<dd>IP address of the network interface used for receiving. Leave empty to listen on all interfaces.</dd>
<dt class="optional">Port <span class="property-type">number</span></dt>
<dd>Network port to listen on. Defaults to <code>5568</code>.</dd>
</dl>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>universe <span class="property-type">number</span></dt>
<dd>
Switches the observed universe at runtime, <code>1</code> to <code>63999</code>. The node stops
listening on the previous universe and starts listening on the new one. Invalid values are
ignored and produce a warning.
</dd>
</dl>
<h3>Outputs</h3>
<p>The properties emitted depend on the selected mode.</p>
<h4>Passthrough mode</h4>
Expand Down Expand Up @@ -131,8 +185,19 @@ <h3>References</h3>
value: "full",
required: true,
},
trigger: {
value: "changes",
required: true,
},
interval: {
value: 1000,
required: true,
},
clearOnUniverseChange: {
value: false,
},
},
inputs: 0,
inputs: 1,
outputs: 1,
paletteLabel: "sACN in",
icon: "font-awesome/fa-lightbulb-o",
Expand All @@ -142,6 +207,17 @@ <h3>References</h3>
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
const select = document.getElementById("node-input-trigger");
const row = document.getElementById("row-interval");
const toggleInterval = () => {
if (row) {
row.style.display = select?.value === "interval" ? "" : "none";
}
};
select?.addEventListener("change", toggleInterval);
toggleInterval();
},
};
RED.nodes.registerType("sacn-in", def);
</script>
148 changes: 120 additions & 28 deletions dist/nodes/sacn-in/sacn-in.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ class NodeHandler {
config;
data = new Map();
sACN;
currentUniverse;
trigger;
interval;
keepaliveTimer;
constructor(node, config) {
this.node = node;
this.config = config;
this.currentUniverse = config.universe;
this.trigger = config.trigger ?? (config.mode === "passthrough" ? "always" : "changes");
this.interval = config.interval !== undefined && config.interval > 0 ? config.interval : 1000;
const options = {
universes: [config.universe],
reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true,
Expand Down Expand Up @@ -36,34 +43,82 @@ class NodeHandler {
}
this.node.on("close", () => {
this.sACN.close();
if (this.keepaliveTimer) {
clearTimeout(this.keepaliveTimer);
}
this.data = new Map();
});
if (config.mode === "passthrough") {
this.sACN.on("packet", (packet) => {
this.node.send({
universe: packet.universe,
payload: this.parsePayload(packet.payload, packet.universe),
sequence: packet.sequence,
source: packet.sourceAddress,
priority: packet.priority,
});
const changed = this.hasChanges(packet.payload, packet.universe);
const payload = this.parsePayload(packet.payload, packet.universe);
if (this.trigger === "always" || changed) {
this.sendData({
universe: packet.universe,
payload,
sequence: packet.sequence,
source: packet.sourceAddress,
priority: packet.priority,
});
}
});
}
else if (config.mode === "ltp") {
else {
this.sACN.on("changed", (data) => {
this.node.send({
universe: data.universe,
payload: this.parsePayload(data.payload, data.universe),
});
const payload = this.parsePayload(data.payload, data.universe);
if (this.trigger !== "always") {
this.sendData({
universe: data.universe,
payload,
});
}
});
}
else if (config.mode === "htp") {
this.sACN.on("changed", (data) => {
this.node.send({
universe: data.universe,
payload: this.parsePayload(data.payload, data.universe),
if (this.trigger === "always") {
this.sACN.on("packet", () => {
this.emitFull(this.currentUniverse);
});
});
}
}
this.node.on("input", (msg) => {
this.handleUniverseChange(msg);
});
this.setStatus();
this.resetKeepalive();
}
setStatus() {
this.node.status({
fill: "green",
shape: "dot",
text: `Universe ${this.currentUniverse}`,
});
}
parseUniverse(value) {
const universe = typeof value === "string" ? parseInt(value, 10) : value;
if (typeof universe !== "number" || !Number.isInteger(universe) || universe < 1 || universe > 63999) {
return undefined;
}
return universe;
}
handleUniverseChange(msg) {
const universe = this.parseUniverse(msg.universe);
if (universe === undefined) {
this.node.warn(`The given "universe"-property "${msg.universe}" (${typeof msg.universe}) is invalid or not between 1 and 63999.`);
return;
}
if (universe === this.currentUniverse) {
return;
}
this.sACN.removeUniverse(this.currentUniverse);
this.sACN.addUniverse(universe);
this.data?.delete(this.currentUniverse);
this.currentUniverse = universe;
this.setStatus();
if (this.config.clearOnUniverseChange) {
this.data?.set(universe, this.getNulledUniverse());
this.emitFull(universe);
}
else {
this.resetKeepalive();
}
}
getNulledUniverse() {
Expand All @@ -73,22 +128,59 @@ class NodeHandler {
}
return universe;
}
getReference(universe) {
if (this.config.output === "changes") {
return {};
hasChanges(payload, universe) {
const full = this.data?.get(universe);
if (full === undefined) {
return true;
}
return this.data?.get(universe) ?? this.getNulledUniverse();
return Object.keys(payload).some((key) => {
const ch = parseInt(key, 10);
return full[ch] !== payload[ch];
});
}
parsePayload(payload, universe) {
const processedPayload = this.getReference(universe);
const full = this.data?.get(universe) ?? this.getNulledUniverse();
Object.keys(payload).forEach((key) => {
const ch = parseInt(key, 10);
processedPayload[ch] = payload[ch];
full[ch] = payload[ch];
});
if (this.config.output !== "changes") {
this.data?.set(universe, processedPayload);
this.data?.set(universe, full);
if (this.config.output === "changes") {
const changes = {};
Object.keys(payload).forEach((key) => {
const ch = parseInt(key, 10);
changes[ch] = payload[ch];
});
return changes;
}
return full;
}
sendData(msg) {
this.node.send(msg);
this.resetKeepalive();
}
emitFull(universe) {
const full = this.data?.get(universe) ?? this.getNulledUniverse();
this.sendData({ universe, payload: { ...full } });
}
resetKeepalive() {
if (this.trigger !== "interval") {
return;
}
if (this.keepaliveTimer) {
clearTimeout(this.keepaliveTimer);
}
this.keepaliveTimer = setTimeout(() => {
this.keepaliveTick();
}, this.interval);
}
keepaliveTick() {
if (this.data?.has(this.currentUniverse)) {
this.emitFull(this.currentUniverse);
}
else {
this.resetKeepalive();
}
return processedPayload;
}
}
exports.default = (RED) => {
Expand Down
30 changes: 30 additions & 0 deletions src/nodes/sacn-in/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,42 @@ <h4>Configuration</h4>
<dd>Whether to emit every received packet or merged values using HTP or LTP.</dd>
<dt>Output <span class="property-type">full | changes</span></dt>
<dd>Emit the whole universe on every update (<i>full</i>) or only the changed channels (<i>changes</i>).</dd>
<dt>Output trigger <span class="property-type">changes | always | interval</span></dt>
<dd>
Controls <b>when</b> a message is emitted, independently of the mode:
<ul>
<li><i>changes only</i> — emit only when the values change.</li>
<li><i>every packet</i> — emit on every received sACN packet, even without a change.</li>
<li>
<i>changes + cyclic</i> — emit on change and reset a keepalive timer; if no change arrives
within the interval, the current full universe is re-emitted.
</li>
</ul>
In the merging modes, <i>every packet</i> and the cyclic re-emit send the full merged universe.
</dd>
<dt class="optional">Interval <span class="property-type">number</span></dt>
<dd>Keepalive interval in milliseconds for the <i>changes + cyclic</i> trigger. Default <code>1000</code>.</dd>
<dt class="optional">Blank on universe change <span class="property-type">boolean</span></dt>
<dd>
When switching the universe via <code>msg.universe</code>, immediately emit a full universe of
zeros for the new universe until the first real data arrives. Default <code>false</code>.
</dd>
<dt class="optional">Interface <span class="property-type">string</span></dt>
<dd>IP address of the network interface used for receiving. Leave empty to listen on all interfaces.</dd>
<dt class="optional">Port <span class="property-type">number</span></dt>
<dd>Network port to listen on. Defaults to <code>5568</code>.</dd>
</dl>

<h3>Inputs</h3>
<dl class="message-properties">
<dt>universe <span class="property-type">number</span></dt>
<dd>
Switches the observed universe at runtime, <code>1</code> to <code>63999</code>. The node stops
listening on the previous universe and starts listening on the new one. Invalid values are
ignored and produce a warning.
</dd>
</dl>

<h3>Outputs</h3>
<p>The properties emitted depend on the selected mode.</p>

Expand Down
Loading
Loading