Add sACN universe discovery announcements

This commit is contained in:
Ian Gulliver
2026-01-28 21:13:22 -08:00
parent 2b833d7913
commit 71d600f481
4 changed files with 131 additions and 12 deletions

View File

@@ -107,13 +107,17 @@ func main() {
} }
defer artSender.Close() defer artSender.Close()
// Create sACN sender
sacnSender, err := sacn.NewSender("artmap", *sacnInterface) sacnSender, err := sacn.NewSender("artmap", *sacnInterface)
if err != nil { if err != nil {
log.Fatalf("sacn sender error: %v", err) log.Fatalf("sacn sender error: %v", err)
} }
defer sacnSender.Close() defer sacnSender.Close()
for _, u := range engine.DestSACNUniverses() {
sacnSender.RegisterUniverse(u)
}
sacnSender.StartDiscovery()
// Create discovery // Create discovery
destNums := engine.DestArtNetUniverses() destNums := engine.DestArtNetUniverses()
inputUnivs := make([]artnet.Universe, len(destNums)) inputUnivs := make([]artnet.Universe, len(destNums))

View File

@@ -92,7 +92,6 @@ func (e *Engine) SourceArtNetUniverses() []uint16 {
return result return result
} }
// DestArtNetUniverses returns destination ArtNet universe numbers (for discovery)
func (e *Engine) DestArtNetUniverses() []uint16 { func (e *Engine) DestArtNetUniverses() []uint16 {
seen := make(map[uint16]bool) seen := make(map[uint16]bool)
for _, m := range e.mappings { for _, m := range e.mappings {
@@ -106,3 +105,17 @@ func (e *Engine) DestArtNetUniverses() []uint16 {
} }
return result 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
}

View File

@@ -8,13 +8,14 @@ import (
const ( const (
Port = 5568 Port = 5568
// ACN packet identifiers ACNPacketIdentifier = 0x41534300
ACNPacketIdentifier = 0x41534300 // "ASC\0" + more bytes
// Vector values
VectorRootE131Data = 0x00000004 VectorRootE131Data = 0x00000004
VectorRootE131Extended = 0x00000008
VectorE131DataPacket = 0x00000002 VectorE131DataPacket = 0x00000002
VectorE131Discovery = 0x00000002
VectorDMPSetProperty = 0x02 VectorDMPSetProperty = 0x02
VectorUniverseDiscovery = 0x00000001
) )
var ( var (
@@ -92,11 +93,49 @@ func BuildDataPacket(universe uint16, sequence uint8, sourceName string, cid [16
return buf return buf
} }
// MulticastAddr returns the multicast address for a given universe
func MulticastAddr(universe uint16) *net.UDPAddr { func MulticastAddr(universe uint16) *net.UDPAddr {
// 239.255.{universe_high}.{universe_low}
return &net.UDPAddr{ return &net.UDPAddr{
IP: net.IPv4(239, 255, byte(universe>>8), byte(universe&0xff)), IP: net.IPv4(239, 255, byte(universe>>8), byte(universe&0xff)),
Port: Port, 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
}

View File

@@ -3,18 +3,21 @@ package sacn
import ( import (
"crypto/rand" "crypto/rand"
"net" "net"
"sort"
"sync" "sync"
"time"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
) )
// Sender sends sACN (E1.31) packets
type Sender struct { type Sender struct {
conn *net.UDPConn conn *net.UDPConn
sourceName string sourceName string
cid [16]byte cid [16]byte
sequences map[uint16]uint8 sequences map[uint16]uint8
seqMu sync.Mutex seqMu sync.Mutex
universes map[uint16]bool
done chan struct{}
} }
// NewSender creates a new sACN sender // NewSender creates a new sACN sender
@@ -45,6 +48,8 @@ func NewSender(sourceName string, ifaceName string) (*Sender, error) {
sourceName: sourceName, sourceName: sourceName,
cid: cid, cid: cid,
sequences: make(map[uint16]uint8), sequences: make(map[uint16]uint8),
universes: make(map[uint16]bool),
done: make(chan struct{}),
}, nil }, nil
} }
@@ -75,7 +80,65 @@ func (s *Sender) SendDMXUnicast(addr *net.UDPAddr, universe uint16, data []byte)
return err return err
} }
// Close closes the sender
func (s *Sender) Close() error { func (s *Sender) Close() error {
select {
case <-s.done:
default:
close(s.done)
}
return s.conn.Close() 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)
}
}