Display Art-Net universes as x:y:z (n) format consistently

This commit is contained in:
Ian Gulliver
2026-02-04 09:27:46 -08:00
parent 8769afd17d
commit 08e8a523d0
6 changed files with 60 additions and 52 deletions

View File

@@ -85,8 +85,17 @@ func (t *Tendrils) processArtmapConfig(cfg *artmapConfig, artmapNode *Node) {
mappings := make([]ArtmapMapping, len(cfg.Mappings)) mappings := make([]ArtmapMapping, len(cfg.Mappings))
for i, m := range cfg.Mappings { for i, m := range cfg.Mappings {
mappings[i] = ArtmapMapping{ mappings[i] = ArtmapMapping{
From: formatArtmapAddr(m.From), From: ArtmapAddr{
To: formatArtmapToAddr(m.To), 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) t.nodes.UpdateArtmapMappings(artmapNode, mappings)
@@ -165,28 +174,6 @@ 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) { func (n *Nodes) UpdateArtmapMappings(node *Node, mappings []ArtmapMapping) {
n.mu.Lock() n.mu.Lock()

View File

@@ -1,5 +1,6 @@
import { getShortLabel, getFirstName, isSwitch, findInterface } from './nodes.js'; import { getShortLabel, getFirstName, isSwitch, findInterface } from './nodes.js';
import { flowViewData, currentMode, currentView } from './state.js'; import { flowViewData, currentMode, currentView } from './state.js';
import { formatUniverse, formatArtmapAddr } from './format.js';
function scrollToNode(typeid) { function scrollToNode(typeid) {
const nodeEl = document.querySelector('.node[data-id="' + typeid + '"]'); const nodeEl = document.querySelector('.node[data-id="' + typeid + '"]');
@@ -124,6 +125,7 @@ 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';
const universeDisplay = formatUniverse(universe, protocol);
flowUniverse = universe; flowUniverse = universe;
flowProtocol = protocol; flowProtocol = protocol;
if (isNaN(universe)) { error = 'Invalid universe'; } if (isNaN(universe)) { error = 'Invalid universe'; }
@@ -152,7 +154,7 @@ export function showFlowView(flowSpec) {
const isDest = destIds.includes(clickedNodeId); const isDest = destIds.includes(clickedNodeId);
if (isSource) { if (isSource) {
const destNames = destIds.filter(id => id !== clickedNodeId).map(id => getFirstName(nodesByTypeId.get(id))).join(', '); 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 => { destIds.forEach(destId => {
if (destId !== clickedNodeId) { if (destId !== clickedNodeId) {
const path = findPath(graph, clickedNodeId, destId); const path = findPath(graph, clickedNodeId, destId);
@@ -161,7 +163,7 @@ export function showFlowView(flowSpec) {
}); });
} else if (isDest) { } else if (isDest) {
const sourceNames = sourceIds.map(id => getFirstName(nodesByTypeId.get(id))).join(', '); 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 => { sourceIds.forEach(sourceId => {
const path = findPath(graph, sourceId, clickedNodeId); const path = findPath(graph, sourceId, clickedNodeId);
if (path) paths.push({ path, sourceId, destId: clickedNodeId }); if (path) paths.push({ path, sourceId, destId: clickedNodeId });
@@ -171,7 +173,7 @@ export function showFlowView(flowSpec) {
} }
} }
} else { } else {
title = protoName + ' Universe ' + universe; title = protoName + ' Universe ' + universeDisplay;
sourceIds.forEach(sourceId => { sourceIds.forEach(sourceId => {
destIds.forEach(destId => { destIds.forEach(destId => {
if (sourceId !== destId) { if (sourceId !== destId) {
@@ -344,31 +346,26 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc
mappingsEl = document.createElement('div'); mappingsEl = document.createElement('div');
mappingsEl.className = 'flow-artmap-mappings'; mappingsEl.className = 'flow-artmap-mappings';
if (isSourceNode) mappingsEl.classList.add('before-node'); if (isSourceNode) mappingsEl.classList.add('before-node');
const currentPrefix = flowProtocol + ':' + flowUniverse;
const nodeName = getFirstName(node); const nodeName = getFirstName(node);
relevantMappings.forEach(m => { relevantMappings.forEach(m => {
const mappingEl = document.createElement('div'); const mappingEl = document.createElement('div');
mappingEl.className = 'artmap-mapping'; mappingEl.className = 'artmap-mapping';
const fromSpan = document.createElement('span'); const fromSpan = document.createElement('span');
fromSpan.className = 'from'; fromSpan.className = 'from';
fromSpan.textContent = m.from; fromSpan.textContent = formatArtmapAddr(m.from);
const arrowSpan = document.createElement('span'); const arrowSpan = document.createElement('span');
arrowSpan.textContent = '→'; arrowSpan.textContent = '→';
const toSpan = document.createElement('span'); const toSpan = document.createElement('span');
toSpan.className = 'to'; toSpan.className = 'to';
toSpan.textContent = m.to; toSpan.textContent = formatArtmapAddr(m.to);
mappingEl.appendChild(fromSpan); mappingEl.appendChild(fromSpan);
mappingEl.appendChild(arrowSpan); mappingEl.appendChild(arrowSpan);
mappingEl.appendChild(toSpan); mappingEl.appendChild(toSpan);
mappingEl.addEventListener('click', (e) => { mappingEl.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const fromBase = m.from.split(':').slice(0, 2).join(':'); const fromMatches = m.from.protocol === flowProtocol && m.from.universe === flowUniverse;
const target = fromBase === currentPrefix ? m.to : m.from; const target = fromMatches ? m.to : m.from;
const targetProto = target.split(':')[0]; openFlowHash(target.protocol, target.universe, nodeName);
const targetUniverse = parseInt(target.split(':')[1], 10);
if (!isNaN(targetUniverse)) {
openFlowHash(targetProto, targetUniverse, nodeName);
}
}); });
mappingsEl.appendChild(mappingEl); mappingsEl.appendChild(mappingEl);
}); });
@@ -388,11 +385,10 @@ export function renderFlowPath(pathInfo, nodesByTypeId, flowUniverse, flowProtoc
} }
function getRelevantMappings(mappings, protocol, universe) { function getRelevantMappings(mappings, protocol, universe) {
const prefix = protocol + ':' + universe;
return mappings.filter(m => { return mappings.filter(m => {
const fromBase = m.from.split(':').slice(0, 2).join(':'); const fromMatches = m.from.protocol === protocol && m.from.universe === universe;
const toBase = m.to.split(':').slice(0, 2).join(':'); const toMatches = m.to.protocol === protocol && m.to.universe === universe;
return fromBase === prefix || toBase === prefix; return fromMatches || toMatches;
}); });
} }

View File

@@ -26,11 +26,29 @@ export function formatLinkSpeed(bps) {
return mbps.toLocaleString() + ' Mbit/s'; return mbps.toLocaleString() + ' Mbit/s';
} }
export function formatUniverse(u) { export function formatUniverse(u, protocol) {
const net = (u >> 8) & 0x7f; if (protocol === 'artnet') {
const subnet = (u >> 4) & 0x0f; const net = (u >> 8) & 0x7f;
const universe = u & 0x0f; const subnet = (u >> 4) & 0x0f;
return net + ':' + subnet + ':' + universe + ' (' + u + ')'; 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) { export function escapeHtml(str) {

View File

@@ -248,7 +248,7 @@ export function render(data, config) {
const inputs = sortedInputs.map(u => { const inputs = sortedInputs.map(u => {
const sources = collapseNames(universeOutputs.get(u) || []); const sources = collapseNames(universeOutputs.get(u) || []);
const uniStr = formatUniverse(u); const uniStr = formatUniverse(u, 'artnet');
if (sources.length > 0) { if (sources.length > 0) {
return { display: sources[0] + ' [' + uniStr + ']', firstTarget: sources[0], universe: u }; 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 outputs = sortedOutputs.map(u => {
const dests = collapseNames(universeInputs.get(u) || []); const dests = collapseNames(universeInputs.get(u) || []);
const uniStr = formatUniverse(u); const uniStr = formatUniverse(u, 'artnet');
if (dests.length > 0) { if (dests.length > 0) {
return { display: dests[0] + ' [' + uniStr + ']', firstTarget: dests[0], universe: u }; return { display: dests[0] + ' [' + uniStr + ']', firstTarget: dests[0], universe: u };
} }

View File

@@ -395,7 +395,7 @@ export function renderArtnetTable() {
for (let i = 0; i < maxLen; i++) { for (let i = 0; i < maxLen; i++) {
rows.push({ rows.push({
universe: u, universe: u,
universeStr: formatUniverse(u), universeStr: formatUniverse(u, 'artnet'),
tx: txNodes[i]?.name || '', tx: txNodes[i]?.name || '',
txTitle: txNodes[i]?.title || '', txTitle: txNodes[i]?.title || '',
rx: rxNodes[i]?.name || '', rx: rxNodes[i]?.name || '',

View File

@@ -43,7 +43,7 @@ func (u ArtNetUniverse) Universe() int {
} }
func (u ArtNetUniverse) String() string { 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 type ArtNetUniverseSet map[ArtNetUniverse]time.Time
@@ -109,8 +109,15 @@ func (s SACNUniverseSet) MarshalJSON() ([]byte, error) {
} }
type ArtmapMapping struct { type ArtmapMapping struct {
From string `json:"from"` From ArtmapAddr `json:"from"`
To string `json:"to"` 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 type MulticastGroupID int