diff --git a/http.go b/http.go index 1103899..ce2882d 100644 --- a/http.go +++ b/http.go @@ -29,6 +29,7 @@ type StatusResponse struct { Links []*Link `json:"links"` MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"` ArtNetNodes []*ArtNetNode `json:"artnet_nodes"` + SACNNodes []*SACNNode `json:"sacn_nodes"` DanteFlows []*DanteFlow `json:"dante_flows"` PortErrors []*PortError `json:"port_errors"` UnreachableNodes []string `json:"unreachable_nodes"` @@ -143,6 +144,7 @@ func (t *Tendrils) GetStatus() *StatusResponse { Links: t.getLinks(), MulticastGroups: t.getMulticastGroups(), ArtNetNodes: t.getArtNetNodes(), + SACNNodes: t.getSACNNodes(), DanteFlows: t.getDanteFlows(), PortErrors: t.errors.GetErrors(), UnreachableNodes: t.errors.GetUnreachableNodes(), diff --git a/sacn.go b/sacn.go new file mode 100644 index 0000000..c155d4b --- /dev/null +++ b/sacn.go @@ -0,0 +1,63 @@ +package tendrils + +import ( + "fmt" + "sort" + "strings" + + "github.com/fvbommel/sortorder" +) + +type SACNNode struct { + TypeID string `json:"typeid"` + Node *Node `json:"node"` + Universes []int `json:"universes"` +} + +func (t *Tendrils) getSACNNodes() []*SACNNode { + t.nodes.mu.Lock() + t.nodes.expireMulticastMemberships() + t.nodes.mu.Unlock() + + t.nodes.mu.RLock() + defer t.nodes.mu.RUnlock() + + nodeUniverses := map[*Node][]int{} + + for _, gm := range t.nodes.multicastGroups { + if !strings.HasPrefix(gm.Group.Name, "sacn:") { + continue + } + + var universe int + if _, err := fmt.Sscanf(gm.Group.Name, "sacn:%d", &universe); err != nil { + continue + } + + for _, membership := range gm.Members { + if membership.Node == nil { + continue + } + universes := nodeUniverses[membership.Node] + if !containsInt(universes, universe) { + nodeUniverses[membership.Node] = append(universes, universe) + } + } + } + + result := make([]*SACNNode, 0, len(nodeUniverses)) + for node, universes := range nodeUniverses { + sort.Ints(universes) + result = append(result, &SACNNode{ + TypeID: newTypeID("sacnnode"), + Node: node, + Universes: universes, + }) + } + + sort.Slice(result, func(i, j int) bool { + return sortorder.NaturalLess(result[i].Node.DisplayName(), result[j].Node.DisplayName()) + }) + + return result +} diff --git a/static/index.html b/static/index.html index 02843a2..568734e 100644 --- a/static/index.html +++ b/static/index.html @@ -533,6 +533,90 @@ bottom: 100%; } + body.sacn-mode .node { + opacity: 0.3; + } + + body.sacn-mode .node.sacn-consumer { + opacity: 1; + background: #26d; + } + + body.sacn-mode .node .switch-port, + body.sacn-mode .node .uplink, + body.sacn-mode .node .root-label { + display: none; + } + + .node .sacn-info { + display: none; + position: absolute; + top: -8px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + font-weight: normal; + padding: 1px 6px; + border-radius: 8px; + white-space: nowrap; + background: #444; + color: #fff; + z-index: 10; + } + + .node .sacn-info .sacn-detail { + display: none; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 4px; + font-size: 10px; + white-space: pre; + text-align: left; + background: #333; + border: 1px solid #555; + border-radius: 6px; + padding: 6px 8px; + line-height: 1.4; + } + + .node .sacn-info .sacn-detail::before { + content: ''; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 8px; + } + + .node .sacn-info::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 120px; + } + + .node .sacn-info:hover { + z-index: 100; + } + + .node .sacn-info:hover .sacn-detail { + display: block; + } + + body.sacn-mode .node.sacn-consumer .sacn-info { + display: block; + } + + .sacn-info .lbl, + .sacn-detail .lbl { + color: #888; + } + .node.has-error { box-shadow: 0 0 0 3px #f66; } @@ -590,10 +674,15 @@ display: none; } + body.sacn-mode .node:not(.sacn-consumer):hover .node-info { + display: none; + } + .node:has(.switch-port:hover) .node-info, .node:has(.uplink:hover) .node-info, .node:has(.dante-info:hover) .node-info, - .node:has(.artnet-info:hover) .node-info { + .node:has(.artnet-info:hover) .node-info, + .node:has(.sacn-info:hover) .node-info { display: none; } @@ -797,6 +886,7 @@ +