Store and display artmap mappings on nodes in flow view

This commit is contained in:
Ian Gulliver
2026-01-30 22:59:58 -08:00
parent d63b8192d2
commit 587049616b
4 changed files with 115 additions and 7 deletions

View File

@@ -74,11 +74,25 @@ func (t *Tendrils) probeArtmap(ip net.IP) {
log.Printf("[artmap] found ip=%s targets=%d mappings=%d", ip, len(cfg.Targets), len(cfg.Mappings)) log.Printf("[artmap] found ip=%s targets=%d mappings=%d", ip, len(cfg.Targets), len(cfg.Mappings))
} }
t.processArtmapConfig(&cfg) artmapNode := t.nodes.GetByIP(ip)
t.processArtmapConfig(&cfg, artmapNode)
} }
func (t *Tendrils) processArtmapConfig(cfg *artmapConfig) { func (t *Tendrils) processArtmapConfig(cfg *artmapConfig, artmapNode *Node) {
updated := false updated := false
if artmapNode != nil && len(cfg.Mappings) > 0 {
mappings := make([]ArtmapMapping, len(cfg.Mappings))
for i, m := range cfg.Mappings {
mappings[i] = ArtmapMapping{
From: formatArtmapAddr(m.From),
To: formatArtmapToAddr(m.To),
}
}
t.nodes.UpdateArtmapMappings(artmapNode, mappings)
updated = true
}
for _, target := range cfg.Targets { for _, target := range cfg.Targets {
ip := parseTargetIP(target.Address) ip := parseTargetIP(target.Address)
if ip == nil { if ip == nil {
@@ -152,3 +166,32 @@ func parseTargetIP(addr string) net.IP {
} }
return net.ParseIP(host) return net.ParseIP(host)
} }
func formatArtmapAddr(a artmapFromAddr) string {
u := formatArtmapUniverse(a.Universe)
if a.ChannelStart == 1 && a.ChannelEnd == 512 {
return u
}
if a.ChannelStart == a.ChannelEnd {
return fmt.Sprintf("%s:%d", u, a.ChannelStart)
}
return fmt.Sprintf("%s:%d-%d", u, a.ChannelStart, a.ChannelEnd)
}
func formatArtmapToAddr(a artmapToAddr) string {
u := formatArtmapUniverse(a.Universe)
if a.ChannelStart == 1 {
return u
}
return fmt.Sprintf("%s:%d", u, a.ChannelStart)
}
func formatArtmapUniverse(u artmapUniverse) string {
return fmt.Sprintf("%s:%d", u.Protocol, u.Number)
}
func (n *Nodes) UpdateArtmapMappings(node *Node, mappings []ArtmapMapping) {
n.mu.Lock()
defer n.mu.Unlock()
node.ArtmapMappings = mappings
}

View File

@@ -75,6 +75,7 @@ export function showFlowView(flowSpec) {
const parts = flowSpec.split('/'); const parts = flowSpec.split('/');
const protocol = parts[0]; const protocol = parts[0];
let title = '', paths = [], error = ''; let title = '', paths = [], error = '';
let flowProtocol, flowUniverse;
if (protocol === 'dante') { if (protocol === 'dante') {
if (parts.includes('to')) { if (parts.includes('to')) {
@@ -123,6 +124,8 @@ export function showFlowView(flowSpec) {
const universe = parseInt(parts[1], 10); const universe = parseInt(parts[1], 10);
const sourceIdent = parts[2]; const sourceIdent = parts[2];
const protoName = protocol === 'sacn' ? 'sACN' : 'Art-Net'; const protoName = protocol === 'sacn' ? 'sACN' : 'Art-Net';
flowUniverse = universe;
flowProtocol = protocol;
if (isNaN(universe)) { error = 'Invalid universe'; } if (isNaN(universe)) { error = 'Invalid universe'; }
else { else {
const sourceIds = []; const sourceIds = [];
@@ -182,10 +185,10 @@ export function showFlowView(flowSpec) {
error = 'Unknown protocol: ' + protocol; error = 'Unknown protocol: ' + protocol;
} }
renderFlowOverlay(title, paths, error, nodesByTypeId); renderFlowOverlay(title, paths, error, nodesByTypeId, flowProtocol, flowUniverse);
} }
export function renderFlowOverlay(title, paths, error, nodesByTypeId) { export function renderFlowOverlay(title, paths, error, nodesByTypeId, flowProtocol, flowUniverse) {
let overlay = document.getElementById('flow-overlay'); let overlay = document.getElementById('flow-overlay');
if (!overlay) { if (!overlay) {
overlay = document.createElement('div'); overlay = document.createElement('div');
@@ -224,7 +227,7 @@ export function renderFlowOverlay(title, paths, error, nodesByTypeId) {
} }
if (paths.length === 1) { if (paths.length === 1) {
const pathEl = renderFlowPath(paths[0], nodesByTypeId); const pathEl = renderFlowPath(paths[0], nodesByTypeId, flowUniverse, flowProtocol);
pathEl.addEventListener('click', (e) => e.stopPropagation()); pathEl.addEventListener('click', (e) => e.stopPropagation());
overlay.appendChild(pathEl); overlay.appendChild(pathEl);
} else { } else {
@@ -241,7 +244,7 @@ export function renderFlowOverlay(title, paths, error, nodesByTypeId) {
: paths.length + ' flow paths (click to expand)'; : paths.length + ' flow paths (click to expand)';
}); });
paths.forEach(p => { paths.forEach(p => {
const pathEl = renderFlowPath(p, nodesByTypeId); const pathEl = renderFlowPath(p, nodesByTypeId, flowUniverse, flowProtocol);
pathEl.addEventListener('click', (e) => e.stopPropagation()); pathEl.addEventListener('click', (e) => e.stopPropagation());
listEl.appendChild(pathEl); listEl.appendChild(pathEl);
}); });
@@ -254,7 +257,7 @@ export function renderFlowOverlay(title, paths, error, nodesByTypeId) {
} }
} }
export function renderFlowPath(pathInfo, nodesByTypeId) { export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtocol) {
const { path, sourceId, destId } = pathInfo; const { path, sourceId, destId } = pathInfo;
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'flow-path'; container.className = 'flow-path';
@@ -331,11 +334,43 @@ export function renderFlowPath(pathInfo, nodesByTypeId) {
scrollToNode(step.nodeId); scrollToNode(step.nodeId);
}); });
container.appendChild(nodeEl); container.appendChild(nodeEl);
if (node.artmap_mappings && node.artmap_mappings.length > 0 && flowUniverse !== undefined) {
const relevantMappings = getRelevantMappings(node.artmap_mappings, flowProtocol, flowUniverse);
if (relevantMappings.length > 0) {
const mappingsEl = document.createElement('div');
mappingsEl.className = 'flow-artmap-mappings';
relevantMappings.forEach(m => {
const mappingEl = document.createElement('div');
mappingEl.className = 'artmap-mapping';
mappingEl.textContent = m.from + ' → ' + m.to;
mappingEl.addEventListener('click', (e) => {
e.stopPropagation();
const toProto = m.to.split(':')[0];
const toUniverse = parseInt(m.to.split(':')[1], 10);
if (!isNaN(toUniverse)) {
openFlowHash(toProto, toUniverse);
}
});
mappingsEl.appendChild(mappingEl);
});
container.appendChild(mappingsEl);
}
}
}); });
return container; return container;
} }
function getRelevantMappings(mappings, protocol, universe) {
const prefix = protocol + ':' + universe;
return mappings.filter(m => {
const fromBase = m.from.split(':').slice(0, 2).join(':');
const toBase = m.to.split(':').slice(0, 2).join(':');
return fromBase === prefix || toBase === prefix;
});
}
export function closeFlowView() { export function closeFlowView() {
const overlay = document.getElementById('flow-overlay'); const overlay = document.getElementById('flow-overlay');
if (overlay) overlay.style.display = 'none'; if (overlay) overlay.style.display = 'none';

View File

@@ -847,6 +847,30 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper {
display: flex; display: flex;
} }
.flow-artmap-mappings {
display: flex;
flex-direction: column;
gap: 4px;
margin-left: 20px;
padding: 8px 12px;
background: #1a1a2e;
border-radius: 6px;
border-left: 3px solid #5a5aff;
}
.artmap-mapping {
font-size: 11px;
color: #aaf;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
}
.artmap-mapping:hover {
background: #2a2a4e;
color: #ccf;
}
.node.has-error { .node.has-error {
box-shadow: 0 0 0 3px #f66; box-shadow: 0 0 0 3px #f66;
} }

View File

@@ -99,6 +99,11 @@ func (s SACNUniverseSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Universes()) return json.Marshal(s.Universes())
} }
type ArtmapMapping struct {
From string `json:"from"`
To string `json:"to"`
}
type MulticastGroupID int type MulticastGroupID int
const ( const (
@@ -452,6 +457,7 @@ type Node struct {
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"` ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
SACNUnicastInputs SACNUniverseSet `json:"sacn_unicast_inputs,omitempty"` SACNUnicastInputs SACNUniverseSet `json:"sacn_unicast_inputs,omitempty"`
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"` SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
ArtmapMappings []ArtmapMapping `json:"artmap_mappings,omitempty"`
Unreachable bool `json:"unreachable,omitempty"` Unreachable bool `json:"unreachable,omitempty"`
errors *ErrorTracker errors *ErrorTracker
pollTrigger chan struct{} pollTrigger chan struct{}