- Add sACN/E1.31 protocol support for both input and output - from_proto = "sacn" to receive from sACN multicast - proto = "sacn" to output via sACN multicast - Fix remap engine to maintain persistent state per output universe - Multiple inputs targeting same output now merge correctly - Prevents flickering when multiple universes feed same output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
103 lines
2.9 KiB
Go
103 lines
2.9 KiB
Go
package sacn
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"net"
|
|
)
|
|
|
|
const (
|
|
Port = 5568
|
|
|
|
// ACN packet identifiers
|
|
ACNPacketIdentifier = 0x41534300 // "ASC\0" + more bytes
|
|
|
|
// Vector values
|
|
VectorRootE131Data = 0x00000004
|
|
VectorE131DataPacket = 0x00000002
|
|
VectorDMPSetProperty = 0x02
|
|
)
|
|
|
|
var (
|
|
// ACN packet identifier (12 bytes)
|
|
packetIdentifier = [12]byte{
|
|
0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00,
|
|
}
|
|
)
|
|
|
|
// BuildDataPacket creates an E1.31 (sACN) data packet
|
|
func BuildDataPacket(universe uint16, sequence uint8, sourceName string, cid [16]byte, data []byte) []byte {
|
|
dataLen := len(data)
|
|
if dataLen > 512 {
|
|
dataLen = 512
|
|
}
|
|
|
|
// Total packet size: Root Layer (38) + Framing Layer (77) + DMP Layer (11 + data)
|
|
// = 126 + dataLen
|
|
pktLen := 126 + dataLen
|
|
buf := make([]byte, pktLen)
|
|
|
|
// Root Layer (38 bytes)
|
|
// Preamble Size (2 bytes)
|
|
binary.BigEndian.PutUint16(buf[0:2], 0x0010)
|
|
// Post-amble Size (2 bytes)
|
|
binary.BigEndian.PutUint16(buf[2:4], 0x0000)
|
|
// ACN Packet Identifier (12 bytes)
|
|
copy(buf[4:16], packetIdentifier[:])
|
|
// Flags and Length (2 bytes) - high 4 bits are flags (0x7), low 12 bits are length
|
|
rootLen := pktLen - 16 // Length from after ACN Packet Identifier
|
|
binary.BigEndian.PutUint16(buf[16:18], 0x7000|uint16(rootLen))
|
|
// Vector (4 bytes)
|
|
binary.BigEndian.PutUint32(buf[18:22], VectorRootE131Data)
|
|
// CID (16 bytes)
|
|
copy(buf[22:38], cid[:])
|
|
|
|
// Framing Layer (77 bytes, starting at offset 38)
|
|
// Flags and Length (2 bytes)
|
|
framingLen := pktLen - 38
|
|
binary.BigEndian.PutUint16(buf[38:40], 0x7000|uint16(framingLen))
|
|
// Vector (4 bytes)
|
|
binary.BigEndian.PutUint32(buf[40:44], VectorE131DataPacket)
|
|
// Source Name (64 bytes, null-terminated)
|
|
copy(buf[44:108], sourceName)
|
|
// Priority (1 byte)
|
|
buf[108] = 100
|
|
// Synchronization Address (2 bytes)
|
|
binary.BigEndian.PutUint16(buf[109:111], 0)
|
|
// Sequence Number (1 byte)
|
|
buf[111] = sequence
|
|
// Options (1 byte)
|
|
buf[112] = 0
|
|
// Universe (2 bytes)
|
|
binary.BigEndian.PutUint16(buf[113:115], universe)
|
|
|
|
// DMP Layer (11 + dataLen bytes, starting at offset 115)
|
|
// Flags and Length (2 bytes)
|
|
dmpLen := 11 + dataLen
|
|
binary.BigEndian.PutUint16(buf[115:117], 0x7000|uint16(dmpLen))
|
|
// Vector (1 byte)
|
|
buf[117] = VectorDMPSetProperty
|
|
// Address Type & Data Type (1 byte)
|
|
buf[118] = 0xa1
|
|
// First Property Address (2 bytes)
|
|
binary.BigEndian.PutUint16(buf[119:121], 0)
|
|
// Address Increment (2 bytes)
|
|
binary.BigEndian.PutUint16(buf[121:123], 1)
|
|
// Property Value Count (2 bytes) - includes START code
|
|
binary.BigEndian.PutUint16(buf[123:125], uint16(dataLen+1))
|
|
// START Code (1 byte)
|
|
buf[125] = 0
|
|
// Property Values (DMX data)
|
|
copy(buf[126:], data[:dataLen])
|
|
|
|
return buf
|
|
}
|
|
|
|
// MulticastAddr returns the multicast address for a given universe
|
|
func MulticastAddr(universe uint16) *net.UDPAddr {
|
|
// 239.255.{universe_high}.{universe_low}
|
|
return &net.UDPAddr{
|
|
IP: net.IPv4(239, 255, byte(universe>>8), byte(universe&0xff)),
|
|
Port: Port,
|
|
}
|
|
}
|