2026-01-30 13:03:35 -08:00
|
|
|
package tendrils
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type artmapConfig struct {
|
|
|
|
|
Targets []artmapTarget `json:"targets"`
|
|
|
|
|
Mappings []artmapMapping `json:"mappings"`
|
2026-01-30 22:11:43 -08:00
|
|
|
Senders []artmapSender `json:"senders"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type artmapSender struct {
|
|
|
|
|
Universe artmapUniverse `json:"universe"`
|
|
|
|
|
IP string `json:"ip"`
|
2026-01-30 13:03:35 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type artmapTarget struct {
|
|
|
|
|
Universe artmapUniverse `json:"universe"`
|
|
|
|
|
Address string `json:"address"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type artmapMapping struct {
|
|
|
|
|
From artmapFromAddr `json:"from"`
|
|
|
|
|
To artmapToAddr `json:"to"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type artmapUniverse struct {
|
|
|
|
|
Protocol string `json:"protocol"`
|
|
|
|
|
Number uint16 `json:"number"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type artmapFromAddr struct {
|
|
|
|
|
Universe artmapUniverse `json:"universe"`
|
|
|
|
|
ChannelStart int `json:"channel_start"`
|
|
|
|
|
ChannelEnd int `json:"channel_end"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type artmapToAddr struct {
|
|
|
|
|
Universe artmapUniverse `json:"universe"`
|
|
|
|
|
ChannelStart int `json:"channel_start"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var artmapClient = &http.Client{Timeout: 2 * time.Second}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) probeArtmap(ip net.IP) {
|
2026-01-30 22:31:58 -08:00
|
|
|
url := fmt.Sprintf("http://%s:8080/artmap/api/status", ip)
|
2026-01-30 13:03:35 -08:00
|
|
|
|
|
|
|
|
resp, err := artmapClient.Get(url)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
2026-01-30 15:45:11 -08:00
|
|
|
if resp.Header.Get("Server") != "artmap" {
|
2026-01-30 13:03:35 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var cfg artmapConfig
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
2026-01-30 15:45:11 -08:00
|
|
|
if t.DebugArtmap {
|
|
|
|
|
log.Printf("[artmap] decode error ip=%s: %v", ip, err)
|
|
|
|
|
}
|
2026-01-30 13:03:35 -08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 15:45:11 -08:00
|
|
|
if t.DebugArtmap {
|
|
|
|
|
log.Printf("[artmap] found ip=%s targets=%d mappings=%d", ip, len(cfg.Targets), len(cfg.Mappings))
|
|
|
|
|
}
|
2026-01-30 13:03:35 -08:00
|
|
|
|
|
|
|
|
t.processArtmapConfig(&cfg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *Tendrils) processArtmapConfig(cfg *artmapConfig) {
|
|
|
|
|
updated := false
|
|
|
|
|
for _, target := range cfg.Targets {
|
|
|
|
|
ip := parseTargetIP(target.Address)
|
|
|
|
|
if ip == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := t.nodes.GetByIP(ip)
|
|
|
|
|
if node == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
universe := int(target.Universe.Number)
|
|
|
|
|
switch target.Universe.Protocol {
|
|
|
|
|
case "artnet":
|
|
|
|
|
t.nodes.UpdateArtNet(node, []int{universe}, nil)
|
2026-01-30 15:45:11 -08:00
|
|
|
if t.DebugArtmap {
|
|
|
|
|
log.Printf("[artmap] marked %s (%s) as artnet input for universe %d", node.DisplayName(), ip, universe)
|
|
|
|
|
}
|
2026-01-30 13:03:35 -08:00
|
|
|
case "sacn":
|
|
|
|
|
t.nodes.UpdateSACNUnicastInputs(node, []int{universe})
|
2026-01-30 15:45:11 -08:00
|
|
|
if t.DebugArtmap {
|
|
|
|
|
log.Printf("[artmap] marked %s (%s) as sacn input for universe %d", node.DisplayName(), ip, universe)
|
|
|
|
|
}
|
2026-01-30 13:03:35 -08:00
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
updated = true
|
|
|
|
|
}
|
2026-01-30 22:11:43 -08:00
|
|
|
|
|
|
|
|
for _, sender := range cfg.Senders {
|
|
|
|
|
ip := net.ParseIP(sender.IP)
|
|
|
|
|
if ip == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node := t.nodes.GetByIP(ip)
|
|
|
|
|
if node == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
universe := int(sender.Universe.Number)
|
|
|
|
|
switch sender.Universe.Protocol {
|
|
|
|
|
case "artnet":
|
|
|
|
|
t.nodes.UpdateArtNet(node, nil, []int{universe})
|
|
|
|
|
if t.DebugArtmap {
|
|
|
|
|
log.Printf("[artmap] marked %s (%s) as artnet output for universe %d", node.DisplayName(), ip, universe)
|
|
|
|
|
}
|
|
|
|
|
case "sacn":
|
|
|
|
|
t.nodes.UpdateSACN(node, []int{universe})
|
|
|
|
|
if t.DebugArtmap {
|
|
|
|
|
log.Printf("[artmap] marked %s (%s) as sacn output for universe %d", node.DisplayName(), ip, universe)
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
updated = true
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 13:03:35 -08:00
|
|
|
if updated {
|
|
|
|
|
t.NotifyUpdate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseTargetIP(addr string) net.IP {
|
|
|
|
|
host := addr
|
|
|
|
|
if idx := strings.LastIndex(addr, ":"); idx != -1 {
|
|
|
|
|
h, _, err := net.SplitHostPort(addr)
|
|
|
|
|
if err == nil {
|
|
|
|
|
host = h
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return net.ParseIP(host)
|
|
|
|
|
}
|