Refactor node storage and use proper types for protocol data

- Rename TypeID to ID throughout
- Remove re-derivable data (MACTableSize, SACNInputs now derived)
- Use typed ArtNetUniverse and SACNUniverse with methods
- Store multicast groups with lastSeen tracking in structs
- Remove int indexes in Nodes, use direct node pointers
- Parse multicast groups into typed struct instead of strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-28 22:36:44 -08:00
parent fc5b36cd1c
commit a912d73169
11 changed files with 552 additions and 412 deletions

View File

@@ -182,53 +182,58 @@ func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
if node.ArtNetInputs == nil {
node.ArtNetInputs = ArtNetUniverseSet{}
}
if node.ArtNetOutputs == nil {
node.ArtNetOutputs = ArtNetUniverseSet{}
}
for _, u := range inputs { for _, u := range inputs {
if !containsInt(node.ArtNetInputs, u) { node.ArtNetInputs.Add(ArtNetUniverse(u))
node.ArtNetInputs = append(node.ArtNetInputs, u)
}
} }
for _, u := range outputs { for _, u := range outputs {
if !containsInt(node.ArtNetOutputs, u) { node.ArtNetOutputs.Add(ArtNetUniverse(u))
node.ArtNetOutputs = append(node.ArtNetOutputs, u)
}
} }
sort.Ints(node.ArtNetInputs)
sort.Ints(node.ArtNetOutputs)
node.artnetLastSeen = time.Now()
} }
func (n *Nodes) expireArtNet() { func (n *Nodes) expireArtNet() {
expireTime := time.Now().Add(-60 * time.Second)
for _, node := range n.nodes { for _, node := range n.nodes {
if !node.artnetLastSeen.IsZero() && node.artnetLastSeen.Before(expireTime) { if node.ArtNetInputs != nil {
node.ArtNetInputs = nil node.ArtNetInputs.Expire(60 * time.Second)
node.ArtNetOutputs = nil }
node.artnetLastSeen = time.Time{} if node.ArtNetOutputs != nil {
node.ArtNetOutputs.Expire(60 * time.Second)
} }
} }
} }
func (n *Nodes) mergeArtNet(keep, merge *Node) { func (n *Nodes) mergeArtNet(keep, merge *Node) {
for _, u := range merge.ArtNetInputs { if merge.ArtNetInputs != nil {
if !containsInt(keep.ArtNetInputs, u) { if keep.ArtNetInputs == nil {
keep.ArtNetInputs = append(keep.ArtNetInputs, u) keep.ArtNetInputs = ArtNetUniverseSet{}
}
for u, lastSeen := range merge.ArtNetInputs {
if existing, ok := keep.ArtNetInputs[u]; !ok || lastSeen.After(existing) {
keep.ArtNetInputs[u] = lastSeen
}
} }
} }
for _, u := range merge.ArtNetOutputs { if merge.ArtNetOutputs != nil {
if !containsInt(keep.ArtNetOutputs, u) { if keep.ArtNetOutputs == nil {
keep.ArtNetOutputs = append(keep.ArtNetOutputs, u) keep.ArtNetOutputs = ArtNetUniverseSet{}
}
for u, lastSeen := range merge.ArtNetOutputs {
if existing, ok := keep.ArtNetOutputs[u]; !ok || lastSeen.After(existing) {
keep.ArtNetOutputs[u] = lastSeen
}
} }
} }
if merge.artnetLastSeen.After(keep.artnetLastSeen) {
keep.artnetLastSeen = merge.artnetLastSeen
}
sort.Ints(keep.ArtNetInputs)
sort.Ints(keep.ArtNetOutputs)
} }
func (n *Nodes) logArtNet() { func (n *Nodes) logArtNet() {
inputUniverses := map[int][]string{} inputUniverses := map[ArtNetUniverse][]string{}
outputUniverses := map[int][]string{} outputUniverses := map[ArtNetUniverse][]string{}
for _, node := range n.nodes { for _, node := range n.nodes {
if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 { if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
@@ -238,10 +243,10 @@ func (n *Nodes) logArtNet() {
if name == "" { if name == "" {
name = "??" name = "??"
} }
for _, u := range node.ArtNetInputs { for u := range node.ArtNetInputs {
inputUniverses[u] = append(inputUniverses[u], name) inputUniverses[u] = append(inputUniverses[u], name)
} }
for _, u := range node.ArtNetOutputs { for u := range node.ArtNetOutputs {
outputUniverses[u] = append(outputUniverses[u], name) outputUniverses[u] = append(outputUniverses[u], name)
} }
} }
@@ -250,8 +255,8 @@ func (n *Nodes) logArtNet() {
return return
} }
var allUniverses []int seen := map[ArtNetUniverse]bool{}
seen := map[int]bool{} var allUniverses []ArtNetUniverse
for u := range inputUniverses { for u := range inputUniverses {
if !seen[u] { if !seen[u] {
allUniverses = append(allUniverses, u) allUniverses = append(allUniverses, u)
@@ -264,7 +269,7 @@ func (n *Nodes) logArtNet() {
seen[u] = true seen[u] = true
} }
} }
sort.Ints(allUniverses) sort.Slice(allUniverses, func(i, j int) bool { return allUniverses[i] < allUniverses[j] })
log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses)) log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses))
for _, u := range allUniverses { for _, u := range allUniverses {
@@ -279,9 +284,6 @@ func (n *Nodes) logArtNet() {
sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) }) sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", "))) parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
} }
netVal := (u >> 8) & 0x7f log.Printf("[sigusr1] artnet:%d (%s) %s", u, u.String(), strings.Join(parts, "; "))
subnet := (u >> 4) & 0x0f
universe := u & 0x0f
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; "))
} }
} }

View File

@@ -102,10 +102,13 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
groupName := multicastGroupName(groupIP) group := ParseMulticastGroup(groupIP)
groupKey := group.String()
for _, node := range n.nodes { for _, node := range n.nodes {
if node.DanteTxChannels != "" && containsString(node.MulticastGroups, groupName) { if node.DanteTxChannels != "" && node.MulticastGroups != nil {
return node if _, exists := node.MulticastGroups[groupKey]; exists {
return node
}
} }
} }
return nil return nil
@@ -883,7 +886,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName) log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
} }
if sourceNode == nil { if sourceNode == nil {
sourceNode = t.nodes.GetOrCreateByName(multicastGroupName(groupIP)) sourceNode = t.nodes.GetOrCreateByName(ParseMulticastGroup(groupIP).String())
} }
subscriberNode := t.nodes.GetOrCreateByName(info.Name) subscriberNode := t.nodes.GetOrCreateByName(info.Name)
t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive) t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive)

View File

@@ -15,7 +15,7 @@ const (
type Error struct { type Error struct {
ID string `json:"id"` ID string `json:"id"`
NodeTypeID string `json:"node_typeid"` NodeID string `json:"node_id"`
NodeName string `json:"node_name"` NodeName string `json:"node_name"`
Type string `json:"type"` Type string `json:"type"`
Port string `json:"port,omitempty"` Port string `json:"port,omitempty"`
@@ -88,7 +88,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats
speedBytes := float64(stats.Speed) / 8.0 speedBytes := float64(stats.Speed) / 8.0
utilization := (maxBytesRate / speedBytes) * 100.0 utilization := (maxBytesRate / speedBytes) * 100.0
key := "util:" + node.TypeID + ":" + portName key := "util:" + node.ID + ":" + portName
now := time.Now() now := time.Now()
if utilization < 70.0 { if utilization < 70.0 {
@@ -107,7 +107,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats
e.nextID++ e.nextID++
e.errors[key] = &Error{ e.errors[key] = &Error{
ID: fmt.Sprintf("err-%d", e.nextID), ID: fmt.Sprintf("err-%d", e.nextID),
NodeTypeID: node.TypeID, NodeID: node.ID,
NodeName: node.DisplayName(), NodeName: node.DisplayName(),
Port: portName, Port: portName,
Type: ErrorTypeHighUtilization, Type: ErrorTypeHighUtilization,
@@ -122,7 +122,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
key := node.TypeID + ":" + portName key := node.ID + ":" + portName
baseline := e.baselines[key] baseline := e.baselines[key]
now := time.Now() now := time.Now()
@@ -137,7 +137,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
e.nextID++ e.nextID++
e.errors[key] = &Error{ e.errors[key] = &Error{
ID: fmt.Sprintf("err-%d", e.nextID), ID: fmt.Sprintf("err-%d", e.nextID),
NodeTypeID: node.TypeID, NodeID: node.ID,
NodeName: node.DisplayName(), NodeName: node.DisplayName(),
Port: portName, Port: portName,
Type: ErrorTypeStartup, Type: ErrorTypeStartup,
@@ -172,7 +172,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
e.nextID++ e.nextID++
e.errors[key] = &Error{ e.errors[key] = &Error{
ID: fmt.Sprintf("err-%d", e.nextID), ID: fmt.Sprintf("err-%d", e.nextID),
NodeTypeID: node.TypeID, NodeID: node.ID,
NodeName: node.DisplayName(), NodeName: node.DisplayName(),
Port: portName, Port: portName,
Type: ErrorTypeNew, Type: ErrorTypeNew,
@@ -253,8 +253,8 @@ func (e *ErrorTracker) GetUnreachableNodeSet() map[string]bool {
defer e.mu.RUnlock() defer e.mu.RUnlock()
result := map[string]bool{} result := map[string]bool{}
for nodeTypeID := range e.unreachableNodes { for nodeID := range e.unreachableNodes {
result[nodeTypeID] = true result[nodeID] = true
} }
return result return result
} }
@@ -271,10 +271,10 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
key := "unreachable:" + node.TypeID key := "unreachable:" + node.ID
wasUnreachable := e.unreachableNodes[node.TypeID] wasUnreachable := e.unreachableNodes[node.ID]
e.unreachableNodes[node.TypeID] = true e.unreachableNodes[node.ID] = true
becameUnreachable = !wasUnreachable becameUnreachable = !wasUnreachable
if e.suppressedUnreachable[key] { if e.suppressedUnreachable[key] {
@@ -289,7 +289,7 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr
e.nextID++ e.nextID++
e.errors[key] = &Error{ e.errors[key] = &Error{
ID: fmt.Sprintf("err-%d", e.nextID), ID: fmt.Sprintf("err-%d", e.nextID),
NodeTypeID: node.TypeID, NodeID: node.ID,
NodeName: node.DisplayName(), NodeName: node.DisplayName(),
Type: ErrorTypeUnreachable, Type: ErrorTypeUnreachable,
FirstSeen: now, FirstSeen: now,
@@ -310,12 +310,12 @@ func (e *ErrorTracker) clearUnreachableLocked(node *Node) (changed bool, becameR
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
key := "unreachable:" + node.TypeID key := "unreachable:" + node.ID
delete(e.suppressedUnreachable, key) delete(e.suppressedUnreachable, key)
wasUnreachable := e.unreachableNodes[node.TypeID] wasUnreachable := e.unreachableNodes[node.ID]
delete(e.unreachableNodes, node.TypeID) delete(e.unreachableNodes, node.ID)
becameReachable = wasUnreachable becameReachable = wasUnreachable
if _, exists := e.errors[key]; exists { if _, exists := e.errors[key]; exists {

View File

@@ -230,7 +230,7 @@ func (t *Tendrils) getNodes() []*Node {
for _, node := range t.nodes.nodes { for _, node := range t.nodes.nodes {
n := new(Node) n := new(Node)
*n = *node *n = *node
n.Unreachable = unreachableNodes[node.TypeID] n.Unreachable = unreachableNodes[node.ID]
nodes = append(nodes, n) nodes = append(nodes, n)
} }

View File

@@ -6,7 +6,7 @@ import (
) )
type Link struct { type Link struct {
TypeID string `json:"typeid"` ID string `json:"id"`
NodeA *Node `json:"node_a"` NodeA *Node `json:"node_a"`
InterfaceA string `json:"interface_a,omitempty"` InterfaceA string `json:"interface_a,omitempty"`
NodeB *Node `json:"node_b"` NodeB *Node `json:"node_b"`
@@ -15,7 +15,7 @@ type Link struct {
func (l *Link) MarshalJSON() ([]byte, error) { func (l *Link) MarshalJSON() ([]byte, error) {
type linkJSON struct { type linkJSON struct {
TypeID string `json:"typeid"` ID string `json:"id"`
NodeA interface{} `json:"node_a"` NodeA interface{} `json:"node_a"`
InterfaceA string `json:"interface_a,omitempty"` InterfaceA string `json:"interface_a,omitempty"`
NodeB interface{} `json:"node_b"` NodeB interface{} `json:"node_b"`
@@ -23,7 +23,7 @@ func (l *Link) MarshalJSON() ([]byte, error) {
} }
return json.Marshal(linkJSON{ return json.Marshal(linkJSON{
TypeID: l.TypeID, ID: l.ID,
NodeA: l.NodeA.WithInterface(l.InterfaceA), NodeA: l.NodeA.WithInterface(l.InterfaceA),
InterfaceA: l.InterfaceA, InterfaceA: l.InterfaceA,
NodeB: l.NodeB.WithInterface(l.InterfaceB), NodeB: l.NodeB.WithInterface(l.InterfaceB),
@@ -83,7 +83,7 @@ func (n *Nodes) getDirectLinks() []*Link {
if !seen[key] { if !seen[key] {
seen[key] = true seen[key] = true
links = append(links, &Link{ links = append(links, &Link{
TypeID: newTypeID("link"), ID: newID("link"),
NodeA: lh.node, NodeA: lh.node,
InterfaceA: lh.port, InterfaceA: lh.port,
NodeB: target, NodeB: target,

View File

@@ -1,80 +1,10 @@
package tendrils package tendrils
import ( import (
"fmt"
"net" "net"
"sort"
"time" "time"
) )
type MulticastGroup struct {
Name string `json:"name"`
IP string `json:"ip"`
}
func (g *MulticastGroup) IsDante() bool {
ip := net.ParseIP(g.IP).To4()
if ip == nil {
return false
}
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
return true
}
if ip[0] == 239 && ip[1] == 253 {
return true
}
return false
}
func multicastGroupName(ip net.IP) string {
ip4 := ip.To4()
if ip4 == nil {
return ip.String()
}
switch ip.String() {
case "224.0.0.251":
return "mdns"
case "224.0.1.129":
return "ptp"
case "224.0.1.130":
return "ptp-announce"
case "224.0.1.131":
return "ptp-sync"
case "224.0.1.132":
return "ptp-delay"
case "224.2.127.254":
return "sap"
case "239.255.254.253":
return "shure-slp"
case "239.255.255.250":
return "ssdp"
case "239.255.255.253":
return "slp"
case "239.255.255.255":
return "admin-scoped-broadcast"
}
if ip4[0] == 239 && ip4[1] == 255 {
universe := int(ip4[2])*256 + int(ip4[3])
if universe >= 1 && universe <= 63999 {
return fmt.Sprintf("sacn:%d", universe)
}
}
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
return fmt.Sprintf("dante-mcast:%d", flowID)
}
if ip4[0] == 239 && ip4[1] == 253 {
flowID := (int(ip4[2]) << 8) | int(ip4[3])
return fmt.Sprintf("dante-av:%d", flowID)
}
return ip.String()
}
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) { func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
@@ -84,27 +14,12 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
return return
} }
groupName := multicastGroupName(groupIP) group := ParseMulticastGroup(groupIP)
if node.multicastLastSeen == nil { if node.MulticastGroups == nil {
node.multicastLastSeen = map[string]time.Time{} node.MulticastGroups = MulticastMembershipSet{}
}
node.multicastLastSeen[groupName] = time.Now()
if !containsString(node.MulticastGroups, groupName) {
node.MulticastGroups = append(node.MulticastGroups, groupName)
sort.Strings(node.MulticastGroups)
}
if len(groupName) > 5 && groupName[:5] == "sacn:" {
var universe int
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
if !containsInt(node.SACNInputs, universe) {
node.SACNInputs = append(node.SACNInputs, universe)
sort.Ints(node.SACNInputs)
}
}
} }
node.MulticastGroups.Add(group)
} }
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) { func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
@@ -116,16 +31,12 @@ func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
return return
} }
groupName := multicastGroupName(groupIP) if node.MulticastGroups == nil {
delete(node.multicastLastSeen, groupName) return
var groups []string
for _, g := range node.MulticastGroups {
if g != groupName {
groups = append(groups, g)
}
} }
node.MulticastGroups = groups
group := ParseMulticastGroup(groupIP)
node.MulticastGroups.Remove(group)
} }
func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP { func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
@@ -133,15 +44,14 @@ func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
defer n.mu.RUnlock() defer n.mu.RUnlock()
node := n.getNodeByIPLocked(deviceIP) node := n.getNodeByIPLocked(deviceIP)
if node == nil { if node == nil || node.MulticastGroups == nil {
return nil return nil
} }
var groups []net.IP var groups []net.IP
for _, groupName := range node.MulticastGroups { for _, group := range node.MulticastGroups.Groups() {
g := &MulticastGroup{Name: groupName} if group.IsDante() {
if g.IsDante() { ip := net.ParseIP(group.String())
ip := net.ParseIP(groupName)
if ip != nil { if ip != nil {
groups = append(groups, ip) groups = append(groups, ip)
} }
@@ -154,10 +64,14 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
groupName := multicastGroupName(groupIP) group := ParseMulticastGroup(groupIP)
groupKey := group.String()
var members []*Node var members []*Node
for _, node := range n.nodes { for _, node := range n.nodes {
if containsString(node.MulticastGroups, groupName) { if node.MulticastGroups == nil {
continue
}
if _, exists := node.MulticastGroups[groupKey]; exists {
members = append(members, node) members = append(members, node)
} }
} }
@@ -165,55 +79,23 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
} }
func (n *Nodes) expireMulticastMemberships() { func (n *Nodes) expireMulticastMemberships() {
expireTime := time.Now().Add(-5 * time.Minute)
for _, node := range n.nodes { for _, node := range n.nodes {
if node.multicastLastSeen == nil { if node.MulticastGroups != nil {
continue node.MulticastGroups.Expire(5 * time.Minute)
} }
var keepGroups []string
var keepSACNInputs []int
for _, groupName := range node.MulticastGroups {
if lastSeen, ok := node.multicastLastSeen[groupName]; ok && !lastSeen.Before(expireTime) {
keepGroups = append(keepGroups, groupName)
if len(groupName) > 5 && groupName[:5] == "sacn:" {
var universe int
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
keepSACNInputs = append(keepSACNInputs, universe)
}
}
} else {
delete(node.multicastLastSeen, groupName)
}
}
node.MulticastGroups = keepGroups
sort.Ints(keepSACNInputs)
node.SACNInputs = keepSACNInputs
} }
} }
func (n *Nodes) mergeMulticast(keep, merge *Node) { func (n *Nodes) mergeMulticast(keep, merge *Node) {
if merge.multicastLastSeen == nil { if merge.MulticastGroups == nil {
return return
} }
if keep.multicastLastSeen == nil { if keep.MulticastGroups == nil {
keep.multicastLastSeen = map[string]time.Time{} keep.MulticastGroups = MulticastMembershipSet{}
} }
for groupName, lastSeen := range merge.multicastLastSeen { for key, membership := range merge.MulticastGroups {
if existing, ok := keep.multicastLastSeen[groupName]; !ok || lastSeen.After(existing) { if existing, ok := keep.MulticastGroups[key]; !ok || membership.LastSeen.After(existing.LastSeen) {
keep.multicastLastSeen[groupName] = lastSeen keep.MulticastGroups[key] = membership
}
if !containsString(keep.MulticastGroups, groupName) {
keep.MulticastGroups = append(keep.MulticastGroups, groupName)
}
if len(groupName) > 5 && groupName[:5] == "sacn:" {
var universe int
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
if !containsInt(keep.SACNInputs, universe) {
keep.SACNInputs = append(keep.SACNInputs, universe)
}
}
} }
} }
sort.Strings(keep.MulticastGroups)
sort.Ints(keep.SACNInputs)
} }

283
nodes.go
View File

@@ -14,30 +14,25 @@ import (
) )
type Nodes struct { type Nodes struct {
mu sync.RWMutex mu sync.RWMutex
nodes map[int]*Node nodes []*Node
ipIndex map[string]int ipIndex map[string]*Node
macIndex map[string]int macIndex map[string]*Node
nameIndex map[string]int nameIndex map[string]*Node
nodeCancel map[int]context.CancelFunc t *Tendrils
nextID int ctx context.Context
t *Tendrils cancelAll context.CancelFunc
ctx context.Context
cancelAll context.CancelFunc
} }
func NewNodes(t *Tendrils) *Nodes { func NewNodes(t *Tendrils) *Nodes {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Nodes{ return &Nodes{
nodes: map[int]*Node{}, ipIndex: map[string]*Node{},
ipIndex: map[string]int{}, macIndex: map[string]*Node{},
macIndex: map[string]int{}, nameIndex: map[string]*Node{},
nameIndex: map[string]int{}, t: t,
nodeCancel: map[int]context.CancelFunc{}, ctx: ctx,
nextID: 1, cancelAll: cancel,
t: t,
ctx: ctx,
cancelAll: cancel,
} }
} }
@@ -60,10 +55,9 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
return false return false
} }
targetID, isNew := n.resolveTargetNode(target, mac, ips, nodeName) node, isNew := n.resolveTargetNode(target, mac, ips, nodeName)
node := n.nodes[targetID]
added := n.applyNodeUpdates(node, targetID, mac, ips, ifaceName, nodeName) added := n.applyNodeUpdates(node, mac, ips, ifaceName, nodeName)
n.logUpdates(node, added, isNew, source) n.logUpdates(node, added, isNew, source)
@@ -74,108 +68,82 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
return isNew || len(added) > 0 return isNew || len(added) > 0
} }
func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (int, bool) { func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (*Node, bool) {
targetID := n.findByTarget(target) node := n.findOrMergeByMAC(target, mac)
targetID = n.findOrMergeByMAC(targetID, mac) if node == nil {
if targetID == -1 { node = n.findByIPs(ips)
targetID = n.findByIPs(ips)
} }
targetID = n.findOrMergeByName(targetID, nodeName) node = n.findOrMergeByName(node, nodeName)
if targetID == -1 { if node == nil {
return n.createNode(), true return n.createNode(), true
} }
return targetID, false return node, false
} }
func (n *Nodes) findByTarget(target *Node) int { func (n *Nodes) findOrMergeByMAC(target *Node, mac net.HardwareAddr) *Node {
if target == nil {
return -1
}
for id, node := range n.nodes {
if node == target {
return id
}
}
return -1
}
func (n *Nodes) findOrMergeByMAC(targetID int, mac net.HardwareAddr) int {
if mac == nil { if mac == nil {
return targetID return target
} }
macKey := mac.String() macKey := mac.String()
id, exists := n.macIndex[macKey] found := n.macIndex[macKey]
if !exists { if found == nil {
return targetID return target
} }
if _, nodeExists := n.nodes[id]; !nodeExists { if target == nil {
delete(n.macIndex, macKey) return found
return targetID
} }
if targetID == -1 { if found != target {
return id n.mergeNodes(target, found)
} }
if id != targetID { return target
n.mergeNodes(targetID, id)
}
return targetID
} }
func (n *Nodes) findByIPs(ips []net.IP) int { func (n *Nodes) findByIPs(ips []net.IP) *Node {
for _, ip := range ips { for _, ip := range ips {
if id, exists := n.ipIndex[ip.String()]; exists { if node := n.ipIndex[ip.String()]; node != nil {
if _, nodeExists := n.nodes[id]; nodeExists { return node
return id
}
} }
} }
return -1 return nil
} }
func (n *Nodes) findOrMergeByName(targetID int, nodeName string) int { func (n *Nodes) findOrMergeByName(target *Node, nodeName string) *Node {
if nodeName == "" { if nodeName == "" {
return targetID return target
} }
id, exists := n.nameIndex[nodeName] found := n.nameIndex[nodeName]
if !exists { if found == nil {
return targetID return target
} }
nameNode, nodeExists := n.nodes[id] if target == nil {
if !nodeExists { return found
delete(n.nameIndex, nodeName)
return targetID
} }
if targetID == -1 { if found != target && len(found.Interfaces) == 0 {
return id n.mergeNodes(target, found)
} }
if id != targetID && len(nameNode.Interfaces) == 0 { return target
n.mergeNodes(targetID, id)
}
return targetID
} }
func (n *Nodes) createNode() int { func (n *Nodes) createNode() *Node {
targetID := n.nextID
n.nextID++
node := &Node{ node := &Node{
TypeID: newTypeID("node"), ID: newID("node"),
Interfaces: InterfaceMap{}, Interfaces: InterfaceMap{},
MACTable: map[string]string{}, MACTable: map[string]string{},
pollTrigger: make(chan struct{}, 1), pollTrigger: make(chan struct{}, 1),
} }
n.nodes[targetID] = node n.nodes = append(n.nodes, node)
n.startNodePoller(targetID, node) n.startNodePoller(node)
return targetID return node
} }
func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName string) []string { func (n *Nodes) applyNodeUpdates(node *Node, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName string) []string {
var added []string var added []string
if mac != nil { if mac != nil {
added = n.updateNodeInterface(node, nodeID, mac, ips, ifaceName) added = n.updateNodeInterface(node, mac, ips, ifaceName)
} else { } else {
added = n.updateNodeIPs(node, nodeID, ips) added = n.updateNodeIPs(node, ips)
} }
if nodeName != "" { if nodeName != "" {
@@ -184,7 +152,7 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
} }
if !node.Names.Has(nodeName) { if !node.Names.Has(nodeName) {
node.Names.Add(nodeName) node.Names.Add(nodeName)
n.nameIndex[nodeName] = nodeID n.nameIndex[nodeName] = node
added = append(added, "name="+nodeName) added = append(added, "name="+nodeName)
} }
} }
@@ -192,22 +160,17 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
return added return added
} }
func (n *Nodes) updateNodeIPs(node *Node, nodeID int, ips []net.IP) []string { func (n *Nodes) updateNodeIPs(node *Node, ips []net.IP) []string {
var added []string var added []string
for _, ip := range ips { for _, ip := range ips {
ipKey := ip.String() ipKey := ip.String()
if existingID, exists := n.ipIndex[ipKey]; exists { if existing := n.ipIndex[ipKey]; existing != nil && existing != node {
if existingID == nodeID { n.mergeNodes(node, existing)
continue if n.t.LogEvents {
} log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey)
if existingNode, nodeExists := n.nodes[existingID]; nodeExists {
n.mergeNodes(nodeID, existingID)
if n.t.LogEvents {
log.Printf("[merge] %s into %s (shared ip %s)", existingNode, node, ipKey)
}
} }
} }
n.ipIndex[ipKey] = nodeID n.ipIndex[ipKey] = node
iface, exists := node.Interfaces[ipKey] iface, exists := node.Interfaces[ipKey]
if !exists { if !exists {
iface = &Interface{IPs: IPSet{}} iface = &Interface{IPs: IPSet{}}
@@ -245,9 +208,9 @@ func (n *Nodes) logUpdates(node *Node, added []string, isNew bool, source string
} }
} }
func (n *Nodes) startNodePoller(nodeID int, node *Node) { func (n *Nodes) startNodePoller(node *Node) {
ctx, cancel := context.WithCancel(n.ctx) ctx, cancel := context.WithCancel(n.ctx)
n.nodeCancel[nodeID] = cancel node.cancelFunc = cancel
go func() { go func() {
pollTicker := time.NewTicker(10 * time.Second) pollTicker := time.NewTicker(10 * time.Second)
@@ -277,7 +240,7 @@ func (n *Nodes) triggerPoll(node *Node) {
} }
} }
func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string { func (n *Nodes) updateNodeInterface(node *Node, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {
macKey := mac.String() macKey := mac.String()
var added []string var added []string
@@ -300,25 +263,23 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
added = append(added, "iface="+ifaceKey) added = append(added, "iface="+ifaceKey)
} }
if _, exists := n.macIndex[macKey]; !exists { if n.macIndex[macKey] == nil {
n.macIndex[macKey] = nodeID n.macIndex[macKey] = node
} }
for _, ip := range ips { for _, ip := range ips {
ipKey := ip.String() ipKey := ip.String()
if existingID, exists := n.ipIndex[ipKey]; exists && existingID != nodeID { if existing := n.ipIndex[ipKey]; existing != nil && existing != node {
if existingNode, nodeExists := n.nodes[existingID]; nodeExists { n.mergeNodes(node, existing)
n.mergeNodes(nodeID, existingID) if n.t.LogEvents {
if n.t.LogEvents { log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey)
log.Printf("[merge] %s into %s (shared ip %s)", existingNode, node, ipKey)
}
} }
} }
if !iface.IPs.Has(ipKey) { if !iface.IPs.Has(ipKey) {
added = append(added, "ip="+ipKey) added = append(added, "ip="+ipKey)
} }
iface.IPs.Add(ip) iface.IPs.Add(ip)
n.ipIndex[ipKey] = nodeID n.ipIndex[ipKey] = node
if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface { if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface {
delete(node.Interfaces, ipKey) delete(node.Interfaces, ipKey)
@@ -356,40 +317,36 @@ func (n *Nodes) Merge(macs []net.HardwareAddr, source string) {
return return
} }
existingIDs := map[int]bool{} existing := map[*Node]bool{}
for _, mac := range macs { for _, mac := range macs {
if id, exists := n.macIndex[mac.String()]; exists { if node := n.macIndex[mac.String()]; node != nil {
existingIDs[id] = true existing[node] = true
} }
} }
if len(existingIDs) < 2 { if len(existing) < 2 {
return return
} }
var ids []int var nodes []*Node
for id := range existingIDs { for node := range existing {
ids = append(ids, id) nodes = append(nodes, node)
} }
sort.Ints(ids)
targetID := ids[0] target := nodes[0]
for i := 1; i < len(ids); i++ { for i := 1; i < len(nodes); i++ {
if n.t.LogEvents { if n.t.LogEvents {
log.Printf("[merge] %s into %s (via %s)", n.nodes[ids[i]], n.nodes[targetID], source) log.Printf("[merge] %s into %s (via %s)", nodes[i], target, source)
} }
n.mergeNodes(targetID, ids[i]) n.mergeNodes(target, nodes[i])
} }
if n.t.LogNodes { if n.t.LogNodes {
n.logNode(n.nodes[targetID]) n.logNode(target)
} }
} }
func (n *Nodes) mergeNodes(keepID, mergeID int) { func (n *Nodes) mergeNodes(keep, merge *Node) {
keep := n.nodes[keepID]
merge := n.nodes[mergeID]
if keep == nil || merge == nil { if keep == nil || merge == nil {
return return
} }
@@ -399,7 +356,7 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
keep.Names = NameSet{} keep.Names = NameSet{}
} }
keep.Names.Add(name) keep.Names.Add(name)
n.nameIndex[name] = keepID n.nameIndex[name] = keep
} }
for _, iface := range merge.Interfaces { for _, iface := range merge.Interfaces {
@@ -408,8 +365,8 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
ips = append(ips, net.ParseIP(ipStr)) ips = append(ips, net.ParseIP(ipStr))
} }
if iface.MAC != "" { if iface.MAC != "" {
n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name) n.updateNodeInterface(keep, iface.MAC.Parse(), ips, iface.Name)
n.macIndex[string(iface.MAC)] = keepID n.macIndex[string(iface.MAC)] = keep
} }
} }
@@ -425,12 +382,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
n.mergeMulticast(keep, merge) n.mergeMulticast(keep, merge)
n.mergeDante(keep, merge) n.mergeDante(keep, merge)
if cancel, exists := n.nodeCancel[mergeID]; exists { if merge.cancelFunc != nil {
cancel() merge.cancelFunc()
delete(n.nodeCancel, mergeID)
} }
delete(n.nodes, mergeID) n.removeNode(merge)
}
func (n *Nodes) removeNode(node *Node) {
for i, nd := range n.nodes {
if nd == node {
n.nodes = append(n.nodes[:i], n.nodes[i+1:]...)
return
}
}
} }
func (n *Nodes) GetByIP(ip net.IP) *Node { func (n *Nodes) GetByIP(ip net.IP) *Node {
@@ -441,55 +406,41 @@ func (n *Nodes) GetByIP(ip net.IP) *Node {
} }
func (n *Nodes) getByIPLocked(ip net.IP) *Node { func (n *Nodes) getByIPLocked(ip net.IP) *Node {
if id, exists := n.ipIndex[ip.String()]; exists { return n.ipIndex[ip.String()]
return n.nodes[id]
}
return nil
} }
func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node { func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
if id, exists := n.macIndex[mac.String()]; exists { return n.macIndex[mac.String()]
return n.nodes[id]
}
return nil
} }
func (n *Nodes) GetByName(name string) *Node { func (n *Nodes) GetByName(name string) *Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
if id, exists := n.nameIndex[name]; exists { return n.nameIndex[name]
return n.nodes[id]
}
return nil
} }
func (n *Nodes) GetOrCreateByName(name string) *Node { func (n *Nodes) GetOrCreateByName(name string) *Node {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
if id, exists := n.nameIndex[name]; exists { if node := n.nameIndex[name]; node != nil {
if node, nodeExists := n.nodes[id]; nodeExists { return node
return node
}
delete(n.nameIndex, name)
} }
targetID := n.nextID
n.nextID++
node := &Node{ node := &Node{
TypeID: newTypeID("node"), ID: newID("node"),
Names: NameSet{name: true}, Names: NameSet{name: true},
Interfaces: InterfaceMap{}, Interfaces: InterfaceMap{},
MACTable: map[string]string{}, MACTable: map[string]string{},
pollTrigger: make(chan struct{}, 1), pollTrigger: make(chan struct{}, 1),
} }
n.nodes[targetID] = node n.nodes = append(n.nodes, node)
n.nameIndex[name] = targetID n.nameIndex[name] = node
n.startNodePoller(targetID, node) n.startNodePoller(node)
if n.t.LogEvents { if n.t.LogEvents {
log.Printf("[add] %s [name=%s] (via name-lookup)", node, name) log.Printf("[add] %s [name=%s] (via name-lookup)", node, name)
@@ -518,16 +469,13 @@ func (n *Nodes) SetDanteClockMaster(ip net.IP) {
node.IsDanteClockMaster = false node.IsDanteClockMaster = false
} }
if id, exists := n.ipIndex[ip.String()]; exists { if node := n.ipIndex[ip.String()]; node != nil {
n.nodes[id].IsDanteClockMaster = true node.IsDanteClockMaster = true
} }
} }
func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node { func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node {
if id, exists := n.ipIndex[ip.String()]; exists { return n.ipIndex[ip.String()]
return n.nodes[id]
}
return nil
} }
func (n *Nodes) logNode(node *Node) { func (n *Nodes) logNode(node *Node) {
@@ -620,12 +568,15 @@ func (n *Nodes) LogAll() {
groupMembers := map[string][]string{} groupMembers := map[string][]string{}
for _, node := range n.nodes { for _, node := range n.nodes {
for _, groupName := range node.MulticastGroups { if node.MulticastGroups == nil {
continue
}
for _, group := range node.MulticastGroups.Groups() {
name := node.DisplayName() name := node.DisplayName()
if name == "" { if name == "" {
name = "??" name = "??"
} }
groupMembers[groupName] = append(groupMembers[groupName], name) groupMembers[group.String()] = append(groupMembers[group.String()], name)
} }
} }

View File

@@ -142,7 +142,7 @@ func (t *Tendrils) pingNode(node *Node) {
t.nodes.mu.RLock() t.nodes.mu.RLock()
var ips []string var ips []string
nodeName := node.DisplayName() nodeName := node.DisplayName()
nodeID := node.TypeID nodeID := node.ID
for _, iface := range node.Interfaces { for _, iface := range node.Interfaces {
for ipStr := range iface.IPs { for ipStr := range iface.IPs {
ip := net.ParseIP(ipStr) ip := net.ParseIP(ipStr)

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"log" "log"
"net" "net"
"sort"
"time" "time"
"github.com/gopatchy/sacn" "github.com/gopatchy/sacn"
@@ -61,29 +60,33 @@ func (n *Nodes) UpdateSACN(node *Node, outputs []int) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
node.SACNOutputs = outputs if node.SACNOutputs == nil {
sort.Ints(node.SACNOutputs) node.SACNOutputs = SACNUniverseSet{}
node.sacnLastSeen = time.Now() }
for _, u := range outputs {
node.SACNOutputs.Add(SACNUniverse(u))
}
} }
func (n *Nodes) expireSACN() { func (n *Nodes) expireSACN() {
expireTime := time.Now().Add(-60 * time.Second)
for _, node := range n.nodes { for _, node := range n.nodes {
if !node.sacnLastSeen.IsZero() && node.sacnLastSeen.Before(expireTime) { if node.SACNOutputs != nil {
node.SACNOutputs = nil node.SACNOutputs.Expire(60 * time.Second)
node.sacnLastSeen = time.Time{}
} }
} }
} }
func (n *Nodes) mergeSACN(keep, merge *Node) { func (n *Nodes) mergeSACN(keep, merge *Node) {
for _, u := range merge.SACNOutputs { if merge.SACNOutputs == nil {
if !containsInt(keep.SACNOutputs, u) { return
keep.SACNOutputs = append(keep.SACNOutputs, u) }
if keep.SACNOutputs == nil {
keep.SACNOutputs = SACNUniverseSet{}
}
for u, lastSeen := range merge.SACNOutputs {
if existing, ok := keep.SACNOutputs[u]; !ok || lastSeen.After(existing) {
keep.SACNOutputs[u] = lastSeen
} }
} }
if merge.sacnLastSeen.After(keep.sacnLastSeen) {
keep.sacnLastSeen = merge.sacnLastSeen
}
sort.Ints(keep.SACNOutputs)
} }

View File

@@ -246,7 +246,7 @@ func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames
outBytes, hasOutBytes := ifHCOutOctets[ifIndex] outBytes, hasOutBytes := ifHCOutOctets[ifIndex]
if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes { if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes {
key := node.TypeID + ":" + name key := node.ID + ":" + name
ifaceTracker.mu.Lock() ifaceTracker.mu.Lock()
prev, hasPrev := ifaceTracker.counters[key] prev, hasPrev := ifaceTracker.counters[key]
if hasPrev { if hasPrev {

347
types.go
View File

@@ -1,6 +1,7 @@
package tendrils package tendrils
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
@@ -12,11 +13,266 @@ import (
"go.jetify.com/typeid" "go.jetify.com/typeid"
) )
func newTypeID(prefix string) string { func newID(prefix string) string {
tid, _ := typeid.WithPrefix(prefix) tid, _ := typeid.WithPrefix(prefix)
return tid.String() return tid.String()
} }
type ArtNetUniverse int
func (u ArtNetUniverse) Net() int {
return (int(u) >> 8) & 0x7f
}
func (u ArtNetUniverse) Subnet() int {
return (int(u) >> 4) & 0x0f
}
func (u ArtNetUniverse) Universe() int {
return int(u) & 0x0f
}
func (u ArtNetUniverse) String() string {
return fmt.Sprintf("%d/%d/%d", u.Net(), u.Subnet(), u.Universe())
}
type ArtNetUniverseSet map[ArtNetUniverse]time.Time
func (s ArtNetUniverseSet) Add(u ArtNetUniverse) {
s[u] = time.Now()
}
func (s ArtNetUniverseSet) Universes() []ArtNetUniverse {
result := make([]ArtNetUniverse, 0, len(s))
for u := range s {
result = append(result, u)
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func (s ArtNetUniverseSet) Expire(maxAge time.Duration) {
expireTime := time.Now().Add(-maxAge)
for u, lastSeen := range s {
if lastSeen.Before(expireTime) {
delete(s, u)
}
}
}
func (s ArtNetUniverseSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Universes())
}
type SACNUniverse int
func (u SACNUniverse) String() string {
return fmt.Sprintf("%d", u)
}
type SACNUniverseSet map[SACNUniverse]time.Time
func (s SACNUniverseSet) Add(u SACNUniverse) {
s[u] = time.Now()
}
func (s SACNUniverseSet) Universes() []SACNUniverse {
result := make([]SACNUniverse, 0, len(s))
for u := range s {
result = append(result, u)
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func (s SACNUniverseSet) Expire(maxAge time.Duration) {
expireTime := time.Now().Add(-maxAge)
for u, lastSeen := range s {
if lastSeen.Before(expireTime) {
delete(s, u)
}
}
}
func (s SACNUniverseSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Universes())
}
type MulticastGroupID int
const (
MulticastUnknown MulticastGroupID = iota
MulticastMDNS
MulticastPTP
MulticastPTPAnnounce
MulticastPTPSync
MulticastPTPDelay
MulticastSAP
MulticastShureSLP
MulticastSSDP
MulticastSLP
MulticastAdminScopedBroadcast
)
type MulticastGroup struct {
ID MulticastGroupID
SACNUniverse SACNUniverse
DanteFlow int
DanteAV int
RawIP string
}
func (g MulticastGroup) String() string {
switch g.ID {
case MulticastMDNS:
return "mdns"
case MulticastPTP:
return "ptp"
case MulticastPTPAnnounce:
return "ptp-announce"
case MulticastPTPSync:
return "ptp-sync"
case MulticastPTPDelay:
return "ptp-delay"
case MulticastSAP:
return "sap"
case MulticastShureSLP:
return "shure-slp"
case MulticastSSDP:
return "ssdp"
case MulticastSLP:
return "slp"
case MulticastAdminScopedBroadcast:
return "admin-scoped-broadcast"
}
if g.SACNUniverse > 0 {
return fmt.Sprintf("sacn:%d", g.SACNUniverse)
}
if g.DanteFlow > 0 {
return fmt.Sprintf("dante-mcast:%d", g.DanteFlow)
}
if g.DanteAV > 0 {
return fmt.Sprintf("dante-av:%d", g.DanteAV)
}
return g.RawIP
}
func (g MulticastGroup) MarshalJSON() ([]byte, error) {
return json.Marshal(g.String())
}
func (g MulticastGroup) IsDante() bool {
return g.DanteFlow > 0 || g.DanteAV > 0
}
func (g MulticastGroup) IsSACN() bool {
return g.SACNUniverse > 0
}
func ParseMulticastGroup(ip net.IP) MulticastGroup {
ip4 := ip.To4()
if ip4 == nil {
return MulticastGroup{RawIP: ip.String()}
}
switch ip.String() {
case "224.0.0.251":
return MulticastGroup{ID: MulticastMDNS}
case "224.0.1.129":
return MulticastGroup{ID: MulticastPTP}
case "224.0.1.130":
return MulticastGroup{ID: MulticastPTPAnnounce}
case "224.0.1.131":
return MulticastGroup{ID: MulticastPTPSync}
case "224.0.1.132":
return MulticastGroup{ID: MulticastPTPDelay}
case "224.2.127.254":
return MulticastGroup{ID: MulticastSAP}
case "239.255.254.253":
return MulticastGroup{ID: MulticastShureSLP}
case "239.255.255.250":
return MulticastGroup{ID: MulticastSSDP}
case "239.255.255.253":
return MulticastGroup{ID: MulticastSLP}
case "239.255.255.255":
return MulticastGroup{ID: MulticastAdminScopedBroadcast}
}
if ip4[0] == 239 && ip4[1] == 255 {
universe := int(ip4[2])*256 + int(ip4[3])
if universe >= 1 && universe <= 63999 {
return MulticastGroup{SACNUniverse: SACNUniverse(universe)}
}
}
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
return MulticastGroup{DanteFlow: flowID}
}
if ip4[0] == 239 && ip4[1] == 253 {
flowID := (int(ip4[2]) << 8) | int(ip4[3])
return MulticastGroup{DanteAV: flowID}
}
return MulticastGroup{RawIP: ip.String()}
}
type MulticastMembership struct {
Group MulticastGroup
LastSeen time.Time
}
type MulticastMembershipSet map[string]*MulticastMembership
func (s MulticastMembershipSet) Add(group MulticastGroup) {
key := group.String()
if m, exists := s[key]; exists {
m.LastSeen = time.Now()
} else {
s[key] = &MulticastMembership{Group: group, LastSeen: time.Now()}
}
}
func (s MulticastMembershipSet) Remove(group MulticastGroup) {
delete(s, group.String())
}
func (s MulticastMembershipSet) Groups() []MulticastGroup {
result := make([]MulticastGroup, 0, len(s))
for _, m := range s {
result = append(result, m.Group)
}
sort.Slice(result, func(i, j int) bool {
return result[i].String() < result[j].String()
})
return result
}
func (s MulticastMembershipSet) SACNInputs() []SACNUniverse {
var result []SACNUniverse
for _, m := range s {
if m.Group.IsSACN() {
result = append(result, m.Group.SACNUniverse)
}
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func (s MulticastMembershipSet) Expire(maxAge time.Duration) {
expireTime := time.Now().Add(-maxAge)
for key, m := range s {
if m.LastSeen.Before(expireTime) {
delete(s, key)
}
}
}
func (s MulticastMembershipSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Groups())
}
type MAC string type MAC string
func (m MAC) Parse() net.HardwareAddr { func (m MAC) Parse() net.HardwareAddr {
@@ -133,28 +389,71 @@ type PoEBudget struct {
} }
type Node struct { type Node struct {
TypeID string `json:"typeid"` ID string `json:"id"`
Names NameSet `json:"names"` Names NameSet `json:"names"`
Interfaces InterfaceMap `json:"interfaces"` Interfaces InterfaceMap `json:"interfaces"`
MACTable map[string]string `json:"-"` MACTable map[string]string `json:"-"`
MACTableSize int `json:"mac_table_size,omitempty"` PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
PoEBudget *PoEBudget `json:"poe_budget,omitempty"` IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"` DanteTxChannels string `json:"dante_tx_channels,omitempty"`
DanteTxChannels string `json:"dante_tx_channels,omitempty"` MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
MulticastGroups []string `json:"multicast_groups,omitempty"` ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
ArtNetInputs []int `json:"artnet_inputs,omitempty"` ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
ArtNetOutputs []int `json:"artnet_outputs,omitempty"` SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
SACNInputs []int `json:"sacn_inputs,omitempty"` DanteTx []*DantePeer `json:"dante_tx,omitempty"`
SACNOutputs []int `json:"sacn_outputs,omitempty"` DanteRx []*DantePeer `json:"dante_rx,omitempty"`
DanteTx []*DantePeer `json:"dante_tx,omitempty"` Unreachable bool `json:"unreachable,omitempty"`
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
Unreachable bool `json:"unreachable,omitempty"`
pollTrigger chan struct{} pollTrigger chan struct{}
cancelFunc context.CancelFunc
danteLastSeen time.Time
}
multicastLastSeen map[string]time.Time func (n *Node) MACTableSize() int {
artnetLastSeen time.Time return len(n.MACTable)
sacnLastSeen time.Time }
danteLastSeen time.Time
func (n *Node) SACNInputs() []SACNUniverse {
if n.MulticastGroups == nil {
return nil
}
return n.MulticastGroups.SACNInputs()
}
func (n *Node) MarshalJSON() ([]byte, error) {
type nodeJSON struct {
ID string `json:"id"`
Names NameSet `json:"names"`
Interfaces InterfaceMap `json:"interfaces"`
MACTableSize int `json:"mac_table_size,omitempty"`
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
SACNInputs []SACNUniverse `json:"sacn_inputs,omitempty"`
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
Unreachable bool `json:"unreachable,omitempty"`
}
return json.Marshal(nodeJSON{
ID: n.ID,
Names: n.Names,
Interfaces: n.Interfaces,
MACTableSize: n.MACTableSize(),
PoEBudget: n.PoEBudget,
IsDanteClockMaster: n.IsDanteClockMaster,
DanteTxChannels: n.DanteTxChannels,
MulticastGroups: n.MulticastGroups,
ArtNetInputs: n.ArtNetInputs,
ArtNetOutputs: n.ArtNetOutputs,
SACNInputs: n.SACNInputs(),
SACNOutputs: n.SACNOutputs,
DanteTx: n.DanteTx,
DanteRx: n.DanteRx,
Unreachable: n.Unreachable,
})
} }
type DantePeer struct { type DantePeer struct {
@@ -170,7 +469,7 @@ func (p *DantePeer) MarshalJSON() ([]byte, error) {
Status map[string]string `json:"status,omitempty"` Status map[string]string `json:"status,omitempty"`
} }
nodeRef := &Node{ nodeRef := &Node{
TypeID: p.Node.TypeID, ID: p.Node.ID,
Names: p.Node.Names, Names: p.Node.Names,
Interfaces: p.Node.Interfaces, Interfaces: p.Node.Interfaces,
} }
@@ -190,10 +489,10 @@ func (n *Node) WithInterface(ifaceKey string) *Node {
return n return n
} }
return &Node{ return &Node{
TypeID: n.TypeID, ID: n.ID,
Names: n.Names, Names: n.Names,
Interfaces: InterfaceMap{ifaceKey: iface}, Interfaces: InterfaceMap{ifaceKey: iface},
MACTableSize: n.MACTableSize, MACTable: n.MACTable,
PoEBudget: n.PoEBudget, PoEBudget: n.PoEBudget,
IsDanteClockMaster: n.IsDanteClockMaster, IsDanteClockMaster: n.IsDanteClockMaster,
DanteTxChannels: n.DanteTxChannels, DanteTxChannels: n.DanteTxChannels,