2026-01-22 23:59:32 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/fvbommel/sortorder"
|
2026-01-28 10:28:06 -08:00
|
|
|
"github.com/gopatchy/artnet"
|
2026-01-22 23:59:32 -08:00
|
|
|
)
|
|
|
|
|
|
2026-01-27 22:57:53 -08:00
|
|
|
func (t *Tendrils) startArtNetListener(ctx context.Context) {
|
2026-01-28 10:28:06 -08:00
|
|
|
conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: artnet.Port})
|
2026-01-22 23:59:32 -08:00
|
|
|
if err != nil {
|
2026-01-27 22:57:53 -08:00
|
|
|
log.Printf("[ERROR] failed to listen artnet: %v", err)
|
2026-01-22 23:59:32 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
2026-01-27 22:57:53 -08:00
|
|
|
t.artnetConn = conn
|
2026-01-22 23:59:32 -08:00
|
|
|
|
|
|
|
|
buf := make([]byte, 65536)
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
|
|
|
|
n, src, err := conn.ReadFromUDP(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
t.handleArtNetPacket(src, buf[:n])
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 22:57:53 -08:00
|
|
|
func (t *Tendrils) startArtNetPoller(ctx context.Context, iface net.Interface) {
|
|
|
|
|
srcIP, broadcast := getInterfaceIPv4(iface)
|
|
|
|
|
if srcIP == nil || broadcast == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendConn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: 0})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("[ERROR] failed to create artnet send socket on %s: %v", iface.Name, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer sendConn.Close()
|
|
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
go t.listenArtNetReplies(ctx, sendConn, iface.Name)
|
|
|
|
|
|
2026-01-27 22:57:53 -08:00
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
|
|
t.sendArtPoll(sendConn, broadcast, iface.Name)
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
t.sendArtPoll(sendConn, broadcast, iface.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
func (t *Tendrils) listenArtNetReplies(ctx context.Context, conn *net.UDPConn, ifaceName string) {
|
|
|
|
|
buf := make([]byte, 1024)
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
|
|
|
|
n, src, err := conn.ReadFromUDP(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
t.handleArtNetPacket(src, buf[:n])
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
func (t *Tendrils) handleArtNetPacket(src *net.UDPAddr, data []byte) {
|
|
|
|
|
opCode, pkt, err := artnet.ParsePacket(data)
|
|
|
|
|
if err != nil {
|
2026-01-22 23:59:32 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
switch opCode {
|
|
|
|
|
case artnet.OpPollReply:
|
|
|
|
|
if reply, ok := pkt.(*artnet.PollReplyPacket); ok {
|
|
|
|
|
t.handleArtPollReply(src.IP, reply)
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
2026-01-28 10:28:06 -08:00
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-28 10:28:06 -08:00
|
|
|
func (t *Tendrils) handleArtPollReply(srcIP net.IP, pkt *artnet.PollReplyPacket) {
|
|
|
|
|
ip := pkt.IP()
|
|
|
|
|
mac := pkt.MACAddr()
|
|
|
|
|
shortName := pkt.GetShortName()
|
|
|
|
|
longName := pkt.GetLongName()
|
2026-01-22 23:59:32 -08:00
|
|
|
|
|
|
|
|
var inputs, outputs []int
|
2026-01-28 10:28:06 -08:00
|
|
|
for _, u := range pkt.InputUniverses() {
|
|
|
|
|
inputs = append(inputs, int(u))
|
|
|
|
|
}
|
|
|
|
|
for _, u := range pkt.OutputUniverses() {
|
|
|
|
|
outputs = append(outputs, int(u))
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.DebugArtNet {
|
2026-01-28 10:28:06 -08:00
|
|
|
log.Printf("[artnet] %s %s short=%q long=%q in=%v out=%v", ip, mac, shortName, longName, inputs, outputs)
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := longName
|
|
|
|
|
if name == "" {
|
|
|
|
|
name = shortName
|
|
|
|
|
}
|
|
|
|
|
if name != "" {
|
|
|
|
|
t.nodes.Update(nil, mac, []net.IP{ip}, "", name, "artnet")
|
|
|
|
|
}
|
2026-01-23 22:09:44 -08:00
|
|
|
|
|
|
|
|
node := t.nodes.GetByIP(ip)
|
|
|
|
|
if node == nil && mac != nil {
|
|
|
|
|
node = t.nodes.GetByMAC(mac)
|
|
|
|
|
}
|
|
|
|
|
if node == nil && name != "" {
|
|
|
|
|
node = t.nodes.GetOrCreateByName(name)
|
|
|
|
|
}
|
|
|
|
|
if node != nil {
|
|
|
|
|
t.nodes.UpdateArtNet(node, inputs, outputs)
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) sendArtPoll(conn *net.UDPConn, broadcast net.IP, ifaceName string) {
|
2026-01-28 10:28:06 -08:00
|
|
|
packet := artnet.BuildPollPacket()
|
|
|
|
|
|
|
|
|
|
_, err := conn.WriteToUDP(packet, &net.UDPAddr{IP: broadcast, Port: artnet.Port})
|
2026-01-22 23:59:32 -08:00
|
|
|
if err != nil {
|
|
|
|
|
if t.DebugArtNet {
|
|
|
|
|
log.Printf("[artnet] %s: failed to send poll: %v", ifaceName, err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.DebugArtNet {
|
|
|
|
|
log.Printf("[artnet] %s: sent poll to %s", ifaceName, broadcast)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsInt(slice []int, val int) bool {
|
|
|
|
|
for _, v := range slice {
|
|
|
|
|
if v == val {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
|
|
|
|
|
n.mu.Lock()
|
|
|
|
|
defer n.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, u := range inputs {
|
|
|
|
|
if !containsInt(node.ArtNetInputs, u) {
|
|
|
|
|
node.ArtNetInputs = append(node.ArtNetInputs, u)
|
2026-01-23 22:24:18 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, u := range outputs {
|
|
|
|
|
if !containsInt(node.ArtNetOutputs, u) {
|
|
|
|
|
node.ArtNetOutputs = append(node.ArtNetOutputs, u)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sort.Ints(node.ArtNetInputs)
|
|
|
|
|
sort.Ints(node.ArtNetOutputs)
|
|
|
|
|
node.artnetLastSeen = time.Now()
|
2026-01-23 22:24:18 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
func (n *Nodes) expireArtNet() {
|
2026-01-22 23:59:32 -08:00
|
|
|
expireTime := time.Now().Add(-60 * time.Second)
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
if !node.artnetLastSeen.IsZero() && node.artnetLastSeen.Before(expireTime) {
|
|
|
|
|
node.ArtNetInputs = nil
|
|
|
|
|
node.ArtNetOutputs = nil
|
|
|
|
|
node.artnetLastSeen = time.Time{}
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
func (n *Nodes) mergeArtNet(keep, merge *Node) {
|
|
|
|
|
for _, u := range merge.ArtNetInputs {
|
|
|
|
|
if !containsInt(keep.ArtNetInputs, u) {
|
|
|
|
|
keep.ArtNetInputs = append(keep.ArtNetInputs, u)
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, u := range merge.ArtNetOutputs {
|
|
|
|
|
if !containsInt(keep.ArtNetOutputs, u) {
|
|
|
|
|
keep.ArtNetOutputs = append(keep.ArtNetOutputs, u)
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
if merge.artnetLastSeen.After(keep.artnetLastSeen) {
|
|
|
|
|
keep.artnetLastSeen = merge.artnetLastSeen
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
sort.Ints(keep.ArtNetInputs)
|
|
|
|
|
sort.Ints(keep.ArtNetOutputs)
|
|
|
|
|
}
|
2026-01-22 23:59:32 -08:00
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
func (n *Nodes) logArtNet() {
|
2026-01-22 23:59:32 -08:00
|
|
|
inputUniverses := map[int][]string{}
|
|
|
|
|
outputUniverses := map[int][]string{}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, node := range n.nodes {
|
|
|
|
|
if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
name := node.DisplayName()
|
2026-01-22 23:59:32 -08:00
|
|
|
if name == "" {
|
2026-01-23 22:09:44 -08:00
|
|
|
name = "??"
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, u := range node.ArtNetInputs {
|
2026-01-22 23:59:32 -08:00
|
|
|
inputUniverses[u] = append(inputUniverses[u], name)
|
|
|
|
|
}
|
2026-01-28 22:15:54 -08:00
|
|
|
for _, u := range node.ArtNetOutputs {
|
2026-01-22 23:59:32 -08:00
|
|
|
outputUniverses[u] = append(outputUniverses[u], name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 22:15:54 -08:00
|
|
|
if len(inputUniverses) == 0 && len(outputUniverses) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 23:59:32 -08:00
|
|
|
var allUniverses []int
|
|
|
|
|
seen := map[int]bool{}
|
|
|
|
|
for u := range inputUniverses {
|
|
|
|
|
if !seen[u] {
|
|
|
|
|
allUniverses = append(allUniverses, u)
|
|
|
|
|
seen[u] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for u := range outputUniverses {
|
|
|
|
|
if !seen[u] {
|
|
|
|
|
allUniverses = append(allUniverses, u)
|
|
|
|
|
seen[u] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sort.Ints(allUniverses)
|
|
|
|
|
|
|
|
|
|
log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses))
|
|
|
|
|
for _, u := range allUniverses {
|
|
|
|
|
ins := inputUniverses[u]
|
|
|
|
|
outs := outputUniverses[u]
|
|
|
|
|
var parts []string
|
|
|
|
|
if len(ins) > 0 {
|
|
|
|
|
sort.Slice(ins, func(i, j int) bool { return sortorder.NaturalLess(ins[i], ins[j]) })
|
2026-01-23 09:47:51 -08:00
|
|
|
parts = append(parts, fmt.Sprintf("in: %s", strings.Join(ins, ", ")))
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
if len(outs) > 0 {
|
|
|
|
|
sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
|
2026-01-23 09:47:51 -08:00
|
|
|
parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
2026-01-28 10:28:06 -08:00
|
|
|
netVal := (u >> 8) & 0x7f
|
2026-01-22 23:59:32 -08:00
|
|
|
subnet := (u >> 4) & 0x0f
|
|
|
|
|
universe := u & 0x0f
|
2026-01-28 10:28:06 -08:00
|
|
|
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; "))
|
2026-01-22 23:59:32 -08:00
|
|
|
}
|
|
|
|
|
}
|