2026-01-28 10:27:25 -08:00
|
|
|
package artnet
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net"
|
|
|
|
|
"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 {
|
2026-01-30 08:43:09 -08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-01-30 09:13:57 -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{},
|
2026-01-30 09:13:57 -08:00
|
|
|
broadcast: broadcast,
|
2026-01-28 10:27:25 -08:00
|
|
|
shortName: shortName,
|
|
|
|
|
longName: longName,
|
|
|
|
|
inputUnivs: inputUnivs,
|
|
|
|
|
outputUnivs: outputUnivs,
|
|
|
|
|
done: make(chan struct{}),
|
|
|
|
|
}
|
2026-01-30 09:13:57 -08:00
|
|
|
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() {
|
2026-01-30 08:43:09 -08:00
|
|
|
d.pollMu.Lock()
|
2026-01-30 09:13:57 -08:00
|
|
|
defer d.pollMu.Unlock()
|
2026-01-30 08:43:09 -08:00
|
|
|
|
2026-01-30 09:13:57 -08:00
|
|
|
if time.Since(d.lastPollHeard) < 15*time.Second {
|
2026-01-30 08:43:09 -08:00
|
|
|
return
|
|
|
|
|
}
|
2026-01-30 09:13:57 -08:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
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) {
|
2026-01-30 09:13:57 -08:00
|
|
|
d.pollMu.Lock()
|
|
|
|
|
d.lastPollHeard = time.Now()
|
|
|
|
|
d.pollMu.Unlock()
|
2026-01-30 08:43:09 -08:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, univs := range groups {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|