Add pcap-based ArtNet receiver to avoid port conflicts
Similar to sACN pcap receiver, allows receiving ArtNet packets without binding to port 6454. Use --artnet-pcap=any to enable. The BPF filter "udp port 6454" captures packets where src OR dst is 6454, so we also catch ArtPollReply responses sent from port 6454. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
120
artnet/receiver_pcap.go
Normal file
120
artnet/receiver_pcap.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
#
|
#
|
||||||
# Flags:
|
# Flags:
|
||||||
# --artnet-listen=:6454 ArtNet listen address (empty to disable)
|
# --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')
|
# --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
|
# Target addresses for ArtNet output universes
|
||||||
# Each output universe needs a target IP (broadcast or unicast)
|
# Each output universe needs a target IP (broadcast or unicast)
|
||||||
|
|||||||
22
main.go
22
main.go
@@ -20,6 +20,7 @@ import (
|
|||||||
type App struct {
|
type App struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
artReceiver *artnet.Receiver
|
artReceiver *artnet.Receiver
|
||||||
|
artPcapReceiver *artnet.PcapReceiver
|
||||||
sacnReceiver *sacn.Receiver
|
sacnReceiver *sacn.Receiver
|
||||||
sacnPcapReceiver *sacn.PcapReceiver
|
sacnPcapReceiver *sacn.PcapReceiver
|
||||||
artSender *artnet.Sender
|
artSender *artnet.Sender
|
||||||
@@ -34,8 +35,9 @@ type App struct {
|
|||||||
func main() {
|
func main() {
|
||||||
configPath := flag.String("config", "config.toml", "path to config file")
|
configPath := flag.String("config", "config.toml", "path to config file")
|
||||||
artnetListen := flag.String("artnet-listen", ":6454", "artnet listen address (empty to disable)")
|
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')")
|
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")
|
debug := flag.Bool("debug", false, "log incoming/outgoing dmx packets")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -129,7 +131,20 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create ArtNet receiver if enabled
|
// 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)
|
addr, err := parseListenAddr(*artnetListen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("artnet listen error: %v", err)
|
log.Fatalf("artnet listen error: %v", err)
|
||||||
@@ -183,6 +198,9 @@ func main() {
|
|||||||
if app.artReceiver != nil {
|
if app.artReceiver != nil {
|
||||||
app.artReceiver.Stop()
|
app.artReceiver.Stop()
|
||||||
}
|
}
|
||||||
|
if app.artPcapReceiver != nil {
|
||||||
|
app.artPcapReceiver.Stop()
|
||||||
|
}
|
||||||
if app.sacnReceiver != nil {
|
if app.sacnReceiver != nil {
|
||||||
app.sacnReceiver.Stop()
|
app.sacnReceiver.Stop()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user