Fix ArtPollReply to advertise both input and output universes

This commit is contained in:
Ian Gulliver
2026-01-27 15:54:32 -08:00
parent 9a694b5178
commit e48a7de384
6 changed files with 76 additions and 27 deletions

View File

@@ -20,25 +20,27 @@ type Node struct {
// Discovery handles ArtNet node discovery // Discovery handles ArtNet node discovery
type Discovery struct { type Discovery struct {
sender *Sender sender *Sender
nodes map[string]*Node // keyed by IP string nodes map[string]*Node // keyed by IP string
nodesMu sync.RWMutex nodesMu sync.RWMutex
localIP [4]byte localIP [4]byte
shortName string shortName string
longName string longName string
universes []Universe inputUnivs []Universe // universes we transmit TO (SwIn)
pollTargets []*net.UDPAddr outputUnivs []Universe // universes we receive FROM (SwOut)
done chan struct{} pollTargets []*net.UDPAddr
done chan struct{}
} }
// NewDiscovery creates a new discovery handler // 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{ return &Discovery{
sender: sender, sender: sender,
nodes: make(map[string]*Node), nodes: make(map[string]*Node),
shortName: shortName, shortName: shortName,
longName: longName, longName: longName,
universes: universes, inputUnivs: inputUnivs,
outputUnivs: outputUnivs,
pollTargets: pollTargets, pollTargets: pollTargets,
done: make(chan struct{}), done: make(chan struct{}),
} }
@@ -185,10 +187,29 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
// HandlePoll processes an incoming ArtPoll and responds // HandlePoll processes an incoming ArtPoll and responds
func (d *Discovery) HandlePoll(src *net.UDPAddr) { func (d *Discovery) HandlePoll(src *net.UDPAddr) {
// Respond with our info d.sendPollReplies(src, d.inputUnivs, true)
err := d.sender.SendPollReply(src, d.localIP, d.shortName, d.longName, d.universes) d.sendPollReplies(src, d.outputUnivs, false)
if err != nil { }
log.Printf("[->artnet] pollreply error: dst=%s err=%v", src.IP, err)
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)
}
}
} }
} }

View File

@@ -254,7 +254,8 @@ func BuildPollPacket() []byte {
} }
// BuildPollReplyPacket creates an ArtPollReply packet // 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) buf := make([]byte, 239)
copy(buf[0:8], ArtNetID[:]) 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.LittleEndian.PutUint16(buf[14:16], Port)
binary.BigEndian.PutUint16(buf[16:18], ProtocolVersion) binary.BigEndian.PutUint16(buf[16:18], ProtocolVersion)
// Net/Subnet from first universe if available
if len(universes) > 0 { if len(universes) > 0 {
buf[18] = universes[0].Net() buf[18] = universes[0].Net()
buf[19] = universes[0].SubNet() buf[19] = universes[0].SubNet()
} }
// Names
copy(buf[26:44], shortName) copy(buf[26:44], shortName)
copy(buf[44:108], longName) copy(buf[44:108], longName)
// Ports
numPorts := len(universes) numPorts := len(universes)
if numPorts > 4 { if numPorts > 4 {
numPorts = 4 numPorts = 4
@@ -281,9 +279,15 @@ func BuildPollReplyPacket(ip [4]byte, shortName, longName string, universes []Un
buf[173] = byte(numPorts) buf[173] = byte(numPorts)
for i := 0; i < numPorts; i++ { for i := 0; i < numPorts; i++ {
buf[174+i] = 0xC0 // Output, can output DMX if isInput {
buf[182+i] = 0x80 // Data transmitted buf[174+i] = 0x40 // Can input to Art-Net (we transmit)
buf[190+i] = universes[i].Universe() 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 buf[200] = 0x00 // StNode

View File

@@ -56,8 +56,8 @@ func (s *Sender) SendPoll(addr *net.UDPAddr) error {
} }
// SendPollReply sends an ArtPollReply to a specific address // SendPollReply sends an ArtPollReply to a specific address
func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, shortName, longName string, universes []Universe) error { func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, shortName, longName string, universes []Universe, isInput bool) error {
pkt := BuildPollReplyPacket(localIP, shortName, longName, universes) pkt := BuildPollReplyPacket(localIP, shortName, longName, universes, isInput)
_, err := s.conn.WriteToUDP(pkt, addr) _, err := s.conn.WriteToUDP(pkt, addr)
return err return err
} }

View File

@@ -2,6 +2,10 @@
from = "artnet:32.0.0" from = "artnet:32.0.0"
to = "sacn:32" to = "sacn:32"
[[mapping]]
from = "artnet:34.0.0"
to = "sacn:34"
# lighting-1 port 1 # lighting-1 port 1
[[target]] [[target]]
universe = "artnet:0.0.0" universe = "artnet:0.0.0"

11
main.go
View File

@@ -115,11 +115,16 @@ func main() {
// Create discovery // Create discovery
destNums := engine.DestArtNetUniverses() destNums := engine.DestArtNetUniverses()
destUniverses := make([]artnet.Universe, len(destNums)) inputUnivs := make([]artnet.Universe, len(destNums))
for i, n := range 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 // Create app
app := &App{ app := &App{

View File

@@ -77,6 +77,21 @@ func (e *Engine) Remap(src config.Universe, srcData [512]byte) []Output {
return result 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) // 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)