Replace --broadcast flag with per-universe target config
- Remove --broadcast CLI flag - Add [[target]] config section for per-universe destination addresses - Each ArtNet output universe can have its own target IP (broadcast or unicast) - ArtPoll discovery broadcasts to all unique target addresses - Falls back to configured target when no nodes discovered for universe 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,17 +27,19 @@ type Discovery struct {
|
||||
shortName string
|
||||
longName string
|
||||
universes []Universe
|
||||
pollTargets []*net.UDPAddr
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// NewDiscovery creates a new discovery handler
|
||||
func NewDiscovery(sender *Sender, shortName, longName string, universes []Universe) *Discovery {
|
||||
func NewDiscovery(sender *Sender, shortName, longName string, universes []Universe, pollTargets []*net.UDPAddr) *Discovery {
|
||||
return &Discovery{
|
||||
sender: sender,
|
||||
nodes: make(map[string]*Node),
|
||||
shortName: shortName,
|
||||
longName: longName,
|
||||
universes: universes,
|
||||
pollTargets: pollTargets,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
@@ -57,10 +59,8 @@ func (d *Discovery) Stop() {
|
||||
}
|
||||
|
||||
func (d *Discovery) pollLoop() {
|
||||
// Send initial poll
|
||||
if err := d.sender.SendPoll(); err != nil {
|
||||
log.Printf("failed to send ArtPoll: %v", err)
|
||||
}
|
||||
// Send initial poll to all targets
|
||||
d.sendPolls()
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -73,15 +73,21 @@ func (d *Discovery) pollLoop() {
|
||||
case <-d.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := d.sender.SendPoll(); err != nil {
|
||||
log.Printf("failed to send ArtPoll: %v", err)
|
||||
}
|
||||
d.sendPolls()
|
||||
case <-cleanupTicker.C:
|
||||
d.cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discovery) sendPolls() {
|
||||
for _, target := range d.pollTargets {
|
||||
if err := d.sender.SendPoll(target); err != nil {
|
||||
log.Printf("failed to send ArtPoll to %s: %v", target.IP, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discovery) cleanup() {
|
||||
d.nodesMu.Lock()
|
||||
defer d.nodesMu.Unlock()
|
||||
|
||||
@@ -8,13 +8,12 @@ import (
|
||||
// Sender sends ArtNet packets
|
||||
type Sender struct {
|
||||
conn *net.UDPConn
|
||||
broadcastAddr *net.UDPAddr
|
||||
sequences map[Universe]uint8
|
||||
seqMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewSender creates a new ArtNet sender
|
||||
func NewSender(broadcastAddr string) (*Sender, error) {
|
||||
func NewSender() (*Sender, error) {
|
||||
// Create a UDP socket for sending
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
@@ -27,21 +26,8 @@ func NewSender(broadcastAddr string) (*Sender, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
broadcast, err := net.ResolveUDPAddr("udp4", broadcastAddr+":"+string(rune(Port)))
|
||||
if err != nil {
|
||||
// Try parsing as IP:Port
|
||||
broadcast, err = net.ResolveUDPAddr("udp4", broadcastAddr)
|
||||
if err != nil {
|
||||
broadcast = &net.UDPAddr{
|
||||
IP: net.ParseIP(broadcastAddr),
|
||||
Port: Port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Sender{
|
||||
conn: conn,
|
||||
broadcastAddr: broadcast,
|
||||
sequences: make(map[Universe]uint8),
|
||||
}, nil
|
||||
}
|
||||
@@ -62,15 +48,10 @@ func (s *Sender) SendDMX(addr *net.UDPAddr, universe Universe, data []byte) erro
|
||||
return err
|
||||
}
|
||||
|
||||
// SendDMXBroadcast sends a DMX packet to the broadcast address
|
||||
func (s *Sender) SendDMXBroadcast(universe Universe, data []byte) error {
|
||||
return s.SendDMX(s.broadcastAddr, universe, data)
|
||||
}
|
||||
|
||||
// SendPoll sends an ArtPoll packet to the broadcast address
|
||||
func (s *Sender) SendPoll() error {
|
||||
// SendPoll sends an ArtPoll packet to the specified address
|
||||
func (s *Sender) SendPoll(addr *net.UDPAddr) error {
|
||||
pkt := BuildPollPacket()
|
||||
_, err := s.conn.WriteToUDP(pkt, s.broadcastAddr)
|
||||
_, err := s.conn.WriteToUDP(pkt, addr)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,8 +66,3 @@ func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, shortName, lo
|
||||
func (s *Sender) Close() error {
|
||||
return s.conn.Close()
|
||||
}
|
||||
|
||||
// BroadcastAddr returns the configured broadcast address
|
||||
func (s *Sender) BroadcastAddr() *net.UDPAddr {
|
||||
return s.broadcastAddr
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# artmap configuration
|
||||
# Run with: go run . -config config.toml [-listen :6454] [-broadcast 2.255.255.255]
|
||||
# Run with: go run . -config config.toml [-listen :6454]
|
||||
|
||||
# Target addresses for ArtNet output universes
|
||||
# Configure the destination IP (broadcast or unicast) for each output universe
|
||||
# ArtPoll discovery will be sent to all unique target addresses
|
||||
[[target]]
|
||||
universe = "0.0.0"
|
||||
address = "2.255.255.255"
|
||||
|
||||
[[target]]
|
||||
universe = "0.0.5"
|
||||
address = "10.50.255.255"
|
||||
|
||||
# Address format:
|
||||
# from: universe[:channels] - range specifies which channels to read
|
||||
|
||||
@@ -11,9 +11,41 @@ import (
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
Targets []Target `toml:"target"`
|
||||
Mappings []Mapping `toml:"mapping"`
|
||||
}
|
||||
|
||||
// Target represents a target address for an output universe
|
||||
type Target struct {
|
||||
Universe TargetUniverse `toml:"universe"`
|
||||
Address string `toml:"address"`
|
||||
}
|
||||
|
||||
// TargetUniverse is a universe that can be parsed from string or int
|
||||
type TargetUniverse struct {
|
||||
artnet.Universe
|
||||
}
|
||||
|
||||
func (t *TargetUniverse) UnmarshalTOML(data interface{}) error {
|
||||
switch v := data.(type) {
|
||||
case string:
|
||||
u, err := parseUniverse(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Universe = u
|
||||
return nil
|
||||
case int64:
|
||||
t.Universe = artnet.Universe(v)
|
||||
return nil
|
||||
case float64:
|
||||
t.Universe = artnet.Universe(int64(v))
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported universe type: %T", data)
|
||||
}
|
||||
}
|
||||
|
||||
// Protocol specifies the output protocol
|
||||
type Protocol string
|
||||
|
||||
@@ -211,6 +243,13 @@ func Load(path string) (*Config, error) {
|
||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
// Validate targets
|
||||
for i, t := range cfg.Targets {
|
||||
if t.Address == "" {
|
||||
return nil, fmt.Errorf("target %d: address is required", i)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range cfg.Mappings {
|
||||
m := &cfg.Mappings[i]
|
||||
if m.From.ChannelStart < 1 || m.From.ChannelStart > 512 {
|
||||
@@ -286,3 +325,12 @@ func (c *Config) SACNSourceUniverses() []uint16 {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TargetMap returns a map of universe to target address
|
||||
func (c *Config) TargetMap() map[artnet.Universe]string {
|
||||
result := make(map[artnet.Universe]string)
|
||||
for _, t := range c.Targets {
|
||||
result[t.Universe.Universe] = t.Address
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
77
main.go
77
main.go
@@ -25,13 +25,13 @@ type App struct {
|
||||
sacnSender *sacn.Sender
|
||||
discovery *artnet.Discovery
|
||||
engine *remap.Engine
|
||||
targets map[artnet.Universe]*net.UDPAddr
|
||||
debug bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.toml", "path to config file")
|
||||
listenAddr := flag.String("listen", ":6454", "listen address (host:port, host, or :port)")
|
||||
broadcastAddr := flag.String("broadcast", "2.255.255.255", "ArtNet broadcast address")
|
||||
debug := flag.Bool("debug", false, "log ArtNet packets")
|
||||
flag.Parse()
|
||||
|
||||
@@ -60,8 +60,27 @@ func main() {
|
||||
m.Protocol, m.To.Universe, m.To.ChannelStart, toEnd)
|
||||
}
|
||||
|
||||
// Parse targets
|
||||
targets := make(map[artnet.Universe]*net.UDPAddr)
|
||||
pollTargets := make(map[string]*net.UDPAddr) // dedupe by address string
|
||||
for _, t := range cfg.Targets {
|
||||
addr, err := parseTargetAddr(t.Address)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid target address %q: %v", t.Address, err)
|
||||
}
|
||||
targets[t.Universe.Universe] = addr
|
||||
pollTargets[addr.String()] = addr
|
||||
log.Printf(" target %s -> %s", t.Universe.Universe, addr)
|
||||
}
|
||||
|
||||
// Convert poll targets to slice
|
||||
pollTargetSlice := make([]*net.UDPAddr, 0, len(pollTargets))
|
||||
for _, addr := range pollTargets {
|
||||
pollTargetSlice = append(pollTargetSlice, addr)
|
||||
}
|
||||
|
||||
// Create ArtNet sender
|
||||
artSender, err := artnet.NewSender(*broadcastAddr)
|
||||
artSender, err := artnet.NewSender()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create artnet sender: %v", err)
|
||||
}
|
||||
@@ -76,7 +95,7 @@ func main() {
|
||||
|
||||
// Create discovery
|
||||
destUniverses := engine.DestUniverses()
|
||||
discovery := artnet.NewDiscovery(artSender, "artmap", "ArtNet Remapping Proxy", destUniverses)
|
||||
discovery := artnet.NewDiscovery(artSender, "artmap", "ArtNet Remapping Proxy", destUniverses, pollTargetSlice)
|
||||
|
||||
// Create app
|
||||
app := &App{
|
||||
@@ -85,6 +104,7 @@ func main() {
|
||||
sacnSender: sacnSender,
|
||||
discovery: discovery,
|
||||
engine: engine,
|
||||
targets: targets,
|
||||
debug: *debug,
|
||||
}
|
||||
|
||||
@@ -112,7 +132,6 @@ func main() {
|
||||
discovery.Start()
|
||||
|
||||
log.Printf("listening for ArtNet on %s", addr)
|
||||
log.Printf("broadcasting to %s", *broadcastAddr)
|
||||
|
||||
// Wait for interrupt
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
@@ -164,13 +183,15 @@ func (a *App) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {
|
||||
log.Printf("failed to send to %s: %v", node.IP, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if target, ok := a.targets[out.Universe]; ok {
|
||||
if a.debug {
|
||||
log.Printf("send ArtNet broadcast: universe=%s", out.Universe)
|
||||
log.Printf("send ArtNet to %s: universe=%s", target.IP, out.Universe)
|
||||
}
|
||||
if err := a.artSender.SendDMXBroadcast(out.Universe, out.Data[:]); err != nil {
|
||||
log.Printf("failed to broadcast: %v", err)
|
||||
if err := a.artSender.SendDMX(target, out.Universe, out.Data[:]); err != nil {
|
||||
log.Printf("failed to send to %s: %v", target.IP, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("no target configured for universe %s", out.Universe)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,13 +249,15 @@ func (a *App) HandleSACN(universe uint16, data [512]byte) {
|
||||
log.Printf("failed to send to %s: %v", node.IP, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if target, ok := a.targets[out.Universe]; ok {
|
||||
if a.debug {
|
||||
log.Printf("send ArtNet broadcast: universe=%s", out.Universe)
|
||||
log.Printf("send ArtNet to %s: universe=%s", target.IP, out.Universe)
|
||||
}
|
||||
if err := a.artSender.SendDMXBroadcast(out.Universe, out.Data[:]); err != nil {
|
||||
log.Printf("failed to broadcast: %v", err)
|
||||
if err := a.artSender.SendDMX(target, out.Universe, out.Data[:]); err != nil {
|
||||
log.Printf("failed to send to %s: %v", target.IP, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("no target configured for universe %s", out.Universe)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,3 +308,33 @@ func parseListenAddr(s string) (*net.UDPAddr, error) {
|
||||
|
||||
return &net.UDPAddr{IP: ip, Port: port}, nil
|
||||
}
|
||||
|
||||
// parseTargetAddr parses target address formats:
|
||||
// - "host:port" -> specific host and port
|
||||
// - "host" -> specific host, default ArtNet port
|
||||
func parseTargetAddr(s string) (*net.UDPAddr, error) {
|
||||
var host string
|
||||
var port int
|
||||
|
||||
if strings.Contains(s, ":") {
|
||||
h, p, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host = h
|
||||
port, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
host = s
|
||||
port = artnet.Port
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %s", host)
|
||||
}
|
||||
|
||||
return &net.UDPAddr{IP: ip, Port: port}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user