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:
153
artnet.go
153
artnet.go
@@ -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
435
dante.go
@@ -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
178
http.go
@@ -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()
|
||||||
|
|||||||
186
multicast.go
186
multicast.go
@@ -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
111
nodes.go
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{}{},
|
||||||
}
|
}
|
||||||
|
|||||||
24
types.go
24
types.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user