diff --git a/README.md b/README.md
index 11b9d11..6c06012 100644
--- a/README.md
+++ b/README.md
@@ -92,20 +92,24 @@ This node can be used to record scenes and play them afterwards.
#### Parameters:
-| Parameter | Description | Possible Values | Default Value | Mandatory |
-| --------- | ------------------------------------------------------------------------------ | --------------------- | ------------- | --------- |
-| values | Interpret channel values as a percentage (0–100) or as raw DMX values (0–255). | `percent`, `absolute` | `percent` | yes |
+| Parameter | Description | Possible Values | Default Value | Mandatory |
+| -------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------------- | --------- |
+| values | Interpret channel values as a percentage (0–100) or as raw DMX values (0–255). | `percent`, `absolute` | `percent` | yes |
+| playMode | Whether a new `play` replaces the running look (`switch`) or stacks scenes additively, combined per channel by HTP (`add`) | `switch`, `add` | `switch` | yes |
+| blackoutOnStop | Send a nulled universe once when the last active scene is stopped, so the output goes dark. | `true`, `false` | `false` | no |
Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node-RED user directory) and survive a restart.
+In `add` mode multiple scenes can be active at once; every `play`, `stop` or `reset` recomputes the combined look (highest value wins per channel) and emits it once. The output only goes dark once the last active scene ends.
+
#### Expected input:
-| Property | Description | Mandatory |
-| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- |
-| `action` | the action to be executed - `save` to save a preset, `play` to play a saved preset, `reset` to reset | yes |
-| `scene` | for action | yes, for actions `save`, `play` |
-| `universe` | if only one universe is handled, this parameter is mandatory and contains the used universe | only for a single universe |
-| `payload` | contains the values to record. it might be an array (key 0-511) containing the values for a single universe,
an object (keys 1-512) containing the values for a single universe or
an object (any numeric keys) containing objects (keys 1-512) containing an universe each. | yes, for action `save` |
+| Property | Description | Mandatory |
+| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
+| `action` | the action to be executed - `save` to save a preset, `play` to play a saved preset, `stop` to stop a preset while keeping it stored, `reset` to reset | yes |
+| `scene` | for action | yes, for actions `save`, `play`, `stop` |
+| `universe` | if only one universe is handled, this parameter is mandatory and contains the used universe | only for a single universe |
+| `payload` | contains the values to record. it might be an array (key 0-511) containing the values for a single universe,
an object (keys 1-512) containing the values for a single universe or
an object (any numeric keys) containing objects (keys 1-512) containing an universe each. | yes, for action `save` |
#### Output for single universe:
@@ -115,6 +119,7 @@ Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node
| `payload` | Array containing the dmx values as **percentage** by dmx channel. DMX-Channel `1` starts at key `1`, not `0`. (`Array`) |
| `scene` | The scene that is played (`number`) |
| `reset` | Identifies a reset message for action `reset`, otherwise it does not exist. (`true`) |
+| `stopped` | Identifies the nulled message emitted for action `stop` (when `blackoutOnStop` is enabled), otherwise it does not exist. (`true`) |
#### Output for multiple universes:
@@ -124,6 +129,7 @@ Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node
| `payload` | Object containing one object per universe. DMX-Channel `1` starts at key `1`, not `0`. (`object>`) |
| `scene` | The scene that is played (`number`) |
| `reset` | Identifies a reset message for action `reset`, otherwise it does not exist. (`true`) |
+| `stopped` | Identifies the nulled message emitted for action `stop` (when `blackoutOnStop` is enabled), otherwise it does not exist. (`true`) |
## Protocol notes and limitations
diff --git a/dist/lib/dmx.js b/dist/lib/dmx.js
index 598c8f3..287e24e 100644
--- a/dist/lib/dmx.js
+++ b/dist/lib/dmx.js
@@ -1,6 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-exports.MAX_ABSOLUTE = exports.MAX_PERCENT = exports.LAST_UNIVERSE = exports.FIRST_UNIVERSE = exports.LAST_CHANNEL = exports.FIRST_CHANNEL = void 0;
+exports.MAX_ABSOLUTE =
+ exports.MAX_PERCENT =
+ exports.LAST_UNIVERSE =
+ exports.FIRST_UNIVERSE =
+ exports.LAST_CHANNEL =
+ exports.FIRST_CHANNEL =
+ void 0;
exports.maxValue = maxValue;
exports.fromPercent = fromPercent;
exports.nulledUniverse = nulledUniverse;
@@ -11,15 +17,15 @@ exports.LAST_UNIVERSE = 63999;
exports.MAX_PERCENT = 100;
exports.MAX_ABSOLUTE = 255;
function maxValue(scale) {
- return scale === "absolute" ? exports.MAX_ABSOLUTE : exports.MAX_PERCENT;
+ return scale === "absolute" ? exports.MAX_ABSOLUTE : exports.MAX_PERCENT;
}
function fromPercent(percent, scale) {
- return scale === "absolute" ? Math.round(percent * 2.55) : percent;
+ return scale === "absolute" ? Math.round(percent * 2.55) : percent;
}
function nulledUniverse() {
- const universe = {};
- for (let channel = exports.FIRST_CHANNEL; channel <= exports.LAST_CHANNEL; channel++) {
- universe[channel] = 0;
- }
- return universe;
+ const universe = {};
+ for (let channel = exports.FIRST_CHANNEL; channel <= exports.LAST_CHANNEL; channel++) {
+ universe[channel] = 0;
+ }
+ return universe;
}
diff --git a/dist/lib/interfaces.js b/dist/lib/interfaces.js
index df0c2e9..bcde03f 100644
--- a/dist/lib/interfaces.js
+++ b/dist/lib/interfaces.js
@@ -4,24 +4,24 @@ exports.listInterfaces = listInterfaces;
exports.registerInterfaceEndpoint = registerInterfaceEndpoint;
const node_os_1 = require("node:os");
function listInterfaces() {
- const result = [];
- const interfaces = (0, node_os_1.networkInterfaces)();
- for (const name of Object.keys(interfaces)) {
- for (const info of interfaces[name] ?? []) {
- if (info.family === "IPv4") {
- result.push({ name, address: info.address });
- }
- }
+ const result = [];
+ const interfaces = (0, node_os_1.networkInterfaces)();
+ for (const name of Object.keys(interfaces)) {
+ for (const info of interfaces[name] ?? []) {
+ if (info.family === "IPv4") {
+ result.push({ name, address: info.address });
+ }
}
- return result;
+ }
+ return result;
}
let registered = false;
function registerInterfaceEndpoint(RED) {
- if (registered) {
- return;
- }
- registered = true;
- RED.httpAdmin.get("/sacn/interfaces", RED.auth.needsPermission("flows.read"), (_req, res) => {
- res.json(listInterfaces());
- });
+ if (registered) {
+ return;
+ }
+ registered = true;
+ RED.httpAdmin.get("/sacn/interfaces", RED.auth.needsPermission("flows.read"), (_req, res) => {
+ res.json(listInterfaces());
+ });
}
diff --git a/dist/lib/network.js b/dist/lib/network.js
index e622fea..266b18a 100644
--- a/dist/lib/network.js
+++ b/dist/lib/network.js
@@ -5,11 +5,11 @@ exports.resolveNetworkOptions = resolveNetworkOptions;
const node_net_1 = require("node:net");
exports.DEFAULT_PORT = 5568;
function resolveNetworkOptions(config) {
- const options = {
- port: config.port !== undefined && config.port > 0 ? config.port : exports.DEFAULT_PORT,
- };
- if (config.interface !== undefined && (0, node_net_1.isIP)(config.interface) === 4) {
- options.iface = config.interface;
- }
- return options;
+ const options = {
+ port: config.port !== undefined && config.port > 0 ? config.port : exports.DEFAULT_PORT,
+ };
+ if (config.interface !== undefined && (0, node_net_1.isIP)(config.interface) === 4) {
+ options.iface = config.interface;
+ }
+ return options;
}
diff --git a/dist/lib/scene-store.js b/dist/lib/scene-store.js
index 70d7770..8ca7c56 100644
--- a/dist/lib/scene-store.js
+++ b/dist/lib/scene-store.js
@@ -4,25 +4,24 @@ exports.SceneStore = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
class SceneStore {
- file;
- constructor(baseDir, nodeId) {
- const dir = (0, node_path_1.join)(baseDir, "sacn-scenes");
- (0, node_fs_1.mkdirSync)(dir, { recursive: true });
- this.file = (0, node_path_1.join)(dir, `${nodeId}.json`);
+ file;
+ constructor(baseDir, nodeId) {
+ const dir = (0, node_path_1.join)(baseDir, "sacn-scenes");
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
+ this.file = (0, node_path_1.join)(dir, `${nodeId}.json`);
+ }
+ load() {
+ if (!(0, node_fs_1.existsSync)(this.file)) {
+ return {};
}
- load() {
- if (!(0, node_fs_1.existsSync)(this.file)) {
- return {};
- }
- try {
- return JSON.parse((0, node_fs_1.readFileSync)(this.file, "utf8"));
- }
- catch {
- return {};
- }
- }
- save(scenes) {
- (0, node_fs_1.writeFileSync)(this.file, JSON.stringify(scenes));
+ try {
+ return JSON.parse((0, node_fs_1.readFileSync)(this.file, "utf8"));
+ } catch {
+ return {};
}
+ }
+ save(scenes) {
+ (0, node_fs_1.writeFileSync)(this.file, JSON.stringify(scenes));
+ }
}
exports.SceneStore = SceneStore;
diff --git a/dist/nodes/sacn-in/sacn-in.js b/dist/nodes/sacn-in/sacn-in.js
index 9ca2791..8b35b96 100644
--- a/dist/nodes/sacn-in/sacn-in.js
+++ b/dist/nodes/sacn-in/sacn-in.js
@@ -5,183 +5,182 @@ const dmx_1 = require("../../lib/dmx");
const network_1 = require("../../lib/network");
const interfaces_1 = require("../../lib/interfaces");
class NodeHandler {
- node;
- config;
- data = new Map();
- sACN;
- currentUniverse;
- trigger;
- interval;
- scale;
- keepaliveTimer;
- constructor(node, config) {
- this.node = node;
- this.config = config;
- this.currentUniverse = config.universe;
- this.scale = config.values ?? "percent";
- this.trigger = config.trigger ?? (config.mode === "passthrough" ? "always" : "changes");
- this.interval = config.interval !== undefined && config.interval > 0 ? config.interval : 1000;
- const network = (0, network_1.resolveNetworkOptions)(config);
- const options = {
- universes: [config.universe],
- reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true,
- port: network.port,
- };
- if (network.iface !== undefined) {
- options.iface = network.iface;
- }
- switch (config.mode) {
- case "passthrough":
- this.sACN = new sacn_1.Receiver(options);
- break;
- case "htp":
- case "ltp":
- options.mode = config.mode.toUpperCase();
- this.sACN = new sacn_1.unstable_MergingReceiver(options);
- break;
- default:
- throw new Error("[node-red-sacn] None or invalid mode selected.");
- }
- this.sACN.on("error", (err) => {
- this.node.error(err);
- this.node.status({ fill: "red", shape: "dot", text: err.message || "receiver error" });
- });
- 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) => {
- const { payload, changed } = this.applyFrame(packet.payload, packet.universe);
- if (this.trigger === "always" || changed) {
- this.sendData({
- universe: packet.universe,
- payload,
- sequence: packet.sequence,
- source: packet.sourceAddress,
- priority: packet.priority,
- });
- }
- });
+ node;
+ config;
+ data = new Map();
+ sACN;
+ currentUniverse;
+ trigger;
+ interval;
+ scale;
+ keepaliveTimer;
+ constructor(node, config) {
+ this.node = node;
+ this.config = config;
+ this.currentUniverse = config.universe;
+ this.scale = config.values ?? "percent";
+ this.trigger = config.trigger ?? (config.mode === "passthrough" ? "always" : "changes");
+ this.interval = config.interval !== undefined && config.interval > 0 ? config.interval : 1000;
+ const network = (0, network_1.resolveNetworkOptions)(config);
+ const options = {
+ universes: [config.universe],
+ reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true,
+ port: network.port,
+ };
+ if (network.iface !== undefined) {
+ options.iface = network.iface;
+ }
+ switch (config.mode) {
+ case "passthrough":
+ this.sACN = new sacn_1.Receiver(options);
+ break;
+ case "htp":
+ case "ltp":
+ options.mode = config.mode.toUpperCase();
+ this.sACN = new sacn_1.unstable_MergingReceiver(options);
+ break;
+ default:
+ throw new Error("[node-red-sacn] None or invalid mode selected.");
+ }
+ this.sACN.on("error", (err) => {
+ this.node.error(err);
+ this.node.status({ fill: "red", shape: "dot", text: err.message || "receiver error" });
+ });
+ 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) => {
+ const { payload, changed } = this.applyFrame(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 {
- this.sACN.on("changed", (data) => {
- const { payload } = this.applyFrame(data.payload, data.universe);
- if (this.trigger !== "always") {
- this.sendData({
- universe: data.universe,
- payload,
- });
- }
- });
- if (this.trigger === "always") {
- this.sACN.on("packet", () => {
- this.emitFull(this.currentUniverse);
- });
- }
+ });
+ } else {
+ this.sACN.on("changed", (data) => {
+ const { payload } = this.applyFrame(data.payload, data.universe);
+ if (this.trigger !== "always") {
+ this.sendData({
+ universe: data.universe,
+ payload,
+ });
}
- this.node.on("input", (msg) => {
- this.handleUniverseChange(msg);
+ });
+ if (this.trigger === "always") {
+ this.sACN.on("packet", () => {
+ this.emitFull(this.currentUniverse);
});
- this.setStatus();
- this.resetKeepalive();
+ }
}
- setStatus() {
- this.node.status({
- fill: "green",
- shape: "dot",
- text: `Universe ${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;
}
- 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;
+ 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;
}
- 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, (0, dmx_1.nulledUniverse)());
- this.emitFull(universe);
- }
- else {
- this.resetKeepalive();
- }
+ if (universe === this.currentUniverse) {
+ return;
}
- applyFrame(incoming, universe) {
- const previous = this.data?.get(universe);
- const full = (0, dmx_1.nulledUniverse)();
- Object.keys(incoming).forEach((key) => {
- const ch = parseInt(key, 10);
- if (ch >= 1 && ch <= 512) {
- full[ch] = (0, dmx_1.fromPercent)(incoming[ch], this.scale);
- }
- });
- let changed = false;
- const changes = {};
- for (let ch = 1; ch <= 512; ch++) {
- const before = previous ? previous[ch] : 0;
- if (before !== full[ch]) {
- changed = true;
- changes[ch] = full[ch];
- }
- }
- this.data?.set(universe, full);
- const payload = this.config.output === "changes" ? changes : full;
- return { payload, changed };
+ 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, (0, dmx_1.nulledUniverse)());
+ this.emitFull(universe);
+ } else {
+ this.resetKeepalive();
}
- sendData(msg) {
- if (msg.payload && typeof msg.payload === "object") {
- msg = { ...msg, payload: { ...msg.payload } };
- }
- this.node.send(msg);
- this.resetKeepalive();
+ }
+ applyFrame(incoming, universe) {
+ const previous = this.data?.get(universe);
+ const full = (0, dmx_1.nulledUniverse)();
+ Object.keys(incoming).forEach((key) => {
+ const ch = parseInt(key, 10);
+ if (ch >= 1 && ch <= 512) {
+ full[ch] = (0, dmx_1.fromPercent)(incoming[ch], this.scale);
+ }
+ });
+ let changed = false;
+ const changes = {};
+ for (let ch = 1; ch <= 512; ch++) {
+ const before = previous ? previous[ch] : 0;
+ if (before !== full[ch]) {
+ changed = true;
+ changes[ch] = full[ch];
+ }
}
- emitFull(universe) {
- const full = this.data?.get(universe) ?? (0, dmx_1.nulledUniverse)();
- this.sendData({ universe, payload: full });
+ this.data?.set(universe, full);
+ const payload = this.config.output === "changes" ? changes : full;
+ return { payload, changed };
+ }
+ sendData(msg) {
+ if (msg.payload && typeof msg.payload === "object") {
+ msg = { ...msg, payload: { ...msg.payload } };
}
- resetKeepalive() {
- if (this.trigger !== "interval") {
- return;
- }
- if (this.keepaliveTimer) {
- clearTimeout(this.keepaliveTimer);
- }
- this.keepaliveTimer = setTimeout(() => {
- this.keepaliveTick();
- }, this.interval);
+ this.node.send(msg);
+ this.resetKeepalive();
+ }
+ emitFull(universe) {
+ const full = this.data?.get(universe) ?? (0, dmx_1.nulledUniverse)();
+ this.sendData({ universe, payload: full });
+ }
+ resetKeepalive() {
+ if (this.trigger !== "interval") {
+ return;
}
- keepaliveTick() {
- if (this.data?.has(this.currentUniverse)) {
- this.emitFull(this.currentUniverse);
- }
- else {
- this.resetKeepalive();
- }
+ 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();
}
+ }
}
exports.default = (RED) => {
- (0, interfaces_1.registerInterfaceEndpoint)(RED);
- RED.nodes.registerType("sacn-in", function (config) {
- RED.nodes.createNode(this, config);
- new NodeHandler(this, config);
- });
+ (0, interfaces_1.registerInterfaceEndpoint)(RED);
+ RED.nodes.registerType("sacn-in", function (config) {
+ RED.nodes.createNode(this, config);
+ new NodeHandler(this, config);
+ });
};
diff --git a/dist/nodes/sacn-out/sacn-out.js b/dist/nodes/sacn-out/sacn-out.js
index da96859..0db05b5 100644
--- a/dist/nodes/sacn-out/sacn-out.js
+++ b/dist/nodes/sacn-out/sacn-out.js
@@ -5,80 +5,79 @@ const dmx_1 = require("../../lib/dmx");
const network_1 = require("../../lib/network");
const interfaces_1 = require("../../lib/interfaces");
class NodeHandler {
- node;
- config;
- sACN;
- options;
- constructor(node, config) {
- this.node = node;
- this.config = config;
- const network = (0, network_1.resolveNetworkOptions)(config);
- this.options = {
- universe: config.universe,
- reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true,
- minRefreshRate: config.speed !== undefined ? config.speed : 0,
- port: network.port,
- defaultPacketOptions: {
- useRawDmxValues: (config.values ?? "percent") === "absolute",
- },
- };
- if (network.iface !== undefined) {
- this.options.iface = network.iface;
- }
- this.sACN = new sacn_1.Sender(this.options);
- this.sACN.on("error", (err) => {
- this.node.error(err);
- this.node.status({ fill: "red", shape: "dot", text: err.message || "sender error" });
- });
- this.node.on("close", (done) => {
- const shutdown = () => {
- this.sACN.close();
- done();
- };
- if (this.config.blankOnClose) {
- this.sACN
- .send({
- payload: (0, dmx_1.nulledUniverse)(),
- sourceName: config.sourceName,
- priority: config.priority || 100,
- })
- .then(shutdown)
- .catch(shutdown);
- }
- else {
- shutdown();
- }
- });
- this.node.on("input", (msg) => {
- this.sACN
- .send({
- payload: msg.payload,
- sourceName: config.sourceName,
- priority: config.priority || 100,
- })
- .then(() => {
- this.setStatus();
- })
- .catch((err) => {
- this.node.error(err, msg);
- this.node.status({ fill: "red", shape: "dot", text: err.message || "send failed" });
- });
- });
- this.setStatus();
+ node;
+ config;
+ sACN;
+ options;
+ constructor(node, config) {
+ this.node = node;
+ this.config = config;
+ const network = (0, network_1.resolveNetworkOptions)(config);
+ this.options = {
+ universe: config.universe,
+ reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true,
+ minRefreshRate: config.speed !== undefined ? config.speed : 0,
+ port: network.port,
+ defaultPacketOptions: {
+ useRawDmxValues: (config.values ?? "percent") === "absolute",
+ },
+ };
+ if (network.iface !== undefined) {
+ this.options.iface = network.iface;
}
- setStatus() {
- const rate = this.config.speed !== undefined && this.config.speed > 0 ? `${this.config.speed} Hz` : "once";
- this.node.status({
- fill: "green",
- shape: "dot",
- text: `Universe ${this.config.universe} · ${rate}`,
+ this.sACN = new sacn_1.Sender(this.options);
+ this.sACN.on("error", (err) => {
+ this.node.error(err);
+ this.node.status({ fill: "red", shape: "dot", text: err.message || "sender error" });
+ });
+ this.node.on("close", (done) => {
+ const shutdown = () => {
+ this.sACN.close();
+ done();
+ };
+ if (this.config.blankOnClose) {
+ this.sACN
+ .send({
+ payload: (0, dmx_1.nulledUniverse)(),
+ sourceName: config.sourceName,
+ priority: config.priority || 100,
+ })
+ .then(shutdown)
+ .catch(shutdown);
+ } else {
+ shutdown();
+ }
+ });
+ this.node.on("input", (msg) => {
+ this.sACN
+ .send({
+ payload: msg.payload,
+ sourceName: config.sourceName,
+ priority: config.priority || 100,
+ })
+ .then(() => {
+ this.setStatus();
+ })
+ .catch((err) => {
+ this.node.error(err, msg);
+ this.node.status({ fill: "red", shape: "dot", text: err.message || "send failed" });
});
- }
+ });
+ this.setStatus();
+ }
+ setStatus() {
+ const rate = this.config.speed !== undefined && this.config.speed > 0 ? `${this.config.speed} Hz` : "once";
+ this.node.status({
+ fill: "green",
+ shape: "dot",
+ text: `Universe ${this.config.universe} · ${rate}`,
+ });
+ }
}
exports.default = (RED) => {
- (0, interfaces_1.registerInterfaceEndpoint)(RED);
- RED.nodes.registerType("sacn-out", function (config) {
- RED.nodes.createNode(this, config);
- new NodeHandler(this, config);
- });
+ (0, interfaces_1.registerInterfaceEndpoint)(RED);
+ RED.nodes.registerType("sacn-out", function (config) {
+ RED.nodes.createNode(this, config);
+ new NodeHandler(this, config);
+ });
};
diff --git a/dist/nodes/scene-controller/locales/de/scene-controller.json b/dist/nodes/scene-controller/locales/de/scene-controller.json
index dbd233b..d4702f5 100644
--- a/dist/nodes/scene-controller/locales/de/scene-controller.json
+++ b/dist/nodes/scene-controller/locales/de/scene-controller.json
@@ -3,7 +3,11 @@
"label": {
"values": "Werte",
"values_percent": "Prozent (0–100)",
- "values_absolute": "Absolut (0–255)"
+ "values_absolute": "Absolut (0–255)",
+ "playMode": "Wiedergabe",
+ "playMode_switch": "Umschalten (neue Szene ersetzt)",
+ "playMode_add": "Addieren (Szenen stapeln, HTP)",
+ "blackoutOnStop": "Beim Stoppen Nullwerte senden"
}
}
}
\ No newline at end of file
diff --git a/dist/nodes/scene-controller/locales/en-US/scene-controller.json b/dist/nodes/scene-controller/locales/en-US/scene-controller.json
index 579358a..7d1498c 100644
--- a/dist/nodes/scene-controller/locales/en-US/scene-controller.json
+++ b/dist/nodes/scene-controller/locales/en-US/scene-controller.json
@@ -3,7 +3,11 @@
"label": {
"values": "values",
"values_percent": "percent (0–100)",
- "values_absolute": "absolute (0–255)"
+ "values_absolute": "absolute (0–255)",
+ "playMode": "Playback",
+ "playMode_switch": "Switch (new scene replaces)",
+ "playMode_add": "Add (stack scenes, HTP)",
+ "blackoutOnStop": "Send zero values on stop"
}
}
}
\ No newline at end of file
diff --git a/dist/nodes/scene-controller/scene-controller.html b/dist/nodes/scene-controller/scene-controller.html
index 812dccd..d287419 100644
--- a/dist/nodes/scene-controller/scene-controller.html
+++ b/dist/nodes/scene-controller/scene-controller.html
@@ -16,6 +16,23 @@
+
+
+
+
+
+
+
+