Files
artnet/discovery.go

236 lines
4.7 KiB
Go
Raw Normal View History

2026-01-28 10:27:25 -08:00
package artnet
import (
"net"
"sort"
2026-01-28 10:27:25 -08:00
"sync"
"time"
)
type Node struct {
IP net.IP
Port uint16
MAC net.HardwareAddr
ShortName string
LongName string
Inputs []Universe
Outputs []Universe
LastSeen time.Time
}
type Discovery struct {
sender *Sender
receiver *Receiver
nodes map[string]*Node
nodesMu sync.RWMutex
localIP [4]byte
localMAC [6]byte
broadcast net.IP
shortName string
longName string
inputUnivs []Universe
outputUnivs []Universe
done chan struct{}
onChange func(*Node)
lastPollHeard time.Time
pollMu sync.Mutex
2026-01-28 10:27:25 -08:00
}
func NewDiscovery(sender *Sender, localIP, broadcast net.IP, localMAC net.HardwareAddr, shortName, longName string, inputUnivs, outputUnivs []Universe) *Discovery {
d := &Discovery{
2026-01-28 10:27:25 -08:00
sender: sender,
nodes: map[string]*Node{},
broadcast: broadcast,
2026-01-28 10:27:25 -08:00
shortName: shortName,
longName: longName,
inputUnivs: inputUnivs,
outputUnivs: outputUnivs,
done: make(chan struct{}),
}
if ip4 := localIP.To4(); ip4 != nil {
copy(d.localIP[:], ip4)
}
if len(localMAC) == 6 {
copy(d.localMAC[:], localMAC)
}
return d
2026-01-28 10:27:25 -08:00
}
func (d *Discovery) Start() {
go d.pollLoop()
}
func (d *Discovery) Stop() {
close(d.done)
}
func (d *Discovery) SetReceiver(r *Receiver) {
d.receiver = r
}
func (d *Discovery) SetOnChange(fn func(*Node)) {
d.onChange = fn
}
func (d *Discovery) pollLoop() {
d.sendPolls()
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
cleanupTicker := time.NewTicker(30 * time.Second)
defer cleanupTicker.Stop()
for {
select {
case <-d.done:
return
case <-ticker.C:
d.sendPolls()
case <-cleanupTicker.C:
d.cleanup()
}
}
}
func (d *Discovery) sendPolls() {
d.pollMu.Lock()
defer d.pollMu.Unlock()
if time.Since(d.lastPollHeard) < 15*time.Second {
return
}
d.sender.SendPoll(&net.UDPAddr{IP: d.broadcast, Port: Port})
2026-01-28 10:27:25 -08:00
}
func (d *Discovery) cleanup() {
d.nodesMu.Lock()
defer d.nodesMu.Unlock()
cutoff := time.Now().Add(-60 * time.Second)
for ip, node := range d.nodes {
if node.LastSeen.Before(cutoff) {
delete(d.nodes, ip)
}
}
}
func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
d.nodesMu.Lock()
defer d.nodesMu.Unlock()
ip := src.IP.String()
localIP := net.IP(d.localIP[:])
if src.IP.Equal(localIP) {
return
}
node, exists := d.nodes[ip]
if !exists {
node = &Node{
IP: src.IP,
Port: pkt.Port,
2026-01-28 10:27:25 -08:00
}
d.nodes[ip] = node
}
node.ShortName = pkt.GetShortName()
node.LongName = pkt.GetLongName()
node.MAC = pkt.MACAddr()
node.LastSeen = time.Now()
for _, u := range pkt.InputUniverses() {
if !containsUniverse(node.Inputs, u) {
node.Inputs = append(node.Inputs, u)
}
}
for _, u := range pkt.OutputUniverses() {
if !containsUniverse(node.Outputs, u) {
node.Outputs = append(node.Outputs, u)
}
}
if d.onChange != nil {
d.onChange(node)
}
}
func (d *Discovery) HandlePoll(src *net.UDPAddr) {
d.pollMu.Lock()
d.lastPollHeard = time.Now()
d.pollMu.Unlock()
2026-01-28 10:27:25 -08:00
if d.receiver == nil {
return
}
dst := &net.UDPAddr{IP: d.broadcast, Port: Port}
d.sendPollReplies(dst, d.inputUnivs, true)
d.sendPollReplies(dst, d.outputUnivs, false)
}
func (d *Discovery) sendPollReplies(dst *net.UDPAddr, universes []Universe, isInput bool) {
groups := map[uint16][]Universe{}
for _, u := range universes {
key := uint16(u.Net())<<8 | uint16(u.SubNet())<<4
groups[key] = append(groups[key], u)
}
keys := make([]uint16, 0, len(groups))
for k := range groups {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
for _, key := range keys {
univs := groups[key]
sort.Slice(univs, func(i, j int) bool { return univs[i] < univs[j] })
2026-01-28 10:27:25 -08:00
for i := 0; i < len(univs); i += 4 {
end := i + 4
if end > len(univs) {
end = len(univs)
}
chunk := univs[i:end]
pkt := BuildPollReplyPacket(d.localIP, d.localMAC, d.shortName, d.longName, chunk, isInput)
d.receiver.SendTo(pkt, dst)
time.Sleep(10 * time.Millisecond)
2026-01-28 10:27:25 -08:00
}
}
}
func (d *Discovery) GetNodesForUniverse(universe Universe) []*Node {
d.nodesMu.RLock()
defer d.nodesMu.RUnlock()
var result []*Node
for _, node := range d.nodes {
for _, u := range node.Outputs {
if u == universe {
result = append(result, node)
break
}
}
}
return result
}
func (d *Discovery) GetAllNodes() []*Node {
d.nodesMu.RLock()
defer d.nodesMu.RUnlock()
result := make([]*Node, 0, len(d.nodes))
for _, node := range d.nodes {
result = append(result, node)
}
return result
}
func containsUniverse(slice []Universe, val Universe) bool {
for _, v := range slice {
if v == val {
return true
}
}
return false
}