UNPKG

@mysterycode/node-red-sacn

Version:

Implementation of sACN (Streaming ACN) for Node-RED

147 lines (121 loc) 4.02 kB
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); }); };