Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c0d503e
Rebuild the received universe from a zeroed base in sacn-in
mutec Jul 12, 2026
cdf7c07
Emit copies of the cached universe state from sacn-in
mutec Jul 12, 2026
e2e1195
Handle receiver and sender socket errors without crashing
mutec Jul 12, 2026
4be255e
Show the sending status on the sACN out node
mutec Jul 12, 2026
a7e165d
Catch send failures on the sACN out node
mutec Jul 12, 2026
4a62345
Use the official E1.31 universe range in the scene controller
mutec Jul 12, 2026
1c3a8f8
Give the in and out nodes meaningful default labels
mutec Jul 12, 2026
c575601
Align the sACN in form defaults with the node definition
mutec Jul 12, 2026
22450dc
Install dependencies with npm ci in the CI workflows
mutec Jul 12, 2026
47f15e9
Gate the release on quality checks and pin the JSON action
mutec Jul 12, 2026
434e5e4
Fix spelling mistakes in the readme and out node label
mutec Jul 12, 2026
f2959c9
Document the new receiver options and runtime universe switch
mutec Jul 12, 2026
1549dac
Document the protocol limitations in the readme
mutec Jul 12, 2026
78df22e
Add safety and legal notices to the readme
mutec Jul 12, 2026
0514703
Add an option to blank the sACN out universe on stop
mutec Jul 12, 2026
a297000
Extract shared DMX and network helpers to remove duplication
mutec Jul 12, 2026
d0b051c
Make the value scale selectable between percent and absolute
mutec Jul 12, 2026
8dd0e3f
Offer available network interfaces and validate addresses
mutec Jul 12, 2026
c22291d
Persist recorded scenes to disk
mutec Jul 12, 2026
ff8af6b
Ship dedicated node icons
mutec Jul 12, 2026
89794bd
Re-enable the strict no-unsafe lint rules
mutec Jul 12, 2026
857e50f
Document the new node options in the readme
mutec Jul 12, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/codestyle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
with:
node-version: "24"
cache: "npm"
- run: npm install
- run: npm ci
- name: Run prettier
run: |
shopt -s globstar
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ jobs:
node-version: "24"
registry-url: "https://registry.npmjs.com/"
- run: npm ci
- name: Type-check
run: npx tsc --noEmit
- name: Lint
run: npx eslint .
- name: Build
run: npm run build
- name: Read package.json
id: package
uses: RadovanPelka/github-action-json@main
# pinned to a commit to avoid pulling an unreviewed moving ref
uses: RadovanPelka/github-action-json@c1e93581d313d6e1d56f7f67582d4051674ebc57 # main
with:
path: "package.json"
- name: Remove old builds
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/typescript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm install
- run: npm ci
- run: |
npx tsc --noEmit
eslint:
Expand All @@ -40,5 +40,5 @@ jobs:
with:
node-version: "24"
cache: "npm"
- run: npm install
- run: npm ci
- run: npx eslint .
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ for [Node-RED](https://nodered.org).

## Requirements

Required version of Node-RED: v4.0.5
- Node-RED `>= 4.0.0`
- Node.js `>= 24.0.0`

This package requires [`sacn`](https://www.npmjs.com/package/sacn) as library to interact by sACN.

Expand All @@ -25,18 +26,26 @@ Copyright MysteryCode and other contributors under [GNU GENERAL PUBLIC LICENSE V

### sACN in

This node can be used to read one or multiple universes send by sACN.
This node can be used to read one or multiple universes sent by sACN.

#### Parameters:

| Paremeter | Description | Possible Values | Default Value | Mandatory |
| Parameter | Description | Possible Values | Default Value | Mandatory |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------- | --------- |
| universe | The universe that is meant to be observed. | `\d+` (`1` to `63999`) | `1` | yes |
| mode | Defines whether the node returns the values of every read sACN package (passthrough mode), or merged values using HTP or LTP. | `passthrough`, `htp`, `ltp` | `htp` | yes |
| output | Defines wether the node sends only changed values or the whole universe. | `full`, `changes` | `full` | yes |
| output | Defines whether the node sends only changed values or the whole universe. | `full`, `changes` | `full` | yes |
| trigger | Controls when a message is emitted: only on change, on every received packet, or on change plus a cyclic keepalive re-emit. | `changes`, `always`, `interval` | `changes` | yes |
| interval | Keepalive interval in milliseconds for the `interval` trigger; the full universe is re-emitted when no change arrives in time. | `\d+` | `1000` | no |
| clearOnUniverseChange | When the observed universe is switched at runtime, emit a full universe of zeros until real data for the new universe arrives. | `true`, `false` | `false` | no |
| values | Emit channel values as a percentage (0–100) or as raw DMX values (0–255). | `percent`, `absolute` | `percent` | yes |
| IP-address | IP-Address of the network-interface that should be used for reading from sACN. | `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\` (_any valid ip-address_) | _empty_ | no |
| port | The network port which should be used for reading sACN. | `\d+` | _empty_ (defaults to `5568`) | no |

#### Input:

The observed universe can be switched at runtime by sending a message with a `universe` property (`1` to `63999`). The node stops listening on the previous universe and starts listening on the new one; invalid values are ignored and produce a warning.

#### Output for direct-mode:

| Property | Description |
Expand All @@ -60,12 +69,14 @@ This node can be used to send one universe using sACN.

#### Parameters:

| Paremeter | Description | Possible Values | Default Value | Mandatory |
| Parameter | Description | Possible Values | Default Value | Mandatory |
| ----------- | ------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------- | --------- |
| universe | The universe that is meant to be observed. | `\d+` (`1` to `63999`) | `1` | yes |
| source-name | The name for the sACN-sender that should be displayed within the network. | _any string below 50 characters_ | `Node-RED` | yes |
| speed | Defines the frequency for sending sACN-packages | `once (0Hz)`, `24Hz`, `27Hz`, `30Hz`, `40Hz`, `44Hz` | `0Hz` | yes |
| priority | The priority that should be used for the sACN-sender. | `\d+` (`1` to `200`) | `100` | yes |
| values | Interpret the payload as a percentage (0–100) or as raw DMX values (0–255). | `percent`, `absolute` | `percent` | yes |
| blank-on-stop | Send an all-zero frame before closing so receivers go dark on stop. | `true`, `false` | `false` | no |
| IP-address | IP-Address of the network-interface that should be used for sending sACN. | `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\` (_any valid ip-address_) | _empty_ | no |
| port | The network port which should be used for reading sACN. | `\d+` | _empty_ (defaults to `5568`) | no |

Expand All @@ -81,16 +92,19 @@ This node can be used to record scenes and play them afterwards.

#### Parameters:

| Paremeter | Description | Possible Values | Default Value | Mandatory |
| --------- | ----------- | --------------- | ------------- | --------- |
| 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 |

Recorded scenes are stored on disk (in a `sacn-scenes` directory inside the Node-RED user directory) and survive a restart.

#### 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 | yes |
| `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 @@ -110,3 +124,23 @@ This node can be used to record scenes and play them afterwards.
| `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`) |

## Protocol notes and limitations

This package builds on the [`sacn`](https://www.npmjs.com/package/sacn) library and inherits its scope. Be aware of the following when integrating it:

- **No Universe Discovery.** The sender does not emit E1.31 Universe Discovery packets, so receivers relying on discovery will not see this source listed automatically.
- **No synchronization.** E1.31 universe synchronization (synchronized multi-universe updates) is not implemented.
- **No stream termination.** When a sender node is stopped or redeployed it simply closes its socket; it does not send packets with the `Stream_Terminated` flag. Receivers therefore hold the last received values until their own signal-loss timeout (typically ~2.5 s) elapses.
- **DMX values are percentages.** Channel values are expressed as a percentage (`0`–`100`, with up to two decimals) rather than as raw 8-bit values (`0`–`255`).

## Safety and legal notice

This package is network/protocol software for controlling DMX/sACN lighting. It is **not** a certified safety system, and sACN (E1.31) is an unauthenticated, best-effort protocol with no delivery guarantees. Operate it on a dedicated, segmented lighting network. Please observe the following before deploying it:

- **No safety or emergency lighting.** Do not use this package to control safety, escape-route or emergency lighting. Such installations require certified, monitored systems (e.g. DE: DIN VDE 0108-100, DIN EN 1838; AT: TRVB E 102, ÖVE/ÖNORM E 8002; CH: SN EN 1838 and the VKF fire-protection guidelines).
- **No functional machine safety.** sACN/DMX is not a safety bus. Do not use it for the functional safety of machinery, kinetics, hoists or moving stage equipment (cf. Machinery Directive 2006/42/EC / Regulation (EU) 2023/1230, DIN 56950, EN ISO 13849, DGUV V3).
- **Strobe / photosensitivity.** The software can drive arbitrary strobe and flashing effects. Operators are responsible for protecting audiences and staff from photosensitive-epilepsy and glare hazards (e.g. DE: VStättVO, DGUV Information 215-313).
- **Lasers.** If DMX is used to control laser sources, the applicable laser-safety rules apply (e.g. DIN EN 60825; DE: OStrV / TROS Laserstrahlung, incl. an appointed laser safety officer).

This is not legal advice. Responsibility for compliance, CE conformity of the controlled hardware and safe operation rests with the integrator and operator.
25 changes: 25 additions & 0 deletions dist/lib/dmx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"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.maxValue = maxValue;
exports.fromPercent = fromPercent;
exports.nulledUniverse = nulledUniverse;
exports.FIRST_CHANNEL = 1;
exports.LAST_CHANNEL = 512;
exports.FIRST_UNIVERSE = 1;
exports.LAST_UNIVERSE = 63999;
exports.MAX_PERCENT = 100;
exports.MAX_ABSOLUTE = 255;
function maxValue(scale) {
return scale === "absolute" ? exports.MAX_ABSOLUTE : exports.MAX_PERCENT;
}
function fromPercent(percent, scale) {
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;
}
27 changes: 27 additions & 0 deletions dist/lib/interfaces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
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 });
}
}
}
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());
});
}
15 changes: 15 additions & 0 deletions dist/lib/network.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_PORT = void 0;
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;
}
28 changes: 28 additions & 0 deletions dist/lib/scene-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
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`);
}
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));
}
}
exports.SceneStore = SceneStore;
5 changes: 4 additions & 1 deletion dist/nodes/sacn-in/locales/de/sacn-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"trigger_always": "Jedes Paket",
"trigger_interval": "Änderungen + zyklisch",
"interval": "Intervall (ms)",
"clearOnUniverseChange": "Bei Universumswechsel auf 0 setzen"
"clearOnUniverseChange": "Bei Universumswechsel auf 0 setzen",
"values": "Werte",
"values_percent": "Prozent (0–100)",
"values_absolute": "Absolut (0–255)"
}
}
}
5 changes: 4 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 @@ -16,7 +16,10 @@
"trigger_always": "every packet",
"trigger_interval": "changes + cyclic",
"interval": "interval (ms)",
"clearOnUniverseChange": "blank on universe change"
"clearOnUniverseChange": "blank on universe change",
"values": "values",
"values_percent": "percent (0–100)",
"values_absolute": "absolute (0–255)"
}
}
}
Loading
Loading