@mysterycode/node-red-sacn
Version:
Implementation of sACN (Streaming ACN) for Node-RED
147 lines (121 loc) • 4.02 kB
text/typescript
import { Node, NodeAPI, NodeDef } from "node-red";
import { NodeMessage } from "@node-red/registry";
import { Packet, Receiver, unstable_MergingReceiver as MergingReceiver } from "sacn";
interface DMXValues {
[key: number]: number | undefined;
}
interface Config extends NodeDef {
universe: number;
reuseAddress?: boolean;
interface?: string;
port?: number;
mode: "htp" | "ltp" | "passthrough";
output: "full" | "changes";
}
export interface MessageIn extends NodeMessage {
universe: number;
payload: number[];
}
export interface DirectMessageIn extends MessageIn {
sequence: number;
source: string;
priority: number;
}
class NodeHandler {
protected node: Node<Config>;
protected config: Config;
protected data: Map<number, DMXValues> | undefined = new Map();
protected sACN: Receiver | MergingReceiver;
constructor(node: Node<Config>, config: Config) {
this.node = node;
this.config = config;
// parse options for receiver instance
const options: Receiver.Props | MergingReceiver.Props = {
universes: [config.universe],
reuseAddr: config.reuseAddress !== undefined ? config.reuseAddress : true,
};
if (config.interface !== undefined && config.interface.length > 7) {
options.iface = config.interface;
}
if (config.port !== undefined && config.port > 0) {
options.port = config.port;
} else {
options.port = 5568;
}
// init sacn receiver
switch (config.mode) {
case "passthrough":
this.sACN = new Receiver(options);
break;
case "htp":
case "ltp":
(options as MergingReceiver.Props).mode = config.mode.toUpperCase() as "HTP" | "LTP";
this.sACN = new MergingReceiver(options);
break;
default:
throw new Error("[node-red-sacn] None or invalid mode selected.");
}
// run cleanup when node is closed
this.node.on("close", () => {
// close all connections; terminate the receiver
this.sACN.close();
this.data = new Map();
});
// handle sacn packets
if (config.mode === "passthrough") {
(this.sACN as Receiver).on("packet", (packet: Packet) => {
this.node.send({
universe: packet.universe,
payload: this.parsePayload(packet.payload, packet.universe),
sequence: packet.sequence,
source: packet.sourceAddress,
priority: packet.priority,
} as DirectMessageIn);
});
} else if (config.mode === "ltp") {
(this.sACN as MergingReceiver).on("changed", (data) => {
this.node.send({
universe: data.universe,
payload: this.parsePayload(data.payload, data.universe),
} as MessageIn);
});
} else if (config.mode === "htp") {
(this.sACN as MergingReceiver).on("changed", (data) => {
this.node.send({
universe: data.universe,
payload: this.parsePayload(data.payload, data.universe),
} as MessageIn);
});
}
}
protected getNulledUniverse(): DMXValues {
const universe: DMXValues = {};
for (let ch = 1; ch <= 512; ch++) {
universe[ch] = 0;
}
return universe;
}
protected getReference(universe: number): DMXValues {
if (this.config.output === "changes") {
return {} as DMXValues;
}
return this.data?.get(universe) ?? this.getNulledUniverse();
}
protected parsePayload(payload: DMXValues, universe: number): DMXValues {
const processedPayload: DMXValues = this.getReference(universe);
Object.keys(payload).forEach((key) => {
const ch = parseInt(key, 10);
processedPayload[ch] = payload[ch];
});
if (this.config.output !== "changes") {
this.data?.set(universe, processedPayload);
}
return processedPayload;
}
}
export default (RED: NodeAPI): void => {
RED.nodes.registerType("sacn-in", function (this: Node<Config>, config: Config) {
RED.nodes.createNode(this, config);
new NodeHandler(this, config);
});
};