From 17ab11b048d936f296241d5e16770989e9f38316 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 30 Jan 2026 09:13:57 -0800 Subject: [PATCH] Add per-interface binding and poll backoff to discovery Co-Authored-By: Claude Opus 4.5 --- discovery.go | 85 ++++++++++------------------------------------------ receiver.go | 25 ++++++++++++++++ sender.go | 24 +++++++++++++++ 3 files changed, 65 insertions(+), 69 deletions(-) diff --git a/discovery.go b/discovery.go index 8f6b4a5..72d0bce 100644 --- a/discovery.go +++ b/discovery.go @@ -29,28 +29,33 @@ type Discovery struct { longName string inputUnivs []Universe outputUnivs []Universe - pollTargets []*net.UDPAddr done chan struct{} onChange func(*Node) lastPollHeard time.Time pollMu sync.Mutex } -func NewDiscovery(sender *Sender, shortName, longName string, inputUnivs, outputUnivs []Universe, pollTargets []*net.UDPAddr) *Discovery { - return &Discovery{ +func NewDiscovery(sender *Sender, localIP, broadcast net.IP, localMAC net.HardwareAddr, shortName, longName string, inputUnivs, outputUnivs []Universe) *Discovery { + d := &Discovery{ sender: sender, nodes: map[string]*Node{}, + broadcast: broadcast, shortName: shortName, longName: longName, inputUnivs: inputUnivs, outputUnivs: outputUnivs, - pollTargets: pollTargets, done: make(chan struct{}), } + if ip4 := localIP.To4(); ip4 != nil { + copy(d.localIP[:], ip4) + } + if len(localMAC) == 6 { + copy(d.localMAC[:], localMAC) + } + return d } func (d *Discovery) Start() { - d.detectInterface() go d.pollLoop() } @@ -89,16 +94,12 @@ func (d *Discovery) pollLoop() { func (d *Discovery) sendPolls() { d.pollMu.Lock() - lastHeard := d.lastPollHeard - d.pollMu.Unlock() + defer d.pollMu.Unlock() - if time.Since(lastHeard) < 15*time.Second { + if time.Since(d.lastPollHeard) < 15*time.Second { return } - - for _, target := range d.pollTargets { - d.sender.SendPoll(target) - } + d.sender.SendPoll(&net.UDPAddr{IP: d.broadcast, Port: Port}) } func (d *Discovery) cleanup() { @@ -155,12 +156,9 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) { } func (d *Discovery) HandlePoll(src *net.UDPAddr) { - localIP := net.IP(d.localIP[:]) - if !src.IP.Equal(localIP) { - d.pollMu.Lock() - d.lastPollHeard = time.Now() - d.pollMu.Unlock() - } + d.pollMu.Lock() + d.lastPollHeard = time.Now() + d.pollMu.Unlock() if d.receiver == nil { return @@ -217,57 +215,6 @@ func (d *Discovery) GetAllNodes() []*Node { return result } -func (d *Discovery) SetLocalIP(ip net.IP) { - if ip4 := ip.To4(); ip4 != nil { - copy(d.localIP[:], ip4) - } -} - -func (d *Discovery) detectInterface() { - d.broadcast = net.IPv4bcast - - ifaces, err := net.Interfaces() - if err != nil { - return - } - - for _, iface := range ifaces { - if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { - continue - } - - addrs, err := iface.Addrs() - if err != nil { - continue - } - - for _, addr := range addrs { - ipnet, ok := addr.(*net.IPNet) - if !ok { - continue - } - - ip4 := ipnet.IP.To4() - if ip4 == nil { - continue - } - - copy(d.localIP[:], ip4) - - if len(iface.HardwareAddr) == 6 { - copy(d.localMAC[:], iface.HardwareAddr) - } - - bcast := make(net.IP, 4) - for i := 0; i < 4; i++ { - bcast[i] = ip4[i] | ^ipnet.Mask[i] - } - d.broadcast = bcast - return - } - } -} - func containsUniverse(slice []Universe, val Universe) bool { for _, v := range slice { if v == val { diff --git a/receiver.go b/receiver.go index c213af2..e50ae74 100644 --- a/receiver.go +++ b/receiver.go @@ -1,7 +1,9 @@ package artnet import ( + "context" "net" + "syscall" "time" ) @@ -34,6 +36,29 @@ func NewDefaultReceiver(handler Handler) (*Receiver, error) { return NewReceiver(&net.UDPAddr{Port: Port}, handler) } +func NewInterfaceReceiver(ifaceName string, handler Handler) (*Receiver, error) { + lc := net.ListenConfig{ + Control: func(network, address string, c syscall.RawConn) error { + var err error + c.Control(func(fd uintptr) { + err = syscall.SetsockoptString(int(fd), syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, ifaceName) + }) + return err + }, + } + + conn, err := lc.ListenPacket(context.Background(), "udp4", ":6454") + if err != nil { + return nil, err + } + + return &Receiver{ + conn: conn.(*net.UDPConn), + handler: handler, + done: make(chan struct{}), + }, nil +} + func (r *Receiver) Start() { go r.loop() } diff --git a/sender.go b/sender.go index 7475ed1..5f7032c 100644 --- a/sender.go +++ b/sender.go @@ -1,8 +1,10 @@ package artnet import ( + "context" "net" "sync" + "syscall" ) type Sender struct { @@ -30,6 +32,28 @@ func NewSenderFromConn(conn *net.UDPConn) *Sender { } } +func NewInterfaceSender(ifaceName string) (*Sender, error) { + lc := net.ListenConfig{ + Control: func(network, address string, c syscall.RawConn) error { + var err error + c.Control(func(fd uintptr) { + err = syscall.SetsockoptString(int(fd), syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, ifaceName) + }) + return err + }, + } + + conn, err := lc.ListenPacket(context.Background(), "udp4", ":0") + if err != nil { + return nil, err + } + + return &Sender{ + conn: conn.(*net.UDPConn), + sequences: map[Universe]uint8{}, + }, nil +} + func (s *Sender) SendDMX(addr *net.UDPAddr, universe Universe, data []byte) error { s.seqMu.Lock() seq := s.sequences[universe]