add shure slp discovery and comma-separate group member lists
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -348,16 +348,16 @@ func (a *ArtNetNodes) LogAll() {
|
||||
var parts []string
|
||||
if len(ins) > 0 {
|
||||
sort.Slice(ins, func(i, j int) bool { return sortorder.NaturalLess(ins[i], ins[j]) })
|
||||
parts = append(parts, fmt.Sprintf("in:%v", ins))
|
||||
parts = append(parts, fmt.Sprintf("in: %s", strings.Join(ins, ", ")))
|
||||
}
|
||||
if len(outs) > 0 {
|
||||
sort.Slice(outs, func(i, j int) bool { return sortorder.NaturalLess(outs[i], outs[j]) })
|
||||
parts = append(parts, fmt.Sprintf("out:%v", outs))
|
||||
parts = append(parts, fmt.Sprintf("out: %s", strings.Join(outs, ", ")))
|
||||
}
|
||||
net := (u >> 8) & 0x7f
|
||||
subnet := (u >> 4) & 0x0f
|
||||
universe := u & 0x0f
|
||||
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, net, subnet, universe, strings.Join(parts, " "))
|
||||
log.Printf("[sigusr1] artnet:%d (%d/%d/%d) %s", u, net, subnet, universe, strings.Join(parts, "; "))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ func main() {
|
||||
noArtNet := flag.Bool("no-artnet", false, "disable Art-Net discovery")
|
||||
noDante := flag.Bool("no-dante", false, "disable Dante discovery")
|
||||
noBMD := flag.Bool("no-bmd", false, "disable Blackmagic discovery")
|
||||
noShure := flag.Bool("no-shure", false, "disable Shure discovery")
|
||||
logEvents := flag.Bool("log-events", false, "log node events")
|
||||
logNodes := flag.Bool("log-nodes", false, "log full node details on changes")
|
||||
debugARP := flag.Bool("debug-arp", false, "debug ARP discovery")
|
||||
@@ -26,6 +27,7 @@ func main() {
|
||||
debugArtNet := flag.Bool("debug-artnet", false, "debug Art-Net discovery")
|
||||
debugDante := flag.Bool("debug-dante", false, "debug Dante discovery")
|
||||
debugBMD := flag.Bool("debug-bmd", false, "debug Blackmagic discovery")
|
||||
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
|
||||
flag.Parse()
|
||||
|
||||
t := tendrils.New()
|
||||
@@ -38,6 +40,7 @@ func main() {
|
||||
t.DisableArtNet = *noArtNet
|
||||
t.DisableDante = *noDante
|
||||
t.DisableBMD = *noBMD
|
||||
t.DisableShure = *noShure
|
||||
t.LogEvents = *logEvents
|
||||
t.LogNodes = *logNodes
|
||||
t.DebugARP = *debugARP
|
||||
@@ -48,5 +51,6 @@ func main() {
|
||||
t.DebugArtNet = *debugArtNet
|
||||
t.DebugDante = *debugDante
|
||||
t.DebugBMD = *debugBMD
|
||||
t.DebugShure = *debugShure
|
||||
t.Run()
|
||||
}
|
||||
|
||||
3
nodes.go
3
nodes.go
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -710,7 +711,7 @@ func (n *Nodes) LogAll() {
|
||||
sort.Slice(memberNames, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(memberNames[i], memberNames[j])
|
||||
})
|
||||
log.Printf("[sigusr1] %s: %v", gm.Group.Name(), memberNames)
|
||||
log.Printf("[sigusr1] %s: %s", gm.Group.Name(), strings.Join(memberNames, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
135
shure.go
Normal file
135
shure.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package tendrils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"net"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
var acnUACNRegex = regexp.MustCompile(`\(acn-uacn=([^)]+)\)`)
|
||||
|
||||
const (
|
||||
shureMulticastAddr = "239.255.254.253:8427"
|
||||
)
|
||||
|
||||
func (t *Tendrils) listenShure(ctx context.Context, iface net.Interface) {
|
||||
addr, err := net.ResolveUDPAddr("udp4", shureMulticastAddr)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] failed to resolve shure address: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.ListenMulticastUDP("udp4", &iface, addr)
|
||||
if err != nil {
|
||||
if t.DebugShure {
|
||||
log.Printf("[shure] %s: failed to listen: %v", iface.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
go t.sendShureDiscovery(ctx, iface.Name, conn)
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||
n, src, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
t.handleShurePacket(iface.Name, src.IP, buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tendrils) sendShureDiscovery(ctx context.Context, ifaceName string, conn *net.UDPConn) {
|
||||
dest := &net.UDPAddr{IP: net.IPv4(239, 255, 254, 253), Port: 8427}
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
t.sendShureQuery(ifaceName, conn, dest)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
t.sendShureQuery(ifaceName, conn, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tendrils) sendShureQuery(ifaceName string, conn *net.UDPConn, dest *net.UDPAddr) {
|
||||
// SLP Service Type Request (SrvTypeRqst)
|
||||
// This asks "what service types are available?"
|
||||
langTag := []byte("en")
|
||||
|
||||
// Build SLP v2 header + SrvTypeRqst body
|
||||
// Header: version(1) + function(1) + length(3) + flags(2) + next-ext(3) + xid(2) + lang-len(2) + lang
|
||||
// SrvTypeRqst body: PR list len(2) + PR list + naming auth len(2) + naming auth + scope list len(2) + scope list
|
||||
|
||||
headerLen := 14 + len(langTag)
|
||||
bodyLen := 2 + 0 + 2 + 0 + 2 + 7 // empty PR list, empty naming auth, "default" scope
|
||||
totalLen := headerLen + bodyLen
|
||||
|
||||
pkt := make([]byte, totalLen)
|
||||
pkt[0] = 0x02 // SLP version 2
|
||||
pkt[1] = 0x09 // Function: SrvTypeRqst (9)
|
||||
pkt[2] = byte(totalLen >> 16) // Length (3 bytes)
|
||||
pkt[3] = byte(totalLen >> 8)
|
||||
pkt[4] = byte(totalLen)
|
||||
pkt[5] = 0x00 // Flags (2 bytes) - multicast
|
||||
pkt[6] = 0x20
|
||||
pkt[7] = 0x00 // Next ext offset (3 bytes)
|
||||
pkt[8] = 0x00
|
||||
pkt[9] = 0x00
|
||||
binary.BigEndian.PutUint16(pkt[10:12], 0x0001) // XID
|
||||
binary.BigEndian.PutUint16(pkt[12:14], uint16(len(langTag)))
|
||||
copy(pkt[14:], langTag)
|
||||
|
||||
offset := 14 + len(langTag)
|
||||
binary.BigEndian.PutUint16(pkt[offset:], 0) // PR list length (0)
|
||||
offset += 2
|
||||
binary.BigEndian.PutUint16(pkt[offset:], 0) // Naming authority length (0 = IANA)
|
||||
offset += 2
|
||||
binary.BigEndian.PutUint16(pkt[offset:], 7) // Scope list length
|
||||
offset += 2
|
||||
copy(pkt[offset:], "default")
|
||||
|
||||
conn.WriteToUDP(pkt, dest)
|
||||
|
||||
if t.DebugShure {
|
||||
log.Printf("[shure] %s: sent slp discovery query", ifaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tendrils) handleShurePacket(ifaceName string, srcIP net.IP, data []byte) {
|
||||
if t.DebugShure {
|
||||
log.Printf("[shure] %s: packet from %s (%d bytes): %q", ifaceName, srcIP, len(data), data)
|
||||
}
|
||||
|
||||
match := acnUACNRegex.FindSubmatch(data)
|
||||
if match == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name := string(match[1])
|
||||
if t.DebugShure {
|
||||
log.Printf("[shure] %s: found device %s at %s", ifaceName, name, srcIP)
|
||||
}
|
||||
|
||||
t.nodes.Update(nil, nil, []net.IP{srcIP}, "", name, "shure")
|
||||
}
|
||||
@@ -24,6 +24,7 @@ type Tendrils struct {
|
||||
DisableArtNet bool
|
||||
DisableDante bool
|
||||
DisableBMD bool
|
||||
DisableShure bool
|
||||
LogEvents bool
|
||||
LogNodes bool
|
||||
DebugARP bool
|
||||
@@ -34,6 +35,7 @@ type Tendrils struct {
|
||||
DebugArtNet bool
|
||||
DebugDante bool
|
||||
DebugBMD bool
|
||||
DebugShure bool
|
||||
}
|
||||
|
||||
func New() *Tendrils {
|
||||
@@ -205,4 +207,7 @@ func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) {
|
||||
if !t.DisableBMD {
|
||||
go t.listenBMD(ctx, iface)
|
||||
}
|
||||
if !t.DisableShure {
|
||||
go t.listenShure(ctx, iface)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user