Add artmap polling to discover sACN unicast receivers
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,7 +87,9 @@
|
|||||||
"Bash(git -C /home/flamingcow/tendrils diff --name-only)",
|
"Bash(git -C /home/flamingcow/tendrils diff --name-only)",
|
||||||
"Bash(git -C /home/flamingcow/artmap diff --name-only)",
|
"Bash(git -C /home/flamingcow/artmap diff --name-only)",
|
||||||
"Bash(git -C /home/flamingcow/sacn diff --name-only)",
|
"Bash(git -C /home/flamingcow/sacn diff --name-only)",
|
||||||
"Bash(git -C /home/flamingcow/multicast diff --name-only)"
|
"Bash(git -C /home/flamingcow/multicast diff --name-only)",
|
||||||
|
"Bash(du:*)",
|
||||||
|
"Bash(tree:*)"
|
||||||
],
|
],
|
||||||
"ask": [
|
"ask": [
|
||||||
"Bash(rm *)"
|
"Bash(rm *)"
|
||||||
|
|||||||
117
artmap.go
Normal file
117
artmap.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package tendrils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type artmapConfig struct {
|
||||||
|
Targets []artmapTarget `json:"targets"`
|
||||||
|
Mappings []artmapMapping `json:"mappings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
url := fmt.Sprintf("http://%s:8080/api/config", ip)
|
||||||
|
|
||||||
|
resp, err := artmapClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
server := resp.Header.Get("Server")
|
||||||
|
if server != "artmap" {
|
||||||
|
if server != "" {
|
||||||
|
log.Printf("[artmap] unexpected server header ip=%s server=%q", ip, server)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg artmapConfig
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||||
|
log.Printf("[artmap] decode error ip=%s: %v", ip, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[artmap] found ip=%s targets=%d mappings=%d", ip, len(cfg.Targets), len(cfg.Mappings))
|
||||||
|
|
||||||
|
t.processArtmapConfig(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) processArtmapConfig(cfg *artmapConfig) {
|
||||||
|
updated := false
|
||||||
|
for _, target := range cfg.Targets {
|
||||||
|
ip := parseTargetIP(target.Address)
|
||||||
|
if ip == nil {
|
||||||
|
log.Printf("[artmap] failed to parse target address %q", target.Address)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
node := t.nodes.GetByIP(ip)
|
||||||
|
if node == nil {
|
||||||
|
log.Printf("[artmap] target ip=%s not found as node", ip)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
universe := int(target.Universe.Number)
|
||||||
|
switch target.Universe.Protocol {
|
||||||
|
case "artnet":
|
||||||
|
t.nodes.UpdateArtNet(node, []int{universe}, nil)
|
||||||
|
log.Printf("[artmap] marked %s (%s) as artnet input for universe %d", node.DisplayName(), ip, universe)
|
||||||
|
case "sacn":
|
||||||
|
t.nodes.UpdateSACNUnicastInputs(node, []int{universe})
|
||||||
|
log.Printf("[artmap] marked %s (%s) as sacn input for universe %d", node.DisplayName(), ip, universe)
|
||||||
|
default:
|
||||||
|
log.Printf("[artmap] unknown protocol %q for target %s", target.Universe.Protocol, target.Address)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ func main() {
|
|||||||
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
|
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
|
||||||
debugYamaha := flag.Bool("debug-yamaha", false, "debug Yamaha discovery")
|
debugYamaha := flag.Bool("debug-yamaha", false, "debug Yamaha discovery")
|
||||||
debugBroadcast := flag.Bool("debug-broadcast", false, "debug broadcast traffic monitoring")
|
debugBroadcast := flag.Bool("debug-broadcast", false, "debug broadcast traffic monitoring")
|
||||||
|
debugArtmap := flag.Bool("debug-artmap", false, "debug artmap polling")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
t := tendrils.New()
|
t := tendrils.New()
|
||||||
@@ -64,5 +65,6 @@ func main() {
|
|||||||
t.DebugShure = *debugShure
|
t.DebugShure = *debugShure
|
||||||
t.DebugYamaha = *debugYamaha
|
t.DebugYamaha = *debugYamaha
|
||||||
t.DebugBroadcast = *debugBroadcast
|
t.DebugBroadcast = *debugBroadcast
|
||||||
|
t.DebugArtmap = *debugArtmap
|
||||||
t.Run()
|
t.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,24 @@ func (n *Nodes) UpdateSACN(node *Node, outputs []int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Nodes) UpdateSACNUnicastInputs(node *Node, inputs []int) {
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
if node.SACNUnicastInputs == nil {
|
||||||
|
node.SACNUnicastInputs = SACNUniverseSet{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range inputs {
|
||||||
|
node.SACNUnicastInputs.Add(SACNUniverse(u))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Nodes) expireSACN() {
|
func (n *Nodes) expireSACN() {
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
|
if node.SACNUnicastInputs != nil {
|
||||||
|
node.SACNUnicastInputs.Expire(60 * time.Second)
|
||||||
|
}
|
||||||
if node.SACNOutputs != nil {
|
if node.SACNOutputs != nil {
|
||||||
node.SACNOutputs.Expire(60 * time.Second)
|
node.SACNOutputs.Expire(60 * time.Second)
|
||||||
}
|
}
|
||||||
@@ -69,15 +85,24 @@ func (n *Nodes) expireSACN() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) mergeSACN(keep, merge *Node) {
|
func (n *Nodes) mergeSACN(keep, merge *Node) {
|
||||||
if merge.SACNOutputs == nil {
|
if merge.SACNUnicastInputs != nil {
|
||||||
return
|
if keep.SACNUnicastInputs == nil {
|
||||||
|
keep.SACNUnicastInputs = SACNUniverseSet{}
|
||||||
|
}
|
||||||
|
for u, lastSeen := range merge.SACNUnicastInputs {
|
||||||
|
if existing, ok := keep.SACNUnicastInputs[u]; !ok || lastSeen.After(existing) {
|
||||||
|
keep.SACNUnicastInputs[u] = lastSeen
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if keep.SACNOutputs == nil {
|
if merge.SACNOutputs != nil {
|
||||||
keep.SACNOutputs = SACNUniverseSet{}
|
if keep.SACNOutputs == nil {
|
||||||
}
|
keep.SACNOutputs = SACNUniverseSet{}
|
||||||
for u, lastSeen := range merge.SACNOutputs {
|
}
|
||||||
if existing, ok := keep.SACNOutputs[u]; !ok || lastSeen.After(existing) {
|
for u, lastSeen := range merge.SACNOutputs {
|
||||||
keep.SACNOutputs[u] = lastSeen
|
if existing, ok := keep.SACNOutputs[u]; !ok || lastSeen.After(existing) {
|
||||||
|
keep.SACNOutputs[u] = lastSeen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ export function showFlowView(flowSpec) {
|
|||||||
if (protocol === 'sacn') {
|
if (protocol === 'sacn') {
|
||||||
if ((node.sacn_outputs || []).includes(universe)) sourceIds.push(node.id);
|
if ((node.sacn_outputs || []).includes(universe)) sourceIds.push(node.id);
|
||||||
const groups = node.multicast_groups || [];
|
const groups = node.multicast_groups || [];
|
||||||
if (groups.some(g => g === 'sacn:' + universe)) destIds.push(node.id);
|
const unicastInputs = node.sacn_unicast_inputs || [];
|
||||||
|
if (groups.some(g => g === 'sacn:' + universe) || unicastInputs.includes(universe)) destIds.push(node.id);
|
||||||
} else {
|
} else {
|
||||||
if ((node.artnet_outputs || []).includes(universe)) sourceIds.push(node.id);
|
if ((node.artnet_outputs || []).includes(universe)) sourceIds.push(node.id);
|
||||||
if ((node.artnet_inputs || []).includes(universe)) destIds.push(node.id);
|
if ((node.artnet_inputs || []).includes(universe)) destIds.push(node.id);
|
||||||
|
|||||||
@@ -210,21 +210,23 @@ export function render(data, config) {
|
|||||||
const sacnUniverseInputs = new Map();
|
const sacnUniverseInputs = new Map();
|
||||||
const sacnUniverseOutputs = new Map();
|
const sacnUniverseOutputs = new Map();
|
||||||
|
|
||||||
function getSacnInputsFromMulticast(node) {
|
function getSacnInputs(node) {
|
||||||
const groups = node.multicast_groups || [];
|
|
||||||
const inputs = [];
|
const inputs = [];
|
||||||
groups.forEach(g => {
|
(node.multicast_groups || []).forEach(g => {
|
||||||
if (typeof g === 'string' && g.startsWith('sacn:')) {
|
if (typeof g === 'string' && g.startsWith('sacn:')) {
|
||||||
const u = parseInt(g.substring(5), 10);
|
const u = parseInt(g.substring(5), 10);
|
||||||
if (!isNaN(u)) inputs.push(u);
|
if (!isNaN(u)) inputs.push(u);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
(node.sacn_unicast_inputs || []).forEach(u => {
|
||||||
|
if (!inputs.includes(u)) inputs.push(u);
|
||||||
|
});
|
||||||
return inputs;
|
return inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
const name = getShortLabel(node);
|
const name = getShortLabel(node);
|
||||||
getSacnInputsFromMulticast(node).forEach(u => {
|
getSacnInputs(node).forEach(u => {
|
||||||
if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []);
|
if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []);
|
||||||
sacnUniverseInputs.get(u).push(name);
|
sacnUniverseInputs.get(u).push(name);
|
||||||
});
|
});
|
||||||
@@ -242,7 +244,7 @@ export function render(data, config) {
|
|||||||
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
const nodeId = node.id;
|
const nodeId = node.id;
|
||||||
const sacnInputs = getSacnInputsFromMulticast(node);
|
const sacnInputs = getSacnInputs(node);
|
||||||
const sacnOutputs = node.sacn_outputs || [];
|
const sacnOutputs = node.sacn_outputs || [];
|
||||||
|
|
||||||
if (sacnInputs.length === 0 && sacnOutputs.length === 0) return;
|
if (sacnInputs.length === 0 && sacnOutputs.length === 0) return;
|
||||||
|
|||||||
@@ -338,6 +338,12 @@ export function renderSacnTable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
(node.sacn_unicast_inputs || []).forEach(u => {
|
||||||
|
if (!rxByUniverse.has(u)) rxByUniverse.set(u, []);
|
||||||
|
if (!rxByUniverse.get(u).includes(name)) {
|
||||||
|
rxByUniverse.get(u).push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const allUniverses = new Set([...txByUniverse.keys(), ...rxByUniverse.keys()]);
|
const allUniverses = new Set([...txByUniverse.keys(), ...rxByUniverse.keys()]);
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type Tendrils struct {
|
|||||||
DebugShure bool
|
DebugShure bool
|
||||||
DebugYamaha bool
|
DebugYamaha bool
|
||||||
DebugBroadcast bool
|
DebugBroadcast bool
|
||||||
|
DebugArtmap bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Tendrils {
|
func New() *Tendrils {
|
||||||
@@ -346,4 +347,8 @@ func (t *Tendrils) pollNode(node *Node) {
|
|||||||
t.probeDanteDevice(ip)
|
t.probeDanteDevice(ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
t.probeArtmap(ip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
types.go
1
types.go
@@ -450,6 +450,7 @@ type Node struct {
|
|||||||
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
|
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
|
||||||
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
|
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
|
||||||
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
|
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
|
||||||
|
SACNUnicastInputs SACNUniverseSet `json:"sacn_unicast_inputs,omitempty"`
|
||||||
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
|
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
|
||||||
Unreachable bool `json:"unreachable,omitempty"`
|
Unreachable bool `json:"unreachable,omitempty"`
|
||||||
errors *ErrorTracker
|
errors *ErrorTracker
|
||||||
|
|||||||
Reference in New Issue
Block a user