├── .gitignore ├── LICENSE.txt ├── README.md ├── index.js ├── package.json ├── src ├── Packet.js ├── Receiver.js └── Sender.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔌stagehack-sACN 2 | Simple library for sending and receiving [sACN (E1.31)](https://tsp.esta.org/tsp/documents/docs/ANSI_E1-31-2018.pdf) lighting data. It is not the full E1.31 protocol, but should be close enough for most projects. 3 | 4 | 5 | **This library supports** 6 | * Multiple network interfaces 7 | * Multicast and Unicast 8 | * ETC Net4 (coming soon) 9 | 10 | 11 | ## Installation 12 | `npm install stagehack-sacn` 13 | 14 | 15 | 16 | # Sender 17 | ```javascript 18 | const ACNSender = require('stagehack-sACN').Sender; 19 | ACNSender.Start([options]); 20 | var universe = new ACNSender.Universe([universe], [priority]); 21 | ``` 22 | ### Sender Options: 23 | * `interfaces`: Array of IPv4 network interfaces on the device to send from. ex: `['192.168.0.40, 10.0.0.5']` 24 | * `cid`: 16-character UUID to represent this device. ex: `"036b2d4932174812"` 25 | * `source`: Plaintext name of this device. ex: `"Tim's MacBook Pro"` 26 | * `type`: `"unicast"` or `"multicast"`. Defaults to `"multicast"` if neither is provided 27 | 28 | ### Universe Options: 29 | * `universe`: Default: `1` 30 | * `priority`: Default: `100` 31 | 32 | ### Example: 33 | ```javascript 34 | const ACNSender = require('stagehack-sACN').Sender; 35 | ACNSender.Start({ 36 | interfaces: ["192.168.0.40"] 37 | }); 38 | 39 | var sender = new ACNSender.Universe(1, 100); 40 | sender.on("ready", function(){ 41 | // send as an array 42 | this.send([255, 0, 0, 255]); 43 | 44 | // or send as key-value pairs 45 | this.send({ 46 | 4: 255, 47 | 11: 150, 48 | 301: 155 49 | }); 50 | }); 51 | ``` 52 | 53 | Sender also provides `sender.getPossibleInterfaces()` which returns a list of all IPv4 network interfaces on the device. Useful for populating a dropdown or other UI. 54 | 55 | 56 | # Receiver 57 | ```javascript 58 | const ACNReceiver = require('stagehack-sACN').Receiver; 59 | ACNReceiver.Start(); 60 | var universe = new ACNReceiver.Universe([universe]); 61 | ``` 62 | ### Universe Options: 63 | * `universe`: Default: `1` 64 | 65 | ### Example: 66 | ```javascript 67 | const ACNReceiver = require('./../stagehack-sACN').Receiver; 68 | ACNReceiver.Start(); 69 | 70 | var receiver = new ACNReceiver.Universe(1); 71 | receiver.on("packet", function(packet){ 72 | console.log(packet.getSlots()); 73 | }); 74 | ``` 75 | 76 | 77 | 78 | # Packet 79 | Setters: 80 | * `setUniverse`: sets the Universe 81 | * `setPriority`: sets the Priority 82 | * `setCID`: sets the CID 83 | * `setSource`: sets the Source 84 | * `setSlots`: sets the Slots 85 | 86 | Getters: 87 | * `getUniverse`: gets the Universe 88 | * `getPriority`: gets the Priority 89 | * `getCID`: gets the CID 90 | * `getSource`: gets the Source 91 | * `getSlots`: gets current Slots (length 1-512) 92 | * `getSequence`: gets current Sequence 93 | * `getBuffer`: returns a Buffer of the complete sACN Packet 94 | 95 | 96 | # TODO 97 | * Add "allReady" event for when all Senders/Receivers are ready 98 | * Implement Net4 99 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Sender = require('./src/Sender'); 2 | module.exports.Receiver = require('./src/Receiver'); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stagehack-sacn", 3 | "version": "1.0.7", 4 | "description": "Library for sending and receiving sACN (E1.31) lighting data", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "keywords": [ 10 | "sACN", 11 | "e131", 12 | "dmx", 13 | "dmx512", 14 | "lighting", 15 | "enttec", 16 | "dmxking" 17 | ], 18 | "author": "Alec Sparks", 19 | "license": "WTFPL", 20 | "repository": "https://github.com/sparks-alec/stagehack-sACN" 21 | } -------------------------------------------------------------------------------- /src/Packet.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | const ACN_PID = Buffer.from('ASC-E1.17'); 4 | const VECTOR_ROOT_E131_DATA = 0x00000004; 5 | const VECTOR_E131_DATA_PACKET = 0x00000002; 6 | const VECTOR_DMP_SET_PROPERTY = 0x02; 7 | 8 | function Packet(universe, priority, cid, source) { 9 | this._buffer = Buffer.alloc(0); 10 | this._rootLayer; 11 | this._framingLayer; 12 | this._DMPLayer; 13 | this._values; 14 | 15 | this._sequence = 0x00; 16 | this._universe = 1; 17 | this._priority = 100; 18 | this._startCode = 0x00; 19 | this._slots = []; 20 | this._cid = Buffer.from([ 21 | 0x29, 0x97, 0x97, 0x45, 0x80, 0x29, 0x97, 0x45, 0x80, 0x29, 0x97, 0x45, 22 | 0x80, 0x29, 0x97, 0x80 23 | ]); 24 | this._source = Buffer.from(os.hostname()); 25 | 26 | this.update(); 27 | } 28 | 29 | Packet.prototype.update = function () { 30 | this._rootLayer = Buffer.alloc(38); 31 | this._rootLayer.writeUInt16BE(0x0010, 0); // Preamble Size 32 | this._rootLayer.writeUInt16BE(0x0000, 2); // Post-amble Size 33 | ACN_PID.copy(this._rootLayer, 4); // ACN Packet Identifier 34 | this._rootLayer.writeUInt16BE(0x7000 | (110 + this._slots.length), 16); // Flags and Length 35 | this._rootLayer.writeUInt32BE(VECTOR_ROOT_E131_DATA, 18); // Vector 36 | this._cid.copy(this._rootLayer, 22); // CID 37 | 38 | this._framingLayer = Buffer.alloc(77); 39 | this._framingLayer.writeUInt16BE(0x7000 | (88 + this._slots.length), 0); // Flags and Length 40 | this._framingLayer.writeUInt32BE(VECTOR_E131_DATA_PACKET, 2); // Vector 41 | this._source.copy(this._framingLayer, 6); // Source Name 42 | this._framingLayer.writeUInt8(this._priority, 70); // Priority 43 | this._framingLayer.writeUInt16BE(0x0000, 71); // Synchronization Address 44 | this._framingLayer.writeUInt8(this._sequence, 73); // Sequence Number 45 | this._framingLayer.writeUInt8(0x00, 74); // Options 46 | this._framingLayer.writeUInt16BE(this._universe, 75); // Universe 47 | 48 | this._DMPLayer = Buffer.alloc(11); 49 | this._DMPLayer.writeUInt16BE(0x7000 | (11 + this._slots.length), 0); // Flags and Length 50 | this._DMPLayer.writeUInt8(VECTOR_DMP_SET_PROPERTY, 2); // Vector 51 | this._DMPLayer.writeUInt8(0xa1, 3); // Address Type & Data Type 52 | this._DMPLayer.writeUInt16BE(0x0000, 4); // First Property Address 53 | this._DMPLayer.writeUInt16BE(0x0001, 6); // Address Increment 54 | this._DMPLayer.writeUInt16BE(this._slots.length + 1, 8); // Property value count 55 | this._DMPLayer.writeUInt8(this._startCode, 10); // Start Code 56 | 57 | this._buffer = Buffer.concat([ 58 | this._rootLayer, 59 | this._framingLayer, 60 | this._DMPLayer, 61 | Buffer.from(this._slots) 62 | ]); 63 | }; 64 | 65 | Packet.prototype.setUniverse = function (universe) { 66 | this._universe = universe; 67 | this.update(); 68 | }; 69 | Packet.prototype.setPriority = function (priority) { 70 | this._priority = priority; 71 | this.update(); 72 | }; 73 | Packet.prototype.setCID = function (cid) { 74 | this._cid = Buffer.from(cid); 75 | this.update(); 76 | }; 77 | Packet.prototype.setSource = function (source) { 78 | this._source = Buffer.from(source); 79 | this.update(); 80 | }; 81 | Packet.prototype.setStartCode = function (code) { 82 | this._startCode = code; 83 | this.update(); 84 | }; 85 | Packet.prototype.setSlots = function (slots) { 86 | this._sequence++; 87 | if (this._sequence > 255) { 88 | this._sequence = 0; 89 | } 90 | 91 | this._slots = slots; 92 | this.update(); 93 | }; 94 | Packet.prototype.getBuffer = function () { 95 | this._framingLayer.writeUInt8(this._sequence, 73); 96 | this.update(); 97 | return this._buffer; 98 | }; 99 | 100 | Packet.prototype.getUniverse = function () { 101 | return this._framingLayer.readUInt16BE(75); 102 | }; 103 | Packet.prototype.getPriority = function () { 104 | return this._framingLayer.readUInt8(70); 105 | }; 106 | Packet.prototype.getCID = function () { 107 | return this._framingLayer 108 | .slice(6, this._framingLayer.indexOf(0x00, 6)) 109 | .toString(); 110 | }; 111 | Packet.prototype.getSource = function () { 112 | return this._framingLayer 113 | .slice(6, this._framingLayer.indexOf(0x00, 6)) 114 | .toString(); 115 | }; 116 | Packet.prototype.getSlots = function () { 117 | return this._slots; 118 | }; 119 | Packet.prototype.getSequence = function () { 120 | return this._framingLayer.readUInt8(73); 121 | }; 122 | 123 | Packet.prototype.fromBuffer = function (buf) { 124 | buf.copy(this._rootLayer, 0, 0, 38); 125 | buf.copy(this._framingLayer, 0, 38, 115); 126 | buf.copy(this._DMPLayer, 0, 115, 126); 127 | 128 | this._priority = this._framingLayer.readUInt8(70); 129 | this._sequence = this._framingLayer.readUInt8(73); 130 | this._universe = this._framingLayer.readUInt16BE(75); 131 | 132 | this._slots = Array.prototype.slice.call(buf, 126); 133 | }; 134 | Packet.prototype.toString = function () { 135 | return { 136 | Universe: this.getUniverse(), 137 | Priority: this.getPriority(), 138 | Sequence: this.getSequence(), 139 | Source: this.getSource(), 140 | Slots: this.getSlots().join(',') 141 | }; 142 | }; 143 | 144 | // From https://github.com/hhromic/e131-node/blob/master/lib/e131.js 145 | function getMulticastGroup(universe) { 146 | if (universe < 1 || universe > 63999) { 147 | throw new RangeError('universe should be in the range [1-63999]'); 148 | } 149 | return '239.255.' + (universe >> 8) + '.' + (universe & 0xff); 150 | 151 | } 152 | 153 | module.exports.Packet = Packet; 154 | module.exports.getMulticastGroup = getMulticastGroup; 155 | -------------------------------------------------------------------------------- /src/Receiver.js: -------------------------------------------------------------------------------- 1 | var dgram = require('dgram'); 2 | const os = require('os'); 3 | const sACNPacket = require('./Packet.js'); 4 | 5 | var _socket; 6 | var _universes = []; 7 | 8 | function Start(){ 9 | var self = this; 10 | this._packet = new sACNPacket.Packet(); 11 | 12 | _socket = dgram.createSocket('udp4'); 13 | _socket.on("message", function(msg, network){ 14 | if(msg.slice(4, 13).toString()=="ASC-E1.17"){ 15 | self._packet.fromBuffer(msg); 16 | _universes[self._packet.getUniverse()]._sourceAddress = network.address; 17 | _universes[self._packet.getUniverse()]._packet = self._packet; 18 | _universes[self._packet.getUniverse()]._packetCallback(self._packet); 19 | } 20 | }); 21 | _socket.on("listening", function(){ 22 | for(var i in _universes){ 23 | _universes[i]._readyCallback(); 24 | } 25 | }); 26 | _socket.bind(5568, function(){ 27 | for(var i in _universes){ 28 | _universes[i].begin(); 29 | } 30 | }); 31 | 32 | } 33 | 34 | function Universe(universe){ 35 | this._universe = universe || 1; 36 | _universes[this._universe+""] = this; 37 | 38 | this._sourceAddress; 39 | this._packet; 40 | this._packetCallback = function(){}; 41 | this._readyCallback = function(){}; 42 | 43 | } 44 | Universe.prototype.begin = function(){ 45 | _socket.addMembership(sACNPacket.getMulticastGroup(this._universe)); 46 | } 47 | Universe.prototype.on = function(event, funct){ 48 | if(event=="packet"){ 49 | this._packetCallback = funct; 50 | }else if(event=="ready"){ 51 | this._readyCallback = funct; 52 | } 53 | } 54 | Universe.prototype.toString = function(){ 55 | return { 56 | "Universe": this._universe, 57 | "SourceAddress": this._sourceAddress, 58 | "Packet": this._packet.toString() 59 | } 60 | } 61 | 62 | 63 | 64 | 65 | 66 | module.exports.Start = Start; 67 | module.exports.Universe = Universe; -------------------------------------------------------------------------------- /src/Sender.js: -------------------------------------------------------------------------------- 1 | var dgram = require('dgram'); 2 | const os = require('os'); 3 | const sACNPacket = require('./Packet.js'); 4 | 5 | var _interfaces = []; 6 | var _options = {}; 7 | var _universes = {}; 8 | var _keepAliveInterval; 9 | 10 | function Start(options) { 11 | if (options){ 12 | if(options.interfaces) { 13 | _interfaces = options.interfaces; 14 | }else{ 15 | _interfaces = getNetworkInterfaces(); 16 | } 17 | if(options.keepAlive != false){ 18 | startKeepAliveInterval(); 19 | } 20 | _options.type = options.type; 21 | } else { 22 | _interfaces = getNetworkInterfaces(); 23 | startKeepAliveInterval(); 24 | } 25 | 26 | } 27 | 28 | function Universe(universe, priority) { 29 | var self = this; 30 | 31 | this.universe = universe || 1; 32 | this.priority = priority || 100; 33 | 34 | this._sockets = []; 35 | this._readyCallback = function () {}; 36 | _universes[this.universe] = this; 37 | 38 | var j = 0; 39 | for (var i in _interfaces) { 40 | this._sockets[_interfaces[i]] = dgram.createSocket('udp4'); 41 | this._sockets[_interfaces[i]].bind({}, function () { 42 | j++; 43 | if (j == _interfaces.length) { 44 | self._readyCallback(); 45 | } 46 | }); 47 | } 48 | 49 | this.packet = new sACNPacket.Packet(); 50 | this.packet.setUniverse(this.universe); 51 | this.packet.setPriority(this.priority); 52 | 53 | if (_options.cid && _options.cid.length <= 16) { 54 | this.packet.setCID(_options.cid); 55 | } 56 | 57 | if (_options.source) { 58 | this.packet.setSource(_options.source); 59 | } 60 | 61 | } 62 | 63 | Universe.prototype.send = function (arg) { 64 | var slots; 65 | if (Array.isArray(arg)) { 66 | // passed an array of addresses 67 | slots = arg; 68 | } else if (typeof arg == 'object') { 69 | // passed an object of non-sequential addresses 70 | slots = new Array(512).fill(0); 71 | for (var addr in arg) { 72 | slots[addr - 1] = arg[addr]; 73 | } 74 | } 75 | this.packet.setSlots(slots); 76 | for (var i in _interfaces) { 77 | if (_options && _options.type && _options.type === 'unicast') { 78 | // send packet unicast to a single destination IP 79 | this._sockets[_interfaces[i]].send( 80 | this.packet.getBuffer(), 81 | 5568, 82 | _interfaces[i] 83 | ); 84 | } else { 85 | // send packet multicast to all members 86 | this._sockets[_interfaces[i]].setMulticastInterface(_interfaces[i]); 87 | this._sockets[_interfaces[i]].send( 88 | this.packet.getBuffer(), 89 | 5568, 90 | sACNPacket.getMulticastGroup(this.universe) 91 | ); 92 | } 93 | } 94 | }; 95 | 96 | Universe.prototype.on = function (event, funct) { 97 | if (event == 'ready') { 98 | this._readyCallback = funct; 99 | } 100 | }; 101 | 102 | Universe.prototype.getUniverse = function () { 103 | return this.universe; 104 | }; 105 | Universe.prototype.getPacket = function () { 106 | return this.packet; 107 | }; 108 | Universe.prototype.getInterfaces = function () { 109 | return _interfaces; 110 | }; 111 | Universe.prototype.toString = function () { 112 | return { 113 | Universe: this.getUniverse(), 114 | Interfaces: this.getInterfaces(), 115 | Packet: this.packet.toString() 116 | }; 117 | }; 118 | 119 | Universe.prototype.getPossibleInterfaces = function () { 120 | return getNetworkInterfaces(); 121 | }; 122 | 123 | Universe.prototype.close = function(){ 124 | delete _universes[this.universe]; 125 | } 126 | 127 | function startKeepAliveInterval(){ 128 | clearInterval(_keepAliveInterval); 129 | _keepAliveInterval = setInterval(sendKeepAliveCache, 900); 130 | } 131 | 132 | function sendKeepAliveCache(){ 133 | for(var i in _universes){ 134 | _universes[i].send(_universes[i].packet.getSlots()); 135 | } 136 | } 137 | 138 | function getNetworkInterfaces() { 139 | var out = []; 140 | var interfaces = os.networkInterfaces(); 141 | for (var iface in interfaces) { 142 | for (var address in interfaces[iface]) { 143 | if (interfaces[iface][address].family == 'IPv4') { 144 | out.push(interfaces[iface][address].address); 145 | } 146 | } 147 | } 148 | return out; 149 | } 150 | 151 | module.exports.Universe = Universe; 152 | module.exports.Start = Start; 153 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const ACNSender = new require('./../stagehack-sACN').Sender; 2 | const ACNReceiver = require('./../stagehack-sACN').Receiver; 3 | 4 | ACNSender.Start(); 5 | ACNReceiver.Start(); 6 | 7 | Test0(); 8 | 9 | function Test0(){ 10 | var test0send = new ACNSender.Universe(); 11 | test0send.on('ready', function () { 12 | console.log(this.getPossibleInterfaces()); 13 | console.log(''); 14 | test0send.close(); 15 | Test1(); 16 | }); 17 | } 18 | 19 | function Test1(){ 20 | var test1send = new ACNSender.Universe(); 21 | test1send.on('ready', function () { 22 | this.send([255, 0, 0, 255]); 23 | }); 24 | 25 | var test1receive = new ACNReceiver.Universe(); 26 | test1receive.begin(); 27 | test1receive.on('packet', function (packet) { 28 | console.log('Test 1'); 29 | test(test1send.toString(), test1receive.toString()); 30 | test1send.close(); 31 | Test2(); 32 | }); 33 | } 34 | 35 | function Test2(){ 36 | var test2send = new ACNSender.Universe(2, 50); 37 | test2send.on('ready', function () { 38 | this.send([255, 255, 0, 0, 0, 0, 140]); 39 | }); 40 | 41 | var test2receive = new ACNReceiver.Universe(2); 42 | test2receive.begin(); 43 | test2receive.on('packet', function (packet) { 44 | console.log('Test 2'); 45 | test(test2send.toString(), test2receive.toString()); 46 | test2send.close(); 47 | Test3(); 48 | }); 49 | } 50 | 51 | function Test3(){ 52 | var test3send = new ACNSender.Universe(3); 53 | test3send.on('ready', function () { 54 | this.send({ 1: 255, 3: 120, 512: 255 }); 55 | }); 56 | 57 | var test3receive = new ACNReceiver.Universe(3); 58 | test3receive.begin(); 59 | test3receive.on('packet', function (packet) { 60 | console.log('Test 3'); 61 | test(test3send.toString(), test3receive.toString()); 62 | test3send.close(); 63 | Test4(); 64 | }); 65 | } 66 | 67 | function Test4(){ 68 | var test4send = new ACNSender.Universe(4); 69 | test4send.on('ready', function () { 70 | this.send([1, 2, 3, 4]); 71 | }); 72 | 73 | var lastTimestamp = 0; 74 | var test4receive = new ACNReceiver.Universe(4); 75 | test4receive.begin(); 76 | test4receive.on('packet', function (packet) { 77 | if(packet._sequence==1){ 78 | console.log('Test 4 - Interval'); 79 | lastTimestamp = Date.now(); 80 | }else if(packet._sequence==6){ 81 | console.log('\x1b[32m%s\x1b[0m', 'PASS'); 82 | console.log(''); 83 | test4send.close(); 84 | Test5(); 85 | }else{ 86 | console.log(packet._sequence + ' ' + (Date.now()-lastTimestamp) + 'ms'); 87 | lastTimestamp = Date.now(); 88 | } 89 | }); 90 | } 91 | 92 | function Test5(){ 93 | ACNSender.Start({interfaces: ['127.0.0.1'], type: 'unicast'}); 94 | var test5send = new ACNSender.Universe(5); 95 | test5send.on('ready', function () { 96 | console.log('Test 5 - Unicast'); 97 | this.send([1, 2, 3, 4, 5, 6, 7]); 98 | }); 99 | 100 | var test5receive = new ACNReceiver.Universe(5); 101 | test5receive.begin(); 102 | test5receive.on('packet', function (packet) { 103 | test(test5send.toString(), test5receive.toString()); 104 | test5send.close(); 105 | Test6(); 106 | }); 107 | } 108 | 109 | function Test6(){ 110 | ACNSender.Start(); 111 | var test6send = new ACNSender.Universe(6); 112 | test6send.on('ready', function () { 113 | console.log('Test 6 - Start Code'); 114 | this.send([0, 255, 0, 255]); 115 | }); 116 | 117 | var test6receive = new ACNReceiver.Universe(6); 118 | test6receive.begin(); 119 | test6receive.on('packet', function (packet) { 120 | test(test6send.toString(), test6receive.toString()); 121 | process.exit(1); 122 | }); 123 | } 124 | 125 | 126 | function test(send, receive) { 127 | var errors = []; 128 | if (send.Universe != receive.Universe) { 129 | errors.push('Universe Error'); 130 | } 131 | if (send.Packet.Universe != receive.Packet.Universe) { 132 | errors.push('Packet Universe Error'); 133 | } 134 | if (send.Packet.Priority != receive.Packet.Priority) { 135 | errors.push('Packet Priority Error'); 136 | } 137 | if (send.Packet.Sequence != receive.Packet.Sequence) { 138 | errors.push('Packet Sequence Error (' + send.Packet.Sequence + ' vs ' + receive.Packet.Sequence + ')'); 139 | } 140 | if (send.Packet.Source != receive.Packet.Source) { 141 | errors.push('Packet Source Error'); 142 | } 143 | if (send.Packet.Slots != receive.Packet.Slots) { 144 | errors.push('Packet Slots Error'); 145 | } 146 | 147 | if (errors.length == 0) { 148 | console.log('\x1b[32m%s\x1b[0m', 'PASS'); 149 | } else { 150 | console.log('\x1b[31m%s\x1b[0m', 'ERRORS: ' + errors.join(', ')); 151 | } 152 | console.log(receive.Packet.Slots); 153 | console.log(''); 154 | } 155 | --------------------------------------------------------------------------------