diff --git a/artnet/receiver_pcap.go b/artnet/receiver_pcap.go new file mode 100644 index 0000000..1659e6b --- /dev/null +++ b/artnet/receiver_pcap.go @@ -0,0 +1,120 @@ +package artnet + +import ( + "net" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" +) + +// PcapReceiver listens for ArtNet packets using packet capture +type PcapReceiver struct { + handle *pcap.Handle + handler PacketHandler + done chan struct{} +} + +// NewPcapReceiver creates a new ArtNet receiver using packet capture +// This requires root/admin privileges but avoids port conflicts +func NewPcapReceiver(iface string, handler PacketHandler) (*PcapReceiver, error) { + // Open device for capturing + handle, err := pcap.OpenLive(iface, 1600, true, pcap.BlockForever) + if err != nil { + return nil, err + } + + // Filter for UDP port 6454 (ArtNet) - captures both directions + if err := handle.SetBPFFilter("udp port 6454"); err != nil { + handle.Close() + return nil, err + } + + return &PcapReceiver{ + handle: handle, + handler: handler, + done: make(chan struct{}), + }, nil +} + +// Start begins receiving packets +func (r *PcapReceiver) Start() { + go r.receiveLoop() +} + +// Stop stops the receiver +func (r *PcapReceiver) Stop() { + close(r.done) + r.handle.Close() +} + +func (r *PcapReceiver) receiveLoop() { + packetSource := gopacket.NewPacketSource(r.handle, r.handle.LinkType()) + + for { + select { + case <-r.done: + return + case packet, ok := <-packetSource.Packets(): + if !ok { + return + } + r.handlePacket(packet) + } + } +} + +func (r *PcapReceiver) handlePacket(packet gopacket.Packet) { + // Extract UDP layer + udpLayer := packet.Layer(layers.LayerTypeUDP) + if udpLayer == nil { + return + } + + udp, _ := udpLayer.(*layers.UDP) + if udp == nil { + return + } + + // Extract IP layer for source address + var srcIP, dstIP [4]byte + if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil { + ip, _ := ipLayer.(*layers.IPv4) + if ip != nil { + copy(srcIP[:], ip.SrcIP.To4()) + copy(dstIP[:], ip.DstIP.To4()) + } + } + + // Get payload + data := udp.Payload + if len(data) < 12 { + return + } + + // Parse the ArtNet packet + opCode, pkt, err := ParsePacket(data) + if err != nil { + return + } + + src := &net.UDPAddr{ + IP: net.IP(srcIP[:]), + Port: int(udp.SrcPort), + } + + switch opCode { + case OpDmx: + if dmx, ok := pkt.(*DMXPacket); ok { + r.handler.HandleDMX(src, dmx) + } + case OpPoll: + if poll, ok := pkt.(*PollPacket); ok { + r.handler.HandlePoll(src, poll) + } + case OpPollReply: + if reply, ok := pkt.(*PollReplyPacket); ok { + r.handler.HandlePollReply(src, reply) + } + } +} diff --git a/config.example.toml b/config.example.toml index 4dddedc..999f099 100644 --- a/config.example.toml +++ b/config.example.toml @@ -3,8 +3,9 @@ # # Flags: # --artnet-listen=:6454 ArtNet listen address (empty to disable) +# --artnet-pcap=any Use pcap for ArtNet (requires root, avoids port conflicts) # --artnet-broadcast=auto Broadcast addresses (comma-separated, or 'auto') -# --sacn-pcap=en0 Use pcap for sACN (requires root, avoids port conflicts) +# --sacn-pcap=any Use pcap for sACN (requires root, avoids port conflicts) # Target addresses for ArtNet output universes # Each output universe needs a target IP (broadcast or unicast) diff --git a/main.go b/main.go index 31f7261..7e9456d 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( type App struct { cfg *config.Config artReceiver *artnet.Receiver + artPcapReceiver *artnet.PcapReceiver sacnReceiver *sacn.Receiver sacnPcapReceiver *sacn.PcapReceiver artSender *artnet.Sender @@ -34,8 +35,9 @@ type App struct { func main() { configPath := flag.String("config", "config.toml", "path to config file") artnetListen := flag.String("artnet-listen", ":6454", "artnet listen address (empty to disable)") + artnetPcap := flag.String("artnet-pcap", "", "use pcap for artnet on interface (e.g. en0, any)") artnetBroadcast := flag.String("artnet-broadcast", "auto", "artnet broadcast addresses (comma-separated, or 'auto')") - sacnPcap := flag.String("sacn-pcap", "", "use pcap for sacn on interface (e.g. en0, eth0)") + sacnPcap := flag.String("sacn-pcap", "", "use pcap for sacn on interface (e.g. en0, any)") debug := flag.Bool("debug", false, "log incoming/outgoing dmx packets") flag.Parse() @@ -129,7 +131,20 @@ func main() { } // Create ArtNet receiver if enabled - if *artnetListen != "" { + if *artnetPcap != "" { + // Use pcap-based receiver (requires root, avoids port conflicts) + iface := *artnetPcap + if iface == "auto" { + iface = "any" + } + pcapReceiver, err := artnet.NewPcapReceiver(iface, app) + if err != nil { + log.Fatalf("artnet pcap error: %v", err) + } + app.artPcapReceiver = pcapReceiver + pcapReceiver.Start() + log.Printf("[artnet] pcap listening iface=%s", iface) + } else if *artnetListen != "" { addr, err := parseListenAddr(*artnetListen) if err != nil { log.Fatalf("artnet listen error: %v", err) @@ -183,6 +198,9 @@ func main() { if app.artReceiver != nil { app.artReceiver.Stop() } + if app.artPcapReceiver != nil { + app.artPcapReceiver.Stop() + } if app.sacnReceiver != nil { app.sacnReceiver.Stop() }