Add --artnet-broadcast flag for smarter broadcast handling

- Accepts comma-separated list of addresses or 'auto'
- Auto-detection calculates broadcast from all network interfaces
- Used as fallback when no per-universe target or discovered nodes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2025-12-24 12:03:00 -08:00
parent ff8d371351
commit cfee18e6e2
2 changed files with 97 additions and 3 deletions

View File

@@ -1,8 +1,10 @@
# artmap configuration
# Run with: go run . -config config.toml [-artnet-listen :6454] [-sacn-pcap en0]
# Run with: go run . --config=config.toml [flags]
#
# Use -sacn-pcap <interface> to capture sACN via pcap instead of binding port 5568.
# This avoids port conflicts but requires root. Use "auto" for auto-detect.
# Flags:
# --artnet-listen=:6454 ArtNet listen address (empty to disable)
# --artnet-broadcast=auto Broadcast addresses (comma-separated, or 'auto')
# --sacn-pcap=en0 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)

92
main.go
View File

@@ -27,12 +27,14 @@ type App struct {
discovery *artnet.Discovery
engine *remap.Engine
targets map[artnet.Universe]*net.UDPAddr
broadcasts []*net.UDPAddr
debug bool
}
func main() {
configPath := flag.String("config", "config.toml", "path to config file")
artnetListen := flag.String("artnet-listen", ":6454", "artnet listen address (empty to disable)")
artnetBroadcast := flag.String("artnet-broadcast", "", "artnet broadcast addresses (comma-separated, or 'auto')")
sacnPcap := flag.String("sacn-pcap", "", "use pcap for sacn on interface (e.g. en0, eth0)")
debug := flag.Bool("debug", false, "log incoming/outgoing dmx packets")
flag.Parse()
@@ -69,6 +71,27 @@ func main() {
log.Printf(" target %s -> %s", t.Universe, addr)
}
// Parse broadcast addresses
var broadcasts []*net.UDPAddr
if *artnetBroadcast != "" {
if *artnetBroadcast == "auto" {
broadcasts = detectBroadcastAddrs()
} else {
for _, addrStr := range strings.Split(*artnetBroadcast, ",") {
addrStr = strings.TrimSpace(addrStr)
addr, err := parseTargetAddr(addrStr)
if err != nil {
log.Fatalf("broadcast error: address=%q err=%v", addrStr, err)
}
broadcasts = append(broadcasts, addr)
}
}
for _, addr := range broadcasts {
pollTargets[addr.String()] = addr
log.Printf(" broadcast %s", addr)
}
}
// Convert poll targets to slice
pollTargetSlice := make([]*net.UDPAddr, 0, len(pollTargets))
for _, addr := range pollTargets {
@@ -101,6 +124,7 @@ func main() {
discovery: discovery,
engine: engine,
targets: targets,
broadcasts: broadcasts,
debug: *debug,
}
@@ -237,6 +261,15 @@ func (a *App) sendOutputs(outputs []remap.Output) {
if err := a.artSender.SendDMX(target, out.Universe, out.Data[:]); err != nil {
log.Printf("[->artnet] error: dst=%s err=%v", target.IP, err)
}
} else if len(a.broadcasts) > 0 {
for _, bcast := range a.broadcasts {
if a.debug {
log.Printf("[->artnet] broadcast dst=%s universe=%s", bcast.IP, out.Universe)
}
if err := a.artSender.SendDMX(bcast, out.Universe, out.Data[:]); err != nil {
log.Printf("[->artnet] error: dst=%s err=%v", bcast.IP, err)
}
}
} else {
log.Printf("[->artnet] no target: universe=%s", out.Universe)
}
@@ -319,3 +352,62 @@ func parseTargetAddr(s string) (*net.UDPAddr, error) {
return &net.UDPAddr{IP: ip, Port: port}, nil
}
// detectBroadcastAddrs returns broadcast addresses for all network interfaces
func detectBroadcastAddrs() []*net.UDPAddr {
var addrs []*net.UDPAddr
seen := make(map[string]bool)
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
for _, iface := range ifaces {
// Skip loopback and down interfaces
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
continue
}
ifaceAddrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range ifaceAddrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip4 := ipnet.IP.To4()
if ip4 == nil {
continue
}
// Calculate broadcast address: IP | ~mask
mask := ipnet.Mask
if len(mask) != 4 {
continue
}
broadcast := make(net.IP, 4)
for i := 0; i < 4; i++ {
broadcast[i] = ip4[i] | ^mask[i]
}
key := broadcast.String()
if seen[key] {
continue
}
seen[key] = true
addrs = append(addrs, &net.UDPAddr{
IP: broadcast,
Port: artnet.Port,
})
}
}
return addrs
}