Refactor node storage and use proper types for protocol data
- Rename TypeID to ID throughout - Remove re-derivable data (MACTableSize, SACNInputs now derived) - Use typed ArtNetUniverse and SACNUniverse with methods - Store multicast groups with lastSeen tracking in structs - Remove int indexes in Nodes, use direct node pointers - Parse multicast groups into typed struct instead of strings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
74
artnet.go
74
artnet.go
@@ -182,53 +182,58 @@ func (n *Nodes) UpdateArtNet(node *Node, inputs, outputs []int) {
|
|||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
if node.ArtNetInputs == nil {
|
||||||
|
node.ArtNetInputs = ArtNetUniverseSet{}
|
||||||
|
}
|
||||||
|
if node.ArtNetOutputs == nil {
|
||||||
|
node.ArtNetOutputs = ArtNetUniverseSet{}
|
||||||
|
}
|
||||||
|
|
||||||
for _, u := range inputs {
|
for _, u := range inputs {
|
||||||
if !containsInt(node.ArtNetInputs, u) {
|
node.ArtNetInputs.Add(ArtNetUniverse(u))
|
||||||
node.ArtNetInputs = append(node.ArtNetInputs, u)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, u := range outputs {
|
for _, u := range outputs {
|
||||||
if !containsInt(node.ArtNetOutputs, u) {
|
node.ArtNetOutputs.Add(ArtNetUniverse(u))
|
||||||
node.ArtNetOutputs = append(node.ArtNetOutputs, u)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sort.Ints(node.ArtNetInputs)
|
|
||||||
sort.Ints(node.ArtNetOutputs)
|
|
||||||
node.artnetLastSeen = time.Now()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) expireArtNet() {
|
func (n *Nodes) expireArtNet() {
|
||||||
expireTime := time.Now().Add(-60 * time.Second)
|
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
if !node.artnetLastSeen.IsZero() && node.artnetLastSeen.Before(expireTime) {
|
if node.ArtNetInputs != nil {
|
||||||
node.ArtNetInputs = nil
|
node.ArtNetInputs.Expire(60 * time.Second)
|
||||||
node.ArtNetOutputs = nil
|
}
|
||||||
node.artnetLastSeen = time.Time{}
|
if node.ArtNetOutputs != nil {
|
||||||
|
node.ArtNetOutputs.Expire(60 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) mergeArtNet(keep, merge *Node) {
|
func (n *Nodes) mergeArtNet(keep, merge *Node) {
|
||||||
for _, u := range merge.ArtNetInputs {
|
if merge.ArtNetInputs != nil {
|
||||||
if !containsInt(keep.ArtNetInputs, u) {
|
if keep.ArtNetInputs == nil {
|
||||||
keep.ArtNetInputs = append(keep.ArtNetInputs, u)
|
keep.ArtNetInputs = ArtNetUniverseSet{}
|
||||||
|
}
|
||||||
|
for u, lastSeen := range merge.ArtNetInputs {
|
||||||
|
if existing, ok := keep.ArtNetInputs[u]; !ok || lastSeen.After(existing) {
|
||||||
|
keep.ArtNetInputs[u] = lastSeen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, u := range merge.ArtNetOutputs {
|
if merge.ArtNetOutputs != nil {
|
||||||
if !containsInt(keep.ArtNetOutputs, u) {
|
if keep.ArtNetOutputs == nil {
|
||||||
keep.ArtNetOutputs = append(keep.ArtNetOutputs, u)
|
keep.ArtNetOutputs = ArtNetUniverseSet{}
|
||||||
|
}
|
||||||
|
for u, lastSeen := range merge.ArtNetOutputs {
|
||||||
|
if existing, ok := keep.ArtNetOutputs[u]; !ok || lastSeen.After(existing) {
|
||||||
|
keep.ArtNetOutputs[u] = lastSeen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if merge.artnetLastSeen.After(keep.artnetLastSeen) {
|
|
||||||
keep.artnetLastSeen = merge.artnetLastSeen
|
|
||||||
}
|
|
||||||
sort.Ints(keep.ArtNetInputs)
|
|
||||||
sort.Ints(keep.ArtNetOutputs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) logArtNet() {
|
func (n *Nodes) logArtNet() {
|
||||||
inputUniverses := map[int][]string{}
|
inputUniverses := map[ArtNetUniverse][]string{}
|
||||||
outputUniverses := map[int][]string{}
|
outputUniverses := map[ArtNetUniverse][]string{}
|
||||||
|
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
|
if len(node.ArtNetInputs) == 0 && len(node.ArtNetOutputs) == 0 {
|
||||||
@@ -238,10 +243,10 @@ func (n *Nodes) logArtNet() {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
name = "??"
|
name = "??"
|
||||||
}
|
}
|
||||||
for _, u := range node.ArtNetInputs {
|
for u := range node.ArtNetInputs {
|
||||||
inputUniverses[u] = append(inputUniverses[u], name)
|
inputUniverses[u] = append(inputUniverses[u], name)
|
||||||
}
|
}
|
||||||
for _, u := range node.ArtNetOutputs {
|
for u := range node.ArtNetOutputs {
|
||||||
outputUniverses[u] = append(outputUniverses[u], name)
|
outputUniverses[u] = append(outputUniverses[u], name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,8 +255,8 @@ func (n *Nodes) logArtNet() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var allUniverses []int
|
seen := map[ArtNetUniverse]bool{}
|
||||||
seen := map[int]bool{}
|
var allUniverses []ArtNetUniverse
|
||||||
for u := range inputUniverses {
|
for u := range inputUniverses {
|
||||||
if !seen[u] {
|
if !seen[u] {
|
||||||
allUniverses = append(allUniverses, u)
|
allUniverses = append(allUniverses, u)
|
||||||
@@ -264,7 +269,7 @@ func (n *Nodes) logArtNet() {
|
|||||||
seen[u] = true
|
seen[u] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Ints(allUniverses)
|
sort.Slice(allUniverses, func(i, j int) bool { return allUniverses[i] < allUniverses[j] })
|
||||||
|
|
||||||
log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses))
|
log.Printf("[sigusr1] ================ %d artnet universes ================", len(allUniverses))
|
||||||
for _, u := range allUniverses {
|
for _, u := range allUniverses {
|
||||||
@@ -279,9 +284,6 @@ func (n *Nodes) logArtNet() {
|
|||||||
sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
|
sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
|
||||||
parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
|
parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
|
||||||
}
|
}
|
||||||
netVal := (u >> 8) & 0x7f
|
log.Printf("[sigusr1] artnet:%d (%s) %s", u, u.String(), strings.Join(parts, "; "))
|
||||||
subnet := (u >> 4) & 0x0f
|
|
||||||
universe := u & 0x0f
|
|
||||||
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, netVal, subnet, universe, strings.Join(parts, "; "))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
dante.go
11
dante.go
@@ -102,10 +102,13 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) *Node {
|
|||||||
n.mu.RLock()
|
n.mu.RLock()
|
||||||
defer n.mu.RUnlock()
|
defer n.mu.RUnlock()
|
||||||
|
|
||||||
groupName := multicastGroupName(groupIP)
|
group := ParseMulticastGroup(groupIP)
|
||||||
|
groupKey := group.String()
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
if node.DanteTxChannels != "" && containsString(node.MulticastGroups, groupName) {
|
if node.DanteTxChannels != "" && node.MulticastGroups != nil {
|
||||||
return node
|
if _, exists := node.MulticastGroups[groupKey]; exists {
|
||||||
|
return node
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -883,7 +886,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
|
|||||||
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
|
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
|
||||||
}
|
}
|
||||||
if sourceNode == nil {
|
if sourceNode == nil {
|
||||||
sourceNode = t.nodes.GetOrCreateByName(multicastGroupName(groupIP))
|
sourceNode = t.nodes.GetOrCreateByName(ParseMulticastGroup(groupIP).String())
|
||||||
}
|
}
|
||||||
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
||||||
t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive)
|
t.nodes.UpdateDanteFlow(sourceNode, subscriberNode, "", DanteFlowActive)
|
||||||
|
|||||||
30
errors.go
30
errors.go
@@ -15,7 +15,7 @@ const (
|
|||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
NodeTypeID string `json:"node_typeid"`
|
NodeID string `json:"node_id"`
|
||||||
NodeName string `json:"node_name"`
|
NodeName string `json:"node_name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Port string `json:"port,omitempty"`
|
Port string `json:"port,omitempty"`
|
||||||
@@ -88,7 +88,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats
|
|||||||
speedBytes := float64(stats.Speed) / 8.0
|
speedBytes := float64(stats.Speed) / 8.0
|
||||||
utilization := (maxBytesRate / speedBytes) * 100.0
|
utilization := (maxBytesRate / speedBytes) * 100.0
|
||||||
|
|
||||||
key := "util:" + node.TypeID + ":" + portName
|
key := "util:" + node.ID + ":" + portName
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
if utilization < 70.0 {
|
if utilization < 70.0 {
|
||||||
@@ -107,7 +107,7 @@ func (e *ErrorTracker) checkUtilizationLocked(node *Node, portName string, stats
|
|||||||
e.nextID++
|
e.nextID++
|
||||||
e.errors[key] = &Error{
|
e.errors[key] = &Error{
|
||||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||||
NodeTypeID: node.TypeID,
|
NodeID: node.ID,
|
||||||
NodeName: node.DisplayName(),
|
NodeName: node.DisplayName(),
|
||||||
Port: portName,
|
Port: portName,
|
||||||
Type: ErrorTypeHighUtilization,
|
Type: ErrorTypeHighUtilization,
|
||||||
@@ -122,7 +122,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
|
|||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
key := node.TypeID + ":" + portName
|
key := node.ID + ":" + portName
|
||||||
baseline := e.baselines[key]
|
baseline := e.baselines[key]
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -137,7 +137,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
|
|||||||
e.nextID++
|
e.nextID++
|
||||||
e.errors[key] = &Error{
|
e.errors[key] = &Error{
|
||||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||||
NodeTypeID: node.TypeID,
|
NodeID: node.ID,
|
||||||
NodeName: node.DisplayName(),
|
NodeName: node.DisplayName(),
|
||||||
Port: portName,
|
Port: portName,
|
||||||
Type: ErrorTypeStartup,
|
Type: ErrorTypeStartup,
|
||||||
@@ -172,7 +172,7 @@ func (e *ErrorTracker) checkPortLocked(node *Node, portName string, stats *Inter
|
|||||||
e.nextID++
|
e.nextID++
|
||||||
e.errors[key] = &Error{
|
e.errors[key] = &Error{
|
||||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||||
NodeTypeID: node.TypeID,
|
NodeID: node.ID,
|
||||||
NodeName: node.DisplayName(),
|
NodeName: node.DisplayName(),
|
||||||
Port: portName,
|
Port: portName,
|
||||||
Type: ErrorTypeNew,
|
Type: ErrorTypeNew,
|
||||||
@@ -253,8 +253,8 @@ func (e *ErrorTracker) GetUnreachableNodeSet() map[string]bool {
|
|||||||
defer e.mu.RUnlock()
|
defer e.mu.RUnlock()
|
||||||
|
|
||||||
result := map[string]bool{}
|
result := map[string]bool{}
|
||||||
for nodeTypeID := range e.unreachableNodes {
|
for nodeID := range e.unreachableNodes {
|
||||||
result[nodeTypeID] = true
|
result[nodeID] = true
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -271,10 +271,10 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr
|
|||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
key := "unreachable:" + node.TypeID
|
key := "unreachable:" + node.ID
|
||||||
|
|
||||||
wasUnreachable := e.unreachableNodes[node.TypeID]
|
wasUnreachable := e.unreachableNodes[node.ID]
|
||||||
e.unreachableNodes[node.TypeID] = true
|
e.unreachableNodes[node.ID] = true
|
||||||
becameUnreachable = !wasUnreachable
|
becameUnreachable = !wasUnreachable
|
||||||
|
|
||||||
if e.suppressedUnreachable[key] {
|
if e.suppressedUnreachable[key] {
|
||||||
@@ -289,7 +289,7 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node) (changed bool, becameUnr
|
|||||||
e.nextID++
|
e.nextID++
|
||||||
e.errors[key] = &Error{
|
e.errors[key] = &Error{
|
||||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||||
NodeTypeID: node.TypeID,
|
NodeID: node.ID,
|
||||||
NodeName: node.DisplayName(),
|
NodeName: node.DisplayName(),
|
||||||
Type: ErrorTypeUnreachable,
|
Type: ErrorTypeUnreachable,
|
||||||
FirstSeen: now,
|
FirstSeen: now,
|
||||||
@@ -310,12 +310,12 @@ func (e *ErrorTracker) clearUnreachableLocked(node *Node) (changed bool, becameR
|
|||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
key := "unreachable:" + node.TypeID
|
key := "unreachable:" + node.ID
|
||||||
|
|
||||||
delete(e.suppressedUnreachable, key)
|
delete(e.suppressedUnreachable, key)
|
||||||
|
|
||||||
wasUnreachable := e.unreachableNodes[node.TypeID]
|
wasUnreachable := e.unreachableNodes[node.ID]
|
||||||
delete(e.unreachableNodes, node.TypeID)
|
delete(e.unreachableNodes, node.ID)
|
||||||
becameReachable = wasUnreachable
|
becameReachable = wasUnreachable
|
||||||
|
|
||||||
if _, exists := e.errors[key]; exists {
|
if _, exists := e.errors[key]; exists {
|
||||||
|
|||||||
2
http.go
2
http.go
@@ -230,7 +230,7 @@ func (t *Tendrils) getNodes() []*Node {
|
|||||||
for _, node := range t.nodes.nodes {
|
for _, node := range t.nodes.nodes {
|
||||||
n := new(Node)
|
n := new(Node)
|
||||||
*n = *node
|
*n = *node
|
||||||
n.Unreachable = unreachableNodes[node.TypeID]
|
n.Unreachable = unreachableNodes[node.ID]
|
||||||
nodes = append(nodes, n)
|
nodes = append(nodes, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
link.go
8
link.go
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
TypeID string `json:"typeid"`
|
ID string `json:"id"`
|
||||||
NodeA *Node `json:"node_a"`
|
NodeA *Node `json:"node_a"`
|
||||||
InterfaceA string `json:"interface_a,omitempty"`
|
InterfaceA string `json:"interface_a,omitempty"`
|
||||||
NodeB *Node `json:"node_b"`
|
NodeB *Node `json:"node_b"`
|
||||||
@@ -15,7 +15,7 @@ type Link struct {
|
|||||||
|
|
||||||
func (l *Link) MarshalJSON() ([]byte, error) {
|
func (l *Link) MarshalJSON() ([]byte, error) {
|
||||||
type linkJSON struct {
|
type linkJSON struct {
|
||||||
TypeID string `json:"typeid"`
|
ID string `json:"id"`
|
||||||
NodeA interface{} `json:"node_a"`
|
NodeA interface{} `json:"node_a"`
|
||||||
InterfaceA string `json:"interface_a,omitempty"`
|
InterfaceA string `json:"interface_a,omitempty"`
|
||||||
NodeB interface{} `json:"node_b"`
|
NodeB interface{} `json:"node_b"`
|
||||||
@@ -23,7 +23,7 @@ func (l *Link) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(linkJSON{
|
return json.Marshal(linkJSON{
|
||||||
TypeID: l.TypeID,
|
ID: l.ID,
|
||||||
NodeA: l.NodeA.WithInterface(l.InterfaceA),
|
NodeA: l.NodeA.WithInterface(l.InterfaceA),
|
||||||
InterfaceA: l.InterfaceA,
|
InterfaceA: l.InterfaceA,
|
||||||
NodeB: l.NodeB.WithInterface(l.InterfaceB),
|
NodeB: l.NodeB.WithInterface(l.InterfaceB),
|
||||||
@@ -83,7 +83,7 @@ func (n *Nodes) getDirectLinks() []*Link {
|
|||||||
if !seen[key] {
|
if !seen[key] {
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
links = append(links, &Link{
|
links = append(links, &Link{
|
||||||
TypeID: newTypeID("link"),
|
ID: newID("link"),
|
||||||
NodeA: lh.node,
|
NodeA: lh.node,
|
||||||
InterfaceA: lh.port,
|
InterfaceA: lh.port,
|
||||||
NodeB: target,
|
NodeB: target,
|
||||||
|
|||||||
172
multicast.go
172
multicast.go
@@ -1,80 +1,10 @@
|
|||||||
package tendrils
|
package tendrils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MulticastGroup struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MulticastGroup) IsDante() bool {
|
|
||||||
ip := net.ParseIP(g.IP).To4()
|
|
||||||
if ip == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ip[0] == 239 && ip[1] == 253 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func multicastGroupName(ip net.IP) string {
|
|
||||||
ip4 := ip.To4()
|
|
||||||
if ip4 == nil {
|
|
||||||
return ip.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ip.String() {
|
|
||||||
case "224.0.0.251":
|
|
||||||
return "mdns"
|
|
||||||
case "224.0.1.129":
|
|
||||||
return "ptp"
|
|
||||||
case "224.0.1.130":
|
|
||||||
return "ptp-announce"
|
|
||||||
case "224.0.1.131":
|
|
||||||
return "ptp-sync"
|
|
||||||
case "224.0.1.132":
|
|
||||||
return "ptp-delay"
|
|
||||||
case "224.2.127.254":
|
|
||||||
return "sap"
|
|
||||||
case "239.255.254.253":
|
|
||||||
return "shure-slp"
|
|
||||||
case "239.255.255.250":
|
|
||||||
return "ssdp"
|
|
||||||
case "239.255.255.253":
|
|
||||||
return "slp"
|
|
||||||
case "239.255.255.255":
|
|
||||||
return "admin-scoped-broadcast"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip4[0] == 239 && ip4[1] == 255 {
|
|
||||||
universe := int(ip4[2])*256 + int(ip4[3])
|
|
||||||
if universe >= 1 && universe <= 63999 {
|
|
||||||
return fmt.Sprintf("sacn:%d", universe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
|
||||||
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
|
||||||
return fmt.Sprintf("dante-mcast:%d", flowID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip4[0] == 239 && ip4[1] == 253 {
|
|
||||||
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
|
||||||
return fmt.Sprintf("dante-av:%d", flowID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
@@ -84,27 +14,12 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupName := multicastGroupName(groupIP)
|
group := ParseMulticastGroup(groupIP)
|
||||||
|
|
||||||
if node.multicastLastSeen == nil {
|
if node.MulticastGroups == nil {
|
||||||
node.multicastLastSeen = map[string]time.Time{}
|
node.MulticastGroups = MulticastMembershipSet{}
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
node.MulticastGroups.Add(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
||||||
@@ -116,16 +31,12 @@ func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupName := multicastGroupName(groupIP)
|
if node.MulticastGroups == nil {
|
||||||
delete(node.multicastLastSeen, groupName)
|
return
|
||||||
|
|
||||||
var groups []string
|
|
||||||
for _, g := range node.MulticastGroups {
|
|
||||||
if g != groupName {
|
|
||||||
groups = append(groups, g)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
node.MulticastGroups = groups
|
|
||||||
|
group := ParseMulticastGroup(groupIP)
|
||||||
|
node.MulticastGroups.Remove(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
||||||
@@ -133,15 +44,14 @@ func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
|||||||
defer n.mu.RUnlock()
|
defer n.mu.RUnlock()
|
||||||
|
|
||||||
node := n.getNodeByIPLocked(deviceIP)
|
node := n.getNodeByIPLocked(deviceIP)
|
||||||
if node == nil {
|
if node == nil || node.MulticastGroups == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var groups []net.IP
|
var groups []net.IP
|
||||||
for _, groupName := range node.MulticastGroups {
|
for _, group := range node.MulticastGroups.Groups() {
|
||||||
g := &MulticastGroup{Name: groupName}
|
if group.IsDante() {
|
||||||
if g.IsDante() {
|
ip := net.ParseIP(group.String())
|
||||||
ip := net.ParseIP(groupName)
|
|
||||||
if ip != nil {
|
if ip != nil {
|
||||||
groups = append(groups, ip)
|
groups = append(groups, ip)
|
||||||
}
|
}
|
||||||
@@ -154,10 +64,14 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
|
|||||||
n.mu.RLock()
|
n.mu.RLock()
|
||||||
defer n.mu.RUnlock()
|
defer n.mu.RUnlock()
|
||||||
|
|
||||||
groupName := multicastGroupName(groupIP)
|
group := ParseMulticastGroup(groupIP)
|
||||||
|
groupKey := group.String()
|
||||||
var members []*Node
|
var members []*Node
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
if containsString(node.MulticastGroups, groupName) {
|
if node.MulticastGroups == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := node.MulticastGroups[groupKey]; exists {
|
||||||
members = append(members, node)
|
members = append(members, node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,55 +79,23 @@ func (n *Nodes) GetMulticastGroupMembers(groupIP net.IP) []*Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) expireMulticastMemberships() {
|
func (n *Nodes) expireMulticastMemberships() {
|
||||||
expireTime := time.Now().Add(-5 * time.Minute)
|
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
if node.multicastLastSeen == nil {
|
if node.MulticastGroups != nil {
|
||||||
continue
|
node.MulticastGroups.Expire(5 * time.Minute)
|
||||||
}
|
}
|
||||||
var keepGroups []string
|
|
||||||
var keepSACNInputs []int
|
|
||||||
for _, groupName := range node.MulticastGroups {
|
|
||||||
if lastSeen, ok := node.multicastLastSeen[groupName]; ok && !lastSeen.Before(expireTime) {
|
|
||||||
keepGroups = append(keepGroups, groupName)
|
|
||||||
if len(groupName) > 5 && groupName[:5] == "sacn:" {
|
|
||||||
var universe int
|
|
||||||
if _, err := fmt.Sscanf(groupName, "sacn:%d", &universe); err == nil {
|
|
||||||
keepSACNInputs = append(keepSACNInputs, universe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete(node.multicastLastSeen, groupName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.MulticastGroups = keepGroups
|
|
||||||
sort.Ints(keepSACNInputs)
|
|
||||||
node.SACNInputs = keepSACNInputs
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) mergeMulticast(keep, merge *Node) {
|
func (n *Nodes) mergeMulticast(keep, merge *Node) {
|
||||||
if merge.multicastLastSeen == nil {
|
if merge.MulticastGroups == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if keep.multicastLastSeen == nil {
|
if keep.MulticastGroups == nil {
|
||||||
keep.multicastLastSeen = map[string]time.Time{}
|
keep.MulticastGroups = MulticastMembershipSet{}
|
||||||
}
|
}
|
||||||
for groupName, lastSeen := range merge.multicastLastSeen {
|
for key, membership := range merge.MulticastGroups {
|
||||||
if existing, ok := keep.multicastLastSeen[groupName]; !ok || lastSeen.After(existing) {
|
if existing, ok := keep.MulticastGroups[key]; !ok || membership.LastSeen.After(existing.LastSeen) {
|
||||||
keep.multicastLastSeen[groupName] = lastSeen
|
keep.MulticastGroups[key] = membership
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
283
nodes.go
283
nodes.go
@@ -14,30 +14,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Nodes struct {
|
type Nodes struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
nodes map[int]*Node
|
nodes []*Node
|
||||||
ipIndex map[string]int
|
ipIndex map[string]*Node
|
||||||
macIndex map[string]int
|
macIndex map[string]*Node
|
||||||
nameIndex map[string]int
|
nameIndex map[string]*Node
|
||||||
nodeCancel map[int]context.CancelFunc
|
t *Tendrils
|
||||||
nextID int
|
ctx context.Context
|
||||||
t *Tendrils
|
cancelAll context.CancelFunc
|
||||||
ctx context.Context
|
|
||||||
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{},
|
ipIndex: map[string]*Node{},
|
||||||
ipIndex: map[string]int{},
|
macIndex: map[string]*Node{},
|
||||||
macIndex: map[string]int{},
|
nameIndex: map[string]*Node{},
|
||||||
nameIndex: map[string]int{},
|
t: t,
|
||||||
nodeCancel: map[int]context.CancelFunc{},
|
ctx: ctx,
|
||||||
nextID: 1,
|
cancelAll: cancel,
|
||||||
t: t,
|
|
||||||
ctx: ctx,
|
|
||||||
cancelAll: cancel,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,10 +55,9 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
targetID, isNew := n.resolveTargetNode(target, mac, ips, nodeName)
|
node, isNew := n.resolveTargetNode(target, mac, ips, nodeName)
|
||||||
node := n.nodes[targetID]
|
|
||||||
|
|
||||||
added := n.applyNodeUpdates(node, targetID, mac, ips, ifaceName, nodeName)
|
added := n.applyNodeUpdates(node, mac, ips, ifaceName, nodeName)
|
||||||
|
|
||||||
n.logUpdates(node, added, isNew, source)
|
n.logUpdates(node, added, isNew, source)
|
||||||
|
|
||||||
@@ -74,108 +68,82 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
|
|||||||
return isNew || len(added) > 0
|
return isNew || len(added) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (int, bool) {
|
func (n *Nodes) resolveTargetNode(target *Node, mac net.HardwareAddr, ips []net.IP, nodeName string) (*Node, bool) {
|
||||||
targetID := n.findByTarget(target)
|
node := n.findOrMergeByMAC(target, mac)
|
||||||
targetID = n.findOrMergeByMAC(targetID, mac)
|
if node == nil {
|
||||||
if targetID == -1 {
|
node = n.findByIPs(ips)
|
||||||
targetID = n.findByIPs(ips)
|
|
||||||
}
|
}
|
||||||
targetID = n.findOrMergeByName(targetID, nodeName)
|
node = n.findOrMergeByName(node, nodeName)
|
||||||
|
|
||||||
if targetID == -1 {
|
if node == nil {
|
||||||
return n.createNode(), true
|
return n.createNode(), true
|
||||||
}
|
}
|
||||||
return targetID, false
|
return node, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) findByTarget(target *Node) int {
|
func (n *Nodes) findOrMergeByMAC(target *Node, mac net.HardwareAddr) *Node {
|
||||||
if target == nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
for id, node := range n.nodes {
|
|
||||||
if node == target {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Nodes) findOrMergeByMAC(targetID int, mac net.HardwareAddr) int {
|
|
||||||
if mac == nil {
|
if mac == nil {
|
||||||
return targetID
|
return target
|
||||||
}
|
}
|
||||||
macKey := mac.String()
|
macKey := mac.String()
|
||||||
id, exists := n.macIndex[macKey]
|
found := n.macIndex[macKey]
|
||||||
if !exists {
|
if found == nil {
|
||||||
return targetID
|
return target
|
||||||
}
|
}
|
||||||
if _, nodeExists := n.nodes[id]; !nodeExists {
|
if target == nil {
|
||||||
delete(n.macIndex, macKey)
|
return found
|
||||||
return targetID
|
|
||||||
}
|
}
|
||||||
if targetID == -1 {
|
if found != target {
|
||||||
return id
|
n.mergeNodes(target, found)
|
||||||
}
|
}
|
||||||
if id != targetID {
|
return target
|
||||||
n.mergeNodes(targetID, id)
|
|
||||||
}
|
|
||||||
return targetID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) findByIPs(ips []net.IP) int {
|
func (n *Nodes) findByIPs(ips []net.IP) *Node {
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
if node := n.ipIndex[ip.String()]; node != nil {
|
||||||
if _, nodeExists := n.nodes[id]; nodeExists {
|
return node
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) findOrMergeByName(targetID int, nodeName string) int {
|
func (n *Nodes) findOrMergeByName(target *Node, nodeName string) *Node {
|
||||||
if nodeName == "" {
|
if nodeName == "" {
|
||||||
return targetID
|
return target
|
||||||
}
|
}
|
||||||
id, exists := n.nameIndex[nodeName]
|
found := n.nameIndex[nodeName]
|
||||||
if !exists {
|
if found == nil {
|
||||||
return targetID
|
return target
|
||||||
}
|
}
|
||||||
nameNode, nodeExists := n.nodes[id]
|
if target == nil {
|
||||||
if !nodeExists {
|
return found
|
||||||
delete(n.nameIndex, nodeName)
|
|
||||||
return targetID
|
|
||||||
}
|
}
|
||||||
if targetID == -1 {
|
if found != target && len(found.Interfaces) == 0 {
|
||||||
return id
|
n.mergeNodes(target, found)
|
||||||
}
|
}
|
||||||
if id != targetID && len(nameNode.Interfaces) == 0 {
|
return target
|
||||||
n.mergeNodes(targetID, id)
|
|
||||||
}
|
|
||||||
return targetID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) createNode() int {
|
func (n *Nodes) createNode() *Node {
|
||||||
targetID := n.nextID
|
|
||||||
n.nextID++
|
|
||||||
node := &Node{
|
node := &Node{
|
||||||
TypeID: newTypeID("node"),
|
ID: newID("node"),
|
||||||
Interfaces: InterfaceMap{},
|
Interfaces: InterfaceMap{},
|
||||||
MACTable: map[string]string{},
|
MACTable: map[string]string{},
|
||||||
pollTrigger: make(chan struct{}, 1),
|
pollTrigger: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
n.nodes[targetID] = node
|
n.nodes = append(n.nodes, node)
|
||||||
n.startNodePoller(targetID, node)
|
n.startNodePoller(node)
|
||||||
return targetID
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName string) []string {
|
func (n *Nodes) applyNodeUpdates(node *Node, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName string) []string {
|
||||||
var added []string
|
var added []string
|
||||||
|
|
||||||
if mac != nil {
|
if mac != nil {
|
||||||
added = n.updateNodeInterface(node, nodeID, mac, ips, ifaceName)
|
added = n.updateNodeInterface(node, mac, ips, ifaceName)
|
||||||
} else {
|
} else {
|
||||||
added = n.updateNodeIPs(node, nodeID, ips)
|
added = n.updateNodeIPs(node, ips)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nodeName != "" {
|
if nodeName != "" {
|
||||||
@@ -184,7 +152,7 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
|
|||||||
}
|
}
|
||||||
if !node.Names.Has(nodeName) {
|
if !node.Names.Has(nodeName) {
|
||||||
node.Names.Add(nodeName)
|
node.Names.Add(nodeName)
|
||||||
n.nameIndex[nodeName] = nodeID
|
n.nameIndex[nodeName] = node
|
||||||
added = append(added, "name="+nodeName)
|
added = append(added, "name="+nodeName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,22 +160,17 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
|
|||||||
return added
|
return added
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) updateNodeIPs(node *Node, nodeID int, ips []net.IP) []string {
|
func (n *Nodes) updateNodeIPs(node *Node, ips []net.IP) []string {
|
||||||
var added []string
|
var added []string
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
ipKey := ip.String()
|
ipKey := ip.String()
|
||||||
if existingID, exists := n.ipIndex[ipKey]; exists {
|
if existing := n.ipIndex[ipKey]; existing != nil && existing != node {
|
||||||
if existingID == nodeID {
|
n.mergeNodes(node, existing)
|
||||||
continue
|
if n.t.LogEvents {
|
||||||
}
|
log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey)
|
||||||
if existingNode, nodeExists := n.nodes[existingID]; nodeExists {
|
|
||||||
n.mergeNodes(nodeID, existingID)
|
|
||||||
if n.t.LogEvents {
|
|
||||||
log.Printf("[merge] %s into %s (shared ip %s)", existingNode, node, ipKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n.ipIndex[ipKey] = nodeID
|
n.ipIndex[ipKey] = node
|
||||||
iface, exists := node.Interfaces[ipKey]
|
iface, exists := node.Interfaces[ipKey]
|
||||||
if !exists {
|
if !exists {
|
||||||
iface = &Interface{IPs: IPSet{}}
|
iface = &Interface{IPs: IPSet{}}
|
||||||
@@ -245,9 +208,9 @@ func (n *Nodes) logUpdates(node *Node, added []string, isNew bool, source string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) startNodePoller(nodeID int, node *Node) {
|
func (n *Nodes) startNodePoller(node *Node) {
|
||||||
ctx, cancel := context.WithCancel(n.ctx)
|
ctx, cancel := context.WithCancel(n.ctx)
|
||||||
n.nodeCancel[nodeID] = cancel
|
node.cancelFunc = cancel
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
pollTicker := time.NewTicker(10 * time.Second)
|
pollTicker := time.NewTicker(10 * time.Second)
|
||||||
@@ -277,7 +240,7 @@ func (n *Nodes) triggerPoll(node *Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {
|
func (n *Nodes) updateNodeInterface(node *Node, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {
|
||||||
macKey := mac.String()
|
macKey := mac.String()
|
||||||
var added []string
|
var added []string
|
||||||
|
|
||||||
@@ -300,25 +263,23 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
|
|||||||
added = append(added, "iface="+ifaceKey)
|
added = append(added, "iface="+ifaceKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := n.macIndex[macKey]; !exists {
|
if n.macIndex[macKey] == nil {
|
||||||
n.macIndex[macKey] = nodeID
|
n.macIndex[macKey] = node
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
ipKey := ip.String()
|
ipKey := ip.String()
|
||||||
if existingID, exists := n.ipIndex[ipKey]; exists && existingID != nodeID {
|
if existing := n.ipIndex[ipKey]; existing != nil && existing != node {
|
||||||
if existingNode, nodeExists := n.nodes[existingID]; nodeExists {
|
n.mergeNodes(node, existing)
|
||||||
n.mergeNodes(nodeID, existingID)
|
if n.t.LogEvents {
|
||||||
if n.t.LogEvents {
|
log.Printf("[merge] %s into %s (shared ip %s)", existing, node, ipKey)
|
||||||
log.Printf("[merge] %s into %s (shared ip %s)", existingNode, node, ipKey)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !iface.IPs.Has(ipKey) {
|
if !iface.IPs.Has(ipKey) {
|
||||||
added = append(added, "ip="+ipKey)
|
added = append(added, "ip="+ipKey)
|
||||||
}
|
}
|
||||||
iface.IPs.Add(ip)
|
iface.IPs.Add(ip)
|
||||||
n.ipIndex[ipKey] = nodeID
|
n.ipIndex[ipKey] = node
|
||||||
|
|
||||||
if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface {
|
if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface {
|
||||||
delete(node.Interfaces, ipKey)
|
delete(node.Interfaces, ipKey)
|
||||||
@@ -356,40 +317,36 @@ func (n *Nodes) Merge(macs []net.HardwareAddr, source string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
existingIDs := map[int]bool{}
|
existing := map[*Node]bool{}
|
||||||
for _, mac := range macs {
|
for _, mac := range macs {
|
||||||
if id, exists := n.macIndex[mac.String()]; exists {
|
if node := n.macIndex[mac.String()]; node != nil {
|
||||||
existingIDs[id] = true
|
existing[node] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(existingIDs) < 2 {
|
if len(existing) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ids []int
|
var nodes []*Node
|
||||||
for id := range existingIDs {
|
for node := range existing {
|
||||||
ids = append(ids, id)
|
nodes = append(nodes, node)
|
||||||
}
|
}
|
||||||
sort.Ints(ids)
|
|
||||||
|
|
||||||
targetID := ids[0]
|
target := nodes[0]
|
||||||
for i := 1; i < len(ids); i++ {
|
for i := 1; i < len(nodes); i++ {
|
||||||
if n.t.LogEvents {
|
if n.t.LogEvents {
|
||||||
log.Printf("[merge] %s into %s (via %s)", n.nodes[ids[i]], n.nodes[targetID], source)
|
log.Printf("[merge] %s into %s (via %s)", nodes[i], target, source)
|
||||||
}
|
}
|
||||||
n.mergeNodes(targetID, ids[i])
|
n.mergeNodes(target, nodes[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.t.LogNodes {
|
if n.t.LogNodes {
|
||||||
n.logNode(n.nodes[targetID])
|
n.logNode(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
func (n *Nodes) mergeNodes(keep, merge *Node) {
|
||||||
keep := n.nodes[keepID]
|
|
||||||
merge := n.nodes[mergeID]
|
|
||||||
|
|
||||||
if keep == nil || merge == nil {
|
if keep == nil || merge == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -399,7 +356,7 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
|||||||
keep.Names = NameSet{}
|
keep.Names = NameSet{}
|
||||||
}
|
}
|
||||||
keep.Names.Add(name)
|
keep.Names.Add(name)
|
||||||
n.nameIndex[name] = keepID
|
n.nameIndex[name] = keep
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, iface := range merge.Interfaces {
|
for _, iface := range merge.Interfaces {
|
||||||
@@ -408,8 +365,8 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
|||||||
ips = append(ips, net.ParseIP(ipStr))
|
ips = append(ips, net.ParseIP(ipStr))
|
||||||
}
|
}
|
||||||
if iface.MAC != "" {
|
if iface.MAC != "" {
|
||||||
n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name)
|
n.updateNodeInterface(keep, iface.MAC.Parse(), ips, iface.Name)
|
||||||
n.macIndex[string(iface.MAC)] = keepID
|
n.macIndex[string(iface.MAC)] = keep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,12 +382,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
|||||||
n.mergeMulticast(keep, merge)
|
n.mergeMulticast(keep, merge)
|
||||||
n.mergeDante(keep, merge)
|
n.mergeDante(keep, merge)
|
||||||
|
|
||||||
if cancel, exists := n.nodeCancel[mergeID]; exists {
|
if merge.cancelFunc != nil {
|
||||||
cancel()
|
merge.cancelFunc()
|
||||||
delete(n.nodeCancel, mergeID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(n.nodes, mergeID)
|
n.removeNode(merge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nodes) removeNode(node *Node) {
|
||||||
|
for i, nd := range n.nodes {
|
||||||
|
if nd == node {
|
||||||
|
n.nodes = append(n.nodes[:i], n.nodes[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) GetByIP(ip net.IP) *Node {
|
func (n *Nodes) GetByIP(ip net.IP) *Node {
|
||||||
@@ -441,55 +406,41 @@ func (n *Nodes) GetByIP(ip net.IP) *Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) getByIPLocked(ip net.IP) *Node {
|
func (n *Nodes) getByIPLocked(ip net.IP) *Node {
|
||||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
return n.ipIndex[ip.String()]
|
||||||
return n.nodes[id]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node {
|
func (n *Nodes) GetByMAC(mac net.HardwareAddr) *Node {
|
||||||
n.mu.RLock()
|
n.mu.RLock()
|
||||||
defer n.mu.RUnlock()
|
defer n.mu.RUnlock()
|
||||||
|
|
||||||
if id, exists := n.macIndex[mac.String()]; exists {
|
return n.macIndex[mac.String()]
|
||||||
return n.nodes[id]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) GetByName(name string) *Node {
|
func (n *Nodes) GetByName(name string) *Node {
|
||||||
n.mu.RLock()
|
n.mu.RLock()
|
||||||
defer n.mu.RUnlock()
|
defer n.mu.RUnlock()
|
||||||
|
|
||||||
if id, exists := n.nameIndex[name]; exists {
|
return n.nameIndex[name]
|
||||||
return n.nodes[id]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) GetOrCreateByName(name string) *Node {
|
func (n *Nodes) GetOrCreateByName(name string) *Node {
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
if id, exists := n.nameIndex[name]; exists {
|
if node := n.nameIndex[name]; node != nil {
|
||||||
if node, nodeExists := n.nodes[id]; nodeExists {
|
return node
|
||||||
return node
|
|
||||||
}
|
|
||||||
delete(n.nameIndex, name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetID := n.nextID
|
|
||||||
n.nextID++
|
|
||||||
node := &Node{
|
node := &Node{
|
||||||
TypeID: newTypeID("node"),
|
ID: newID("node"),
|
||||||
Names: NameSet{name: true},
|
Names: NameSet{name: true},
|
||||||
Interfaces: InterfaceMap{},
|
Interfaces: InterfaceMap{},
|
||||||
MACTable: map[string]string{},
|
MACTable: map[string]string{},
|
||||||
pollTrigger: make(chan struct{}, 1),
|
pollTrigger: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
n.nodes[targetID] = node
|
n.nodes = append(n.nodes, node)
|
||||||
n.nameIndex[name] = targetID
|
n.nameIndex[name] = node
|
||||||
n.startNodePoller(targetID, node)
|
n.startNodePoller(node)
|
||||||
|
|
||||||
if n.t.LogEvents {
|
if n.t.LogEvents {
|
||||||
log.Printf("[add] %s [name=%s] (via name-lookup)", node, name)
|
log.Printf("[add] %s [name=%s] (via name-lookup)", node, name)
|
||||||
@@ -518,16 +469,13 @@ func (n *Nodes) SetDanteClockMaster(ip net.IP) {
|
|||||||
node.IsDanteClockMaster = false
|
node.IsDanteClockMaster = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
if node := n.ipIndex[ip.String()]; node != nil {
|
||||||
n.nodes[id].IsDanteClockMaster = true
|
node.IsDanteClockMaster = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node {
|
func (n *Nodes) getNodeByIPLocked(ip net.IP) *Node {
|
||||||
if id, exists := n.ipIndex[ip.String()]; exists {
|
return n.ipIndex[ip.String()]
|
||||||
return n.nodes[id]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) logNode(node *Node) {
|
func (n *Nodes) logNode(node *Node) {
|
||||||
@@ -620,12 +568,15 @@ func (n *Nodes) LogAll() {
|
|||||||
|
|
||||||
groupMembers := map[string][]string{}
|
groupMembers := map[string][]string{}
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
for _, groupName := range node.MulticastGroups {
|
if node.MulticastGroups == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, group := range node.MulticastGroups.Groups() {
|
||||||
name := node.DisplayName()
|
name := node.DisplayName()
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = "??"
|
name = "??"
|
||||||
}
|
}
|
||||||
groupMembers[groupName] = append(groupMembers[groupName], name)
|
groupMembers[group.String()] = append(groupMembers[group.String()], name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
ping.go
2
ping.go
@@ -142,7 +142,7 @@ func (t *Tendrils) pingNode(node *Node) {
|
|||||||
t.nodes.mu.RLock()
|
t.nodes.mu.RLock()
|
||||||
var ips []string
|
var ips []string
|
||||||
nodeName := node.DisplayName()
|
nodeName := node.DisplayName()
|
||||||
nodeID := node.TypeID
|
nodeID := node.ID
|
||||||
for _, iface := range node.Interfaces {
|
for _, iface := range node.Interfaces {
|
||||||
for ipStr := range iface.IPs {
|
for ipStr := range iface.IPs {
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gopatchy/sacn"
|
"github.com/gopatchy/sacn"
|
||||||
@@ -61,29 +60,33 @@ func (n *Nodes) UpdateSACN(node *Node, outputs []int) {
|
|||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
node.SACNOutputs = outputs
|
if node.SACNOutputs == nil {
|
||||||
sort.Ints(node.SACNOutputs)
|
node.SACNOutputs = SACNUniverseSet{}
|
||||||
node.sacnLastSeen = time.Now()
|
}
|
||||||
|
|
||||||
|
for _, u := range outputs {
|
||||||
|
node.SACNOutputs.Add(SACNUniverse(u))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) expireSACN() {
|
func (n *Nodes) expireSACN() {
|
||||||
expireTime := time.Now().Add(-60 * time.Second)
|
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
if !node.sacnLastSeen.IsZero() && node.sacnLastSeen.Before(expireTime) {
|
if node.SACNOutputs != nil {
|
||||||
node.SACNOutputs = nil
|
node.SACNOutputs.Expire(60 * time.Second)
|
||||||
node.sacnLastSeen = time.Time{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) mergeSACN(keep, merge *Node) {
|
func (n *Nodes) mergeSACN(keep, merge *Node) {
|
||||||
for _, u := range merge.SACNOutputs {
|
if merge.SACNOutputs == nil {
|
||||||
if !containsInt(keep.SACNOutputs, u) {
|
return
|
||||||
keep.SACNOutputs = append(keep.SACNOutputs, u)
|
}
|
||||||
|
if keep.SACNOutputs == nil {
|
||||||
|
keep.SACNOutputs = SACNUniverseSet{}
|
||||||
|
}
|
||||||
|
for u, lastSeen := range merge.SACNOutputs {
|
||||||
|
if existing, ok := keep.SACNOutputs[u]; !ok || lastSeen.After(existing) {
|
||||||
|
keep.SACNOutputs[u] = lastSeen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if merge.sacnLastSeen.After(keep.sacnLastSeen) {
|
|
||||||
keep.sacnLastSeen = merge.sacnLastSeen
|
|
||||||
}
|
|
||||||
sort.Ints(keep.SACNOutputs)
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
snmp.go
2
snmp.go
@@ -246,7 +246,7 @@ func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames
|
|||||||
outBytes, hasOutBytes := ifHCOutOctets[ifIndex]
|
outBytes, hasOutBytes := ifHCOutOctets[ifIndex]
|
||||||
|
|
||||||
if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes {
|
if hasInPkts && hasOutPkts && hasInBytes && hasOutBytes {
|
||||||
key := node.TypeID + ":" + name
|
key := node.ID + ":" + name
|
||||||
ifaceTracker.mu.Lock()
|
ifaceTracker.mu.Lock()
|
||||||
prev, hasPrev := ifaceTracker.counters[key]
|
prev, hasPrev := ifaceTracker.counters[key]
|
||||||
if hasPrev {
|
if hasPrev {
|
||||||
|
|||||||
347
types.go
347
types.go
@@ -1,6 +1,7 @@
|
|||||||
package tendrils
|
package tendrils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -12,11 +13,266 @@ import (
|
|||||||
"go.jetify.com/typeid"
|
"go.jetify.com/typeid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTypeID(prefix string) string {
|
func newID(prefix string) string {
|
||||||
tid, _ := typeid.WithPrefix(prefix)
|
tid, _ := typeid.WithPrefix(prefix)
|
||||||
return tid.String()
|
return tid.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArtNetUniverse int
|
||||||
|
|
||||||
|
func (u ArtNetUniverse) Net() int {
|
||||||
|
return (int(u) >> 8) & 0x7f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u ArtNetUniverse) Subnet() int {
|
||||||
|
return (int(u) >> 4) & 0x0f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u ArtNetUniverse) Universe() int {
|
||||||
|
return int(u) & 0x0f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u ArtNetUniverse) String() string {
|
||||||
|
return fmt.Sprintf("%d/%d/%d", u.Net(), u.Subnet(), u.Universe())
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtNetUniverseSet map[ArtNetUniverse]time.Time
|
||||||
|
|
||||||
|
func (s ArtNetUniverseSet) Add(u ArtNetUniverse) {
|
||||||
|
s[u] = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ArtNetUniverseSet) Universes() []ArtNetUniverse {
|
||||||
|
result := make([]ArtNetUniverse, 0, len(s))
|
||||||
|
for u := range s {
|
||||||
|
result = append(result, u)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ArtNetUniverseSet) Expire(maxAge time.Duration) {
|
||||||
|
expireTime := time.Now().Add(-maxAge)
|
||||||
|
for u, lastSeen := range s {
|
||||||
|
if lastSeen.Before(expireTime) {
|
||||||
|
delete(s, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ArtNetUniverseSet) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(s.Universes())
|
||||||
|
}
|
||||||
|
|
||||||
|
type SACNUniverse int
|
||||||
|
|
||||||
|
func (u SACNUniverse) String() string {
|
||||||
|
return fmt.Sprintf("%d", u)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SACNUniverseSet map[SACNUniverse]time.Time
|
||||||
|
|
||||||
|
func (s SACNUniverseSet) Add(u SACNUniverse) {
|
||||||
|
s[u] = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SACNUniverseSet) Universes() []SACNUniverse {
|
||||||
|
result := make([]SACNUniverse, 0, len(s))
|
||||||
|
for u := range s {
|
||||||
|
result = append(result, u)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SACNUniverseSet) Expire(maxAge time.Duration) {
|
||||||
|
expireTime := time.Now().Add(-maxAge)
|
||||||
|
for u, lastSeen := range s {
|
||||||
|
if lastSeen.Before(expireTime) {
|
||||||
|
delete(s, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SACNUniverseSet) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(s.Universes())
|
||||||
|
}
|
||||||
|
|
||||||
|
type MulticastGroupID int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MulticastUnknown MulticastGroupID = iota
|
||||||
|
MulticastMDNS
|
||||||
|
MulticastPTP
|
||||||
|
MulticastPTPAnnounce
|
||||||
|
MulticastPTPSync
|
||||||
|
MulticastPTPDelay
|
||||||
|
MulticastSAP
|
||||||
|
MulticastShureSLP
|
||||||
|
MulticastSSDP
|
||||||
|
MulticastSLP
|
||||||
|
MulticastAdminScopedBroadcast
|
||||||
|
)
|
||||||
|
|
||||||
|
type MulticastGroup struct {
|
||||||
|
ID MulticastGroupID
|
||||||
|
SACNUniverse SACNUniverse
|
||||||
|
DanteFlow int
|
||||||
|
DanteAV int
|
||||||
|
RawIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MulticastGroup) String() string {
|
||||||
|
switch g.ID {
|
||||||
|
case MulticastMDNS:
|
||||||
|
return "mdns"
|
||||||
|
case MulticastPTP:
|
||||||
|
return "ptp"
|
||||||
|
case MulticastPTPAnnounce:
|
||||||
|
return "ptp-announce"
|
||||||
|
case MulticastPTPSync:
|
||||||
|
return "ptp-sync"
|
||||||
|
case MulticastPTPDelay:
|
||||||
|
return "ptp-delay"
|
||||||
|
case MulticastSAP:
|
||||||
|
return "sap"
|
||||||
|
case MulticastShureSLP:
|
||||||
|
return "shure-slp"
|
||||||
|
case MulticastSSDP:
|
||||||
|
return "ssdp"
|
||||||
|
case MulticastSLP:
|
||||||
|
return "slp"
|
||||||
|
case MulticastAdminScopedBroadcast:
|
||||||
|
return "admin-scoped-broadcast"
|
||||||
|
}
|
||||||
|
if g.SACNUniverse > 0 {
|
||||||
|
return fmt.Sprintf("sacn:%d", g.SACNUniverse)
|
||||||
|
}
|
||||||
|
if g.DanteFlow > 0 {
|
||||||
|
return fmt.Sprintf("dante-mcast:%d", g.DanteFlow)
|
||||||
|
}
|
||||||
|
if g.DanteAV > 0 {
|
||||||
|
return fmt.Sprintf("dante-av:%d", g.DanteAV)
|
||||||
|
}
|
||||||
|
return g.RawIP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MulticastGroup) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(g.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MulticastGroup) IsDante() bool {
|
||||||
|
return g.DanteFlow > 0 || g.DanteAV > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g MulticastGroup) IsSACN() bool {
|
||||||
|
return g.SACNUniverse > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMulticastGroup(ip net.IP) MulticastGroup {
|
||||||
|
ip4 := ip.To4()
|
||||||
|
if ip4 == nil {
|
||||||
|
return MulticastGroup{RawIP: ip.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ip.String() {
|
||||||
|
case "224.0.0.251":
|
||||||
|
return MulticastGroup{ID: MulticastMDNS}
|
||||||
|
case "224.0.1.129":
|
||||||
|
return MulticastGroup{ID: MulticastPTP}
|
||||||
|
case "224.0.1.130":
|
||||||
|
return MulticastGroup{ID: MulticastPTPAnnounce}
|
||||||
|
case "224.0.1.131":
|
||||||
|
return MulticastGroup{ID: MulticastPTPSync}
|
||||||
|
case "224.0.1.132":
|
||||||
|
return MulticastGroup{ID: MulticastPTPDelay}
|
||||||
|
case "224.2.127.254":
|
||||||
|
return MulticastGroup{ID: MulticastSAP}
|
||||||
|
case "239.255.254.253":
|
||||||
|
return MulticastGroup{ID: MulticastShureSLP}
|
||||||
|
case "239.255.255.250":
|
||||||
|
return MulticastGroup{ID: MulticastSSDP}
|
||||||
|
case "239.255.255.253":
|
||||||
|
return MulticastGroup{ID: MulticastSLP}
|
||||||
|
case "239.255.255.255":
|
||||||
|
return MulticastGroup{ID: MulticastAdminScopedBroadcast}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip4[0] == 239 && ip4[1] == 255 {
|
||||||
|
universe := int(ip4[2])*256 + int(ip4[3])
|
||||||
|
if universe >= 1 && universe <= 63999 {
|
||||||
|
return MulticastGroup{SACNUniverse: SACNUniverse(universe)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
||||||
|
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
||||||
|
return MulticastGroup{DanteFlow: flowID}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip4[0] == 239 && ip4[1] == 253 {
|
||||||
|
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
||||||
|
return MulticastGroup{DanteAV: flowID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MulticastGroup{RawIP: ip.String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MulticastMembership struct {
|
||||||
|
Group MulticastGroup
|
||||||
|
LastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type MulticastMembershipSet map[string]*MulticastMembership
|
||||||
|
|
||||||
|
func (s MulticastMembershipSet) Add(group MulticastGroup) {
|
||||||
|
key := group.String()
|
||||||
|
if m, exists := s[key]; exists {
|
||||||
|
m.LastSeen = time.Now()
|
||||||
|
} else {
|
||||||
|
s[key] = &MulticastMembership{Group: group, LastSeen: time.Now()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MulticastMembershipSet) Remove(group MulticastGroup) {
|
||||||
|
delete(s, group.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MulticastMembershipSet) Groups() []MulticastGroup {
|
||||||
|
result := make([]MulticastGroup, 0, len(s))
|
||||||
|
for _, m := range s {
|
||||||
|
result = append(result, m.Group)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
return result[i].String() < result[j].String()
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MulticastMembershipSet) SACNInputs() []SACNUniverse {
|
||||||
|
var result []SACNUniverse
|
||||||
|
for _, m := range s {
|
||||||
|
if m.Group.IsSACN() {
|
||||||
|
result = append(result, m.Group.SACNUniverse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MulticastMembershipSet) Expire(maxAge time.Duration) {
|
||||||
|
expireTime := time.Now().Add(-maxAge)
|
||||||
|
for key, m := range s {
|
||||||
|
if m.LastSeen.Before(expireTime) {
|
||||||
|
delete(s, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MulticastMembershipSet) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(s.Groups())
|
||||||
|
}
|
||||||
|
|
||||||
type MAC string
|
type MAC string
|
||||||
|
|
||||||
func (m MAC) Parse() net.HardwareAddr {
|
func (m MAC) Parse() net.HardwareAddr {
|
||||||
@@ -133,28 +389,71 @@ type PoEBudget struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
TypeID string `json:"typeid"`
|
ID string `json:"id"`
|
||||||
Names NameSet `json:"names"`
|
Names NameSet `json:"names"`
|
||||||
Interfaces InterfaceMap `json:"interfaces"`
|
Interfaces InterfaceMap `json:"interfaces"`
|
||||||
MACTable map[string]string `json:"-"`
|
MACTable map[string]string `json:"-"`
|
||||||
MACTableSize int `json:"mac_table_size,omitempty"`
|
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||||
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||||
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||||
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
|
||||||
MulticastGroups []string `json:"multicast_groups,omitempty"`
|
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
|
||||||
ArtNetInputs []int `json:"artnet_inputs,omitempty"`
|
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
|
||||||
ArtNetOutputs []int `json:"artnet_outputs,omitempty"`
|
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
|
||||||
SACNInputs []int `json:"sacn_inputs,omitempty"`
|
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
|
||||||
SACNOutputs []int `json:"sacn_outputs,omitempty"`
|
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
|
||||||
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
|
Unreachable bool `json:"unreachable,omitempty"`
|
||||||
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
|
|
||||||
Unreachable bool `json:"unreachable,omitempty"`
|
|
||||||
pollTrigger chan struct{}
|
pollTrigger chan struct{}
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
danteLastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
multicastLastSeen map[string]time.Time
|
func (n *Node) MACTableSize() int {
|
||||||
artnetLastSeen time.Time
|
return len(n.MACTable)
|
||||||
sacnLastSeen time.Time
|
}
|
||||||
danteLastSeen time.Time
|
|
||||||
|
func (n *Node) SACNInputs() []SACNUniverse {
|
||||||
|
if n.MulticastGroups == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return n.MulticastGroups.SACNInputs()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) MarshalJSON() ([]byte, error) {
|
||||||
|
type nodeJSON struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Names NameSet `json:"names"`
|
||||||
|
Interfaces InterfaceMap `json:"interfaces"`
|
||||||
|
MACTableSize int `json:"mac_table_size,omitempty"`
|
||||||
|
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||||
|
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||||
|
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||||
|
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
|
||||||
|
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
|
||||||
|
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
|
||||||
|
SACNInputs []SACNUniverse `json:"sacn_inputs,omitempty"`
|
||||||
|
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
|
||||||
|
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
|
||||||
|
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
|
||||||
|
Unreachable bool `json:"unreachable,omitempty"`
|
||||||
|
}
|
||||||
|
return json.Marshal(nodeJSON{
|
||||||
|
ID: n.ID,
|
||||||
|
Names: n.Names,
|
||||||
|
Interfaces: n.Interfaces,
|
||||||
|
MACTableSize: n.MACTableSize(),
|
||||||
|
PoEBudget: n.PoEBudget,
|
||||||
|
IsDanteClockMaster: n.IsDanteClockMaster,
|
||||||
|
DanteTxChannels: n.DanteTxChannels,
|
||||||
|
MulticastGroups: n.MulticastGroups,
|
||||||
|
ArtNetInputs: n.ArtNetInputs,
|
||||||
|
ArtNetOutputs: n.ArtNetOutputs,
|
||||||
|
SACNInputs: n.SACNInputs(),
|
||||||
|
SACNOutputs: n.SACNOutputs,
|
||||||
|
DanteTx: n.DanteTx,
|
||||||
|
DanteRx: n.DanteRx,
|
||||||
|
Unreachable: n.Unreachable,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type DantePeer struct {
|
type DantePeer struct {
|
||||||
@@ -170,7 +469,7 @@ func (p *DantePeer) MarshalJSON() ([]byte, error) {
|
|||||||
Status map[string]string `json:"status,omitempty"`
|
Status map[string]string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
nodeRef := &Node{
|
nodeRef := &Node{
|
||||||
TypeID: p.Node.TypeID,
|
ID: p.Node.ID,
|
||||||
Names: p.Node.Names,
|
Names: p.Node.Names,
|
||||||
Interfaces: p.Node.Interfaces,
|
Interfaces: p.Node.Interfaces,
|
||||||
}
|
}
|
||||||
@@ -190,10 +489,10 @@ func (n *Node) WithInterface(ifaceKey string) *Node {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
return &Node{
|
return &Node{
|
||||||
TypeID: n.TypeID,
|
ID: n.ID,
|
||||||
Names: n.Names,
|
Names: n.Names,
|
||||||
Interfaces: InterfaceMap{ifaceKey: iface},
|
Interfaces: InterfaceMap{ifaceKey: iface},
|
||||||
MACTableSize: n.MACTableSize,
|
MACTable: n.MACTable,
|
||||||
PoEBudget: n.PoEBudget,
|
PoEBudget: n.PoEBudget,
|
||||||
IsDanteClockMaster: n.IsDanteClockMaster,
|
IsDanteClockMaster: n.IsDanteClockMaster,
|
||||||
DanteTxChannels: n.DanteTxChannels,
|
DanteTxChannels: n.DanteTxChannels,
|
||||||
|
|||||||
Reference in New Issue
Block a user