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
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,<br/>an object (keys 1-512) containing the values for a single universe or<br/>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,<br/>an object (keys 1-512) containing the values for a single universe or<br/>an object (any numeric keys) containing objects (keys 1-512) containing an universe each. | yes, for action `save` |

#### Output for single universe:

Expand All @@ -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<number, number>`) |
| `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:

Expand All @@ -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<number, object<number, number>>`) |
| `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

Expand Down
22 changes: 14 additions & 8 deletions dist/lib/dmx.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
32 changes: 16 additions & 16 deletions dist/lib/interfaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
}
14 changes: 7 additions & 7 deletions dist/lib/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
35 changes: 17 additions & 18 deletions dist/lib/scene-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading
Loading