Store and display artmap mappings on nodes in flow view
This commit is contained in:
47
artmap.go
47
artmap.go
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
6
types.go
6
types.go
@@ -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{}
|
||||||
|
|||||||
Reference in New Issue
Block a user