Add HTTP API with JSON status endpoint and typeid support

This commit is contained in:
Ian Gulliver
2026-01-24 11:03:34 -08:00
parent 168cdedbcb
commit 9aebe8b83d
13 changed files with 409 additions and 115 deletions

View File

@@ -5,5 +5,5 @@
- Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence
- Always push after commiting
- Use git add -A so you don't miss files when committing
- Never use go build -- use go run instead
- DO NOT commit unless asked to
- Never use go build -- use go vet instead
- DO NOT commit unless asked to

View File

@@ -23,10 +23,11 @@ const (
)
type ArtNetNode struct {
Node *Node
Inputs []int
Outputs []int
LastSeen time.Time
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) listenArtNet(ctx context.Context, iface net.Interface) {
@@ -217,6 +218,7 @@ func (a *ArtNetNodes) Update(node *Node, inputs, outputs []int) {
existing.LastSeen = time.Now()
} else {
a.nodes[node] = &ArtNetNode{
TypeID: newTypeID("artnetnode"),
Node: node,
Inputs: inputs,
Outputs: outputs,

View File

@@ -30,6 +30,7 @@ func main() {
debugBMD := flag.Bool("debug-bmd", false, "debug Blackmagic discovery")
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
debugYamaha := flag.Bool("debug-yamaha", false, "debug Yamaha discovery")
httpPort := flag.String("http", ":80", "HTTP server port (empty to disable)")
flag.Parse()
t := tendrils.New()
@@ -56,5 +57,6 @@ func main() {
t.DebugBMD = *debugBMD
t.DebugShure = *debugShure
t.DebugYamaha = *debugYamaha
t.HTTPPort = *httpPort
t.Run()
}

View File

@@ -3,6 +3,7 @@ package tendrils
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"log"
"net"
@@ -118,16 +119,30 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) string {
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 {
Source *Node
Subscribers map[*Node]*DanteFlowSubscriber
TypeID string `json:"typeid"`
Source *Node `json:"source"`
Subscribers DanteSubscriberMap `json:"subscribers"`
}
type DanteFlowSubscriber struct {
Node *Node
Channels []string
ChannelStatus map[string]DanteFlowStatus
LastSeen time.Time
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 {
@@ -157,8 +172,9 @@ func (d *DanteFlows) Update(source, subscriber *Node, channelInfo string, flowSt
flow := d.flows[source]
if flow == nil {
flow = &DanteFlow{
TypeID: newTypeID("danteflow"),
Source: source,
Subscribers: map[*Node]*DanteFlowSubscriber{},
Subscribers: DanteSubscriberMap{},
}
d.flows[source] = flow
}
@@ -437,6 +453,10 @@ func (s DanteFlowStatus) String() string {
}
}
func (s DanteFlowStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
type DanteSubscription struct {
RxChannel int
TxDeviceName string
@@ -866,7 +886,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
}
if sourceName == "" {
sourceName = (&MulticastGroup{IP: groupIP}).Name()
sourceName = multicastGroupName(groupIP)
}
sourceNode := t.nodes.GetOrCreateByName(sourceName)
subscriberNode := t.nodes.GetOrCreateByName(info.Name)

2
go.mod
View File

@@ -7,9 +7,11 @@ require (
github.com/google/gopacket v1.1.19
github.com/gosnmp/gosnmp v1.42.1
github.com/miekg/dns v1.1.72
go.jetify.com/typeid v1.3.0
)
require (
github.com/gofrs/uuid/v5 v5.2.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect

6
go.sum
View File

@@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
@@ -14,6 +16,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.jetify.com/typeid v1.3.0 h1:fuWV7oxO4mSsgpxwhaVpFXgt0IfjogR29p+XAjDCVKY=
go.jetify.com/typeid v1.3.0/go.mod h1:CtVGyt2+TSp4Rq5+ARLvGsJqdNypKBAC6INQ9TLPlmk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
@@ -36,5 +40,7 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

140
http.go Normal file
View File

@@ -0,0 +1,140 @@
package tendrils
import (
"encoding/json"
"log"
"net/http"
"sort"
"github.com/fvbommel/sortorder"
)
type StatusResponse struct {
Nodes []*Node `json:"nodes"`
Links []*Link `json:"links"`
MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"`
ArtNetNodes []*ArtNetNode `json:"artnet_nodes"`
DanteFlows []*DanteFlow `json:"dante_flows"`
}
func (t *Tendrils) startHTTPServer() {
if t.HTTPPort == "" {
return
}
mux := http.NewServeMux()
mux.HandleFunc("/api/status", t.handleAPIStatus)
mux.Handle("/", http.FileServer(http.Dir("static")))
log.Printf("[http] listening on %s", t.HTTPPort)
go func() {
if err := http.ListenAndServe(t.HTTPPort, mux); err != nil {
log.Printf("[ERROR] http server failed: %v", err)
}
}()
}
func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
status := t.GetStatus()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
}
func (t *Tendrils) GetStatus() *StatusResponse {
return &StatusResponse{
Nodes: t.getNodes(),
Links: t.getLinks(),
MulticastGroups: t.getMulticastGroups(),
ArtNetNodes: t.getArtNetNodes(),
DanteFlows: t.getDanteFlows(),
}
}
func (t *Tendrils) getNodes() []*Node {
t.nodes.mu.RLock()
defer t.nodes.mu.RUnlock()
nodes := make([]*Node, 0, len(t.nodes.nodes))
for _, node := range t.nodes.nodes {
nodes = append(nodes, node)
}
sort.Slice(nodes, func(i, j int) bool {
return sortorder.NaturalLess(nodes[i].DisplayName(), nodes[j].DisplayName())
})
return nodes
}
func (t *Tendrils) getLinks() []*Link {
t.nodes.mu.RLock()
defer t.nodes.mu.RUnlock()
links := t.nodes.getDirectLinks()
sort.Slice(links, func(i, j int) bool {
if links[i].NodeA.DisplayName() != links[j].NodeA.DisplayName() {
return sortorder.NaturalLess(links[i].NodeA.DisplayName(), links[j].NodeA.DisplayName())
}
if links[i].InterfaceA != links[j].InterfaceA {
return sortorder.NaturalLess(links[i].InterfaceA, links[j].InterfaceA)
}
if links[i].NodeB.DisplayName() != links[j].NodeB.DisplayName() {
return sortorder.NaturalLess(links[i].NodeB.DisplayName(), links[j].NodeB.DisplayName())
}
return sortorder.NaturalLess(links[i].InterfaceB, links[j].InterfaceB)
})
return links
}
func (t *Tendrils) getMulticastGroups() []*MulticastGroupMembers {
t.nodes.mu.Lock()
t.nodes.expireMulticastMemberships()
t.nodes.mu.Unlock()
t.nodes.mu.RLock()
defer t.nodes.mu.RUnlock()
groups := make([]*MulticastGroupMembers, 0, len(t.nodes.multicastGroups))
for _, gm := range t.nodes.multicastGroups {
groups = append(groups, gm)
}
sort.Slice(groups, func(i, j int) bool {
return sortorder.NaturalLess(groups[i].Group.Name, groups[j].Group.Name)
})
return groups
}
func (t *Tendrils) getArtNetNodes() []*ArtNetNode {
t.artnet.Expire()
t.artnet.mu.RLock()
defer t.artnet.mu.RUnlock()
nodes := make([]*ArtNetNode, 0, len(t.artnet.nodes))
for _, node := range t.artnet.nodes {
nodes = append(nodes, node)
}
sort.Slice(nodes, func(i, j int) bool {
return sortorder.NaturalLess(nodes[i].Node.DisplayName(), nodes[j].Node.DisplayName())
})
return nodes
}
func (t *Tendrils) getDanteFlows() []*DanteFlow {
t.danteFlows.Expire()
t.danteFlows.mu.RLock()
defer t.danteFlows.mu.RUnlock()
flows := make([]*DanteFlow, 0, len(t.danteFlows.flows))
for _, flow := range t.danteFlows.flows {
flows = append(flows, flow)
}
sort.Slice(flows, func(i, j int) bool {
return sortorder.NaturalLess(flows[i].Source.DisplayName(), flows[j].Source.DisplayName())
})
return flows
}

18
link.go
View File

@@ -3,10 +3,11 @@ package tendrils
import "fmt"
type Link struct {
NodeA *Node
InterfaceA string
NodeB *Node
InterfaceB string
TypeID string `json:"typeid"`
NodeA *Node `json:"node_a"`
InterfaceA string `json:"interface_a,omitempty"`
NodeB *Node `json:"node_b"`
InterfaceB string `json:"interface_b,omitempty"`
}
func (l *Link) String() string {
@@ -33,8 +34,8 @@ func (n *Nodes) getDirectLinks() []*Link {
macToNode := map[string]*Node{}
for _, node := range n.nodes {
for _, iface := range node.Interfaces {
if iface.MAC != nil {
macToNode[iface.MAC.String()] = node
if iface.MAC != "" {
macToNode[string(iface.MAC)] = node
}
}
}
@@ -45,10 +46,10 @@ func (n *Nodes) getDirectLinks() []*Link {
for _, target := range n.nodes {
seenMACs := map[string]bool{}
for _, iface := range target.Interfaces {
if iface.MAC == nil {
if iface.MAC == "" {
continue
}
mac := iface.MAC.String()
mac := string(iface.MAC)
if seenMACs[mac] {
continue
}
@@ -64,6 +65,7 @@ func (n *Nodes) getDirectLinks() []*Link {
if !seen[key] {
seen[key] = true
links = append(links, &Link{
TypeID: newTypeID("link"),
NodeA: lastHop,
InterfaceA: lastPort,
NodeB: target,

View File

@@ -1,32 +1,74 @@
package tendrils
import (
"encoding/json"
"fmt"
"net"
"sort"
"time"
"github.com/fvbommel/sortorder"
)
type MulticastGroup struct {
IP net.IP
Name string `json:"name"`
IP string `json:"ip"`
}
type MulticastMembership struct {
Node *Node
LastSeen time.Time
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 {
Group *MulticastGroup
Members map[string]*MulticastMembership // source IP -> membership
TypeID string `json:"typeid"`
Group *MulticastGroup `json:"group"`
Members MulticastMembershipMap `json:"members"`
}
func (g *MulticastGroup) Name() string {
ip := g.IP.To4()
func (g *MulticastGroup) IsDante() bool {
ip := net.ParseIP(g.IP).To4()
if ip == nil {
return g.IP.String()
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 g.IP.String() {
switch ip.String() {
case "224.0.0.251":
return "mdns"
case "224.0.1.129":
@@ -49,41 +91,24 @@ func (g *MulticastGroup) Name() string {
return "admin-scoped-broadcast"
}
// sACN (239.255.x.x, universes 1-63999)
if ip[0] == 239 && ip[1] == 255 {
universe := int(ip[2])*256 + int(ip[3])
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)
}
}
// Dante audio multicast (239.69-71.x.x)
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
flowID := (int(ip[1]-69) << 16) | (int(ip[2]) << 8) | int(ip[3])
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)
}
// Dante AV multicast (239.253.x.x)
if ip[0] == 239 && ip[1] == 253 {
flowID := (int(ip[2]) << 8) | int(ip[3])
if ip4[0] == 239 && ip4[1] == 253 {
flowID := (int(ip4[2]) << 8) | int(ip4[3])
return fmt.Sprintf("dante-av:%d", flowID)
}
return g.IP.String()
}
func (g *MulticastGroup) IsDante() bool {
ip := 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
return ip.String()
}
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
@@ -98,16 +123,25 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
gm := n.multicastGroups[groupKey]
if gm == nil {
gm = &MulticastGroupMembers{
Group: &MulticastGroup{IP: groupIP},
Members: map[string]*MulticastMembership{},
TypeID: newTypeID("mcastgroup"),
Group: &MulticastGroup{
Name: multicastGroupName(groupIP),
IP: groupKey,
},
Members: MulticastMembershipMap{},
}
n.multicastGroups[groupKey] = gm
}
gm.Members[sourceKey] = &MulticastMembership{
Node: node,
LastSeen: time.Now(),
membership := gm.Members[sourceKey]
if membership == nil {
membership = &MulticastMembership{
SourceIP: sourceKey,
}
gm.Members[sourceKey] = membership
}
membership.Node = node
membership.LastSeen = time.Now()
}
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
@@ -137,7 +171,7 @@ func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
continue
}
if _, exists := gm.Members[deviceKey]; exists {
groups = append(groups, gm.Group.IP)
groups = append(groups, net.ParseIP(gm.Group.IP))
}
}
return groups

View File

@@ -152,7 +152,8 @@ func (n *Nodes) createNode() int {
targetID := n.nextID
n.nextID++
node := &Node{
Interfaces: map[string]*Interface{},
TypeID: newTypeID("node"),
Interfaces: InterfaceMap{},
MACTable: map[string]string{},
pollTrigger: make(chan struct{}, 1),
}
@@ -172,10 +173,10 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
if nodeName != "" {
if node.Names == nil {
node.Names = map[string]bool{}
node.Names = NameSet{}
}
if !node.Names[nodeName] {
node.Names[nodeName] = true
if !node.Names.Has(nodeName) {
node.Names.Add(nodeName)
n.nameIndex[nodeName] = nodeID
added = append(added, "name="+nodeName)
}
@@ -194,10 +195,10 @@ func (n *Nodes) updateNodeIPs(node *Node, nodeID int, ips []net.IP) []string {
n.ipIndex[ipKey] = nodeID
iface, exists := node.Interfaces[ipKey]
if !exists {
iface = &Interface{IPs: map[string]net.IP{}}
iface = &Interface{IPs: IPSet{}}
node.Interfaces[ipKey] = iface
}
iface.IPs[ipKey] = ip
iface.IPs.Add(ip)
added = append(added, "ip="+ipKey)
go n.t.requestARP(ip)
}
@@ -273,8 +274,8 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
if !exists {
iface = &Interface{
Name: ifaceName,
MAC: mac,
IPs: map[string]net.IP{},
MAC: MACFrom(mac),
IPs: IPSet{},
}
node.Interfaces[ifaceKey] = iface
added = append(added, "iface="+ifaceKey)
@@ -286,10 +287,10 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
for _, ip := range ips {
ipKey := ip.String()
if _, exists := iface.IPs[ipKey]; !exists {
if !iface.IPs.Has(ipKey) {
added = append(added, "ip="+ipKey)
}
iface.IPs[ipKey] = ip
iface.IPs.Add(ip)
n.ipIndex[ipKey] = nodeID
}
@@ -300,7 +301,7 @@ func (n *Nodes) findOrCreateInterface(node *Node, macKey, ifaceName, ifaceKey st
var added []string
if ifaceName != "" {
if oldIface, oldExists := node.Interfaces[macKey]; oldExists && oldIface.MAC.String() == macKey {
if oldIface, oldExists := node.Interfaces[macKey]; oldExists && string(oldIface.MAC) == macKey {
oldIface.Name = ifaceName
delete(node.Interfaces, macKey)
node.Interfaces[ifaceKey] = oldIface
@@ -308,7 +309,7 @@ func (n *Nodes) findOrCreateInterface(node *Node, macKey, ifaceName, ifaceKey st
}
} else {
for _, existing := range node.Interfaces {
if existing.MAC.String() == macKey {
if string(existing.MAC) == macKey {
return existing, true, added
}
}
@@ -364,20 +365,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
for name := range merge.Names {
if keep.Names == nil {
keep.Names = map[string]bool{}
keep.Names = NameSet{}
}
keep.Names[name] = true
keep.Names.Add(name)
n.nameIndex[name] = keepID
}
for _, iface := range merge.Interfaces {
var ips []net.IP
for _, ip := range iface.IPs {
ips = append(ips, ip)
for ipStr := range iface.IPs {
ips = append(ips, net.ParseIP(ipStr))
}
if iface.MAC != nil {
n.updateNodeInterface(keep, keepID, iface.MAC, ips, iface.Name)
n.macIndex[iface.MAC.String()] = keepID
if iface.MAC != "" {
n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name)
n.macIndex[string(iface.MAC)] = keepID
}
}
@@ -451,8 +452,9 @@ func (n *Nodes) GetOrCreateByName(name string) *Node {
targetID := n.nextID
n.nextID++
node := &Node{
Names: map[string]bool{name: true},
Interfaces: map[string]*Interface{},
TypeID: newTypeID("node"),
Names: NameSet{name: true},
Interfaces: InterfaceMap{},
MACTable: map[string]string{},
pollTrigger: make(chan struct{}, 1),
}
@@ -593,7 +595,7 @@ func (n *Nodes) LogAll() {
groups = append(groups, gm)
}
sort.Slice(groups, func(i, j int) bool {
return sortorder.NaturalLess(groups[i].Group.Name(), groups[j].Group.Name())
return sortorder.NaturalLess(groups[i].Group.Name, groups[j].Group.Name)
})
log.Printf("[sigusr1] ================ %d multicast groups ================", len(groups))
@@ -614,7 +616,7 @@ func (n *Nodes) LogAll() {
sort.Slice(memberNames, func(i, j int) bool {
return sortorder.NaturalLess(memberNames[i], memberNames[j])
})
log.Printf("[sigusr1] %s: %s", gm.Group.Name(), strings.Join(memberNames, ", "))
log.Printf("[sigusr1] %s: %s", gm.Group.Name, strings.Join(memberNames, ", "))
}
}

0
static/.gitkeep Normal file
View File

View File

@@ -58,6 +58,7 @@ type Tendrils struct {
DebugBMD bool
DebugShure bool
DebugYamaha bool
HTTPPort string
}
func New() *Tendrils {
@@ -83,6 +84,7 @@ func (t *Tendrils) Run() {
}()
t.populateLocalAddresses()
t.startHTTPServer()
if !t.DisableARP {
t.readARPTable()
@@ -239,8 +241,9 @@ func (t *Tendrils) pollNode(node *Node) {
t.nodes.mu.RLock()
var ips []net.IP
for _, iface := range node.Interfaces {
for _, ip := range iface.IPs {
if ip.To4() != nil {
for ipStr := range iface.IPs {
ip := net.ParseIP(ipStr)
if ip != nil && ip.To4() != nil {
ips = append(ips, ip)
}
}

139
types.go
View File

@@ -1,62 +1,143 @@
package tendrils
import (
"encoding/json"
"fmt"
"net"
"sort"
"strings"
"github.com/fvbommel/sortorder"
"go.jetify.com/typeid"
)
func newTypeID(prefix string) string {
tid, _ := typeid.WithPrefix(prefix)
return tid.String()
}
type MAC string
func (m MAC) Parse() net.HardwareAddr {
mac, _ := net.ParseMAC(string(m))
return mac
}
func MACFrom(mac net.HardwareAddr) MAC {
if mac == nil {
return ""
}
return MAC(mac.String())
}
type IPSet map[string]bool
func (s IPSet) MarshalJSON() ([]byte, error) {
ips := make([]string, 0, len(s))
for ip := range s {
ips = append(ips, ip)
}
sort.Strings(ips)
return json.Marshal(ips)
}
func (s IPSet) Add(ip net.IP) {
s[ip.String()] = true
}
func (s IPSet) Has(ip string) bool {
return s[ip]
}
func (s IPSet) Slice() []string {
ips := make([]string, 0, len(s))
for ip := range s {
ips = append(ips, ip)
}
sort.Strings(ips)
return ips
}
type NameSet map[string]bool
func (s NameSet) MarshalJSON() ([]byte, error) {
names := make([]string, 0, len(s))
for name := range s {
names = append(names, name)
}
sort.Strings(names)
return json.Marshal(names)
}
func (s NameSet) Add(name string) {
s[name] = true
}
func (s NameSet) Has(name string) bool {
return s[name]
}
type InterfaceMap map[string]*Interface
func (m InterfaceMap) MarshalJSON() ([]byte, error) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return sortorder.NaturalLess(keys[i], keys[j])
})
ifaces := make([]*Interface, 0, len(m))
for _, k := range keys {
ifaces = append(ifaces, m[k])
}
return json.Marshal(ifaces)
}
type Interface struct {
Name string
MAC net.HardwareAddr
IPs map[string]net.IP
Stats *InterfaceStats
Name string `json:"name,omitempty"`
MAC MAC `json:"mac"`
IPs IPSet `json:"ips"`
Stats *InterfaceStats `json:"stats,omitempty"`
}
type InterfaceStats struct {
Speed uint64 // bits per second
InErrors uint64
OutErrors uint64
PoE *PoEStats
Speed uint64 `json:"speed,omitempty"`
InErrors uint64 `json:"in_errors,omitempty"`
OutErrors uint64 `json:"out_errors,omitempty"`
PoE *PoEStats `json:"poe,omitempty"`
}
type PoEStats struct {
Power float64 // watts in use
MaxPower float64 // watts allocated/negotiated
Power float64 `json:"power"`
MaxPower float64 `json:"max_power"`
}
type PoEBudget struct {
Power float64 // watts in use
MaxPower float64 // watts total budget
Power float64 `json:"power"`
MaxPower float64 `json:"max_power"`
}
type Node struct {
Names map[string]bool
Interfaces map[string]*Interface
MACTable map[string]string // peer MAC -> local interface name
PoEBudget *PoEBudget
IsDanteClockMaster bool
DanteTxChannels string
TypeID string `json:"typeid"`
Names NameSet `json:"names"`
Interfaces InterfaceMap `json:"interfaces"`
MACTable map[string]string `json:"-"`
MACTableSize int `json:"mac_table_size,omitempty"`
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
pollTrigger chan struct{}
}
func (i *Interface) String() string {
var ips []string
for _, ip := range i.IPs {
ips = append(ips, ip.String())
}
sort.Strings(ips)
var parts []string
parts = append(parts, i.MAC.String())
parts = append(parts, string(i.MAC))
if i.Name != "" {
parts = append(parts, fmt.Sprintf("(%s)", i.Name))
}
if len(ips) > 0 {
parts = append(parts, fmt.Sprintf("%v", ips))
if len(i.IPs) > 0 {
parts = append(parts, fmt.Sprintf("%v", i.IPs.Slice()))
}
if i.Stats != nil {
parts = append(parts, i.Stats.String())
@@ -135,8 +216,8 @@ func (n *Node) DisplayName() string {
func (n *Node) FirstMAC() string {
for _, iface := range n.Interfaces {
if iface.MAC != nil {
return iface.MAC.String()
if iface.MAC != "" {
return string(iface.MAC)
}
}
return "??"