From e48a7de38483298c4c0f808f32067992da727984 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 27 Jan 2026 15:54:32 -0800 Subject: [PATCH] Fix ArtPollReply to advertise both input and output universes --- artnet/discovery.go | 51 ++++++++++++++++++++++++++++++++------------- artnet/protocol.go | 18 +++++++++------- artnet/sender.go | 4 ++-- config.toml | 4 ++++ main.go | 11 +++++++--- remap/engine.go | 15 +++++++++++++ 6 files changed, 76 insertions(+), 27 deletions(-) diff --git a/artnet/discovery.go b/artnet/discovery.go index d2a79e0..6bed6a4 100644 --- a/artnet/discovery.go +++ b/artnet/discovery.go @@ -20,25 +20,27 @@ type Node struct { // Discovery handles ArtNet node discovery type Discovery struct { - sender *Sender - nodes map[string]*Node // keyed by IP string - nodesMu sync.RWMutex - localIP [4]byte - shortName string - longName string - universes []Universe - pollTargets []*net.UDPAddr - done chan struct{} + sender *Sender + nodes map[string]*Node // keyed by IP string + nodesMu sync.RWMutex + localIP [4]byte + shortName string + longName string + inputUnivs []Universe // universes we transmit TO (SwIn) + outputUnivs []Universe // universes we receive FROM (SwOut) + pollTargets []*net.UDPAddr + done chan struct{} } // NewDiscovery creates a new discovery handler -func NewDiscovery(sender *Sender, shortName, longName string, universes []Universe, pollTargets []*net.UDPAddr) *Discovery { +func NewDiscovery(sender *Sender, shortName, longName string, inputUnivs, outputUnivs []Universe, pollTargets []*net.UDPAddr) *Discovery { return &Discovery{ sender: sender, nodes: make(map[string]*Node), shortName: shortName, longName: longName, - universes: universes, + inputUnivs: inputUnivs, + outputUnivs: outputUnivs, pollTargets: pollTargets, done: make(chan struct{}), } @@ -185,10 +187,29 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) { // HandlePoll processes an incoming ArtPoll and responds func (d *Discovery) HandlePoll(src *net.UDPAddr) { - // Respond with our info - err := d.sender.SendPollReply(src, d.localIP, d.shortName, d.longName, d.universes) - if err != nil { - log.Printf("[->artnet] pollreply error: dst=%s err=%v", src.IP, err) + d.sendPollReplies(src, d.inputUnivs, true) + d.sendPollReplies(src, d.outputUnivs, false) +} + +func (d *Discovery) sendPollReplies(dst *net.UDPAddr, universes []Universe, isInput bool) { + groups := make(map[uint16][]Universe) + for _, u := range universes { + key := uint16(u.Net())<<8 | uint16(u.SubNet())<<4 + groups[key] = append(groups[key], u) + } + + for _, univs := range groups { + for i := 0; i < len(univs); i += 4 { + end := i + 4 + if end > len(univs) { + end = len(univs) + } + chunk := univs[i:end] + err := d.sender.SendPollReply(dst, d.localIP, d.shortName, d.longName, chunk, isInput) + if err != nil { + log.Printf("[->artnet] pollreply error: dst=%s err=%v", dst.IP, err) + } + } } } diff --git a/artnet/protocol.go b/artnet/protocol.go index 27bdc3b..5a14d0f 100644 --- a/artnet/protocol.go +++ b/artnet/protocol.go @@ -254,7 +254,8 @@ func BuildPollPacket() []byte { } // BuildPollReplyPacket creates an ArtPollReply packet -func BuildPollReplyPacket(ip [4]byte, shortName, longName string, universes []Universe) []byte { +// isInput: true = we transmit to network (SwIn), false = we receive from network (SwOut) +func BuildPollReplyPacket(ip [4]byte, shortName, longName string, universes []Universe, isInput bool) []byte { buf := make([]byte, 239) copy(buf[0:8], ArtNetID[:]) @@ -263,17 +264,14 @@ func BuildPollReplyPacket(ip [4]byte, shortName, longName string, universes []Un binary.LittleEndian.PutUint16(buf[14:16], Port) binary.BigEndian.PutUint16(buf[16:18], ProtocolVersion) - // Net/Subnet from first universe if available if len(universes) > 0 { buf[18] = universes[0].Net() buf[19] = universes[0].SubNet() } - // Names copy(buf[26:44], shortName) copy(buf[44:108], longName) - // Ports numPorts := len(universes) if numPorts > 4 { numPorts = 4 @@ -281,9 +279,15 @@ func BuildPollReplyPacket(ip [4]byte, shortName, longName string, universes []Un buf[173] = byte(numPorts) for i := 0; i < numPorts; i++ { - buf[174+i] = 0xC0 // Output, can output DMX - buf[182+i] = 0x80 // Data transmitted - buf[190+i] = universes[i].Universe() + if isInput { + buf[174+i] = 0x40 // Can input to Art-Net (we transmit) + buf[178+i] = 0x80 // Data received + buf[186+i] = universes[i].Universe() + } else { + buf[174+i] = 0x80 // Can output from Art-Net (we receive) + buf[182+i] = 0x80 // Data transmitted + buf[190+i] = universes[i].Universe() + } } buf[200] = 0x00 // StNode diff --git a/artnet/sender.go b/artnet/sender.go index e058408..65938b8 100644 --- a/artnet/sender.go +++ b/artnet/sender.go @@ -56,8 +56,8 @@ func (s *Sender) SendPoll(addr *net.UDPAddr) error { } // SendPollReply sends an ArtPollReply to a specific address -func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, shortName, longName string, universes []Universe) error { - pkt := BuildPollReplyPacket(localIP, shortName, longName, universes) +func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, shortName, longName string, universes []Universe, isInput bool) error { + pkt := BuildPollReplyPacket(localIP, shortName, longName, universes, isInput) _, err := s.conn.WriteToUDP(pkt, addr) return err } diff --git a/config.toml b/config.toml index 7687cc6..8a95245 100644 --- a/config.toml +++ b/config.toml @@ -2,6 +2,10 @@ from = "artnet:32.0.0" to = "sacn:32" +[[mapping]] +from = "artnet:34.0.0" +to = "sacn:34" + # lighting-1 port 1 [[target]] universe = "artnet:0.0.0" diff --git a/main.go b/main.go index a842481..bdb2579 100644 --- a/main.go +++ b/main.go @@ -115,11 +115,16 @@ func main() { // Create discovery destNums := engine.DestArtNetUniverses() - destUniverses := make([]artnet.Universe, len(destNums)) + inputUnivs := make([]artnet.Universe, len(destNums)) for i, n := range destNums { - destUniverses[i] = artnet.Universe(n) + inputUnivs[i] = artnet.Universe(n) } - discovery := artnet.NewDiscovery(artSender, "artmap", "ArtNet Remapping Proxy", destUniverses, pollTargetSlice) + srcNums := engine.SourceArtNetUniverses() + outputUnivs := make([]artnet.Universe, len(srcNums)) + for i, n := range srcNums { + outputUnivs[i] = artnet.Universe(n) + } + discovery := artnet.NewDiscovery(artSender, "artmap", "artmap", inputUnivs, outputUnivs, pollTargetSlice) // Create app app := &App{ diff --git a/remap/engine.go b/remap/engine.go index 2361552..d8b3e9d 100644 --- a/remap/engine.go +++ b/remap/engine.go @@ -77,6 +77,21 @@ func (e *Engine) Remap(src config.Universe, srcData [512]byte) []Output { return result } +// SourceArtNetUniverses returns source ArtNet universe numbers (for discovery) +func (e *Engine) SourceArtNetUniverses() []uint16 { + seen := make(map[uint16]bool) + for _, m := range e.mappings { + if m.From.Protocol == config.ProtocolArtNet { + seen[m.From.Number] = true + } + } + result := make([]uint16, 0, len(seen)) + for u := range seen { + result = append(result, u) + } + return result +} + // DestArtNetUniverses returns destination ArtNet universe numbers (for discovery) func (e *Engine) DestArtNetUniverses() []uint16 { seen := make(map[uint16]bool)