diff --git a/main.go b/main.go index c3387e0..d44cfd5 100644 --- a/main.go +++ b/main.go @@ -107,13 +107,17 @@ func main() { } defer artSender.Close() - // Create sACN sender sacnSender, err := sacn.NewSender("artmap", *sacnInterface) if err != nil { log.Fatalf("sacn sender error: %v", err) } defer sacnSender.Close() + for _, u := range engine.DestSACNUniverses() { + sacnSender.RegisterUniverse(u) + } + sacnSender.StartDiscovery() + // Create discovery destNums := engine.DestArtNetUniverses() inputUnivs := make([]artnet.Universe, len(destNums)) diff --git a/remap/engine.go b/remap/engine.go index d8b3e9d..48a2872 100644 --- a/remap/engine.go +++ b/remap/engine.go @@ -92,7 +92,6 @@ func (e *Engine) SourceArtNetUniverses() []uint16 { return result } -// DestArtNetUniverses returns destination ArtNet universe numbers (for discovery) func (e *Engine) DestArtNetUniverses() []uint16 { seen := make(map[uint16]bool) for _, m := range e.mappings { @@ -106,3 +105,17 @@ func (e *Engine) DestArtNetUniverses() []uint16 { } return result } + +func (e *Engine) DestSACNUniverses() []uint16 { + seen := make(map[uint16]bool) + for _, m := range e.mappings { + if m.To.Protocol == config.ProtocolSACN { + seen[m.To.Number] = true + } + } + result := make([]uint16, 0, len(seen)) + for u := range seen { + result = append(result, u) + } + return result +} diff --git a/sacn/protocol.go b/sacn/protocol.go index 71d12a1..cbb65f8 100644 --- a/sacn/protocol.go +++ b/sacn/protocol.go @@ -8,13 +8,14 @@ import ( const ( Port = 5568 - // ACN packet identifiers - ACNPacketIdentifier = 0x41534300 // "ASC\0" + more bytes + ACNPacketIdentifier = 0x41534300 - // Vector values - VectorRootE131Data = 0x00000004 - VectorE131DataPacket = 0x00000002 - VectorDMPSetProperty = 0x02 + VectorRootE131Data = 0x00000004 + VectorRootE131Extended = 0x00000008 + VectorE131DataPacket = 0x00000002 + VectorE131Discovery = 0x00000002 + VectorDMPSetProperty = 0x02 + VectorUniverseDiscovery = 0x00000001 ) var ( @@ -92,11 +93,49 @@ func BuildDataPacket(universe uint16, sequence uint8, sourceName string, cid [16 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, } } + +var DiscoveryAddr = &net.UDPAddr{ + IP: net.IPv4(239, 255, 250, 214), + Port: Port, +} + +func BuildDiscoveryPacket(sourceName string, cid [16]byte, page, lastPage uint8, universes []uint16) []byte { + universeCount := len(universes) + if universeCount > 512 { + universeCount = 512 + } + + pktLen := 120 + universeCount*2 + buf := make([]byte, pktLen) + + binary.BigEndian.PutUint16(buf[0:2], 0x0010) + binary.BigEndian.PutUint16(buf[2:4], 0x0000) + copy(buf[4:16], packetIdentifier[:]) + rootLen := pktLen - 16 + binary.BigEndian.PutUint16(buf[16:18], 0x7000|uint16(rootLen)) + binary.BigEndian.PutUint32(buf[18:22], VectorRootE131Extended) + copy(buf[22:38], cid[:]) + + framingLen := pktLen - 38 + binary.BigEndian.PutUint16(buf[38:40], 0x7000|uint16(framingLen)) + binary.BigEndian.PutUint32(buf[40:44], VectorE131Discovery) + copy(buf[44:108], sourceName) + binary.BigEndian.PutUint32(buf[108:112], 0) + + discoveryLen := pktLen - 112 + binary.BigEndian.PutUint16(buf[112:114], 0x7000|uint16(discoveryLen)) + binary.BigEndian.PutUint32(buf[114:118], VectorUniverseDiscovery) + buf[118] = page + buf[119] = lastPage + for i := 0; i < universeCount; i++ { + binary.BigEndian.PutUint16(buf[120+i*2:122+i*2], universes[i]) + } + + return buf +} diff --git a/sacn/sender.go b/sacn/sender.go index 7f75aae..60ee88d 100644 --- a/sacn/sender.go +++ b/sacn/sender.go @@ -3,18 +3,21 @@ package sacn import ( "crypto/rand" "net" + "sort" "sync" + "time" "golang.org/x/net/ipv4" ) -// Sender sends sACN (E1.31) packets type Sender struct { conn *net.UDPConn sourceName string cid [16]byte sequences map[uint16]uint8 seqMu sync.Mutex + universes map[uint16]bool + done chan struct{} } // NewSender creates a new sACN sender @@ -45,6 +48,8 @@ func NewSender(sourceName string, ifaceName string) (*Sender, error) { sourceName: sourceName, cid: cid, sequences: make(map[uint16]uint8), + universes: make(map[uint16]bool), + done: make(chan struct{}), }, nil } @@ -75,7 +80,65 @@ func (s *Sender) SendDMXUnicast(addr *net.UDPAddr, universe uint16, data []byte) return err } -// Close closes the sender func (s *Sender) Close() error { + select { + case <-s.done: + default: + close(s.done) + } return s.conn.Close() } + +func (s *Sender) RegisterUniverse(universe uint16) { + s.seqMu.Lock() + s.universes[universe] = true + s.seqMu.Unlock() +} + +func (s *Sender) StartDiscovery() { + go s.discoveryLoop() +} + +func (s *Sender) discoveryLoop() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + s.sendDiscovery() + + for { + select { + case <-s.done: + return + case <-ticker.C: + s.sendDiscovery() + } + } +} + +func (s *Sender) sendDiscovery() { + s.seqMu.Lock() + universes := make([]uint16, 0, len(s.universes)) + for u := range s.universes { + universes = append(universes, u) + } + s.seqMu.Unlock() + + if len(universes) == 0 { + return + } + + sort.Slice(universes, func(i, j int) bool { return universes[i] < universes[j] }) + + const maxPerPage = 512 + totalPages := (len(universes) + maxPerPage - 1) / maxPerPage + + for page := 0; page < totalPages; page++ { + start := page * maxPerPage + end := start + maxPerPage + if end > len(universes) { + end = len(universes) + } + pkt := BuildDiscoveryPacket(s.sourceName, s.cid, uint8(page), uint8(totalPages-1), universes[start:end]) + s.conn.WriteToUDP(pkt, DiscoveryAddr) + } +}