Remove separate tracking structures and store protocol data directly on nodes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-28 22:15:54 -08:00
parent 41000bd4a0
commit fc5b36cd1c
8 changed files with 484 additions and 709 deletions

153
artnet.go
View File

@@ -7,21 +7,12 @@ import (
"net" "net"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/gopatchy/artnet" "github.com/gopatchy/artnet"
) )
type ArtNetNode struct {
TypeID string `json:"typeid"`
Node *Node `json:"node"`
Inputs []int `json:"inputs,omitempty"`
Outputs []int `json:"outputs,omitempty"`
LastSeen time.Time `json:"last_seen"`
}
func (t *Tendrils) startArtNetListener(ctx context.Context) { func (t *Tendrils) startArtNetListener(ctx context.Context) {
conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: artnet.Port}) conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: artnet.Port})
if err != nil { if err != nil {
@@ -178,45 +169,6 @@ func (t *Tendrils) sendArtPoll(conn *net.UDPConn, broadcast net.IP, ifaceName st
} }
} }
type ArtNetNodes struct {
mu sync.RWMutex
nodes map[*Node]*ArtNetNode
}
func NewArtNetNodes() *ArtNetNodes {
return &ArtNetNodes{
nodes: map[*Node]*ArtNetNode{},
}
}
func (a *ArtNetNodes) Update(node *Node, inputs, outputs []int) {
a.mu.Lock()
defer a.mu.Unlock()
existing, exists := a.nodes[node]
if exists {
for _, u := range inputs {
if !containsInt(existing.Inputs, u) {
existing.Inputs = append(existing.Inputs, u)
}
}
for _, u := range outputs {
if !containsInt(existing.Outputs, u) {
existing.Outputs = append(existing.Outputs, u)
}
}
existing.LastSeen = time.Now()
} else {
a.nodes[node] = &ArtNetNode{
TypeID: newTypeID("artnetnode"),
Node: node,
Inputs: inputs,
Outputs: outputs,
LastSeen: time.Now(),
}
}
}
func containsInt(slice []int, val int) bool { func containsInt(slice []int, val int) bool {
for _, v := range slice { for _, v := range slice {
if v == val { if v == val {
@@ -226,85 +178,78 @@ func containsInt(slice []int, val int) bool {
return false return false
} }
func (a *ArtNetNodes) ReplaceNode(oldNode, newNode *Node) { func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
a.mu.Lock() n.mu.Lock()
defer a.mu.Unlock() defer n.mu.Unlock()
if artNode, exists := a.nodes[oldNode]; exists { for _, u := range inputs {
delete(a.nodes, oldNode) if !containsInt(node.ArtNetInputs, u) {
if existing, hasNew := a.nodes[newNode]; hasNew { node.ArtNetInputs = append(node.ArtNetInputs, u)
for _, u := range artNode.Inputs {
if !containsInt(existing.Inputs, u) {
existing.Inputs = append(existing.Inputs, u)
}
}
for _, u := range artNode.Outputs {
if !containsInt(existing.Outputs, u) {
existing.Outputs = append(existing.Outputs, u)
}
}
} else {
artNode.Node = newNode
a.nodes[newNode] = artNode
} }
} }
for _, u := range outputs {
if !containsInt(node.ArtNetOutputs, u) {
node.ArtNetOutputs = append(node.ArtNetOutputs, u)
}
}
sort.Ints(node.ArtNetInputs)
sort.Ints(node.ArtNetOutputs)
node.artnetLastSeen = time.Now()
} }
func (a *ArtNetNodes) Expire() { func (n *Nodes) expireArtNet() {
a.mu.Lock()
defer a.mu.Unlock()
expireTime := time.Now().Add(-60 * time.Second) expireTime := time.Now().Add(-60 * time.Second)
for nodePtr, artNode := range a.nodes { for _, node := range n.nodes {
if artNode.LastSeen.Before(expireTime) { if !node.artnetLastSeen.IsZero() && node.artnetLastSeen.Before(expireTime) {
delete(a.nodes, nodePtr) node.ArtNetInputs = nil
node.ArtNetOutputs = nil
node.artnetLastSeen = time.Time{}
} }
} }
} }
func (a *ArtNetNodes) GetAll() []*ArtNetNode { func (n *Nodes) mergeArtNet(keep, merge *Node) {
a.mu.RLock() for _, u := range merge.ArtNetInputs {
defer a.mu.RUnlock() if !containsInt(keep.ArtNetInputs, u) {
result := make([]*ArtNetNode, 0, len(a.nodes)) keep.ArtNetInputs = append(keep.ArtNetInputs, u)
for _, node := range a.nodes { }
result = append(result, node)
} }
return result for _, u := range merge.ArtNetOutputs {
if !containsInt(keep.ArtNetOutputs, u) {
keep.ArtNetOutputs = append(keep.ArtNetOutputs, u)
}
}
if merge.artnetLastSeen.After(keep.artnetLastSeen) {
keep.artnetLastSeen = merge.artnetLastSeen
}
sort.Ints(keep.ArtNetInputs)
sort.Ints(keep.ArtNetOutputs)
} }
func (a *ArtNetNodes) LogAll() { func (n *Nodes) logArtNet() {
a.Expire()
a.mu.RLock()
defer a.mu.RUnlock()
if len(a.nodes) == 0 {
return
}
var artNodes []*ArtNetNode
for _, artNode := range a.nodes {
artNodes = append(artNodes, artNode)
}
sort.Slice(artNodes, func(i, j int) bool {
return sortorder.NaturalLess(artNodes[i].Node.DisplayName(), artNodes[j].Node.DisplayName())
})
inputUniverses := map[int][]string{} inputUniverses := map[int][]string{}
outputUniverses := map[int][]string{} outputUniverses := map[int][]string{}
for _, artNode := range artNodes { for _, node := range n.nodes {
name := artNode.Node.DisplayName() if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
continue
}
name := node.DisplayName()
if name == "" { if name == "" {
name = "??" name = "??"
} }
for _, u := range artNode.Inputs { for _, u := range node.ArtNetInputs {
inputUniverses[u] = append(inputUniverses[u], name) inputUniverses[u] = append(inputUniverses[u], name)
} }
for _, u := range artNode.Outputs { for _, u := range node.ArtNetOutputs {
outputUniverses[u] = append(outputUniverses[u], name) outputUniverses[u] = append(outputUniverses[u], name)
} }
} }
if len(inputUniverses) == 0 && len(outputUniverses) == 0 {
return
}
var allUniverses []int var allUniverses []int
seen := map[int]bool{} seen := map[int]bool{}
for u := range inputUniverses { for u := range inputUniverses {
@@ -340,7 +285,3 @@ func (a *ArtNetNodes) LogAll() {
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; ")) log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; "))
} }
} }
func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
n.t.artnet.Update(node, inputs, outputs)
}

435
dante.go
View File

@@ -9,7 +9,6 @@ import (
"net" "net"
"sort" "sort"
"strings" "strings"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -103,15 +102,10 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
groupKey := groupIP.String() groupName := multicastGroupName(groupIP)
gm := n.multicastGroups[groupKey] for _, node := range n.nodes {
if gm == nil { if node.DanteTxChannels != "" && containsString(node.MulticastGroups, groupName) {
return nil return node
}
for _, membership := range gm.Members {
if membership.Node != nil && membership.Node.DanteTxChannels != "" {
return membership.Node
} }
} }
return nil return nil
@@ -119,43 +113,6 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node {
var danteSeqID uint32 var danteSeqID uint32
type DanteSubscriberMap map[*Node]*DanteFlowSubscriber
func (m DanteSubscriberMap) MarshalJSON() ([]byte, error) {
subs := make([]*DanteFlowSubscriber, 0, len(m))
for _, sub := range m {
subs = append(subs, sub)
}
sort.Slice(subs, func(i, j int) bool {
return sortorder.NaturalLess(subs[i].Node.DisplayName(), subs[j].Node.DisplayName())
})
return json.Marshal(subs)
}
type DanteFlow struct {
TypeID string `json:"typeid"`
Source *Node `json:"source"`
Subscribers DanteSubscriberMap `json:"subscribers"`
}
type DanteFlowSubscriber struct {
Node *Node `json:"node"`
Channels []string `json:"channels,omitempty"`
ChannelStatus map[string]DanteFlowStatus `json:"channel_status,omitempty"`
LastSeen time.Time `json:"last_seen"`
}
type DanteFlows struct {
mu sync.RWMutex
flows map[*Node]*DanteFlow
}
func NewDanteFlows() *DanteFlows {
return &DanteFlows{
flows: map[*Node]*DanteFlow{},
}
}
func containsString(slice []string, val string) bool { func containsString(slice []string, val string) bool {
for _, s := range slice { for _, s := range slice {
if s == val { if s == val {
@@ -165,116 +122,210 @@ func containsString(slice []string, val string) bool {
return false return false
} }
func (d *DanteFlows) Update(source, subscriber *Node, channelInfo string, flowStatus DanteFlowStatus) { type DanteFlowStatus uint8
d.mu.Lock()
defer d.mu.Unlock()
flow := d.flows[source] const (
if flow == nil { DanteFlowUnsubscribed DanteFlowStatus = 0x00
flow = &DanteFlow{ DanteFlowNoSource DanteFlowStatus = 0x01
TypeID: newTypeID("danteflow"), DanteFlowActive DanteFlowStatus = 0x09
Source: source, )
Subscribers: DanteSubscriberMap{},
func (s DanteFlowStatus) String() string {
switch s {
case DanteFlowActive:
return "active"
case DanteFlowNoSource:
return "no-source"
default:
return ""
}
}
func (s DanteFlowStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
type DanteChannelType uint16
const (
DanteChannelUnknown DanteChannelType = 0
DanteChannelAudio DanteChannelType = 0x000f
DanteChannelAudio2 DanteChannelType = 0x0006
DanteChannelVideo DanteChannelType = 0x000e
)
func (t DanteChannelType) String() string {
switch t {
case DanteChannelAudio, DanteChannelAudio2:
return "audio"
case DanteChannelVideo:
return "video"
default:
return ""
}
}
type DanteSubscription struct {
RxChannel int
TxDeviceName string
TxChannelName string
ChannelType DanteChannelType
FlowStatus DanteFlowStatus
}
type DanteDeviceInfo struct {
IP net.IP
Name string
RxChannelCount int
TxChannelCount int
Subscriptions []DanteSubscription
HasMulticast bool
}
func (n *Nodes) UpdateDanteFlow(source, subscriber *Node, channelInfo string, flowStatus DanteFlowStatus) {
n.mu.Lock()
defer n.mu.Unlock()
n.updateDanteTx(source, subscriber, channelInfo, flowStatus)
n.updateDanteRx(subscriber, source, channelInfo, flowStatus)
source.danteLastSeen = time.Now()
subscriber.danteLastSeen = time.Now()
}
func (n *Nodes) updateDanteTx(source, subscriber *Node, channelInfo string, flowStatus DanteFlowStatus) {
var peer *DantePeer
for _, p := range source.DanteTx {
if p.Node == subscriber {
peer = p
break
} }
d.flows[source] = flow }
if peer == nil {
peer = &DantePeer{
Node: subscriber,
Status: map[string]string{},
}
source.DanteTx = append(source.DanteTx, peer)
} }
sub := flow.Subscribers[subscriber] if channelInfo != "" && !containsString(peer.Channels, channelInfo) {
if sub == nil { peer.Channels = append(peer.Channels, channelInfo)
sub = &DanteFlowSubscriber{ sort.Strings(peer.Channels)
Node: subscriber,
ChannelStatus: map[string]DanteFlowStatus{},
}
flow.Subscribers[subscriber] = sub
}
if channelInfo != "" && !containsString(sub.Channels, channelInfo) {
sub.Channels = append(sub.Channels, channelInfo)
sort.Strings(sub.Channels)
} }
if channelInfo != "" { if channelInfo != "" {
sub.ChannelStatus[channelInfo] = flowStatus peer.Status[channelInfo] = flowStatus.String()
} }
sub.LastSeen = time.Now() sort.Slice(source.DanteTx, func(i, j int) bool {
} return sortorder.NaturalLess(source.DanteTx[i].Node.DisplayName(), source.DanteTx[j].Node.DisplayName())
func (d *DanteFlows) ReplaceNode(oldNode, newNode *Node) {
d.mu.Lock()
defer d.mu.Unlock()
if flow, exists := d.flows[oldNode]; exists {
delete(d.flows, oldNode)
if existingFlow, hasNew := d.flows[newNode]; hasNew {
for subNode, sub := range flow.Subscribers {
if existingSub, hasSub := existingFlow.Subscribers[subNode]; hasSub {
for _, ch := range sub.Channels {
if !containsString(existingSub.Channels, ch) {
existingSub.Channels = append(existingSub.Channels, ch)
}
}
} else {
existingFlow.Subscribers[subNode] = sub
}
}
} else {
flow.Source = newNode
d.flows[newNode] = flow
}
}
for _, flow := range d.flows {
if sub, exists := flow.Subscribers[oldNode]; exists {
delete(flow.Subscribers, oldNode)
if existingSub, hasNew := flow.Subscribers[newNode]; hasNew {
for _, ch := range sub.Channels {
if !containsString(existingSub.Channels, ch) {
existingSub.Channels = append(existingSub.Channels, ch)
}
}
} else {
sub.Node = newNode
flow.Subscribers[newNode] = sub
}
}
}
}
func (d *DanteFlows) Expire() {
d.mu.Lock()
defer d.mu.Unlock()
expireTime := time.Now().Add(-5 * time.Minute)
for source, flow := range d.flows {
for subNode, sub := range flow.Subscribers {
if sub.LastSeen.Before(expireTime) {
delete(flow.Subscribers, subNode)
}
}
if len(flow.Subscribers) == 0 {
delete(d.flows, source)
}
}
}
func (d *DanteFlows) LogAll() {
d.Expire()
d.mu.RLock()
defer d.mu.RUnlock()
if len(d.flows) == 0 {
return
}
var flows []*DanteFlow
for _, flow := range d.flows {
flows = append(flows, flow)
}
sort.Slice(flows, func(i, j int) bool {
return sortorder.NaturalLess(flows[i].Source.DisplayName(), flows[j].Source.DisplayName())
}) })
}
func (n *Nodes) updateDanteRx(subscriber, source *Node, channelInfo string, flowStatus DanteFlowStatus) {
var peer *DantePeer
for _, p := range subscriber.DanteRx {
if p.Node == source {
peer = p
break
}
}
if peer == nil {
peer = &DantePeer{
Node: source,
Status: map[string]string{},
}
subscriber.DanteRx = append(subscriber.DanteRx, peer)
}
if channelInfo != "" && !containsString(peer.Channels, channelInfo) {
peer.Channels = append(peer.Channels, channelInfo)
sort.Strings(peer.Channels)
}
if channelInfo != "" {
peer.Status[channelInfo] = flowStatus.String()
}
sort.Slice(subscriber.DanteRx, func(i, j int) bool {
return sortorder.NaturalLess(subscriber.DanteRx[i].Node.DisplayName(), subscriber.DanteRx[j].Node.DisplayName())
})
}
func (n *Nodes) expireDante() {
expireTime := time.Now().Add(-5 * time.Minute)
for _, node := range n.nodes {
if !node.danteLastSeen.IsZero() && node.danteLastSeen.Before(expireTime) {
node.DanteTx = nil
node.DanteRx = nil
node.danteLastSeen = time.Time{}
}
}
}
func (n *Nodes) mergeDante(keep, merge *Node) {
for _, peer := range merge.DanteTx {
var existing *DantePeer
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 {
var existing *DantePeer
for _, p := range keep.DanteRx {
if p.Node == peer.Node {
existing = p
break
}
}
if existing == nil {
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 {
existing.Status[ch] = status
}
}
}
if merge.danteLastSeen.After(keep.danteLastSeen) {
keep.danteLastSeen = merge.danteLastSeen
}
for _, node := range n.nodes {
for _, peer := range node.DanteTx {
if peer.Node == merge {
peer.Node = keep
}
}
for _, peer := range node.DanteRx {
if peer.Node == merge {
peer.Node = keep
}
}
}
}
func (n *Nodes) logDante() {
type channelFlow struct { type channelFlow struct {
sourceName string sourceName string
txCh string txCh string
@@ -286,21 +337,24 @@ func (d *DanteFlows) LogAll() {
var allChannelFlows []channelFlow var allChannelFlows []channelFlow
var allNoChannelFlows []string var allNoChannelFlows []string
for _, flow := range flows { for _, node := range n.nodes {
sourceName := flow.Source.DisplayName() if len(node.DanteTx) == 0 {
continue
}
sourceName := node.DisplayName()
if sourceName == "" { if sourceName == "" {
sourceName = "??" sourceName = "??"
} }
for _, sub := range flow.Subscribers { for _, peer := range node.DanteTx {
subName := sub.Node.DisplayName() subName := peer.Node.DisplayName()
if subName == "" { if subName == "" {
subName = "??" subName = "??"
} }
if len(sub.Channels) == 0 { if len(peer.Channels) == 0 {
allNoChannelFlows = append(allNoChannelFlows, fmt.Sprintf("%s -> %s", sourceName, subName)) allNoChannelFlows = append(allNoChannelFlows, fmt.Sprintf("%s -> %s", sourceName, subName))
} else { } else {
for _, ch := range sub.Channels { for _, ch := range peer.Channels {
parts := strings.Split(ch, "->") parts := strings.Split(ch, "->")
if len(parts) == 2 { if len(parts) == 2 {
rxPart := parts[1] rxPart := parts[1]
@@ -315,7 +369,7 @@ func (d *DanteFlows) LogAll() {
rxName: subName, rxName: subName,
rxCh: rxPart, rxCh: rxPart,
channelType: chType, channelType: chType,
down: sub.ChannelStatus[ch] == DanteFlowNoSource, down: peer.Status[ch] == "no-source",
}) })
} else { } else {
allNoChannelFlows = append(allNoChannelFlows, fmt.Sprintf("%s -> %s[%s]", sourceName, subName, ch)) allNoChannelFlows = append(allNoChannelFlows, fmt.Sprintf("%s -> %s[%s]", sourceName, subName, ch))
@@ -326,6 +380,10 @@ func (d *DanteFlows) LogAll() {
} }
totalFlows := len(allChannelFlows) + len(allNoChannelFlows) totalFlows := len(allChannelFlows) + len(allNoChannelFlows)
if totalFlows == 0 {
return
}
log.Printf("[sigusr1] ================ %d dante flows ================", totalFlows) log.Printf("[sigusr1] ================ %d dante flows ================", totalFlows)
sort.Slice(allChannelFlows, func(i, j int) bool { sort.Slice(allChannelFlows, func(i, j int) bool {
@@ -405,66 +463,6 @@ func (t *Tendrils) queryDanteDeviceWithPort(ip net.IP, port int) *DanteDeviceInf
return info return info
} }
type DanteDeviceInfo struct {
IP net.IP
Name string
RxChannelCount int
TxChannelCount int
Subscriptions []DanteSubscription
HasMulticast bool
}
type DanteChannelType uint16
const (
DanteChannelUnknown DanteChannelType = 0
DanteChannelAudio DanteChannelType = 0x000f
DanteChannelAudio2 DanteChannelType = 0x0006
DanteChannelVideo DanteChannelType = 0x000e
)
func (t DanteChannelType) String() string {
switch t {
case DanteChannelAudio, DanteChannelAudio2:
return "audio"
case DanteChannelVideo:
return "video"
default:
return ""
}
}
type DanteFlowStatus uint8
const (
DanteFlowUnsubscribed DanteFlowStatus = 0x00
DanteFlowNoSource DanteFlowStatus = 0x01
DanteFlowActive DanteFlowStatus = 0x09
)
func (s DanteFlowStatus) String() string {
switch s {
case DanteFlowActive:
return "active"
case DanteFlowNoSource:
return "no-source"
default:
return ""
}
}
func (s DanteFlowStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
type DanteSubscription struct {
RxChannel int
TxDeviceName string
TxChannelName string
ChannelType DanteChannelType
FlowStatus DanteFlowStatus
}
func buildDantePacket(packetType byte, cmd uint16, args []byte) []byte { func buildDantePacket(packetType byte, cmd uint16, args []byte) []byte {
seq := nextDanteSeq() seq := nextDanteSeq()
totalLen := 10 + len(args) totalLen := 10 + len(args)
@@ -530,24 +528,19 @@ func (t *Tendrils) sendDanteCommand28(conn *net.UDPConn, ip net.IP, cmd uint16,
} }
func (t *Tendrils) queryDanteDeviceName(conn *net.UDPConn, ip net.IP) string { func (t *Tendrils) queryDanteDeviceName(conn *net.UDPConn, ip net.IP) string {
// 0x1003 returns device info - name position varies by device
resp := t.sendDanteCommand(conn, ip, 0x1003, nil) resp := t.sendDanteCommand(conn, ip, 0x1003, nil)
if resp == nil || len(resp) < 40 { if resp == nil || len(resp) < 40 {
return "" return ""
} }
// Find the first printable string that looks like a device name
// Look for patterns like "AJA-", "ULXD", etc starting from offset 40
for i := 40; i < len(resp)-4; i++ { for i := 40; i < len(resp)-4; i++ {
if resp[i] >= 'A' && resp[i] <= 'Z' { if resp[i] >= 'A' && resp[i] <= 'Z' {
// Found uppercase letter, might be start of name
end := i end := i
for end < len(resp) && resp[end] != 0 && resp[end] >= 0x20 && resp[end] < 0x7f { for end < len(resp) && resp[end] != 0 && resp[end] >= 0x20 && resp[end] < 0x7f {
end++ end++
} }
if end-i >= 4 && end-i < 40 { if end-i >= 4 && end-i < 40 {
name := string(resp[i:end]) name := string(resp[i:end])
// Skip "Audinate" which is the platform name
if name != "Audinate DCM" && !strings.HasPrefix(name, "Audinate") { if name != "Audinate DCM" && !strings.HasPrefix(name, "Audinate") {
if t.DebugDante { if t.DebugDante {
log.Printf("[dante] %s: device name: %q", ip, name) log.Printf("[dante] %s: device name: %q", ip, name)
@@ -873,7 +866,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
} }
sourceNode := t.nodes.GetOrCreateByName(txDeviceName) sourceNode := t.nodes.GetOrCreateByName(txDeviceName)
subscriberNode := t.nodes.GetOrCreateByName(info.Name) subscriberNode := t.nodes.GetOrCreateByName(info.Name)
t.danteFlows.Update(sourceNode, subscriberNode, channelInfo, sub.FlowStatus) t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, channelInfo, sub.FlowStatus)
needIGMPFallback = false needIGMPFallback = false
} }
} }
@@ -893,7 +886,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
sourceNode = t.nodes.GetOrCreateByName(multicastGroupName(groupIP)) sourceNode = t.nodes.GetOrCreateByName(multicastGroupName(groupIP))
} }
subscriberNode := t.nodes.GetOrCreateByName(info.Name) subscriberNode := t.nodes.GetOrCreateByName(info.Name)
t.danteFlows.Update(sourceNode, subscriberNode, "", DanteFlowActive) t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive)
} }
} }
} }

178
http.go
View File

@@ -117,7 +117,9 @@ func ensureCert() error {
func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) { func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
status := t.GetStatus() status := t.GetStatus()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status) if err := json.NewEncoder(w).Encode(status); err != nil {
log.Printf("[ERROR] failed to encode status: %v", err)
}
} }
func (t *Tendrils) GetStatus() *StatusResponse { func (t *Tendrils) GetStatus() *StatusResponse {
@@ -175,6 +177,7 @@ func (t *Tendrils) handleAPIStatusStream(w http.ResponseWriter, r *http.Request)
sendStatus := func() error { sendStatus := func() error {
data, err := json.Marshal(t.GetStatus()) data, err := json.Marshal(t.GetStatus())
if err != nil { if err != nil {
log.Printf("[ERROR] failed to marshal status: %v", err)
return err return err
} }
_, err = fmt.Fprintf(w, "event: status\ndata: %s\n\n", data) _, err = fmt.Fprintf(w, "event: status\ndata: %s\n\n", data)
@@ -213,37 +216,22 @@ func (t *Tendrils) handleAPIStatusStream(w http.ResponseWriter, r *http.Request)
func (t *Tendrils) getNodes() []*Node { func (t *Tendrils) getNodes() []*Node {
t.nodes.mu.Lock() t.nodes.mu.Lock()
t.nodes.expireMulticastMemberships() t.nodes.expireMulticastMemberships()
t.nodes.expireArtNet()
t.nodes.expireSACN()
t.nodes.expireDante()
t.nodes.mu.Unlock() t.nodes.mu.Unlock()
t.artnet.Expire()
t.sacnSources.Expire()
t.danteFlows.Expire()
t.nodes.mu.RLock() t.nodes.mu.RLock()
defer t.nodes.mu.RUnlock() defer t.nodes.mu.RUnlock()
multicastByNode := t.buildMulticastByNode()
artnetByNode := t.buildArtNetByNode()
sacnByNode := t.buildSACNByNode()
danteTxByNode, danteRxByNode := t.buildDanteByNode()
unreachableNodes := t.errors.GetUnreachableNodeSet() unreachableNodes := t.errors.GetUnreachableNodeSet()
nodes := make([]*Node, 0, len(t.nodes.nodes)) nodes := make([]*Node, 0, len(t.nodes.nodes))
for _, node := range t.nodes.nodes { for _, node := range t.nodes.nodes {
nodeCopy := *node n := new(Node)
nodeCopy.MulticastGroups = multicastByNode[node] *n = *node
if artnet := artnetByNode[node]; artnet != nil { n.Unreachable = unreachableNodes[node.TypeID]
nodeCopy.ArtNetInputs = artnet.Inputs nodes = append(nodes, n)
nodeCopy.ArtNetOutputs = artnet.Outputs
}
if sacn := sacnByNode[node]; sacn != nil {
nodeCopy.SACNInputs = sacn.Inputs
nodeCopy.SACNOutputs = sacn.Outputs
}
nodeCopy.DanteTx = danteTxByNode[node]
nodeCopy.DanteRx = danteRxByNode[node]
nodeCopy.Unreachable = unreachableNodes[node.TypeID]
nodes = append(nodes, &nodeCopy)
} }
sort.Slice(nodes, func(i, j int) bool { sort.Slice(nodes, func(i, j int) bool {
@@ -259,150 +247,6 @@ func (t *Tendrils) getNodes() []*Node {
return nodes return nodes
} }
func (t *Tendrils) buildMulticastByNode() map[*Node][]string {
result := map[*Node][]string{}
for _, gm := range t.nodes.multicastGroups {
for _, membership := range gm.Members {
if membership.Node != nil {
result[membership.Node] = append(result[membership.Node], gm.Group.Name)
}
}
}
for node, groups := range result {
sort.Strings(groups)
result[node] = groups
}
return result
}
type artnetNodeData struct {
Inputs []int
Outputs []int
}
func (t *Tendrils) buildArtNetByNode() map[*Node]*artnetNodeData {
t.artnet.mu.RLock()
defer t.artnet.mu.RUnlock()
result := map[*Node]*artnetNodeData{}
for _, an := range t.artnet.nodes {
inputs := make([]int, len(an.Inputs))
for i, u := range an.Inputs {
inputs[i] = int(u)
}
outputs := make([]int, len(an.Outputs))
for i, u := range an.Outputs {
outputs[i] = int(u)
}
sort.Ints(inputs)
sort.Ints(outputs)
result[an.Node] = &artnetNodeData{Inputs: inputs, Outputs: outputs}
}
return result
}
type sacnNodeData struct {
Inputs []int
Outputs []int
}
func (t *Tendrils) buildSACNByNode() map[*Node]*sacnNodeData {
result := map[*Node]*sacnNodeData{}
for _, gm := range t.nodes.multicastGroups {
if len(gm.Group.Name) < 5 || gm.Group.Name[:5] != "sacn:" {
continue
}
var universe int
if _, err := fmt.Sscanf(gm.Group.Name, "sacn:%d", &universe); err != nil {
continue
}
for _, membership := range gm.Members {
if membership.Node == nil {
continue
}
data := result[membership.Node]
if data == nil {
data = &sacnNodeData{}
result[membership.Node] = data
}
if !containsInt(data.Inputs, universe) {
data.Inputs = append(data.Inputs, universe)
}
}
}
t.sacnSources.mu.RLock()
for _, source := range t.sacnSources.sources {
if source.SrcIP == nil {
continue
}
node := t.nodes.getByIPLocked(source.SrcIP)
if node == nil {
continue
}
data := result[node]
if data == nil {
data = &sacnNodeData{}
result[node] = data
}
for _, u := range source.Universes {
if !containsInt(data.Outputs, u) {
data.Outputs = append(data.Outputs, u)
}
}
}
t.sacnSources.mu.RUnlock()
for _, data := range result {
sort.Ints(data.Inputs)
sort.Ints(data.Outputs)
}
return result
}
func (t *Tendrils) buildDanteByNode() (map[*Node][]*DantePeer, map[*Node][]*DantePeer) {
t.danteFlows.mu.RLock()
defer t.danteFlows.mu.RUnlock()
txByNode := map[*Node][]*DantePeer{}
rxByNode := map[*Node][]*DantePeer{}
for source, flow := range t.danteFlows.flows {
for subNode, sub := range flow.Subscribers {
status := map[string]string{}
for ch, st := range sub.ChannelStatus {
status[ch] = st.String()
}
txByNode[source] = append(txByNode[source], &DantePeer{
Node: subNode,
Channels: sub.Channels,
Status: status,
})
rxByNode[subNode] = append(rxByNode[subNode], &DantePeer{
Node: source,
Channels: sub.Channels,
Status: status,
})
}
}
for node, peers := range txByNode {
sort.Slice(peers, func(i, j int) bool {
return sortorder.NaturalLess(peers[i].Node.DisplayName(), peers[j].Node.DisplayName())
})
txByNode[node] = peers
}
for node, peers := range rxByNode {
sort.Slice(peers, func(i, j int) bool {
return sortorder.NaturalLess(peers[i].Node.DisplayName(), peers[j].Node.DisplayName())
})
rxByNode[node] = peers
}
return txByNode, rxByNode
}
func (t *Tendrils) getLinks() []*Link { func (t *Tendrils) getLinks() []*Link {
t.nodes.mu.RLock() t.nodes.mu.RLock()

View File

@@ -1,13 +1,10 @@
package tendrils package tendrils
import ( import (
"encoding/json"
"fmt" "fmt"
"net" "net"
"sort" "sort"
"time" "time"
"github.com/fvbommel/sortorder"
) )
type MulticastGroup struct { type MulticastGroup struct {
@@ -15,39 +12,6 @@ type MulticastGroup struct {
IP string `json:"ip"` IP string `json:"ip"`
} }
type MulticastMembership struct {
SourceIP string `json:"source_ip"`
Node *Node `json:"node,omitempty"`
LastSeen time.Time `json:"last_seen"`
}
type MulticastMembershipMap map[string]*MulticastMembership
func (m MulticastMembershipMap) MarshalJSON() ([]byte, error) {
members := make([]*MulticastMembership, 0, len(m))
for _, membership := range m {
members = append(members, membership)
}
sort.Slice(members, func(i, j int) bool {
nameI := members[i].SourceIP
if members[i].Node != nil && members[i].Node.DisplayName() != "" {
nameI = members[i].Node.DisplayName()
}
nameJ := members[j].SourceIP
if members[j].Node != nil && members[j].Node.DisplayName() != "" {
nameJ = members[j].Node.DisplayName()
}
return sortorder.NaturalLess(nameI, nameJ)
})
return json.Marshal(members)
}
type MulticastGroupMembers struct {
TypeID string `json:"typeid"`
Group *MulticastGroup `json:"group"`
Members MulticastMembershipMap `json:"members"`
}
func (g *MulticastGroup) IsDante() bool { func (g *MulticastGroup) IsDante() bool {
ip := net.ParseIP(g.IP).To4() ip := net.ParseIP(g.IP).To4()
if ip == nil { if ip == nil {
@@ -116,62 +80,71 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
defer n.mu.Unlock() defer n.mu.Unlock()
node := n.getNodeByIPLocked(sourceIP) node := n.getNodeByIPLocked(sourceIP)
if node == nil {
groupKey := groupIP.String() return
sourceKey := sourceIP.String()
gm := n.multicastGroups[groupKey]
if gm == nil {
gm = &MulticastGroupMembers{
TypeID: newTypeID("mcastgroup"),
Group: &MulticastGroup{
Name: multicastGroupName(groupIP),
IP: groupKey,
},
Members: MulticastMembershipMap{},
}
n.multicastGroups[groupKey] = gm
} }
membership := gm.Members[sourceKey] groupName := multicastGroupName(groupIP)
if membership == nil {
membership = &MulticastMembership{ if node.multicastLastSeen == nil {
SourceIP: sourceKey, node.multicastLastSeen = map[string]time.Time{}
} }
gm.Members[sourceKey] = membership 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)
}
}
} }
membership.Node = node
membership.LastSeen = time.Now()
} }
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) { func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
groupKey := groupIP.String() node := n.getNodeByIPLocked(sourceIP)
sourceKey := sourceIP.String() if node == nil {
return
}
if gm := n.multicastGroups[groupKey]; gm != nil { groupName := multicastGroupName(groupIP)
delete(gm.Members, sourceKey) delete(node.multicastLastSeen, groupName)
if len(gm.Members) == 0 {
delete(n.multicastGroups, groupKey) var groups []string
for _, g := range node.MulticastGroups {
if g != groupName {
groups = append(groups, g)
} }
} }
node.MulticastGroups = groups
} }
func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP { func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
deviceKey := deviceIP.String() node := n.getNodeByIPLocked(deviceIP)
var groups []net.IP if node == nil {
return nil
}
for _, gm := range n.multicastGroups { var groups []net.IP
if !gm.Group.IsDante() { for _, groupName := range node.MulticastGroups {
continue g := &MulticastGroup{Name: groupName}
} if g.IsDante() {
if _, exists := gm.Members[deviceKey]; exists { ip := net.ParseIP(groupName)
groups = append(groups, net.ParseIP(gm.Group.IP)) if ip != nil {
groups = append(groups, ip)
}
} }
} }
return groups return groups
@@ -181,16 +154,11 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
n.mu.RLock() n.mu.RLock()
defer n.mu.RUnlock() defer n.mu.RUnlock()
groupKey := groupIP.String() groupName := multicastGroupName(groupIP)
gm := n.multicastGroups[groupKey]
if gm == nil {
return nil
}
var members []*Node var members []*Node
for _, membership := range gm.Members { for _, node := range n.nodes {
if membership.Node != nil { if containsString(node.MulticastGroups, groupName) {
members = append(members, membership.Node) members = append(members, node)
} }
} }
return members return members
@@ -198,14 +166,54 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
func (n *Nodes) expireMulticastMemberships() { func (n *Nodes) expireMulticastMemberships() {
expireTime := time.Now().Add(-5 * time.Minute) expireTime := time.Now().Add(-5 * time.Minute)
for groupKey, gm := range n.multicastGroups { for _, node := range n.nodes {
for sourceKey, membership := range gm.Members { if node.multicastLastSeen == nil {
if membership.LastSeen.Before(expireTime) { continue
delete(gm.Members, sourceKey) }
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)
} }
} }
if len(gm.Members) == 0 { node.MulticastGroups = keepGroups
delete(n.multicastGroups, groupKey) sort.Ints(keepSACNInputs)
} node.SACNInputs = keepSACNInputs
} }
} }
func (n *Nodes) mergeMulticast(keep, merge *Node) {
if merge.multicastLastSeen == nil {
return
}
if keep.multicastLastSeen == nil {
keep.multicastLastSeen = map[string]time.Time{}
}
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)
}
}
}
}
sort.Strings(keep.MulticastGroups)
sort.Ints(keep.SACNInputs)
}

111
nodes.go
View File

@@ -14,32 +14,30 @@ import (
) )
type Nodes struct { type Nodes struct {
mu sync.RWMutex mu sync.RWMutex
nodes map[int]*Node nodes map[int]*Node
ipIndex map[string]int ipIndex map[string]int
macIndex map[string]int macIndex map[string]int
nameIndex map[string]int nameIndex map[string]int
nodeCancel map[int]context.CancelFunc nodeCancel map[int]context.CancelFunc
multicastGroups map[string]*MulticastGroupMembers nextID int
nextID int t *Tendrils
t *Tendrils ctx context.Context
ctx context.Context cancelAll context.CancelFunc
cancelAll context.CancelFunc
} }
func NewNodes(t *Tendrils) *Nodes { func NewNodes(t *Tendrils) *Nodes {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Nodes{ return &Nodes{
nodes: map[int]*Node{}, nodes: map[int]*Node{},
ipIndex: map[string]int{}, ipIndex: map[string]int{},
macIndex: map[string]int{}, macIndex: map[string]int{},
nameIndex: map[string]int{}, nameIndex: map[string]int{},
nodeCancel: map[int]context.CancelFunc{}, nodeCancel: map[int]context.CancelFunc{},
multicastGroups: map[string]*MulticastGroupMembers{}, nextID: 1,
nextID: 1, t: t,
t: t, ctx: ctx,
ctx: ctx, cancelAll: cancel,
cancelAll: cancel,
} }
} }
@@ -422,16 +420,10 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
keep.MACTable[peerMAC] = ifaceName keep.MACTable[peerMAC] = ifaceName
} }
n.t.danteFlows.ReplaceNode(merge, keep) n.mergeArtNet(keep, merge)
n.t.artnet.ReplaceNode(merge, keep) n.mergeSACN(keep, merge)
n.mergeMulticast(keep, merge)
for _, gm := range n.multicastGroups { n.mergeDante(keep, merge)
for _, membership := range gm.Members {
if membership.Node == merge {
membership.Node = keep
}
}
}
if cancel, exists := n.nodeCancel[mergeID]; exists { if cancel, exists := n.nodeCancel[mergeID]; exists {
cancel() cancel()
@@ -626,37 +618,36 @@ func (n *Nodes) LogAll() {
n.expireMulticastMemberships() n.expireMulticastMemberships()
if len(n.multicastGroups) > 0 { groupMembers := map[string][]string{}
var groups []*MulticastGroupMembers for _, node := range n.nodes {
for _, gm := range n.multicastGroups { for _, groupName := range node.MulticastGroups {
groups = append(groups, gm) name := node.DisplayName()
} if name == "" {
sort.Slice(groups, func(i, j int) bool { name = "??"
return sortorder.NaturalLess(groups[i].Group.Name, groups[j].Group.Name)
})
log.Printf("[sigusr1] ================ %d multicast groups ================", len(groups))
for _, gm := range groups {
var memberNames []string
for sourceIP, membership := range gm.Members {
var name string
if membership.Node != nil {
name = membership.Node.DisplayName()
if name == "" {
name = sourceIP
}
} else {
name = sourceIP
}
memberNames = append(memberNames, name)
} }
sort.Slice(memberNames, func(i, j int) bool { groupMembers[groupName] = append(groupMembers[groupName], name)
return sortorder.NaturalLess(memberNames[i], memberNames[j])
})
log.Printf("[sigusr1] %s: %s", gm.Group.Name, strings.Join(memberNames, ", "))
} }
} }
n.t.artnet.LogAll() if len(groupMembers) > 0 {
n.t.danteFlows.LogAll() var groupNames []string
for name := range groupMembers {
groupNames = append(groupNames, name)
}
sort.Slice(groupNames, func(i, j int) bool {
return sortorder.NaturalLess(groupNames[i], groupNames[j])
})
log.Printf("[sigusr1] ================ %d multicast groups ================", len(groupNames))
for _, groupName := range groupNames {
members := groupMembers[groupName]
sort.Slice(members, func(i, j int) bool {
return sortorder.NaturalLess(members[i], members[j])
})
log.Printf("[sigusr1] %s: %s", groupName, strings.Join(members, ", "))
}
}
n.logArtNet()
n.logDante()
} }

View File

@@ -4,70 +4,12 @@ import (
"context" "context"
"log" "log"
"net" "net"
"sync" "sort"
"time" "time"
"github.com/gopatchy/sacn" "github.com/gopatchy/sacn"
) )
type SACNSource struct {
CID string
SourceName string
Universes []int
SrcIP net.IP
LastSeen time.Time
}
type SACNSources struct {
mu sync.RWMutex
sources map[string]*SACNSource
}
func NewSACNSources() *SACNSources {
return &SACNSources{
sources: map[string]*SACNSource{},
}
}
func (s *SACNSources) Update(cid [16]byte, sourceName string, universes []uint16, srcIP net.IP) {
s.mu.Lock()
defer s.mu.Unlock()
cidStr := sacn.FormatCID(cid)
intUniverses := make([]int, len(universes))
for i, u := range universes {
intUniverses[i] = int(u)
}
existing, exists := s.sources[cidStr]
if exists {
existing.SourceName = sourceName
existing.Universes = intUniverses
existing.SrcIP = srcIP
existing.LastSeen = time.Now()
} else {
s.sources[cidStr] = &SACNSource{
CID: cidStr,
SourceName: sourceName,
Universes: intUniverses,
SrcIP: srcIP,
LastSeen: time.Now(),
}
}
}
func (s *SACNSources) Expire() {
s.mu.Lock()
defer s.mu.Unlock()
expireTime := time.Now().Add(-60 * time.Second)
for cid, source := range s.sources {
if source.LastSeen.Before(expireTime) {
delete(s.sources, cid)
}
}
}
func (t *Tendrils) startSACNDiscoveryListener(ctx context.Context, iface net.Interface) { func (t *Tendrils) startSACNDiscoveryListener(ctx context.Context, iface net.Interface) {
receiver, err := sacn.NewReceiver("") receiver, err := sacn.NewReceiver("")
if err != nil { if err != nil {
@@ -104,6 +46,44 @@ func (t *Tendrils) handleSACNDiscoveryPacket(srcIP net.IP, pkt *sacn.DiscoveryPa
t.nodes.Update(nil, nil, []net.IP{srcIP}, "", pkt.SourceName, "sacn") t.nodes.Update(nil, nil, []net.IP{srcIP}, "", pkt.SourceName, "sacn")
} }
t.sacnSources.Update(pkt.CID, pkt.SourceName, pkt.Universes, srcIP) node := t.nodes.GetByIP(srcIP)
if node != nil {
intUniverses := make([]int, len(pkt.Universes))
for i, u := range pkt.Universes {
intUniverses[i] = int(u)
}
t.nodes.UpdateSACN(node, intUniverses)
}
t.NotifyUpdate() t.NotifyUpdate()
} }
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()
}
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{}
}
}
}
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.sacnLastSeen.After(keep.sacnLastSeen) {
keep.sacnLastSeen = merge.sacnLastSeen
}
sort.Ints(keep.SACNOutputs)
}

View File

@@ -33,10 +33,7 @@ func getInterfaceIPv4(iface net.Interface) (srcIP, broadcast net.IP) {
type Tendrils struct { type Tendrils struct {
activeInterfaces map[string]context.CancelFunc activeInterfaces map[string]context.CancelFunc
nodes *Nodes nodes *Nodes
artnet *ArtNetNodes
artnetConn *net.UDPConn artnetConn *net.UDPConn
sacnSources *SACNSources
danteFlows *DanteFlows
errors *ErrorTracker errors *ErrorTracker
ping *PingManager ping *PingManager
broadcast *BroadcastStats broadcast *BroadcastStats
@@ -78,9 +75,6 @@ type Tendrils struct {
func New() *Tendrils { func New() *Tendrils {
t := &Tendrils{ t := &Tendrils{
activeInterfaces: map[string]context.CancelFunc{}, activeInterfaces: map[string]context.CancelFunc{},
artnet: NewArtNetNodes(),
sacnSources: NewSACNSources(),
danteFlows: NewDanteFlows(),
ping: NewPingManager(), ping: NewPingManager(),
sseSubs: map[int]chan struct{}{}, sseSubs: map[int]chan struct{}{},
} }

View File

@@ -6,6 +6,7 @@ import (
"net" "net"
"sort" "sort"
"strings" "strings"
"time"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"go.jetify.com/typeid" "go.jetify.com/typeid"
@@ -149,6 +150,11 @@ type Node struct {
DanteRx []*DantePeer `json:"dante_rx,omitempty"` DanteRx []*DantePeer `json:"dante_rx,omitempty"`
Unreachable bool `json:"unreachable,omitempty"` Unreachable bool `json:"unreachable,omitempty"`
pollTrigger chan struct{} pollTrigger chan struct{}
multicastLastSeen map[string]time.Time
artnetLastSeen time.Time
sacnLastSeen time.Time
danteLastSeen time.Time
} }
type DantePeer struct { type DantePeer struct {
@@ -157,6 +163,24 @@ type DantePeer struct {
Status map[string]string `json:"status,omitempty"` Status map[string]string `json:"status,omitempty"`
} }
func (p *DantePeer) MarshalJSON() ([]byte, error) {
type peerJSON struct {
Node *Node `json:"node"`
Channels []string `json:"channels,omitempty"`
Status map[string]string `json:"status,omitempty"`
}
nodeRef := &Node{
TypeID: p.Node.TypeID,
Names: p.Node.Names,
Interfaces: p.Node.Interfaces,
}
return json.Marshal(peerJSON{
Node: nodeRef,
Channels: p.Channels,
Status: p.Status,
})
}
func (n *Node) WithInterface(ifaceKey string) *Node { func (n *Node) WithInterface(ifaceKey string) *Node {
if ifaceKey == "" { if ifaceKey == "" {
return n return n