From 08e8a523d0151be7b3bb2c035ad5d5b9188cb203 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 4 Feb 2026 09:27:46 -0800 Subject: [PATCH] Display Art-Net universes as x:y:z (n) format consistently --- artmap.go | 35 +++++++++++------------------------ static/js/flow.js | 30 +++++++++++++----------------- static/js/format.js | 28 +++++++++++++++++++++++----- static/js/render.js | 4 ++-- static/js/table.js | 2 +- types.go | 13 ++++++++++--- 6 files changed, 60 insertions(+), 52 deletions(-) diff --git a/artmap.go b/artmap.go index e3fb8a1..1643db3 100644 --- a/artmap.go +++ b/artmap.go @@ -85,8 +85,17 @@ func (t *Tendrils) processArtmapConfig(cfg *artmapConfig, artmapNode *Node) { mappings := make([]ArtmapMapping, len(cfg.Mappings)) for i, m := range cfg.Mappings { mappings[i] = ArtmapMapping{ - From: formatArtmapAddr(m.From), - To: formatArtmapToAddr(m.To), + From: ArtmapAddr{ + Protocol: m.From.Universe.Protocol, + Universe: int(m.From.Universe.Number), + ChannelStart: m.From.ChannelStart, + ChannelEnd: m.From.ChannelEnd, + }, + To: ArtmapAddr{ + Protocol: m.To.Universe.Protocol, + Universe: int(m.To.Universe.Number), + ChannelStart: m.To.ChannelStart, + }, } } t.nodes.UpdateArtmapMappings(artmapNode, mappings) @@ -165,28 +174,6 @@ func parseTargetIP(addr string) net.IP { 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() diff --git a/static/js/flow.js b/static/js/flow.js index f8d2f26..1ba7c12 100644 --- a/static/js/flow.js +++ b/static/js/flow.js @@ -1,5 +1,6 @@ import { getShortLabel, getFirstName, isSwitch, findInterface } from './nodes.js'; import { flowViewData, currentMode, currentView } from './state.js'; +import { formatUniverse, formatArtmapAddr } from './format.js'; function scrollToNode(typeid) { const nodeEl = document.querySelector('.node[data-id="' + typeid + '"]'); @@ -124,6 +125,7 @@ export function showFlowView(flowSpec) { const universe = parseInt(parts[1], 10); const sourceIdent = parts[2]; const protoName = protocol === 'sacn' ? 'sACN' : 'Art-Net'; + const universeDisplay = formatUniverse(universe, protocol); flowUniverse = universe; flowProtocol = protocol; if (isNaN(universe)) { error = 'Invalid universe'; } @@ -152,7 +154,7 @@ export function showFlowView(flowSpec) { const isDest = destIds.includes(clickedNodeId); if (isSource) { const destNames = destIds.filter(id => id !== clickedNodeId).map(id => getFirstName(nodesByTypeId.get(id))).join(', '); - title = protoName + ' ' + universe + ': ' + getFirstName(clickedNode) + ' → ' + (destNames || '?'); + title = protoName + ' ' + universeDisplay + ': ' + getFirstName(clickedNode) + ' → ' + (destNames || '?'); destIds.forEach(destId => { if (destId !== clickedNodeId) { const path = findPath(graph, clickedNodeId, destId); @@ -161,7 +163,7 @@ export function showFlowView(flowSpec) { }); } else if (isDest) { const sourceNames = sourceIds.map(id => getFirstName(nodesByTypeId.get(id))).join(', '); - title = protoName + ' ' + universe + ': ' + (sourceNames || '?') + ' → ' + getFirstName(clickedNode); + title = protoName + ' ' + universeDisplay + ': ' + (sourceNames || '?') + ' → ' + getFirstName(clickedNode); sourceIds.forEach(sourceId => { const path = findPath(graph, sourceId, clickedNodeId); if (path) paths.push({ path, sourceId, destId: clickedNodeId }); @@ -171,7 +173,7 @@ export function showFlowView(flowSpec) { } } } else { - title = protoName + ' Universe ' + universe; + title = protoName + ' Universe ' + universeDisplay; sourceIds.forEach(sourceId => { destIds.forEach(destId => { if (sourceId !== destId) { @@ -344,31 +346,26 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc mappingsEl = document.createElement('div'); mappingsEl.className = 'flow-artmap-mappings'; if (isSourceNode) mappingsEl.classList.add('before-node'); - const currentPrefix = flowProtocol + ':' + flowUniverse; const nodeName = getFirstName(node); relevantMappings.forEach(m => { const mappingEl = document.createElement('div'); mappingEl.className = 'artmap-mapping'; const fromSpan = document.createElement('span'); fromSpan.className = 'from'; - fromSpan.textContent = m.from; + fromSpan.textContent = formatArtmapAddr(m.from); const arrowSpan = document.createElement('span'); arrowSpan.textContent = '→'; const toSpan = document.createElement('span'); toSpan.className = 'to'; - toSpan.textContent = m.to; + toSpan.textContent = formatArtmapAddr(m.to); mappingEl.appendChild(fromSpan); mappingEl.appendChild(arrowSpan); mappingEl.appendChild(toSpan); mappingEl.addEventListener('click', (e) => { e.stopPropagation(); - const fromBase = m.from.split(':').slice(0, 2).join(':'); - const target = fromBase === currentPrefix ? m.to : m.from; - const targetProto = target.split(':')[0]; - const targetUniverse = parseInt(target.split(':')[1], 10); - if (!isNaN(targetUniverse)) { - openFlowHash(targetProto, targetUniverse, nodeName); - } + const fromMatches = m.from.protocol === flowProtocol && m.from.universe === flowUniverse; + const target = fromMatches ? m.to : m.from; + openFlowHash(target.protocol, target.universe, nodeName); }); mappingsEl.appendChild(mappingEl); }); @@ -388,11 +385,10 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc } 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; + const fromMatches = m.from.protocol === protocol && m.from.universe === universe; + const toMatches = m.to.protocol === protocol && m.to.universe === universe; + return fromMatches || toMatches; }); } diff --git a/static/js/format.js b/static/js/format.js index 6d008fd..02548c0 100644 --- a/static/js/format.js +++ b/static/js/format.js @@ -26,11 +26,29 @@ export function formatLinkSpeed(bps) { return mbps.toLocaleString() + ' Mbit/s'; } -export function formatUniverse(u) { - const net = (u >> 8) & 0x7f; - const subnet = (u >> 4) & 0x0f; - const universe = u & 0x0f; - return net + ':' + subnet + ':' + universe + ' (' + u + ')'; +export function formatUniverse(u, protocol) { + if (protocol === 'artnet') { + const net = (u >> 8) & 0x7f; + const subnet = (u >> 4) & 0x0f; + const universe = u & 0x0f; + return net + ':' + subnet + ':' + universe + ' (' + u + ')'; + } + return String(u); +} + +export function formatArtmapAddr(addr) { + const uniStr = formatUniverse(addr.universe, addr.protocol); + let result = addr.protocol + ' ' + uniStr; + if (addr.channel_start && addr.channel_end && !(addr.channel_start === 1 && addr.channel_end === 512)) { + if (addr.channel_start === addr.channel_end) { + result += ' ch' + addr.channel_start; + } else { + result += ' ch' + addr.channel_start + '-' + addr.channel_end; + } + } else if (addr.channel_start && addr.channel_start !== 1) { + result += ' ch' + addr.channel_start; + } + return result; } export function escapeHtml(str) { diff --git a/static/js/render.js b/static/js/render.js index 5a43015..35cf1c3 100644 --- a/static/js/render.js +++ b/static/js/render.js @@ -248,7 +248,7 @@ export function render(data, config) { const inputs = sortedInputs.map(u => { const sources = collapseNames(universeOutputs.get(u) || []); - const uniStr = formatUniverse(u); + const uniStr = formatUniverse(u, 'artnet'); if (sources.length > 0) { return { display: sources[0] + ' [' + uniStr + ']', firstTarget: sources[0], universe: u }; } @@ -256,7 +256,7 @@ export function render(data, config) { }); const outputs = sortedOutputs.map(u => { const dests = collapseNames(universeInputs.get(u) || []); - const uniStr = formatUniverse(u); + const uniStr = formatUniverse(u, 'artnet'); if (dests.length > 0) { return { display: dests[0] + ' [' + uniStr + ']', firstTarget: dests[0], universe: u }; } diff --git a/static/js/table.js b/static/js/table.js index 7687b9e..87017ef 100644 --- a/static/js/table.js +++ b/static/js/table.js @@ -395,7 +395,7 @@ export function renderArtnetTable() { for (let i = 0; i < maxLen; i++) { rows.push({ universe: u, - universeStr: formatUniverse(u), + universeStr: formatUniverse(u, 'artnet'), tx: txNodes[i]?.name || '', txTitle: txNodes[i]?.title || '', rx: rxNodes[i]?.name || '', diff --git a/types.go b/types.go index 792d1c0..ab9e48a 100644 --- a/types.go +++ b/types.go @@ -43,7 +43,7 @@ func (u ArtNetUniverse) Universe() int { } func (u ArtNetUniverse) String() string { - return fmt.Sprintf("%d/%d/%d", u.Net(), u.Subnet(), u.Universe()) + return fmt.Sprintf("%d:%d:%d (%d)", u.Net(), u.Subnet(), u.Universe(), int(u)) } type ArtNetUniverseSet map[ArtNetUniverse]time.Time @@ -109,8 +109,15 @@ func (s SACNUniverseSet) MarshalJSON() ([]byte, error) { } type ArtmapMapping struct { - From string `json:"from"` - To string `json:"to"` + From ArtmapAddr `json:"from"` + To ArtmapAddr `json:"to"` +} + +type ArtmapAddr struct { + Protocol string `json:"protocol"` + Universe int `json:"universe"` + ChannelStart int `json:"channel_start,omitempty"` + ChannelEnd int `json:"channel_end,omitempty"` } type MulticastGroupID int