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()
defer n.mu.Unlock()
for _, u := range inputs {
if !containsInt(node.ArtNetInputs, u) {
node.ArtNetInputs = append(node.ArtNetInputs, u)
if node.ArtNetInputs == nil {
node.ArtNetInputs = ArtNetUniverseSet{}
}
if node.ArtNetOutputs == nil {
node.ArtNetOutputs = ArtNetUniverseSet{}
}
for _, u := range inputs {
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, "; "))
}
}

View File

@@ -102,12 +102,15 @@ 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) {
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)

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)
if node.MulticastGroups == nil {
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 {
@@ -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)
}

263
nodes.go
View File

@@ -15,12 +15,10 @@ 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
nodes []*Node
ipIndex map[string]*Node
macIndex map[string]*Node
nameIndex map[string]*Node
t *Tendrils
ctx context.Context
cancelAll context.CancelFunc
@@ -29,12 +27,9 @@ type Nodes struct {
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,
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 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)", existingNode, node, ipKey)
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 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)", existingNode, node, ipKey)
}
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 {
if node := n.nameIndex[name]; node != nil {
return node
}
delete(n.nameIndex, name)
}
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)
}
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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 {

329
types.go
View File

@@ -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,30 +389,73 @@ type PoEBudget struct {
}
type Node struct {
TypeID string `json:"typeid"`
ID string `json:"id"`
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"`
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{}
multicastLastSeen map[string]time.Time
artnetLastSeen time.Time
sacnLastSeen time.Time
cancelFunc context.CancelFunc
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 {
Node *Node `json:"node"`
Channels []string `json:"channels,omitempty"`
@@ -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,