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 longName string
inputUnivs []Universe inputUnivs []Universe
outputUnivs []Universe outputUnivs []Universe
pollTargets []*net.UDPAddr
done chan struct{} done chan struct{}
onChange func(*Node) onChange func(*Node)
lastPollHeard time.Time lastPollHeard time.Time
pollMu sync.Mutex pollMu sync.Mutex
} }
func NewDiscovery(sender *Sender, shortName, longName string, inputUnivs, outputUnivs []Universe, pollTargets []*net.UDPAddr) *Discovery { func NewDiscovery(sender *Sender, localIP, broadcast net.IP, localMAC net.HardwareAddr, shortName, longName string, inputUnivs, outputUnivs []Universe) *Discovery {
return &Discovery{ d := &Discovery{
sender: sender, sender: sender,
nodes: map[string]*Node{}, nodes: map[string]*Node{},
broadcast: broadcast,
shortName: shortName, shortName: shortName,
longName: longName, longName: longName,
inputUnivs: inputUnivs, inputUnivs: inputUnivs,
outputUnivs: outputUnivs, outputUnivs: outputUnivs,
pollTargets: pollTargets,
done: make(chan struct{}), 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() { func (d *Discovery) Start() {
d.detectInterface()
go d.pollLoop() go d.pollLoop()
} }
@@ -89,16 +94,12 @@ func (d *Discovery) pollLoop() {
func (d *Discovery) sendPolls() { func (d *Discovery) sendPolls() {
d.pollMu.Lock() d.pollMu.Lock()
lastHeard := d.lastPollHeard defer d.pollMu.Unlock()
d.pollMu.Unlock()
if time.Since(lastHeard) < 15*time.Second { if time.Since(d.lastPollHeard) < 15*time.Second {
return return
} }
d.sender.SendPoll(&net.UDPAddr{IP: d.broadcast, Port: Port})
for _, target := range d.pollTargets {
d.sender.SendPoll(target)
}
} }
func (d *Discovery) cleanup() { func (d *Discovery) cleanup() {
@@ -155,12 +156,9 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
} }
func (d *Discovery) HandlePoll(src *net.UDPAddr) { func (d *Discovery) HandlePoll(src *net.UDPAddr) {
localIP := net.IP(d.localIP[:]) d.pollMu.Lock()
if !src.IP.Equal(localIP) { d.lastPollHeard = time.Now()
d.pollMu.Lock() d.pollMu.Unlock()
d.lastPollHeard = time.Now()
d.pollMu.Unlock()
}
if d.receiver == nil { if d.receiver == nil {
return return
@@ -217,57 +215,6 @@ func (d *Discovery) GetAllNodes() []*Node {
return result 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 { func containsUniverse(slice []Universe, val Universe) bool {
for _, v := range slice { for _, v := range slice {
if v == val { if v == val {

View File

@@ -1,7 +1,9 @@
package artnet package artnet
import ( import (
"context"
"net" "net"
"syscall"
"time" "time"
) )
@@ -34,6 +36,29 @@ func NewDefaultReceiver(handler Handler) (*Receiver, error) {
return NewReceiver(&net.UDPAddr{Port: Port}, handler) 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() { func (r *Receiver) Start() {
go r.loop() go r.loop()
} }

View File

@@ -1,8 +1,10 @@
package artnet package artnet
import ( import (
"context"
"net" "net"
"sync" "sync"
"syscall"
) )
type Sender struct { 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 { func (s *Sender) SendDMX(addr *net.UDPAddr, universe Universe, data []byte) error {
s.seqMu.Lock() s.seqMu.Lock()
seq := s.sequences[universe] seq := s.sequences[universe]