From 81796dbdb6b5da6ae06a51027215cbddbf95077b Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 25 Jan 2026 11:28:56 -0800 Subject: [PATCH] Add mDNS SRV linkage for Dante devices and improve UI --- mdns.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++- static/index.html | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/mdns.go b/mdns.go index 6086eeb..915baa6 100644 --- a/mdns.go +++ b/mdns.go @@ -26,6 +26,20 @@ func extractSkaarhojName(s string) string { return strings.ReplaceAll(s[:idx], "\\", "") } +func isNetaudioService(s string) bool { + return strings.Contains(s, "_netaudio-cmc._udp") || strings.Contains(s, "_netaudio-arc._udp") +} + +func extractNetaudioName(s string) string { + for _, suffix := range []string{"._netaudio-cmc._udp", "._netaudio-arc._udp"} { + idx := strings.Index(s, suffix) + if idx > 0 { + return strings.ReplaceAll(s[:idx], "\\", "") + } + } + return "" +} + func (t *Tendrils) listenMDNS(ctx context.Context, iface net.Interface) { addr, err := net.ResolveUDPAddr("udp4", mdnsAddr) if err != nil { @@ -86,6 +100,7 @@ func (t *Tendrils) processMDNSResponse(ifaceName string, srcIP net.IP, msg *dns. aRecords := map[string]net.IP{} srvTargets := map[string]string{} skaarhojNames := map[string]bool{} + netaudioNames := map[string]bool{} for _, rr := range allRecords { switch r := rr.(type) { @@ -112,6 +127,15 @@ func (t *Tendrils) processMDNSResponse(ifaceName string, srcIP net.IP, msg *dns. } } } + if isNetaudioService(r.Hdr.Name) { + name := extractNetaudioName(r.Hdr.Name) + if name != "" { + netaudioNames[name] = true + if target != "" { + srvTargets[name] = target + } + } + } } } @@ -129,7 +153,26 @@ func (t *Tendrils) processMDNSResponse(ifaceName string, srcIP net.IP, msg *dns. t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "skaarhoj") } - if len(skaarhojNames) == 0 { + for name := range netaudioNames { + var ip net.IP + var targetHostname string + if target, ok := srvTargets[name]; ok { + ip = aRecords[target] + targetHostname = strings.TrimSuffix(target, ".local") + } + if ip == nil { + ip = srcIP + } + if t.DebugMDNS { + log.Printf("[mdns] %s: netaudio %s -> %s (target %s)", ifaceName, name, ip, targetHostname) + } + t.nodes.Update(nil, nil, []net.IP{ip}, "", name, "mdns-srv") + if targetHostname != "" && targetHostname != name { + t.nodes.Update(nil, nil, []net.IP{ip}, "", targetHostname, "mdns-srv") + } + } + + if len(skaarhojNames) == 0 && len(netaudioNames) == 0 { for aName, ip := range aRecords { hostname := strings.TrimSuffix(aName, ".local") if hostname != "" && hostname != aName && !strings.Contains(hostname, "in-addr") && !strings.Contains(hostname, "ip6.arpa") { @@ -150,6 +193,8 @@ var mdnsServices = []string{ "_qlab._tcp.local.", "_blackmagic._tcp.local.", "_hyperdeck_ctrl._tcp.local.", + "_netaudio-cmc._udp.local.", + "_netaudio-arc._udp.local.", } func (t *Tendrils) runMDNSQuerier(ctx context.Context, iface net.Interface, conn *net.UDPConn) { diff --git a/static/index.html b/static/index.html index 519ecef..73ad0aa 100644 --- a/static/index.html +++ b/static/index.html @@ -55,6 +55,18 @@ function getLabel(node) { if (node.names && node.names.length > 0) return node.names.join('\n'); + if (node.interfaces && node.interfaces.length > 0) { + const ips = []; + node.interfaces.forEach(iface => { + if (iface.ips) iface.ips.forEach(ip => ips.push(ip)); + }); + if (ips.length > 0) return ips.join('\n'); + const macs = []; + node.interfaces.forEach(iface => { + if (iface.mac) macs.push(iface.mac); + }); + if (macs.length > 0) return macs.join('\n'); + } return '??'; } @@ -298,7 +310,8 @@ id: id, label: getLabel(n), isSwitch: sw, - parent: parent + parent: parent, + rawData: n } }); }); @@ -314,7 +327,8 @@ source: idA, target: idB, sourceLabel: link.interface_a || '', - targetLabel: link.interface_b || '' + targetLabel: link.interface_b || '', + rawData: link } }); }); @@ -414,6 +428,32 @@ layout: { name: 'preset' } }); + cy.on('click', 'node', function(evt) { + const node = evt.target; + const rawData = node.data('rawData'); + if (rawData && !node.data('isLocation')) { + const json = JSON.stringify(rawData, null, 2); + navigator.clipboard.writeText(json).then(() => { + console.log('Copied node data'); + }).catch(err => { + console.error('Copy failed:', err); + }); + } + }); + + cy.on('click', 'edge', function(evt) { + const edge = evt.target; + const rawData = edge.data('rawData'); + if (rawData) { + const json = JSON.stringify(rawData, null, 2); + navigator.clipboard.writeText(json).then(() => { + console.log('Copied link data'); + }).catch(err => { + console.error('Copy failed:', err); + }); + } + }); + doLayout(); }