Use artnet library for per-interface discovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-30 09:14:04 -08:00
parent d40102941b
commit d970d1db86
5 changed files with 64 additions and 137 deletions

View File

@@ -66,7 +66,13 @@
"Bash(snmpwalk:*)", "Bash(snmpwalk:*)",
"Bash(go work sync:*)", "Bash(go work sync:*)",
"Bash(GOWORK=/home/flamingcow/go.work go vet:*)", "Bash(GOWORK=/home/flamingcow/go.work go vet:*)",
"Bash(git pull:*)" "Bash(git pull:*)",
"Bash(GOPROXY=direct go get:*)",
"Bash(git -C /home/flamingcow/tendrils diff)",
"Bash(git -C /home/flamingcow/artmap diff)",
"Bash(git -C /home/flamingcow/artnet status)",
"Bash(git -C /home/flamingcow/tendrils status)",
"Bash(git -C /home/flamingcow/artmap status)"
], ],
"ask": [ "ask": [
"Bash(rm *)" "Bash(rm *)"

162
artnet.go
View File

@@ -13,119 +13,69 @@ import (
"github.com/gopatchy/artnet" "github.com/gopatchy/artnet"
) )
func (t *Tendrils) startArtNetListener(ctx context.Context) { type artnetHandler struct {
conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: artnet.Port}) t *Tendrils
if err != nil { discovery *artnet.Discovery
log.Printf("[ERROR] failed to listen artnet: %v", err)
return
}
defer conn.Close()
t.artnetConn = conn
buf := make([]byte, 65536)
for {
select {
case <-ctx.Done():
return
default:
} }
conn.SetReadDeadline(time.Now().Add(1 * time.Second)) func (h *artnetHandler) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {}
n, src, err := conn.ReadFromUDP(buf)
if err != nil { func (h *artnetHandler) HandlePoll(src *net.UDPAddr, pkt *artnet.PollPacket) {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { h.discovery.HandlePoll(src)
continue
}
continue
} }
t.handleArtNetPacket(src, buf[:n]) func (h *artnetHandler) HandlePollReply(src *net.UDPAddr, pkt *artnet.PollReplyPacket) {
} h.discovery.HandlePollReply(src, pkt)
} }
func (t *Tendrils) startArtNetPoller(ctx context.Context, iface net.Interface) { func (t *Tendrils) startArtNet(ctx context.Context, iface net.Interface) {
srcIP, broadcast := getInterfaceIPv4(iface) srcIP, broadcast := getInterfaceIPv4(iface)
if srcIP == nil || broadcast == nil { if srcIP == nil || broadcast == nil {
return return
} }
sendConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: 0}) sender, err := artnet.NewInterfaceSender(iface.Name)
if err != nil { if err != nil {
log.Printf("[ERROR] failed to create artnet send socket on %s: %v", iface.Name, err) log.Printf("[ERROR] failed to create artnet sender for %s: %v", iface.Name, err)
return return
} }
defer sendConn.Close()
go t.listenArtNetReplies(ctx, sendConn, iface.Name) discovery := artnet.NewDiscovery(sender, srcIP, broadcast, iface.HardwareAddr, "", "", nil, nil)
discovery.SetOnChange(func(node *artnet.Node) {
t.handleArtNetNode(node)
})
ticker := time.NewTicker(10 * time.Second) handler := &artnetHandler{t: t, discovery: discovery}
defer ticker.Stop()
t.sendArtPoll(sendConn, broadcast, iface.Name) receiver, err := artnet.NewInterfaceReceiver(iface.Name, handler)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
t.sendArtPoll(sendConn, broadcast, iface.Name)
}
}
}
func (t *Tendrils) listenArtNetReplies(ctx context.Context, conn *net.UDPConn, ifaceName string) {
buf := make([]byte, 1024)
for {
select {
case <-ctx.Done():
return
default:
}
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, src, err := conn.ReadFromUDP(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
select {
case <-ctx.Done():
return
default:
continue
}
}
t.handleArtNetPacket(src, buf[:n])
}
}
func (t *Tendrils) handleArtNetPacket(src *net.UDPAddr, data []byte) {
opCode, pkt, err := artnet.ParsePacket(data)
if err != nil { if err != nil {
log.Printf("[ERROR] failed to create artnet receiver for %s: %v", iface.Name, err)
sender.Close()
return return
} }
switch opCode { discovery.SetReceiver(receiver)
case artnet.OpPollReply: receiver.Start()
if reply, ok := pkt.(*artnet.PollReplyPacket); ok { discovery.Start()
t.handleArtPollReply(src.IP, reply)
} <-ctx.Done()
}
discovery.Stop()
receiver.Stop()
sender.Close()
} }
func (t *Tendrils) handleArtPollReply(srcIP net.IP, pkt *artnet.PollReplyPacket) { func (t *Tendrils) handleArtNetNode(node *artnet.Node) {
ip := pkt.IP() ip := node.IP
mac := pkt.MACAddr() mac := node.MAC
shortName := pkt.GetShortName() shortName := node.ShortName
longName := pkt.GetLongName() longName := node.LongName
var inputs, outputs []int var inputs, outputs []int
for _, u := range pkt.InputUniverses() { for _, u := range node.Inputs {
inputs = append(inputs, int(u)) inputs = append(inputs, int(u))
} }
for _, u := range pkt.OutputUniverses() { for _, u := range node.Outputs {
outputs = append(outputs, int(u)) outputs = append(outputs, int(u))
} }
@@ -141,41 +91,17 @@ func (t *Tendrils) handleArtPollReply(srcIP net.IP, pkt *artnet.PollReplyPacket)
t.nodes.Update(nil, mac, []net.IP{ip}, "", name, "artnet") t.nodes.Update(nil, mac, []net.IP{ip}, "", name, "artnet")
} }
node := t.nodes.GetByIP(ip) n := t.nodes.GetByIP(ip)
if node == nil && mac != nil { if n == nil && mac != nil {
node = t.nodes.GetByMAC(mac) n = t.nodes.GetByMAC(mac)
} }
if node == nil && name != "" { if n == nil && name != "" {
node = t.nodes.GetOrCreateByName(name) n = t.nodes.GetOrCreateByName(name)
} }
if node != nil { if n != nil {
t.nodes.UpdateArtNet(node, inputs, outputs) t.nodes.UpdateArtNet(n, inputs, outputs)
} }
} t.NotifyUpdate()
func (t *Tendrils) sendArtPoll(conn *net.UDPConn, broadcast net.IP, ifaceName string) {
packet := artnet.BuildPollPacket()
_, err := conn.WriteToUDP(packet, &net.UDPAddr{IP: broadcast, Port: artnet.Port})
if err != nil {
if t.DebugArtNet {
log.Printf("[artnet] %s: failed to send poll: %v", ifaceName, err)
}
return
}
if t.DebugArtNet {
log.Printf("[artnet] %s: sent poll to %s", ifaceName, broadcast)
}
}
func containsInt(slice []int, val int) bool {
for _, v := range slice {
if v == val {
return true
}
}
return false
} }
func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) { func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.25.6
require ( require (
github.com/fvbommel/sortorder v1.1.0 github.com/fvbommel/sortorder v1.1.0
github.com/google/gopacket v1.1.19 github.com/google/gopacket v1.1.19
github.com/gopatchy/artnet v0.0.0-20260128203426-0a3e9b1daf66 github.com/gopatchy/artnet v0.0.0-20260130164309-5e7400fe514e
github.com/gopatchy/multicast v0.0.0-20260130055828-12d0b38af995 github.com/gopatchy/multicast v0.0.0-20260130055828-12d0b38af995
github.com/gopatchy/sacn v0.0.0-20260130055955-54a46fbfe1f0 github.com/gopatchy/sacn v0.0.0-20260130055955-54a46fbfe1f0
github.com/gosnmp/gosnmp v1.43.2 github.com/gosnmp/gosnmp v1.43.2

4
go.sum
View File

@@ -9,8 +9,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/gopatchy/artnet v0.0.0-20260128203426-0a3e9b1daf66 h1:QZrypvWOUbZeJsFRRx8UXf+MUbvkF/WR2KvNUynWFTM= github.com/gopatchy/artnet v0.0.0-20260130164309-5e7400fe514e h1:KaCtixVKARhtTzqlqWyzXurNACaesAGxjFgnFd3jYT4=
github.com/gopatchy/artnet v0.0.0-20260128203426-0a3e9b1daf66/go.mod h1:V/D32mh1xfK/llCKbrqI2jxw4xL4hf6Ge2yLiIrp9/4= github.com/gopatchy/artnet v0.0.0-20260130164309-5e7400fe514e/go.mod h1:V/D32mh1xfK/llCKbrqI2jxw4xL4hf6Ge2yLiIrp9/4=
github.com/gopatchy/multicast v0.0.0-20260130055828-12d0b38af995 h1:dwu07X0JnN6Ar3oZMLa/BokX04JRa3AeM38Nz7nwKnM= github.com/gopatchy/multicast v0.0.0-20260130055828-12d0b38af995 h1:dwu07X0JnN6Ar3oZMLa/BokX04JRa3AeM38Nz7nwKnM=
github.com/gopatchy/multicast v0.0.0-20260130055828-12d0b38af995/go.mod h1:mSeh6GX+fL6SWZYqxYHTdnddvzDx4qsGSBnlGwY5ZsA= github.com/gopatchy/multicast v0.0.0-20260130055828-12d0b38af995/go.mod h1:mSeh6GX+fL6SWZYqxYHTdnddvzDx4qsGSBnlGwY5ZsA=
github.com/gopatchy/sacn v0.0.0-20260130055955-54a46fbfe1f0 h1:j1uxCRJSu7G8UwlTykIDacbcQY2qoUpZ9t/SBHqLET8= github.com/gopatchy/sacn v0.0.0-20260130055955-54a46fbfe1f0 h1:j1uxCRJSu7G8UwlTykIDacbcQY2qoUpZ9t/SBHqLET8=

View File

@@ -33,7 +33,6 @@ func getInterfaceIPv4(iface net.Interface) (srcIP, broadcast net.IP) {
type Tendrils struct { type Tendrils struct {
activeInterfaces map[string]context.CancelFunc activeInterfaces map[string]context.CancelFunc
nodes *Nodes nodes *Nodes
artnetConn *net.UDPConn
errors *ErrorTracker errors *ErrorTracker
ping *PingManager ping *PingManager
broadcast *BroadcastStats broadcast *BroadcastStats
@@ -158,10 +157,6 @@ func (t *Tendrils) Run() {
go t.pollARP(ctx) go t.pollARP(ctx)
} }
if !t.DisableArtNet {
go t.startArtNetListener(ctx)
}
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() defer ticker.Stop()
@@ -298,7 +293,7 @@ func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) {
go t.listenMDNS(ctx, iface) go t.listenMDNS(ctx, iface)
} }
if !t.DisableArtNet { if !t.DisableArtNet {
go t.startArtNetPoller(ctx, iface) go t.startArtNet(ctx, iface)
} }
if !t.DisableSACN { if !t.DisableSACN {
go t.startSACNDiscoveryListener(ctx, iface) go t.startSACNDiscoveryListener(ctx, iface)