Merge branch 'main' of github.com:gopatchy/tendrils
This commit is contained in:
35
artmap.go
35
artmap.go
@@ -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()
|
||||||
|
|||||||
@@ -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,17 +163,17 @@ 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 });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
error = 'Node is not a source or destination for universe ' + universe;
|
error = 'Node is not a source or destination for universe ' + universeDisplay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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) {
|
||||||
@@ -181,7 +183,7 @@ export function showFlowView(flowSpec) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!error && paths.length === 0) error = 'No active flows for universe ' + universe;
|
if (!error && paths.length === 0) error = 'No active flows for universe ' + universeDisplay;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error = 'Unknown protocol: ' + protocol;
|
error = 'Unknown protocol: ' + protocol;
|
||||||
@@ -344,31 +346,41 @@ 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 fromMatches = m.from.protocol === flowProtocol && m.from.universe === flowUniverse;
|
||||||
|
const target = fromMatches ? m.to : m.from;
|
||||||
|
const targetValid = hasValidFlow(nodesByTypeId, target.protocol, target.universe);
|
||||||
|
const leftIcon = document.createElement('span');
|
||||||
|
leftIcon.className = 'flow-validity-icon left';
|
||||||
|
const rightIcon = document.createElement('span');
|
||||||
|
rightIcon.className = 'flow-validity-icon right';
|
||||||
|
const activeIcon = fromMatches ? rightIcon : leftIcon;
|
||||||
|
if (targetValid) {
|
||||||
|
activeIcon.textContent = '●';
|
||||||
|
activeIcon.classList.add('valid');
|
||||||
|
} else {
|
||||||
|
activeIcon.textContent = '⊘';
|
||||||
|
activeIcon.classList.add('invalid');
|
||||||
|
}
|
||||||
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(leftIcon);
|
||||||
mappingEl.appendChild(fromSpan);
|
mappingEl.appendChild(fromSpan);
|
||||||
mappingEl.appendChild(arrowSpan);
|
mappingEl.appendChild(arrowSpan);
|
||||||
mappingEl.appendChild(toSpan);
|
mappingEl.appendChild(toSpan);
|
||||||
|
mappingEl.appendChild(rightIcon);
|
||||||
mappingEl.addEventListener('click', (e) => {
|
mappingEl.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const fromBase = m.from.split(':').slice(0, 2).join(':');
|
openFlowHash(target.protocol, target.universe, nodeName);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
mappingsEl.appendChild(mappingEl);
|
mappingsEl.appendChild(mappingEl);
|
||||||
});
|
});
|
||||||
@@ -388,14 +400,31 @@ 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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasValidFlow(nodesByTypeId, protocol, universe) {
|
||||||
|
let hasSources = false;
|
||||||
|
let hasDests = false;
|
||||||
|
for (const node of nodesByTypeId.values()) {
|
||||||
|
if (protocol === 'sacn') {
|
||||||
|
if ((node.sacn_outputs || []).includes(universe)) hasSources = true;
|
||||||
|
const groups = node.multicast_groups || [];
|
||||||
|
const unicastInputs = node.sacn_unicast_inputs || [];
|
||||||
|
if (groups.some(g => g === 'sacn:' + universe) || unicastInputs.includes(universe)) hasDests = true;
|
||||||
|
} else if (protocol === 'artnet') {
|
||||||
|
if ((node.artnet_inputs || []).includes(universe)) hasSources = true;
|
||||||
|
if ((node.artnet_outputs || []).includes(universe)) hasDests = true;
|
||||||
|
}
|
||||||
|
if (hasSources && hasDests) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
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';
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
|
|||||||
@@ -982,7 +982,7 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper {
|
|||||||
|
|
||||||
.artmap-mapping {
|
.artmap-mapping {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1em 1fr auto 1fr 1em;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #aaf;
|
color: #aaf;
|
||||||
@@ -991,6 +991,26 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.artmap-mapping .flow-validity-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artmap-mapping .flow-validity-icon.left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artmap-mapping .flow-validity-icon.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artmap-mapping .flow-validity-icon.valid {
|
||||||
|
color: #4c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artmap-mapping .flow-validity-icon.invalid {
|
||||||
|
color: #e44;
|
||||||
|
}
|
||||||
|
|
||||||
.artmap-mapping .from {
|
.artmap-mapping .from {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -1004,6 +1024,14 @@ body.sacn-mode .node.sacn-out.sacn-in .sacn-in-hover .sacn-detail-wrapper {
|
|||||||
color: #ccf;
|
color: #ccf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.artmap-mapping:hover .flow-validity-icon.valid {
|
||||||
|
color: #6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artmap-mapping:hover .flow-validity-icon.invalid {
|
||||||
|
color: #f66;
|
||||||
|
}
|
||||||
|
|
||||||
.node.has-error {
|
.node.has-error {
|
||||||
box-shadow: 0 0 0 3px #f66;
|
box-shadow: 0 0 0 3px #f66;
|
||||||
}
|
}
|
||||||
|
|||||||
13
types.go
13
types.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user