diff --git a/artnet.go b/artnet.go index 690881c..a62885b 100644 --- a/artnet.go +++ b/artnet.go @@ -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, "; ")) } } diff --git a/cmd/tendrils/main.go b/cmd/tendrils/main.go index 9719553..d3183ab 100644 --- a/cmd/tendrils/main.go +++ b/cmd/tendrils/main.go @@ -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() } diff --git a/nodes.go b/nodes.go index 119f7d9..2f265f7 100644 --- a/nodes.go +++ b/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, ", ")) } } diff --git a/shure.go b/shure.go new file mode 100644 index 0000000..aa78254 --- /dev/null +++ b/shure.go @@ -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") +} diff --git a/tendrils.go b/tendrils.go index 78c3ee9..47c6c28 100644 --- a/tendrils.go +++ b/tendrils.go @@ -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) + } }