2026-01-28 21:13:22 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-01-28 21:36:46 -08:00
|
|
|
"github.com/gopatchy/sacn"
|
2026-01-28 21:13:22 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) startSACNDiscoveryListener(ctx context.Context, iface net.Interface) {
|
2026-01-28 21:36:46 -08:00
|
|
|
receiver, err := sacn.NewReceiver("")
|
2026-01-28 21:13:22 -08:00
|
|
|
if err != nil {
|
2026-01-28 21:36:46 -08:00
|
|
|
log.Printf("[ERROR] failed to create sacn receiver: %v", err)
|
2026-01-28 21:13:22 -08:00
|
|
|
return
|
|
|
|
|
}
|
2026-01-28 21:36:46 -08:00
|
|
|
defer receiver.Stop()
|
2026-01-28 21:13:22 -08:00
|
|
|
|
2026-01-28 21:36:46 -08:00
|
|
|
if err := receiver.JoinDiscovery(&iface); err != nil {
|
2026-01-28 21:13:22 -08:00
|
|
|
log.Printf("[ERROR] failed to join sacn discovery multicast on %s: %v", iface.Name, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.DebugSACN {
|
|
|
|
|
log.Printf("[sacn] listening for discovery on %s", iface.Name)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 21:36:46 -08:00
|
|
|
receiver.SetHandler(func(src *net.UDPAddr, pkt interface{}) {
|
|
|
|
|
if disc, ok := pkt.(*sacn.DiscoveryPacket); ok {
|
|
|
|
|
t.handleSACNDiscoveryPacket(src.IP, disc)
|
2026-01-28 21:27:35 -08:00
|
|
|
}
|
2026-01-28 21:36:46 -08:00
|
|
|
})
|
2026-01-28 21:27:35 -08:00
|
|
|
|
2026-01-28 21:36:46 -08:00
|
|
|
receiver.Start()
|
|
|
|
|
<-ctx.Done()
|
2026-01-28 21:13:22 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 21:36:46 -08:00
|
|
|
func (t *Tendrils) handleSACNDiscoveryPacket(srcIP net.IP, pkt *sacn.DiscoveryPacket) {
|
2026-01-28 21:13:22 -08:00
|
|
|
if t.DebugSACN {
|
2026-01-28 21:36:46 -08:00
|
|
|
log.Printf("[sacn] discovery from %q cid=%s ip=%s universes=%v", pkt.SourceName, sacn.FormatCID(pkt.CID), srcIP, pkt.Universes)
|
2026-01-28 21:27:35 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 21:36:46 -08:00
|
|
|
if srcIP != nil && pkt.SourceName != "" {
|
|
|
|
|
t.nodes.Update(nil, nil, []net.IP{srcIP}, "", pkt.SourceName, "sacn")
|
2026-01-28 21:13:22 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
node := t.nodes.GetByIP(srcIP)
|
|
|
|
|
if node != nil {
|
|
|
|
|
intUniverses := make([]int, len(pkt.Universes))
|
|
|
|
|
for i, u := range pkt.Universes {
|
|
|
|
|
intUniverses[i] = int(u)
|
|
|
|
|
}
|
|
|
|
|
t.nodes.UpdateSACN(node, intUniverses)
|
|
|
|
|
}
|
2026-01-28 21:13:22 -08:00
|
|
|
t.NotifyUpdate()
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
|
|
|
|
|
func (n *Nodes) UpdateSACN(node *Node, outputs []int) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
2026-01-28 22:36:44 -08:00
|
|
|
if node.SACNOutputs == nil {
|
|
|
|
|
node.SACNOutputs = SACNUniverseSet{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, u := range outputs {
|
|
|
|
|
node.SACNOutputs.Add(SACNUniverse(u))
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) expireSACN() {
|
|
|
|
|
for _, node := range n.nodes {
|
2026-01-28 22:36:44 -08:00
|
|
|
if node.SACNOutputs != nil {
|
|
|
|
|
node.SACNOutputs.Expire(60 * time.Second)
|
2026-01-28 22:15:54 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Nodes) mergeSACN(keep, merge *Node) {
|
2026-01-28 22:36:44 -08:00
|
|
|
if merge.SACNOutputs == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if keep.SACNOutputs == nil {
|
|
|
|
|
keep.SACNOutputs = SACNUniverseSet{}
|
2026-01-28 22:15:54 -08:00
|
|
|
}
|
2026-01-28 22:36:44 -08:00
|
|
|
for u, lastSeen := range merge.SACNOutputs {
|
|
|
|
|
if existing, ok := keep.SACNOutputs[u]; !ok || lastSeen.After(existing) {
|
|
|
|
|
keep.SACNOutputs[u] = lastSeen
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
}
|
|
|
|
|
}
|