From d970d1db86da5cc1ac903a8ab8ba72cd8c57e9c8 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 30 Jan 2026 09:14:04 -0800 Subject: [PATCH] Use artnet library for per-interface discovery Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 8 +- artnet.go | 180 +++++++++++------------------------- go.mod | 2 +- go.sum | 4 +- tendrils.go | 7 +- 5 files changed, 64 insertions(+), 137 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c968866..f291d18 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -66,7 +66,13 @@ "Bash(snmpwalk:*)", "Bash(go work sync:*)", "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": [ "Bash(rm *)" diff --git a/artnet.go b/artnet.go index 1f203ec..5dc53ca 100644 --- a/artnet.go +++ b/artnet.go @@ -13,119 +13,69 @@ import ( "github.com/gopatchy/artnet" ) -func (t *Tendrils) startArtNetListener(ctx context.Context) { - conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: artnet.Port}) - if err != nil { - 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)) - n, src, err := conn.ReadFromUDP(buf) - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - continue - } - continue - } - - t.handleArtNetPacket(src, buf[:n]) - } +type artnetHandler struct { + t *Tendrils + discovery *artnet.Discovery } -func (t *Tendrils) startArtNetPoller(ctx context.Context, iface net.Interface) { +func (h *artnetHandler) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {} + +func (h *artnetHandler) HandlePoll(src *net.UDPAddr, pkt *artnet.PollPacket) { + h.discovery.HandlePoll(src) +} + +func (h *artnetHandler) HandlePollReply(src *net.UDPAddr, pkt *artnet.PollReplyPacket) { + h.discovery.HandlePollReply(src, pkt) +} + +func (t *Tendrils) startArtNet(ctx context.Context, iface net.Interface) { srcIP, broadcast := getInterfaceIPv4(iface) if srcIP == nil || broadcast == nil { return } - sendConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: 0}) - if err != nil { - log.Printf("[ERROR] failed to create artnet send socket on %s: %v", iface.Name, err) - return - } - defer sendConn.Close() - - go t.listenArtNetReplies(ctx, sendConn, iface.Name) - - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - t.sendArtPoll(sendConn, broadcast, iface.Name) - - 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) + sender, err := artnet.NewInterfaceSender(iface.Name) if err != nil { + log.Printf("[ERROR] failed to create artnet sender for %s: %v", iface.Name, err) return } - switch opCode { - case artnet.OpPollReply: - if reply, ok := pkt.(*artnet.PollReplyPacket); ok { - t.handleArtPollReply(src.IP, reply) - } + discovery := artnet.NewDiscovery(sender, srcIP, broadcast, iface.HardwareAddr, "", "", nil, nil) + discovery.SetOnChange(func(node *artnet.Node) { + t.handleArtNetNode(node) + }) + + handler := &artnetHandler{t: t, discovery: discovery} + + receiver, err := artnet.NewInterfaceReceiver(iface.Name, handler) + if err != nil { + log.Printf("[ERROR] failed to create artnet receiver for %s: %v", iface.Name, err) + sender.Close() + return } + + discovery.SetReceiver(receiver) + receiver.Start() + discovery.Start() + + <-ctx.Done() + + discovery.Stop() + receiver.Stop() + sender.Close() } -func (t *Tendrils) handleArtPollReply(srcIP net.IP, pkt *artnet.PollReplyPacket) { - ip := pkt.IP() - mac := pkt.MACAddr() - shortName := pkt.GetShortName() - longName := pkt.GetLongName() +func (t *Tendrils) handleArtNetNode(node *artnet.Node) { + ip := node.IP + mac := node.MAC + shortName := node.ShortName + longName := node.LongName var inputs, outputs []int - for _, u := range pkt.InputUniverses() { + for _, u := range node.Inputs { inputs = append(inputs, int(u)) } - for _, u := range pkt.OutputUniverses() { + for _, u := range node.Outputs { 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") } - node := t.nodes.GetByIP(ip) - if node == nil && mac != nil { - node = t.nodes.GetByMAC(mac) + n := t.nodes.GetByIP(ip) + if n == nil && mac != nil { + n = t.nodes.GetByMAC(mac) } - if node == nil && name != "" { - node = t.nodes.GetOrCreateByName(name) + if n == nil && name != "" { + n = t.nodes.GetOrCreateByName(name) } - if node != nil { - t.nodes.UpdateArtNet(node, inputs, outputs) + if n != nil { + t.nodes.UpdateArtNet(n, inputs, outputs) } -} - -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 + t.NotifyUpdate() } func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) { diff --git a/go.mod b/go.mod index 2fc46ca..a51371e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.6 require ( github.com/fvbommel/sortorder v1.1.0 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/sacn v0.0.0-20260130055955-54a46fbfe1f0 github.com/gosnmp/gosnmp v1.43.2 diff --git a/go.sum b/go.sum index 65ef897..1bc6dfd 100644 --- a/go.sum +++ b/go.sum @@ -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/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 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-20260128203426-0a3e9b1daf66/go.mod h1:V/D32mh1xfK/llCKbrqI2jxw4xL4hf6Ge2yLiIrp9/4= +github.com/gopatchy/artnet v0.0.0-20260130164309-5e7400fe514e h1:KaCtixVKARhtTzqlqWyzXurNACaesAGxjFgnFd3jYT4= +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/go.mod h1:mSeh6GX+fL6SWZYqxYHTdnddvzDx4qsGSBnlGwY5ZsA= github.com/gopatchy/sacn v0.0.0-20260130055955-54a46fbfe1f0 h1:j1uxCRJSu7G8UwlTykIDacbcQY2qoUpZ9t/SBHqLET8= diff --git a/tendrils.go b/tendrils.go index 69fbbbd..55cc0c9 100644 --- a/tendrils.go +++ b/tendrils.go @@ -33,7 +33,6 @@ func getInterfaceIPv4(iface net.Interface) (srcIP, broadcast net.IP) { type Tendrils struct { activeInterfaces map[string]context.CancelFunc nodes *Nodes - artnetConn *net.UDPConn errors *ErrorTracker ping *PingManager broadcast *BroadcastStats @@ -158,10 +157,6 @@ func (t *Tendrils) Run() { go t.pollARP(ctx) } - if !t.DisableArtNet { - go t.startArtNetListener(ctx) - } - ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() @@ -298,7 +293,7 @@ func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) { go t.listenMDNS(ctx, iface) } if !t.DisableArtNet { - go t.startArtNetPoller(ctx, iface) + go t.startArtNet(ctx, iface) } if !t.DisableSACN { go t.startSACNDiscoveryListener(ctx, iface)