Add sACN unicast target support

This commit is contained in:
Ian Gulliver
2026-01-27 14:45:45 -08:00
parent e45121c277
commit 9a673bcd23
2 changed files with 53 additions and 30 deletions

View File

@@ -5,9 +5,9 @@
# --artnet-listen=:6454 ArtNet listen address (empty to disable) # --artnet-listen=:6454 ArtNet listen address (empty to disable)
# --artnet-broadcast=auto Broadcast addresses (comma-separated, or 'auto') # --artnet-broadcast=auto Broadcast addresses (comma-separated, or 'auto')
# Target addresses for ArtNet output universes # Target addresses for output universes
# Each output universe needs a target IP (broadcast or unicast) # ArtNet: target IP (broadcast or unicast), ArtPoll discovery sent to all
# ArtPoll discovery will be sent to all unique target addresses # sACN: unicast targets sent in addition to multicast
[[target]] [[target]]
universe = "artnet:0.0.0" universe = "artnet:0.0.0"
address = "2.255.255.255" address = "2.255.255.255"
@@ -16,6 +16,10 @@ address = "2.255.255.255"
universe = "artnet:0.0.5" universe = "artnet:0.0.5"
address = "10.50.255.255" address = "10.50.255.255"
[[target]]
universe = "sacn:1"
address = "192.168.1.100"
# Address format: # Address format:
# proto:universe[:channels] # proto:universe[:channels]
# #

73
main.go
View File

@@ -25,7 +25,8 @@ type App struct {
sacnSender *sacn.Sender sacnSender *sacn.Sender
discovery *artnet.Discovery discovery *artnet.Discovery
engine *remap.Engine engine *remap.Engine
targets map[artnet.Universe]*net.UDPAddr artTargets map[artnet.Universe]*net.UDPAddr
sacnTargets map[uint16][]*net.UDPAddr
debug bool debug bool
} }
@@ -53,19 +54,28 @@ func main() {
} }
// Parse targets // Parse targets
targets := make(map[artnet.Universe]*net.UDPAddr) artTargets := make(map[artnet.Universe]*net.UDPAddr)
sacnTargets := make(map[uint16][]*net.UDPAddr)
pollTargets := make(map[string]*net.UDPAddr) // dedupe by address string pollTargets := make(map[string]*net.UDPAddr) // dedupe by address string
for _, t := range cfg.Targets { for _, t := range cfg.Targets {
if t.Universe.Protocol != config.ProtocolArtNet { switch t.Universe.Protocol {
continue // only artnet targets need addresses case config.ProtocolArtNet:
addr, err := parseTargetAddr(t.Address, artnet.Port)
if err != nil {
log.Fatalf("target error: address=%q err=%v", t.Address, err)
}
artTargets[t.Universe.Universe] = addr
pollTargets[addr.String()] = addr
log.Printf("[config] target %s -> %s", t.Universe, addr)
case config.ProtocolSACN:
addr, err := parseTargetAddr(t.Address, sacn.Port)
if err != nil {
log.Fatalf("target error: address=%q err=%v", t.Address, err)
}
u := uint16(t.Universe.Universe)
sacnTargets[u] = append(sacnTargets[u], addr)
log.Printf("[config] target %s -> %s", t.Universe, addr)
} }
addr, err := parseTargetAddr(t.Address)
if err != nil {
log.Fatalf("target error: address=%q err=%v", t.Address, err)
}
targets[t.Universe.Universe] = addr
pollTargets[addr.String()] = addr
log.Printf("[config] target %s -> %s", t.Universe, addr)
} }
// Parse broadcast addresses // Parse broadcast addresses
@@ -76,7 +86,7 @@ func main() {
} else { } else {
for _, addrStr := range strings.Split(*artnetBroadcast, ",") { for _, addrStr := range strings.Split(*artnetBroadcast, ",") {
addrStr = strings.TrimSpace(addrStr) addrStr = strings.TrimSpace(addrStr)
addr, err := parseTargetAddr(addrStr) addr, err := parseTargetAddr(addrStr, artnet.Port)
if err != nil { if err != nil {
log.Fatalf("broadcast error: address=%q err=%v", addrStr, err) log.Fatalf("broadcast error: address=%q err=%v", addrStr, err)
} }
@@ -115,13 +125,14 @@ func main() {
// Create app // Create app
app := &App{ app := &App{
cfg: cfg, cfg: cfg,
artSender: artSender, artSender: artSender,
sacnSender: sacnSender, sacnSender: sacnSender,
discovery: discovery, discovery: discovery,
engine: engine, engine: engine,
targets: targets, artTargets: artTargets,
debug: *debug, sacnTargets: sacnTargets,
debug: *debug,
} }
// Create ArtNet receiver if enabled // Create ArtNet receiver if enabled
@@ -208,16 +219,24 @@ func (a *App) sendOutputs(outputs []remap.Output) {
for _, out := range outputs { for _, out := range outputs {
switch out.Protocol { switch out.Protocol {
case config.ProtocolSACN: case config.ProtocolSACN:
u := uint16(out.Universe)
if a.debug { if a.debug {
log.Printf("[->sacn] universe=%d", uint16(out.Universe)) log.Printf("[->sacn] universe=%d", u)
} }
if err := a.sacnSender.SendDMX(uint16(out.Universe), out.Data[:]); err != nil { if err := a.sacnSender.SendDMX(u, out.Data[:]); err != nil {
log.Printf("[->sacn] error: universe=%d err=%v", uint16(out.Universe), err) log.Printf("[->sacn] error: universe=%d err=%v", u, err)
}
for _, target := range a.sacnTargets[u] {
if a.debug {
log.Printf("[->sacn] unicast dst=%s universe=%d", target.IP, u)
}
if err := a.sacnSender.SendDMXUnicast(target, u, out.Data[:]); err != nil {
log.Printf("[->sacn] error: dst=%s err=%v", target.IP, err)
}
} }
default: // ArtNet default: // ArtNet
// Configured target takes priority over discovery if target, ok := a.artTargets[out.Universe]; ok {
if target, ok := a.targets[out.Universe]; ok {
if a.debug { if a.debug {
log.Printf("[->artnet] dst=%s universe=%s", target.IP, out.Universe) log.Printf("[->artnet] dst=%s universe=%s", target.IP, out.Universe)
} }
@@ -288,8 +307,8 @@ func parseListenAddr(s string) (*net.UDPAddr, error) {
// parseTargetAddr parses target address formats: // parseTargetAddr parses target address formats:
// - "host:port" -> specific host and port // - "host:port" -> specific host and port
// - "host" -> specific host, default ArtNet port // - "host" -> specific host, default port
func parseTargetAddr(s string) (*net.UDPAddr, error) { func parseTargetAddr(s string, defaultPort int) (*net.UDPAddr, error) {
var host string var host string
var port int var port int
@@ -305,7 +324,7 @@ func parseTargetAddr(s string) (*net.UDPAddr, error) {
} }
} else { } else {
host = s host = s
port = artnet.Port port = defaultPort
} }
ip := net.ParseIP(host) ip := net.ParseIP(host)