From a912d7316964fbab23856b14a305b61259cadfb3 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 28 Jan 2026 22:36:44 -0800 Subject: [PATCH] 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 --- artnet.go | 74 +++++----- dante.go | 11 +- errors.go | 30 ++-- http.go | 2 +- link.go | 8 +- multicast.go | 172 ++++------------------- nodes.go | 283 ++++++++++++++++--------------------- ping.go | 2 +- sacn_discovery.go | 33 +++-- snmp.go | 2 +- types.go | 347 ++++++++++++++++++++++++++++++++++++++++++---- 11 files changed, 552 insertions(+), 412 deletions(-) diff --git a/artnet.go b/artnet.go index 74bfe81..1f203ec 100644 --- a/artnet.go +++ b/artnet.go @@ -182,53 +182,58 @@ func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) { n.mu.Lock() defer n.mu.Unlock() + if node.ArtNetInputs == nil { + node.ArtNetInputs = ArtNetUniverseSet{} + } + if node.ArtNetOutputs == nil { + node.ArtNetOutputs = ArtNetUniverseSet{} + } + for _, u := range inputs { - if !containsInt(node.ArtNetInputs, u) { - node.ArtNetInputs = append(node.ArtNetInputs, u) - } + node.ArtNetInputs.Add(ArtNetUniverse(u)) } for _, u := range outputs { - if !containsInt(node.ArtNetOutputs, u) { - node.ArtNetOutputs = append(node.ArtNetOutputs, u) - } + node.ArtNetOutputs.Add(ArtNetUniverse(u)) } - sort.Ints(node.ArtNetInputs) - sort.Ints(node.ArtNetOutputs) - node.artnetLastSeen = time.Now() } func (n *Nodes) expireArtNet() { - expireTime := time.Now().Add(-60 * time.Second) for _, node := range n.nodes { - if !node.artnetLastSeen.IsZero() && node.artnetLastSeen.Before(expireTime) { - node.ArtNetInputs = nil - node.ArtNetOutputs = nil - node.artnetLastSeen = time.Time{} + if node.ArtNetInputs != nil { + node.ArtNetInputs.Expire(60 * time.Second) + } + if node.ArtNetOutputs != nil { + node.ArtNetOutputs.Expire(60 * time.Second) } } } func (n *Nodes) mergeArtNet(keep, merge *Node) { - for _, u := range merge.ArtNetInputs { - if !containsInt(keep.ArtNetInputs, u) { - keep.ArtNetInputs = append(keep.ArtNetInputs, u) + if merge.ArtNetInputs != nil { + if keep.ArtNetInputs == nil { + 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 !containsInt(keep.ArtNetOutputs, u) { - keep.ArtNetOutputs = append(keep.ArtNetOutputs, u) + if merge.ArtNetOutputs != nil { + if keep.ArtNetOutputs == nil { + 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() { - inputUniverses := map[int][]string{} - outputUniverses := map[int][]string{} + inputUniverses := map[ArtNetUniverse][]string{} + outputUniverses := map[ArtNetUniverse][]string{} for _, node := range n.nodes { if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 { @@ -238,10 +243,10 @@ func (n *Nodes) logArtNet() { if name == "" { name = "??" } - for _, u := range node.ArtNetInputs { + for u := range node.ArtNetInputs { inputUniverses[u] = append(inputUniverses[u], name) } - for _, u := range node.ArtNetOutputs { + for u := range node.ArtNetOutputs { outputUniverses[u] = append(outputUniverses[u], name) } } @@ -250,8 +255,8 @@ func (n *Nodes) logArtNet() { return } - var allUniverses []int - seen := map[int]bool{} + seen := map[ArtNetUniverse]bool{} + var allUniverses []ArtNetUniverse for u := range inputUniverses { if !seen[u] { allUniverses = append(allUniverses, u) @@ -264,7 +269,7 @@ func (n *Nodes) logArtNet() { 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)) 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]) }) parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", "))) } - netVal := (u >> 8) & 0x7f - subnet := (u >> 4) & 0x0f - universe := u & 0x0f - log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; ")) + log.Printf("[sigusr1] artnet:%d (%s) %s", u, u.String(), strings.Join(parts, "; ")) } } diff --git a/dante.go b/dante.go index e1e5556..a1685f4 100644 --- a/dante.go +++ b/dante.go @@ -102,10 +102,13 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node { n.mu.RLock() defer n.mu.RUnlock() - groupName := multicastGroupName(groupIP) + group := ParseMulticastGroup(groupIP) + groupKey := group.String() for _, node := range n.nodes { - if node.DanteTxChannels != "" && containsString(node.MulticastGroups, groupName) { - return node + if node.DanteTxChannels != "" && node.MulticastGroups != nil { + if _, exists := node.MulticastGroups[groupKey]; exists { + return node + } } } 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) } if sourceNode == nil { - sourceNode = t.nodes.GetOrCreateByName(multicastGroupName(groupIP)) + sourceNode = t.nodes.GetOrCreateByName(ParseMulticastGroup(groupIP).String()) } subscriberNode := t.nodes.GetOrCreateByName(info.Name) t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive) diff --git a/errors.go b/errors.go index ca16199..39eba5d 100644 --- a/errors.go +++ b/errors.go @@ -15,7 +15,7 @@ const ( type Error struct { ID string `json:"id"` - NodeTypeID string `json:"node_typeid"` + NodeID string `json:"node_id"` NodeName string `json:"node_name"` Type string `json:"type"` Port string `json:"port,omitempty"` @@ -88,7 +88,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats speedBytes := float64(stats.Speed) / 8.0 utilization := (maxBytesRate / speedBytes) * 100.0 - key := "util:" + node.TypeID + ":" + portName + key := "util:" + node.ID + ":" + portName now := time.Now() if utilization < 70.0 { @@ -107,7 +107,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats e.nextID++ e.errors[key] = &Error{ ID: fmt.Sprintf("err-%d", e.nextID), - NodeTypeID: node.TypeID, + NodeID: node.ID, NodeName: node.DisplayName(), Port: portName, Type: ErrorTypeHighUtilization, @@ -122,7 +122,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter e.mu.Lock() defer e.mu.Unlock() - key := node.TypeID + ":" + portName + key := node.ID + ":" + portName baseline := e.baselines[key] now := time.Now() @@ -137,7 +137,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter e.nextID++ e.errors[key] = &Error{ ID: fmt.Sprintf("err-%d", e.nextID), - NodeTypeID: node.TypeID, + NodeID: node.ID, NodeName: node.DisplayName(), Port: portName, Type: ErrorTypeStartup, @@ -172,7 +172,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter e.nextID++ e.errors[key] = &Error{ ID: fmt.Sprintf("err-%d", e.nextID), - NodeTypeID: node.TypeID, + NodeID: node.ID, NodeName: node.DisplayName(), Port: portName, Type: ErrorTypeNew, @@ -253,8 +253,8 @@ func (e *ErrorTracker) GetUnreachableNodeSet() map[string]bool { defer e.mu.RUnlock() result := map[string]bool{} - for nodeTypeID := range e.unreachableNodes { - result[nodeTypeID] = true + for nodeID := range e.unreachableNodes { + result[nodeID] = true } return result } @@ -271,10 +271,10 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr e.mu.Lock() defer e.mu.Unlock() - key := "unreachable:" + node.TypeID + key := "unreachable:" + node.ID - wasUnreachable := e.unreachableNodes[node.TypeID] - e.unreachableNodes[node.TypeID] = true + wasUnreachable := e.unreachableNodes[node.ID] + e.unreachableNodes[node.ID] = true becameUnreachable = !wasUnreachable if e.suppressedUnreachable[key] { @@ -289,7 +289,7 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr e.nextID++ e.errors[key] = &Error{ ID: fmt.Sprintf("err-%d", e.nextID), - NodeTypeID: node.TypeID, + NodeID: node.ID, NodeName: node.DisplayName(), Type: ErrorTypeUnreachable, FirstSeen: now, @@ -310,12 +310,12 @@ func (e *ErrorTracker) clearUnreachableLocked(node *Node) (changed bool, becameR e.mu.Lock() defer e.mu.Unlock() - key := "unreachable:" + node.TypeID + key := "unreachable:" + node.ID delete(e.suppressedUnreachable, key) - wasUnreachable := e.unreachableNodes[node.TypeID] - delete(e.unreachableNodes, node.TypeID) + wasUnreachable := e.unreachableNodes[node.ID] + delete(e.unreachableNodes, node.ID) becameReachable = wasUnreachable if _, exists := e.errors[key]; exists { diff --git a/http.go b/http.go index a81d8f6..5865e7b 100644 --- a/http.go +++ b/http.go @@ -230,7 +230,7 @@ func (t *Tendrils) getNodes() []*Node { for _, node := range t.nodes.nodes { n := new(Node) *n = *node - n.Unreachable = unreachableNodes[node.TypeID] + n.Unreachable = unreachableNodes[node.ID] nodes = append(nodes, n) } diff --git a/link.go b/link.go index ef5679c..7256825 100644 --- a/link.go +++ b/link.go @@ -6,7 +6,7 @@ import ( ) type Link struct { - TypeID string `json:"typeid"` + ID string `json:"id"` NodeA *Node `json:"node_a"` InterfaceA string `json:"interface_a,omitempty"` NodeB *Node `json:"node_b"` @@ -15,7 +15,7 @@ type Link struct { func (l *Link) MarshalJSON() ([]byte, error) { type linkJSON struct { - TypeID string `json:"typeid"` + ID string `json:"id"` NodeA interface{} `json:"node_a"` InterfaceA string `json:"interface_a,omitempty"` NodeB interface{} `json:"node_b"` @@ -23,7 +23,7 @@ func (l *Link) MarshalJSON() ([]byte, error) { } return json.Marshal(linkJSON{ - TypeID: l.TypeID, + ID: l.ID, NodeA: l.NodeA.WithInterface(l.InterfaceA), InterfaceA: l.InterfaceA, NodeB: l.NodeB.WithInterface(l.InterfaceB), @@ -83,7 +83,7 @@ func (n *Nodes) getDirectLinks() []*Link { if !seen[key] { seen[key] = true links = append(links, &Link{ - TypeID: newTypeID("link"), + ID: newID("link"), NodeA: lh.node, InterfaceA: lh.port, NodeB: target, diff --git a/multicast.go b/multicast.go index 635913e..50d7feb 100644 --- a/multicast.go +++ b/multicast.go @@ -1,80 +1,10 @@ package tendrils import ( - "fmt" "net" - "sort" "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) { n.mu.Lock() defer n.mu.Unlock() @@ -84,27 +14,12 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) { return } - groupName := multicastGroupName(groupIP) + group := ParseMulticastGroup(groupIP) - if node.multicastLastSeen == nil { - node.multicastLastSeen = map[string]time.Time{} - } - 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) - } - } + if node.MulticastGroups == nil { + node.MulticastGroups = MulticastMembershipSet{} } + node.MulticastGroups.Add(group) } func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) { @@ -116,16 +31,12 @@ func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) { return } - groupName := multicastGroupName(groupIP) - delete(node.multicastLastSeen, groupName) - - var groups []string - for _, g := range node.MulticastGroups { - if g != groupName { - groups = append(groups, g) - } + if node.MulticastGroups == nil { + return } - node.MulticastGroups = groups + + group := ParseMulticastGroup(groupIP) + node.MulticastGroups.Remove(group) } 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() node := n.getNodeByIPLocked(deviceIP) - if node == nil { + if node == nil || node.MulticastGroups == nil { return nil } var groups []net.IP - for _, groupName := range node.MulticastGroups { - g := &MulticastGroup{Name: groupName} - if g.IsDante() { - ip := net.ParseIP(groupName) + for _, group := range node.MulticastGroups.Groups() { + if group.IsDante() { + ip := net.ParseIP(group.String()) if ip != nil { groups = append(groups, ip) } @@ -154,10 +64,14 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node { n.mu.RLock() defer n.mu.RUnlock() - groupName := multicastGroupName(groupIP) + group := ParseMulticastGroup(groupIP) + groupKey := group.String() var members []*Node 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) } } @@ -165,55 +79,23 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node { } func (n *Nodes) expireMulticastMemberships() { - expireTime := time.Now().Add(-5 * time.Minute) for _, node := range n.nodes { - if node.multicastLastSeen == nil { - continue + if node.MulticastGroups != nil { + 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) { - if merge.multicastLastSeen == nil { + if merge.MulticastGroups == nil { return } - if keep.multicastLastSeen == nil { - keep.multicastLastSeen = map[string]time.Time{} + if keep.MulticastGroups == nil { + keep.MulticastGroups = MulticastMembershipSet{} } - for groupName, lastSeen := range merge.multicastLastSeen { - if existing, ok := keep.multicastLastSeen[groupName]; !ok || lastSeen.After(existing) { - keep.multicastLastSeen[groupName] = lastSeen - } - 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) - } - } + for key, membership := range merge.MulticastGroups { + if existing, ok := keep.MulticastGroups[key]; !ok || membership.LastSeen.After(existing.LastSeen) { + keep.MulticastGroups[key] = membership } } - sort.Strings(keep.MulticastGroups) - sort.Ints(keep.SACNInputs) } diff --git a/nodes.go b/nodes.go index 3f28bbb..2b81d35 100644 --- a/nodes.go +++ b/nodes.go @@ -14,30 +14,25 @@ import ( ) type Nodes struct { - mu sync.RWMutex - nodes map[int]*Node - ipIndex map[string]int - macIndex map[string]int - nameIndex map[string]int - nodeCancel map[int]context.CancelFunc - nextID int - t *Tendrils - ctx context.Context - cancelAll context.CancelFunc + mu sync.RWMutex + nodes []*Node + ipIndex map[string]*Node + macIndex map[string]*Node + nameIndex map[string]*Node + t *Tendrils + ctx context.Context + cancelAll context.CancelFunc } func NewNodes(t *Tendrils) *Nodes { ctx, cancel := context.WithCancel(context.Background()) return &Nodes{ - nodes: map[int]*Node{}, - ipIndex: map[string]int{}, - macIndex: map[string]int{}, - nameIndex: map[string]int{}, - nodeCancel: map[int]context.CancelFunc{}, - nextID: 1, - t: t, - ctx: ctx, - cancelAll: cancel, + ipIndex: map[string]*Node{}, + macIndex: map[string]*Node{}, + nameIndex: map[string]*Node{}, + 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 } - targetID, isNew := n.resolveTargetNode(target, mac, ips, nodeName) - node := n.nodes[targetID] + node, isNew := n.resolveTargetNode(target, mac, ips, nodeName) - added := n.applyNodeUpdates(node, targetID, mac, ips, ifaceName, nodeName) + added := n.applyNodeUpdates(node, mac, ips, ifaceName, nodeName) 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 } -func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (int, bool) { - targetID := n.findByTarget(target) - targetID = n.findOrMergeByMAC(targetID, mac) - if targetID == -1 { - targetID = n.findByIPs(ips) +func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (*Node, bool) { + node := n.findOrMergeByMAC(target, mac) + if node == nil { + node = n.findByIPs(ips) } - targetID = n.findOrMergeByName(targetID, nodeName) + node = n.findOrMergeByName(node, nodeName) - if targetID == -1 { + if node == nil { return n.createNode(), true } - return targetID, false + return node, false } -func (n *Nodes) findByTarget(target *Node) int { - 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 { +func (n *Nodes) findOrMergeByMAC(target *Node, mac net.HardwareAddr) *Node { if mac == nil { - return targetID + return target } macKey := mac.String() - id, exists := n.macIndex[macKey] - if !exists { - return targetID + found := n.macIndex[macKey] + if found == nil { + return target } - if _, nodeExists := n.nodes[id]; !nodeExists { - delete(n.macIndex, macKey) - return targetID + if target == nil { + return found } - if targetID == -1 { - return id + if found != target { + n.mergeNodes(target, found) } - if id != targetID { - n.mergeNodes(targetID, id) - } - return targetID + return target } -func (n *Nodes) findByIPs(ips []net.IP) int { +func (n *Nodes) findByIPs(ips []net.IP) *Node { for _, ip := range ips { - if id, exists := n.ipIndex[ip.String()]; exists { - if _, nodeExists := n.nodes[id]; nodeExists { - return id - } + if node := n.ipIndex[ip.String()]; node != nil { + return node } } - return -1 + return nil } -func (n *Nodes) findOrMergeByName(targetID int, nodeName string) int { +func (n *Nodes) findOrMergeByName(target *Node, nodeName string) *Node { if nodeName == "" { - return targetID + return target } - id, exists := n.nameIndex[nodeName] - if !exists { - return targetID + found := n.nameIndex[nodeName] + if found == nil { + return target } - nameNode, nodeExists := n.nodes[id] - if !nodeExists { - delete(n.nameIndex, nodeName) - return targetID + if target == nil { + return found } - if targetID == -1 { - return id + if found != target && len(found.Interfaces) == 0 { + n.mergeNodes(target, found) } - if id != targetID && len(nameNode.Interfaces) == 0 { - n.mergeNodes(targetID, id) - } - return targetID + return target } -func (n *Nodes) createNode() int { - targetID := n.nextID - n.nextID++ +func (n *Nodes) createNode() *Node { node := &Node{ - TypeID: newTypeID("node"), + ID: newID("node"), Interfaces: InterfaceMap{}, MACTable: map[string]string{}, pollTrigger: make(chan struct{}, 1), } - n.nodes[targetID] = node - n.startNodePoller(targetID, node) - return targetID + n.nodes = append(n.nodes, node) + n.startNodePoller(node) + 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 if mac != nil { - added = n.updateNodeInterface(node, nodeID, mac, ips, ifaceName) + added = n.updateNodeInterface(node, mac, ips, ifaceName) } else { - added = n.updateNodeIPs(node, nodeID, ips) + added = n.updateNodeIPs(node, ips) } if nodeName != "" { @@ -184,7 +152,7 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i } if !node.Names.Has(nodeName) { node.Names.Add(nodeName) - n.nameIndex[nodeName] = nodeID + n.nameIndex[nodeName] = node added = append(added, "name="+nodeName) } } @@ -192,22 +160,17 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i 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 for _, ip := range ips { ipKey := ip.String() - if existingID, exists := n.ipIndex[ipKey]; exists { - if existingID == nodeID { - continue - } - 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) - } + if existing := n.ipIndex[ipKey]; existing != nil && existing != node { + n.mergeNodes(node, existing) + if n.t.LogEvents { + log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey) } } - n.ipIndex[ipKey] = nodeID + n.ipIndex[ipKey] = node iface, exists := node.Interfaces[ipKey] if !exists { 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) - n.nodeCancel[nodeID] = cancel + node.cancelFunc = cancel go func() { 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() var added []string @@ -300,25 +263,23 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr added = append(added, "iface="+ifaceKey) } - if _, exists := n.macIndex[macKey]; !exists { - n.macIndex[macKey] = nodeID + if n.macIndex[macKey] == nil { + n.macIndex[macKey] = node } for _, ip := range ips { ipKey := ip.String() - if existingID, exists := n.ipIndex[ipKey]; exists && existingID != nodeID { - 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) - } + if existing := n.ipIndex[ipKey]; existing != nil && existing != node { + n.mergeNodes(node, existing) + if n.t.LogEvents { + log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey) } } if !iface.IPs.Has(ipKey) { added = append(added, "ip="+ipKey) } iface.IPs.Add(ip) - n.ipIndex[ipKey] = nodeID + n.ipIndex[ipKey] = node if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface { delete(node.Interfaces, ipKey) @@ -356,40 +317,36 @@ func (n *Nodes) Merge(macs []net.HardwareAddr, source string) { return } - existingIDs := map[int]bool{} + existing := map[*Node]bool{} for _, mac := range macs { - if id, exists := n.macIndex[mac.String()]; exists { - existingIDs[id] = true + if node := n.macIndex[mac.String()]; node != nil { + existing[node] = true } } - if len(existingIDs) < 2 { + if len(existing) < 2 { return } - var ids []int - for id := range existingIDs { - ids = append(ids, id) + var nodes []*Node + for node := range existing { + nodes = append(nodes, node) } - sort.Ints(ids) - targetID := ids[0] - for i := 1; i < len(ids); i++ { + target := nodes[0] + for i := 1; i < len(nodes); i++ { 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 { - n.logNode(n.nodes[targetID]) + n.logNode(target) } } -func (n *Nodes) mergeNodes(keepID, mergeID int) { - keep := n.nodes[keepID] - merge := n.nodes[mergeID] - +func (n *Nodes) mergeNodes(keep, merge *Node) { if keep == nil || merge == nil { return } @@ -399,7 +356,7 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) { keep.Names = NameSet{} } keep.Names.Add(name) - n.nameIndex[name] = keepID + n.nameIndex[name] = keep } for _, iface := range merge.Interfaces { @@ -408,8 +365,8 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) { ips = append(ips, net.ParseIP(ipStr)) } if iface.MAC != "" { - n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name) - n.macIndex[string(iface.MAC)] = keepID + n.updateNodeInterface(keep, iface.MAC.Parse(), ips, iface.Name) + n.macIndex[string(iface.MAC)] = keep } } @@ -425,12 +382,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) { n.mergeMulticast(keep, merge) n.mergeDante(keep, merge) - if cancel, exists := n.nodeCancel[mergeID]; exists { - cancel() - delete(n.nodeCancel, mergeID) + if merge.cancelFunc != nil { + merge.cancelFunc() } - 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 { @@ -441,55 +406,41 @@ func (n *Nodes) GetByIP(ip net.IP) *Node { } func (n *Nodes) getByIPLocked(ip net.IP) *Node { - if id, exists := n.ipIndex[ip.String()]; exists { - return n.nodes[id] - } - return nil + return n.ipIndex[ip.String()] } func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node { n.mu.RLock() defer n.mu.RUnlock() - if id, exists := n.macIndex[mac.String()]; exists { - return n.nodes[id] - } - return nil + return n.macIndex[mac.String()] } func (n *Nodes) GetByName(name string) *Node { n.mu.RLock() defer n.mu.RUnlock() - if id, exists := n.nameIndex[name]; exists { - return n.nodes[id] - } - return nil + return n.nameIndex[name] } func (n *Nodes) GetOrCreateByName(name string) *Node { n.mu.Lock() defer n.mu.Unlock() - if id, exists := n.nameIndex[name]; exists { - if node, nodeExists := n.nodes[id]; nodeExists { - return node - } - delete(n.nameIndex, name) + if node := n.nameIndex[name]; node != nil { + return node } - targetID := n.nextID - n.nextID++ node := &Node{ - TypeID: newTypeID("node"), + ID: newID("node"), Names: NameSet{name: true}, Interfaces: InterfaceMap{}, MACTable: map[string]string{}, pollTrigger: make(chan struct{}, 1), } - n.nodes[targetID] = node - n.nameIndex[name] = targetID - n.startNodePoller(targetID, node) + n.nodes = append(n.nodes, node) + n.nameIndex[name] = node + n.startNodePoller(node) if n.t.LogEvents { 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 } - if id, exists := n.ipIndex[ip.String()]; exists { - n.nodes[id].IsDanteClockMaster = true + if node := n.ipIndex[ip.String()]; node != nil { + node.IsDanteClockMaster = true } } func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node { - if id, exists := n.ipIndex[ip.String()]; exists { - return n.nodes[id] - } - return nil + return n.ipIndex[ip.String()] } func (n *Nodes) logNode(node *Node) { @@ -620,12 +568,15 @@ func (n *Nodes) LogAll() { groupMembers := map[string][]string{} for _, node := range n.nodes { - for _, groupName := range node.MulticastGroups { + if node.MulticastGroups == nil { + continue + } + for _, group := range node.MulticastGroups.Groups() { name := node.DisplayName() if name == "" { name = "??" } - groupMembers[groupName] = append(groupMembers[groupName], name) + groupMembers[group.String()] = append(groupMembers[group.String()], name) } } diff --git a/ping.go b/ping.go index cd88e21..6acad47 100644 --- a/ping.go +++ b/ping.go @@ -142,7 +142,7 @@ func (t *Tendrils) pingNode(node *Node) { t.nodes.mu.RLock() var ips []string nodeName := node.DisplayName() - nodeID := node.TypeID + nodeID := node.ID for _, iface := range node.Interfaces { for ipStr := range iface.IPs { ip := net.ParseIP(ipStr) diff --git a/sacn_discovery.go b/sacn_discovery.go index 23a53f2..5e191ef 100644 --- a/sacn_discovery.go +++ b/sacn_discovery.go @@ -4,7 +4,6 @@ import ( "context" "log" "net" - "sort" "time" "github.com/gopatchy/sacn" @@ -61,29 +60,33 @@ func (n *Nodes) UpdateSACN(node *Node, outputs []int) { n.mu.Lock() defer n.mu.Unlock() - node.SACNOutputs = outputs - sort.Ints(node.SACNOutputs) - node.sacnLastSeen = time.Now() + if node.SACNOutputs == nil { + node.SACNOutputs = SACNUniverseSet{} + } + + for _, u := range outputs { + node.SACNOutputs.Add(SACNUniverse(u)) + } } func (n *Nodes) expireSACN() { - expireTime := time.Now().Add(-60 * time.Second) for _, node := range n.nodes { - if !node.sacnLastSeen.IsZero() && node.sacnLastSeen.Before(expireTime) { - node.SACNOutputs = nil - node.sacnLastSeen = time.Time{} + if node.SACNOutputs != nil { + node.SACNOutputs.Expire(60 * time.Second) } } } func (n *Nodes) mergeSACN(keep, merge *Node) { - for _, u := range merge.SACNOutputs { - if !containsInt(keep.SACNOutputs, u) { - keep.SACNOutputs = append(keep.SACNOutputs, u) + if merge.SACNOutputs == nil { + return + } + 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) } diff --git a/snmp.go b/snmp.go index db1b08b..8fdf204 100644 --- a/snmp.go +++ b/snmp.go @@ -246,7 +246,7 @@ func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames outBytes, hasOutBytes := ifHCOutOctets[ifIndex] if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes { - key := node.TypeID + ":" + name + key := node.ID + ":" + name ifaceTracker.mu.Lock() prev, hasPrev := ifaceTracker.counters[key] if hasPrev { diff --git a/types.go b/types.go index 4401336..0db1801 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,7 @@ package tendrils import ( + "context" "encoding/json" "fmt" "net" @@ -12,11 +13,266 @@ import ( "go.jetify.com/typeid" ) -func newTypeID(prefix string) string { +func newID(prefix string) string { tid, _ := typeid.WithPrefix(prefix) 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 func (m MAC) Parse() net.HardwareAddr { @@ -133,28 +389,71 @@ type PoEBudget struct { } type Node struct { - TypeID string `json:"typeid"` - Names NameSet `json:"names"` - Interfaces InterfaceMap `json:"interfaces"` - MACTable map[string]string `json:"-"` - 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 []string `json:"multicast_groups,omitempty"` - ArtNetInputs []int `json:"artnet_inputs,omitempty"` - ArtNetOutputs []int `json:"artnet_outputs,omitempty"` - SACNInputs []int `json:"sacn_inputs,omitempty"` - SACNOutputs []int `json:"sacn_outputs,omitempty"` - DanteTx []*DantePeer `json:"dante_tx,omitempty"` - DanteRx []*DantePeer `json:"dante_rx,omitempty"` - Unreachable bool `json:"unreachable,omitempty"` + ID string `json:"id"` + Names NameSet `json:"names"` + Interfaces InterfaceMap `json:"interfaces"` + MACTable map[string]string `json:"-"` + 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"` + SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"` + DanteTx []*DantePeer `json:"dante_tx,omitempty"` + DanteRx []*DantePeer `json:"dante_rx,omitempty"` + Unreachable bool `json:"unreachable,omitempty"` pollTrigger chan struct{} + cancelFunc context.CancelFunc + danteLastSeen time.Time +} - multicastLastSeen map[string]time.Time - artnetLastSeen time.Time - sacnLastSeen time.Time - danteLastSeen time.Time +func (n *Node) MACTableSize() int { + return len(n.MACTable) +} + +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 { @@ -170,7 +469,7 @@ func (p *DantePeer) MarshalJSON() ([]byte, error) { Status map[string]string `json:"status,omitempty"` } nodeRef := &Node{ - TypeID: p.Node.TypeID, + ID: p.Node.ID, Names: p.Node.Names, Interfaces: p.Node.Interfaces, } @@ -190,10 +489,10 @@ func (n *Node) WithInterface(ifaceKey string) *Node { return n } return &Node{ - TypeID: n.TypeID, + ID: n.ID, Names: n.Names, Interfaces: InterfaceMap{ifaceKey: iface}, - MACTableSize: n.MACTableSize, + MACTable: n.MACTable, PoEBudget: n.PoEBudget, IsDanteClockMaster: n.IsDanteClockMaster, DanteTxChannels: n.DanteTxChannels,