Add per-interface binding and poll backoff to discovery

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-30 09:13:57 -08:00
parent 5e7400fe51
commit 17ab11b048
3 changed files with 65 additions and 69 deletions

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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]