Add sACN input/output support and fix multi-source merging
- 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>
This commit is contained in:
140
sacn/receiver.go
Normal file
140
sacn/receiver.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package sacn
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// DMXHandler is called when DMX data is received
|
||||
type DMXHandler func(universe uint16, data [512]byte)
|
||||
|
||||
// Receiver listens for sACN packets
|
||||
type Receiver struct {
|
||||
conn *ipv4.PacketConn
|
||||
universes []uint16
|
||||
handler DMXHandler
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewReceiver creates a new sACN receiver for the given universes
|
||||
func NewReceiver(universes []uint16, handler DMXHandler) (*Receiver, error) {
|
||||
// Listen on sACN port
|
||||
c, err := net.ListenPacket("udp4", ":5568")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := ipv4.NewPacketConn(c)
|
||||
|
||||
// Join multicast groups for each universe
|
||||
for _, u := range universes {
|
||||
group := net.IPv4(239, 255, byte(u>>8), byte(u&0xff))
|
||||
iface, _ := net.InterfaceByIndex(0) // Use default interface
|
||||
if err := p.JoinGroup(iface, &net.UDPAddr{IP: group}); err != nil {
|
||||
// Try joining on all interfaces
|
||||
ifaces, _ := net.Interfaces()
|
||||
for _, iface := range ifaces {
|
||||
p.JoinGroup(&iface, &net.UDPAddr{IP: group})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Receiver{
|
||||
conn: p,
|
||||
universes: universes,
|
||||
handler: handler,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start begins receiving packets
|
||||
func (r *Receiver) Start() {
|
||||
go r.receiveLoop()
|
||||
}
|
||||
|
||||
// Stop stops the receiver
|
||||
func (r *Receiver) Stop() {
|
||||
close(r.done)
|
||||
r.conn.Close()
|
||||
}
|
||||
|
||||
func (r *Receiver) receiveLoop() {
|
||||
buf := make([]byte, 638) // Max sACN packet size
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
n, _, _, err := r.conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-r.done:
|
||||
return
|
||||
default:
|
||||
log.Printf("sacn read error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
r.handlePacket(buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Receiver) handlePacket(data []byte) {
|
||||
// Minimum packet size check
|
||||
if len(data) < 126 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check ACN packet identifier
|
||||
if data[4] != 0x41 || data[5] != 0x53 || data[6] != 0x43 {
|
||||
return
|
||||
}
|
||||
|
||||
// Check root vector (E1.31 data)
|
||||
rootVector := binary.BigEndian.Uint32(data[18:22])
|
||||
if rootVector != VectorRootE131Data {
|
||||
return
|
||||
}
|
||||
|
||||
// Check framing vector (DMP data)
|
||||
framingVector := binary.BigEndian.Uint32(data[40:44])
|
||||
if framingVector != VectorE131DataPacket {
|
||||
return
|
||||
}
|
||||
|
||||
// Get universe
|
||||
universe := binary.BigEndian.Uint16(data[113:115])
|
||||
|
||||
// Check DMP vector
|
||||
if data[117] != VectorDMPSetProperty {
|
||||
return
|
||||
}
|
||||
|
||||
// Get property count (includes START code)
|
||||
propCount := binary.BigEndian.Uint16(data[123:125])
|
||||
if propCount < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip START code at data[125]
|
||||
dmxLen := int(propCount) - 1
|
||||
if dmxLen > 512 {
|
||||
dmxLen = 512
|
||||
}
|
||||
|
||||
if len(data) < 126+dmxLen {
|
||||
return
|
||||
}
|
||||
|
||||
var dmxData [512]byte
|
||||
copy(dmxData[:], data[126:126+dmxLen])
|
||||
|
||||
r.handler(universe, dmxData)
|
||||
}
|
||||
Reference in New Issue
Block a user