Refactor Dante fields to use proper types and group flows with lastSeen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-28 23:15:24 -08:00
parent 99083ecde5
commit b966ad0feb
4 changed files with 129 additions and 99 deletions

151
dante.go
View File

@@ -87,7 +87,7 @@ func (t *Tendrils) handlePTPPacket(ifaceName string, srcIP net.IP, data []byte)
t.nodes.SetDanteClockMaster(srcIP) t.nodes.SetDanteClockMaster(srcIP)
} }
func (n *Nodes) UpdateDanteTxChannels(name string, ip net.IP, channels string) { func (n *Nodes) UpdateDanteTxChannels(name string, ip net.IP, channels int) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
@@ -104,7 +104,7 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node {
group := ParseMulticastGroup(groupIP) group := ParseMulticastGroup(groupIP)
for _, node := range n.nodes { for _, node := range n.nodes {
if node.DanteTxChannels != "" && node.MulticastGroups != nil { if node.DanteTxChannels > 0 && node.MulticastGroups != nil {
if _, exists := node.MulticastGroups[group]; exists { if _, exists := node.MulticastGroups[group]; exists {
return node return node
} }
@@ -188,16 +188,24 @@ func (n *Nodes) UpdateDanteFlow(source, subscriber *Node, channelInfo string, fl
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
n.updateDanteTx(source, subscriber, channelInfo, flowStatus) now := time.Now()
n.updateDanteRx(subscriber, source, channelInfo, flowStatus) n.updateDanteTx(source, subscriber, channelInfo, flowStatus, now)
n.updateDanteRx(subscriber, source, channelInfo, flowStatus, now)
source.danteLastSeen = time.Now()
subscriber.danteLastSeen = time.Now()
} }
func (n *Nodes) updateDanteTx(source, subscriber *Node, channelInfo string, flowStatus DanteFlowStatus) { func (n *Nodes) ensureDanteFlows(node *Node) *DanteFlows {
if node.DanteFlows == nil {
node.DanteFlows = &DanteFlows{}
}
return node.DanteFlows
}
func (n *Nodes) updateDanteTx(source, subscriber *Node, channelInfo string, flowStatus DanteFlowStatus, now time.Time) {
flows := n.ensureDanteFlows(source)
flows.lastSeen = now
var peer *DantePeer var peer *DantePeer
for _, p := range source.DanteTx { for _, p := range flows.Tx {
if p.Node == subscriber { if p.Node == subscriber {
peer = p peer = p
break break
@@ -208,7 +216,7 @@ func (n *Nodes) updateDanteTx(source, subscriber *Node, channelInfo string, flow
Node: subscriber, Node: subscriber,
Status: map[string]string{}, Status: map[string]string{},
} }
source.DanteTx = append(source.DanteTx, peer) flows.Tx = append(flows.Tx, peer)
} }
if channelInfo != "" && !containsString(peer.Channels, channelInfo) { if channelInfo != "" && !containsString(peer.Channels, channelInfo) {
@@ -219,14 +227,17 @@ func (n *Nodes) updateDanteTx(source, subscriber *Node, channelInfo string, flow
peer.Status[channelInfo] = flowStatus.String() peer.Status[channelInfo] = flowStatus.String()
} }
sort.Slice(source.DanteTx, func(i, j int) bool { sort.Slice(flows.Tx, func(i, j int) bool {
return sortorder.NaturalLess(source.DanteTx[i].Node.DisplayName(), source.DanteTx[j].Node.DisplayName()) return sortorder.NaturalLess(flows.Tx[i].Node.DisplayName(), flows.Tx[j].Node.DisplayName())
}) })
} }
func (n *Nodes) updateDanteRx(subscriber, source *Node, channelInfo string, flowStatus DanteFlowStatus) { func (n *Nodes) updateDanteRx(subscriber, source *Node, channelInfo string, flowStatus DanteFlowStatus, now time.Time) {
flows := n.ensureDanteFlows(subscriber)
flows.lastSeen = now
var peer *DantePeer var peer *DantePeer
for _, p := range subscriber.DanteRx { for _, p := range flows.Rx {
if p.Node == source { if p.Node == source {
peer = p peer = p
break break
@@ -237,7 +248,7 @@ func (n *Nodes) updateDanteRx(subscriber, source *Node, channelInfo string, flow
Node: source, Node: source,
Status: map[string]string{}, Status: map[string]string{},
} }
subscriber.DanteRx = append(subscriber.DanteRx, peer) flows.Rx = append(flows.Rx, peer)
} }
if channelInfo != "" && !containsString(peer.Channels, channelInfo) { if channelInfo != "" && !containsString(peer.Channels, channelInfo) {
@@ -248,78 +259,86 @@ func (n *Nodes) updateDanteRx(subscriber, source *Node, channelInfo string, flow
peer.Status[channelInfo] = flowStatus.String() peer.Status[channelInfo] = flowStatus.String()
} }
sort.Slice(subscriber.DanteRx, func(i, j int) bool { sort.Slice(flows.Rx, func(i, j int) bool {
return sortorder.NaturalLess(subscriber.DanteRx[i].Node.DisplayName(), subscriber.DanteRx[j].Node.DisplayName()) return sortorder.NaturalLess(flows.Rx[i].Node.DisplayName(), flows.Rx[j].Node.DisplayName())
}) })
} }
func (n *Nodes) expireDante() { func (n *Nodes) expireDante() {
expireTime := time.Now().Add(-5 * time.Minute)
for _, node := range n.nodes { for _, node := range n.nodes {
if !node.danteLastSeen.IsZero() && node.danteLastSeen.Before(expireTime) { if node.DanteFlows != nil && node.DanteFlows.Expire(5*time.Minute) {
node.DanteTx = nil node.DanteFlows = nil
node.DanteRx = nil
node.danteLastSeen = time.Time{}
} }
} }
} }
func (n *Nodes) mergeDante(keep, merge *Node) { func (n *Nodes) mergeDante(keep, merge *Node) {
for _, peer := range merge.DanteTx { if merge.DanteFlows == nil {
var existing *DantePeer return
for _, p := range keep.DanteTx {
if p.Node == peer.Node {
existing = p
break
}
}
if existing == nil {
keep.DanteTx = append(keep.DanteTx, peer)
} else {
for _, ch := range peer.Channels {
if !containsString(existing.Channels, ch) {
existing.Channels = append(existing.Channels, ch)
}
}
for ch, status := range peer.Status {
existing.Status[ch] = status
}
}
} }
for _, peer := range merge.DanteRx { if keep.DanteFlows == nil {
var existing *DantePeer keep.DanteFlows = merge.DanteFlows
for _, p := range keep.DanteRx { } else {
if p.Node == peer.Node { for _, peer := range merge.DanteFlows.Tx {
existing = p var existing *DantePeer
break for _, p := range keep.DanteFlows.Tx {
} if p.Node == peer.Node {
} existing = p
if existing == nil { break
keep.DanteRx = append(keep.DanteRx, peer)
} else {
for _, ch := range peer.Channels {
if !containsString(existing.Channels, ch) {
existing.Channels = append(existing.Channels, ch)
} }
} }
for ch, status := range peer.Status { if existing == nil {
existing.Status[ch] = status keep.DanteFlows.Tx = append(keep.DanteFlows.Tx, peer)
} else {
for _, ch := range peer.Channels {
if !containsString(existing.Channels, ch) {
existing.Channels = append(existing.Channels, ch)
}
}
for ch, status := range peer.Status {
existing.Status[ch] = status
}
} }
} }
}
if merge.danteLastSeen.After(keep.danteLastSeen) { for _, peer := range merge.DanteFlows.Rx {
keep.danteLastSeen = merge.danteLastSeen var existing *DantePeer
for _, p := range keep.DanteFlows.Rx {
if p.Node == peer.Node {
existing = p
break
}
}
if existing == nil {
keep.DanteFlows.Rx = append(keep.DanteFlows.Rx, peer)
} else {
for _, ch := range peer.Channels {
if !containsString(existing.Channels, ch) {
existing.Channels = append(existing.Channels, ch)
}
}
for ch, status := range peer.Status {
existing.Status[ch] = status
}
}
}
if merge.DanteFlows.lastSeen.After(keep.DanteFlows.lastSeen) {
keep.DanteFlows.lastSeen = merge.DanteFlows.lastSeen
}
} }
for _, node := range n.nodes { for _, node := range n.nodes {
for _, peer := range node.DanteTx { if node.DanteFlows == nil {
continue
}
for _, peer := range node.DanteFlows.Tx {
if peer.Node == merge { if peer.Node == merge {
peer.Node = keep peer.Node = keep
} }
} }
for _, peer := range node.DanteRx { for _, peer := range node.DanteFlows.Rx {
if peer.Node == merge { if peer.Node == merge {
peer.Node = keep peer.Node = keep
} }
@@ -340,7 +359,7 @@ func (n *Nodes) logDante() {
var allNoChannelFlows []string var allNoChannelFlows []string
for _, node := range n.nodes { for _, node := range n.nodes {
if len(node.DanteTx) == 0 { if node.DanteFlows == nil || len(node.DanteFlows.Tx) == 0 {
continue continue
} }
sourceName := node.DisplayName() sourceName := node.DisplayName()
@@ -348,7 +367,7 @@ func (n *Nodes) logDante() {
sourceName = "??" sourceName = "??"
} }
for _, peer := range node.DanteTx { for _, peer := range node.DanteFlows.Tx {
subName := peer.Node.DisplayName() subName := peer.Node.DisplayName()
if subName == "" { if subName == "" {
subName = "??" subName = "??"
@@ -459,7 +478,7 @@ func (t *Tendrils) queryDanteDeviceWithPort(ip net.IP, port int) *DanteDeviceInf
if info.TxChannelCount > 0 { if info.TxChannelCount > 0 {
t.queryDanteTxChannels(conn, ip, info.TxChannelCount) t.queryDanteTxChannels(conn, ip, info.TxChannelCount)
t.nodes.UpdateDanteTxChannels(info.Name, ip, fmt.Sprintf("%d", info.TxChannelCount)) t.nodes.UpdateDanteTxChannels(info.Name, ip, info.TxChannelCount)
} }
return info return info

View File

@@ -487,12 +487,8 @@ func (n *Nodes) SetDanteClockMaster(ip net.IP) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
for _, node := range n.nodes {
node.IsDanteClockMaster = false
}
if node := n.ipIndex[ip.String()]; node != nil { if node := n.ipIndex[ip.String()]; node != nil {
node.IsDanteClockMaster = true node.DanteClockMasterSeen = time.Now()
} }
} }
@@ -509,7 +505,7 @@ func (n *Nodes) logNode(node *Node) {
if node.PoEBudget != nil { if node.PoEBudget != nil {
tags = append(tags, fmt.Sprintf("poe:%.0f/%.0fW", node.PoEBudget.Power, node.PoEBudget.MaxPower)) tags = append(tags, fmt.Sprintf("poe:%.0f/%.0fW", node.PoEBudget.Power, node.PoEBudget.MaxPower))
} }
if node.IsDanteClockMaster { if node.IsDanteClockMaster() {
tags = append(tags, "dante-clock-master") tags = append(tags, "dante-clock-master")
} }
if len(tags) > 0 { if len(tags) > 0 {

View File

@@ -1895,8 +1895,8 @@
nodes.forEach(node => { nodes.forEach(node => {
const nodeId = node.id; const nodeId = node.id;
const danteTx = node.dante_tx || []; const danteTx = node.dante_flows?.tx || [];
const danteRx = node.dante_rx || []; const danteRx = node.dante_flows?.rx || [];
if (danteTx.length === 0 && danteRx.length === 0) return; if (danteTx.length === 0 && danteRx.length === 0) return;

View File

@@ -378,25 +378,40 @@ type PoEBudget struct {
MaxPower float64 `json:"max_power"` MaxPower float64 `json:"max_power"`
} }
type DanteFlows struct {
Tx []*DantePeer `json:"tx,omitempty"`
Rx []*DantePeer `json:"rx,omitempty"`
lastSeen time.Time
}
func (f *DanteFlows) Expire(maxAge time.Duration) bool {
if f.lastSeen.IsZero() {
return false
}
return time.Since(f.lastSeen) > maxAge
}
type Node struct { type Node struct {
ID string `json:"id"` 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:"-"`
PoEBudget *PoEBudget `json:"poe_budget,omitempty"` PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"` DanteTxChannels int `json:"dante_tx_channels,omitempty"`
DanteTxChannels string `json:"dante_tx_channels,omitempty"` DanteClockMasterSeen time.Time `json:"-"`
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"` DanteFlows *DanteFlows `json:"dante_flows,omitempty"`
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"` MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"` ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"` ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
DanteTx []*DantePeer `json:"dante_tx,omitempty"` SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
DanteRx []*DantePeer `json:"dante_rx,omitempty"` Unreachable bool `json:"unreachable,omitempty"`
Unreachable bool `json:"unreachable,omitempty"` errors *ErrorTracker
errors *ErrorTracker pollTrigger chan struct{}
pollTrigger chan struct{} cancelFunc context.CancelFunc
cancelFunc context.CancelFunc }
danteLastSeen time.Time
func (n *Node) IsDanteClockMaster() bool {
return !n.DanteClockMasterSeen.IsZero() && time.Since(n.DanteClockMasterSeen) < 5*time.Minute
} }
func (n *Node) SetUnreachable(unreachable bool) bool { func (n *Node) SetUnreachable(unreachable bool) bool {
@@ -507,13 +522,13 @@ func (n *Node) WithInterface(ifaceKey string) *Node {
return n return n
} }
return &Node{ return &Node{
ID: n.ID, ID: n.ID,
Names: n.Names, Names: n.Names,
Interfaces: InterfaceMap{ifaceKey: iface}, Interfaces: InterfaceMap{ifaceKey: iface},
MACTable: n.MACTable, MACTable: n.MACTable,
PoEBudget: n.PoEBudget, PoEBudget: n.PoEBudget,
IsDanteClockMaster: n.IsDanteClockMaster, DanteClockMasterSeen: n.DanteClockMasterSeen,
DanteTxChannels: n.DanteTxChannels, DanteTxChannels: n.DanteTxChannels,
} }
} }