diff --git a/bmd.go b/bmd.go new file mode 100644 index 0000000..cc2ea73 --- /dev/null +++ b/bmd.go @@ -0,0 +1,111 @@ +package tendrils + +import ( + "context" + "log" + "net" + "time" +) + +func (t *Tendrils) listenBMD(ctx context.Context, iface net.Interface) { + addrs, err := iface.Addrs() + if err != nil { + return + } + + var srcIP net.IP + var broadcast net.IP + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil { + srcIP = ipnet.IP.To4() + mask := ipnet.Mask + broadcast = make(net.IP, 4) + for i := 0; i < 4; i++ { + broadcast[i] = srcIP[i] | ^mask[i] + } + break + } + } + if srcIP == nil { + return + } + + conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: srcIP, Port: 0}) + if err != nil { + return + } + defer conn.Close() + + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + t.sendATEMDiscovery(conn, broadcast, iface.Name) + + go t.receiveATEMResponses(ctx, conn, iface.Name) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.sendATEMDiscovery(conn, broadcast, iface.Name) + } + } +} + +func (t *Tendrils) sendATEMDiscovery(conn *net.UDPConn, broadcast net.IP, ifaceName string) { + // ATEM protocol hello packet + // Flag 0x10 (hello), length 20 bytes + packet := []byte{ + 0x10, 0x14, // Flags (0x10 = hello) + length (20 = 0x14) + 0x00, 0x00, // Session ID + 0x00, 0x00, // Ack number + 0x00, 0x00, // Local sequence + 0x00, 0x00, // Remote sequence + 0x00, 0x00, // Unknown + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client hello data + } + + conn.WriteToUDP(packet, &net.UDPAddr{IP: broadcast, Port: 9910}) + + if t.DebugBMD { + log.Printf("[bmd] %s: sent atem discovery to %s", ifaceName, broadcast) + } +} + +func (t *Tendrils) receiveATEMResponses(ctx context.Context, conn *net.UDPConn, ifaceName string) { + seen := map[string]bool{} + buf := make([]byte, 2048) + 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 + } + + if n < 12 { + continue + } + + ipKey := src.IP.String() + if seen[ipKey] { + continue + } + seen[ipKey] = true + + if t.DebugBMD { + log.Printf("[bmd] %s: atem at %s", ifaceName, src.IP) + } + + t.nodes.Update(nil, nil, []net.IP{src.IP}, "", "atem", "bmd") + } +} diff --git a/cmd/tendrils/main.go b/cmd/tendrils/main.go index 57e596d..9719553 100644 --- a/cmd/tendrils/main.go +++ b/cmd/tendrils/main.go @@ -15,6 +15,7 @@ func main() { noMDNS := flag.Bool("no-mdns", false, "disable mDNS discovery") 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") 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") @@ -24,6 +25,7 @@ func main() { debugMDNS := flag.Bool("debug-mdns", false, "debug mDNS discovery") 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") flag.Parse() t := tendrils.New() @@ -35,6 +37,7 @@ func main() { t.DisableMDNS = *noMDNS t.DisableArtNet = *noArtNet t.DisableDante = *noDante + t.DisableBMD = *noBMD t.LogEvents = *logEvents t.LogNodes = *logNodes t.DebugARP = *debugARP @@ -44,5 +47,6 @@ func main() { t.DebugMDNS = *debugMDNS t.DebugArtNet = *debugArtNet t.DebugDante = *debugDante + t.DebugBMD = *debugBMD t.Run() } diff --git a/tendrils.go b/tendrils.go index 817c0f2..78c3ee9 100644 --- a/tendrils.go +++ b/tendrils.go @@ -23,6 +23,7 @@ type Tendrils struct { DisableMDNS bool DisableArtNet bool DisableDante bool + DisableBMD bool LogEvents bool LogNodes bool DebugARP bool @@ -32,6 +33,7 @@ type Tendrils struct { DebugMDNS bool DebugArtNet bool DebugDante bool + DebugBMD bool } func New() *Tendrils { @@ -200,4 +202,7 @@ func (t *Tendrils) startInterface(ctx context.Context, iface net.Interface) { if !t.DisableDante { go t.listenDante(ctx, iface) } + if !t.DisableBMD { + go t.listenBMD(ctx, iface) + } }