Refactor config format and improve consistency
Config changes: - Use protocol prefixes in addresses: "sacn:64:361-450" -> "artnet:0.0.0:1" - Remove separate from_proto/proto fields - Targets now include protocol: universe = "artnet:0.0.0" CLI changes: - Rename --listen to --artnet-listen (empty to disable) - Fix --debug help text Logging changes: - Use [<-proto] and [->proto] prefixes for direction - Consistent lowercase key=value format - Refactor duplicate send code into sendOutputs() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,7 +83,7 @@ func (d *Discovery) pollLoop() {
|
|||||||
func (d *Discovery) sendPolls() {
|
func (d *Discovery) sendPolls() {
|
||||||
for _, target := range d.pollTargets {
|
for _, target := range d.pollTargets {
|
||||||
if err := d.sender.SendPoll(target); err != nil {
|
if err := d.sender.SendPoll(target); err != nil {
|
||||||
log.Printf("failed to send ArtPoll to %s: %v", target.IP, err)
|
log.Printf("[->artnet] poll error: dst=%s err=%v", target.IP, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func (d *Discovery) cleanup() {
|
|||||||
cutoff := time.Now().Add(-60 * time.Second)
|
cutoff := time.Now().Add(-60 * time.Second)
|
||||||
for ip, node := range d.nodes {
|
for ip, node := range d.nodes {
|
||||||
if node.LastSeen.Before(cutoff) {
|
if node.LastSeen.Before(cutoff) {
|
||||||
log.Printf("node %s (%s) timed out", ip, node.ShortName)
|
log.Printf("discovery timeout: ip=%s name=%s", ip, node.ShortName)
|
||||||
delete(d.nodes, ip)
|
delete(d.nodes, ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ func (d *Discovery) HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket) {
|
|||||||
Port: uint16(src.Port),
|
Port: uint16(src.Port),
|
||||||
}
|
}
|
||||||
d.nodes[ip] = node
|
d.nodes[ip] = node
|
||||||
log.Printf("discovered node: %s (%s) - universes: %v", ip, shortName, universes)
|
log.Printf("discovery found: ip=%s name=%s universes=%v", ip, shortName, universes)
|
||||||
}
|
}
|
||||||
|
|
||||||
node.ShortName = shortName
|
node.ShortName = shortName
|
||||||
@@ -168,7 +168,7 @@ func (d *Discovery) HandlePoll(src *net.UDPAddr) {
|
|||||||
// Respond with our info
|
// Respond with our info
|
||||||
err := d.sender.SendPollReply(src, d.localIP, d.shortName, d.longName, d.universes)
|
err := d.sender.SendPollReply(src, d.localIP, d.shortName, d.longName, d.universes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to send ArtPollReply: %v", err)
|
log.Printf("[->artnet] pollreply error: dst=%s err=%v", src.IP, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +1,68 @@
|
|||||||
# artmap configuration
|
# artmap configuration
|
||||||
# Run with: go run . -config config.toml [-listen :6454]
|
# Run with: go run . -config config.toml [-artnet-listen :6454]
|
||||||
|
|
||||||
# Target addresses for ArtNet output universes
|
# Target addresses for ArtNet output universes
|
||||||
# Configure the destination IP (broadcast or unicast) for each output universe
|
# Each output universe needs a target IP (broadcast or unicast)
|
||||||
# ArtPoll discovery will be sent to all unique target addresses
|
# ArtPoll discovery will be sent to all unique target addresses
|
||||||
[[target]]
|
[[target]]
|
||||||
universe = "0.0.0"
|
universe = "artnet:0.0.0"
|
||||||
address = "2.255.255.255"
|
address = "2.255.255.255"
|
||||||
|
|
||||||
[[target]]
|
[[target]]
|
||||||
universe = "0.0.5"
|
universe = "artnet:0.0.5"
|
||||||
address = "10.50.255.255"
|
address = "10.50.255.255"
|
||||||
|
|
||||||
# Address format:
|
# Address format:
|
||||||
# from: universe[:channels] - range specifies which channels to read
|
# proto:universe[:channels]
|
||||||
# to: universe[:channel] - single channel specifies where to start writing
|
#
|
||||||
|
# Protocol prefix (required):
|
||||||
|
# artnet: - ArtNet protocol
|
||||||
|
# sacn: - sACN/E1.31 protocol
|
||||||
#
|
#
|
||||||
# Universe: "net.subnet.universe" or plain number (all 0-indexed, 0-127.0-15.0-15)
|
# Universe: "net.subnet.universe" or plain number (all 0-indexed, 0-127.0-15.0-15)
|
||||||
# Channels: 1-indexed (1-512), matching DMX convention
|
# Channels: 1-indexed (1-512), matching DMX convention
|
||||||
#
|
#
|
||||||
# From examples:
|
# From examples:
|
||||||
# "0.0.1" - universe 1, all channels (1-512)
|
# "artnet:0.0.1" - universe 1, all channels (1-512)
|
||||||
# "0.0.1:50" - universe 1, channel 50 only
|
# "sacn:64:50" - universe 64, channel 50 only
|
||||||
# "0.0.1:50-" - universe 1, channels 50-512
|
# "artnet:0.0.1:50-" - universe 1, channels 50-512
|
||||||
# "0.0.1:50-100" - universe 1, channels 50-100
|
# "sacn:1:50-100" - universe 1, channels 50-100
|
||||||
# 1 - universe 1, all channels
|
|
||||||
# "1:50-100" - universe 1, channels 50-100
|
|
||||||
#
|
#
|
||||||
# To examples:
|
# To examples:
|
||||||
# "0.0.1" - universe 1, starting at channel 1
|
# "artnet:0.0.1" - universe 1, starting at channel 1
|
||||||
# "0.0.1:50" - universe 1, starting at channel 50
|
# "sacn:1:50" - universe 1, starting at channel 50
|
||||||
#
|
|
||||||
# Input protocol (optional, default "artnet"):
|
|
||||||
# from_proto = "artnet" - receive via ArtNet
|
|
||||||
# from_proto = "sacn" - receive via sACN/E1.31 (multicast)
|
|
||||||
#
|
|
||||||
# Output protocol (optional, default "artnet"):
|
|
||||||
# proto = "artnet" - output via ArtNet (broadcast or discovered nodes)
|
|
||||||
# proto = "sacn" - output via sACN/E1.31 (multicast)
|
|
||||||
|
|
||||||
# Remap entire universe
|
# Remap entire universe
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = "0.0.0"
|
from = "artnet:0.0.0"
|
||||||
to = "0.0.5"
|
to = "artnet:0.0.5"
|
||||||
|
|
||||||
# Channel-level remap for fixture spillover
|
# Channel-level remap for fixture spillover
|
||||||
# Channels 450-512 from universe 0 -> channels 1-63 of universe 1
|
# Channels 450-512 from universe 0 -> channels 1-63 of universe 1
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = "0.0.0:450-512"
|
from = "artnet:0.0.0:450-512"
|
||||||
to = "0.0.1:1"
|
to = "artnet:0.0.1:1"
|
||||||
|
|
||||||
# Using plain universe numbers
|
# Using plain universe numbers
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = 2
|
from = "artnet:2"
|
||||||
to = 10
|
to = "artnet:10"
|
||||||
|
|
||||||
# Multiple outputs from same source
|
# Multiple outputs from same source
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = "0.0.3"
|
from = "artnet:0.0.3"
|
||||||
to = "0.0.7"
|
to = "artnet:0.0.7"
|
||||||
|
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = "0.0.3"
|
from = "artnet:0.0.3"
|
||||||
to = "0.0.8"
|
to = "artnet:0.0.8"
|
||||||
|
|
||||||
# Output to sACN instead of ArtNet
|
# Output to sACN instead of ArtNet
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = "0.0.4"
|
from = "artnet:0.0.4"
|
||||||
to = 1
|
to = "sacn:1"
|
||||||
proto = "sacn"
|
|
||||||
|
|
||||||
# Convert sACN input to ArtNet output
|
# Convert sACN input to ArtNet output
|
||||||
[[mapping]]
|
[[mapping]]
|
||||||
from = 5
|
from = "sacn:5"
|
||||||
from_proto = "sacn"
|
to = "artnet:0.0.5"
|
||||||
to = "0.0.5"
|
|
||||||
|
|||||||
125
config/config.go
125
config/config.go
@@ -17,28 +17,36 @@ type Config struct {
|
|||||||
|
|
||||||
// Target represents a target address for an output universe
|
// Target represents a target address for an output universe
|
||||||
type Target struct {
|
type Target struct {
|
||||||
Universe TargetUniverse `toml:"universe"`
|
Universe TargetAddr `toml:"universe"`
|
||||||
Address string `toml:"address"`
|
Address string `toml:"address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TargetUniverse is a universe that can be parsed from string or int
|
// TargetAddr is a protocol-prefixed universe address
|
||||||
type TargetUniverse struct {
|
type TargetAddr struct {
|
||||||
artnet.Universe
|
Protocol Protocol
|
||||||
|
Universe artnet.Universe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TargetUniverse) UnmarshalTOML(data interface{}) error {
|
func (t *TargetAddr) UnmarshalTOML(data interface{}) error {
|
||||||
switch v := data.(type) {
|
switch v := data.(type) {
|
||||||
case string:
|
case string:
|
||||||
u, err := parseUniverse(v)
|
proto, rest, err := splitProtoPrefix(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.Protocol = proto
|
||||||
|
u, err := parseUniverse(rest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Universe = u
|
t.Universe = u
|
||||||
return nil
|
return nil
|
||||||
case int64:
|
case int64:
|
||||||
|
t.Protocol = ProtocolArtNet
|
||||||
t.Universe = artnet.Universe(v)
|
t.Universe = artnet.Universe(v)
|
||||||
return nil
|
return nil
|
||||||
case float64:
|
case float64:
|
||||||
|
t.Protocol = ProtocolArtNet
|
||||||
t.Universe = artnet.Universe(int64(v))
|
t.Universe = artnet.Universe(int64(v))
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
@@ -46,6 +54,10 @@ func (t *TargetUniverse) UnmarshalTOML(data interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t TargetAddr) String() string {
|
||||||
|
return fmt.Sprintf("%s:%s", t.Protocol, t.Universe)
|
||||||
|
}
|
||||||
|
|
||||||
// Protocol specifies the output protocol
|
// Protocol specifies the output protocol
|
||||||
type Protocol string
|
type Protocol string
|
||||||
|
|
||||||
@@ -56,14 +68,13 @@ const (
|
|||||||
|
|
||||||
// Mapping represents a single channel mapping rule
|
// Mapping represents a single channel mapping rule
|
||||||
type Mapping struct {
|
type Mapping struct {
|
||||||
From FromAddr `toml:"from"`
|
From FromAddr `toml:"from"`
|
||||||
FromProto Protocol `toml:"from_proto"`
|
To ToAddr `toml:"to"`
|
||||||
To ToAddr `toml:"to"`
|
|
||||||
Protocol Protocol `toml:"proto"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromAddr represents a source universe address with channel range
|
// FromAddr represents a source universe address with channel range
|
||||||
type FromAddr struct {
|
type FromAddr struct {
|
||||||
|
Protocol Protocol
|
||||||
Universe artnet.Universe
|
Universe artnet.Universe
|
||||||
ChannelStart int // 1-indexed
|
ChannelStart int // 1-indexed
|
||||||
ChannelEnd int // 1-indexed
|
ChannelEnd int // 1-indexed
|
||||||
@@ -74,11 +85,13 @@ func (a *FromAddr) UnmarshalTOML(data interface{}) error {
|
|||||||
case string:
|
case string:
|
||||||
return a.parse(v)
|
return a.parse(v)
|
||||||
case int64:
|
case int64:
|
||||||
|
a.Protocol = ProtocolArtNet
|
||||||
a.Universe = artnet.Universe(v)
|
a.Universe = artnet.Universe(v)
|
||||||
a.ChannelStart = 1
|
a.ChannelStart = 1
|
||||||
a.ChannelEnd = 512
|
a.ChannelEnd = 512
|
||||||
return nil
|
return nil
|
||||||
case float64:
|
case float64:
|
||||||
|
a.Protocol = ProtocolArtNet
|
||||||
a.Universe = artnet.Universe(int64(v))
|
a.Universe = artnet.Universe(int64(v))
|
||||||
a.ChannelStart = 1
|
a.ChannelStart = 1
|
||||||
a.ChannelEnd = 512
|
a.ChannelEnd = 512
|
||||||
@@ -88,15 +101,21 @@ func (a *FromAddr) UnmarshalTOML(data interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse parses address formats:
|
// parse parses address formats with protocol prefix:
|
||||||
// - "0.0.1" - all channels
|
// - "artnet:0.0.1" - all channels
|
||||||
// - "0.0.1:50" - single channel
|
// - "sacn:64:50" - single channel
|
||||||
// - "0.0.1:50-" - channel 50 through end
|
// - "artnet:0.0.1:50-" - channel 50 through end
|
||||||
// - "0.0.1:50-100" - channel range
|
// - "sacn:1:50-100" - channel range
|
||||||
func (a *FromAddr) parse(s string) error {
|
func (a *FromAddr) parse(s string) error {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
universeStr, channelSpec := splitAddr(s)
|
proto, rest, err := splitProtoPrefix(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Protocol = proto
|
||||||
|
|
||||||
|
universeStr, channelSpec := splitAddr(rest)
|
||||||
|
|
||||||
universe, err := parseUniverse(universeStr)
|
universe, err := parseUniverse(universeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -141,12 +160,23 @@ func (a *FromAddr) parse(s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a FromAddr) String() string {
|
||||||
|
if a.ChannelStart == 1 && a.ChannelEnd == 512 {
|
||||||
|
return fmt.Sprintf("%s:%s", a.Protocol, a.Universe)
|
||||||
|
}
|
||||||
|
if a.ChannelStart == a.ChannelEnd {
|
||||||
|
return fmt.Sprintf("%s:%s:%d", a.Protocol, a.Universe, a.ChannelStart)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%s:%d-%d", a.Protocol, a.Universe, a.ChannelStart, a.ChannelEnd)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *FromAddr) Count() int {
|
func (a *FromAddr) Count() int {
|
||||||
return a.ChannelEnd - a.ChannelStart + 1
|
return a.ChannelEnd - a.ChannelStart + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToAddr represents a destination universe address with starting channel
|
// ToAddr represents a destination universe address with starting channel
|
||||||
type ToAddr struct {
|
type ToAddr struct {
|
||||||
|
Protocol Protocol
|
||||||
Universe artnet.Universe
|
Universe artnet.Universe
|
||||||
ChannelStart int // 1-indexed
|
ChannelStart int // 1-indexed
|
||||||
}
|
}
|
||||||
@@ -156,10 +186,12 @@ func (a *ToAddr) UnmarshalTOML(data interface{}) error {
|
|||||||
case string:
|
case string:
|
||||||
return a.parse(v)
|
return a.parse(v)
|
||||||
case int64:
|
case int64:
|
||||||
|
a.Protocol = ProtocolArtNet
|
||||||
a.Universe = artnet.Universe(v)
|
a.Universe = artnet.Universe(v)
|
||||||
a.ChannelStart = 1
|
a.ChannelStart = 1
|
||||||
return nil
|
return nil
|
||||||
case float64:
|
case float64:
|
||||||
|
a.Protocol = ProtocolArtNet
|
||||||
a.Universe = artnet.Universe(int64(v))
|
a.Universe = artnet.Universe(int64(v))
|
||||||
a.ChannelStart = 1
|
a.ChannelStart = 1
|
||||||
return nil
|
return nil
|
||||||
@@ -168,13 +200,19 @@ func (a *ToAddr) UnmarshalTOML(data interface{}) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse parses address formats:
|
// parse parses address formats with protocol prefix:
|
||||||
// - "0.0.1" - starting at channel 1
|
// - "artnet:0.0.1" - starting at channel 1
|
||||||
// - "0.0.1:50" - starting at channel 50
|
// - "sacn:1:50" - starting at channel 50
|
||||||
func (a *ToAddr) parse(s string) error {
|
func (a *ToAddr) parse(s string) error {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
universeStr, channelSpec := splitAddr(s)
|
proto, rest, err := splitProtoPrefix(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.Protocol = proto
|
||||||
|
|
||||||
|
universeStr, channelSpec := splitAddr(rest)
|
||||||
|
|
||||||
universe, err := parseUniverse(universeStr)
|
universe, err := parseUniverse(universeStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -200,6 +238,23 @@ func (a *ToAddr) parse(s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a ToAddr) String() string {
|
||||||
|
if a.ChannelStart == 1 {
|
||||||
|
return fmt.Sprintf("%s:%s", a.Protocol, a.Universe)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%s:%d", a.Protocol, a.Universe, a.ChannelStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitProtoPrefix(s string) (Protocol, string, error) {
|
||||||
|
if strings.HasPrefix(s, "artnet:") {
|
||||||
|
return ProtocolArtNet, s[7:], nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(s, "sacn:") {
|
||||||
|
return ProtocolSACN, s[5:], nil
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("address must start with 'artnet:' or 'sacn:' prefix")
|
||||||
|
}
|
||||||
|
|
||||||
func splitAddr(s string) (universe, channel string) {
|
func splitAddr(s string) (universe, channel string) {
|
||||||
if idx := strings.LastIndex(s, ":"); idx != -1 {
|
if idx := strings.LastIndex(s, ":"); idx != -1 {
|
||||||
return s[:idx], s[idx+1:]
|
return s[:idx], s[idx+1:]
|
||||||
@@ -250,8 +305,7 @@ func Load(path string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range cfg.Mappings {
|
for i, m := range cfg.Mappings {
|
||||||
m := &cfg.Mappings[i]
|
|
||||||
if m.From.ChannelStart < 1 || m.From.ChannelStart > 512 {
|
if m.From.ChannelStart < 1 || m.From.ChannelStart > 512 {
|
||||||
return nil, fmt.Errorf("mapping %d: from channel start must be 1-512", i)
|
return nil, fmt.Errorf("mapping %d: from channel start must be 1-512", i)
|
||||||
}
|
}
|
||||||
@@ -268,16 +322,6 @@ func Load(path string) (*Config, error) {
|
|||||||
if toEnd > 512 {
|
if toEnd > 512 {
|
||||||
return nil, fmt.Errorf("mapping %d: to channels exceed 512", i)
|
return nil, fmt.Errorf("mapping %d: to channels exceed 512", i)
|
||||||
}
|
}
|
||||||
if m.FromProto == "" {
|
|
||||||
m.FromProto = ProtocolArtNet
|
|
||||||
} else if m.FromProto != ProtocolArtNet && m.FromProto != ProtocolSACN {
|
|
||||||
return nil, fmt.Errorf("mapping %d: from_proto must be 'artnet' or 'sacn'", i)
|
|
||||||
}
|
|
||||||
if m.Protocol == "" {
|
|
||||||
m.Protocol = ProtocolArtNet
|
|
||||||
} else if m.Protocol != ProtocolArtNet && m.Protocol != ProtocolSACN {
|
|
||||||
return nil, fmt.Errorf("mapping %d: proto must be 'artnet' or 'sacn'", i)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
@@ -301,11 +345,11 @@ func (c *Config) Normalize() []NormalizedMapping {
|
|||||||
result[i] = NormalizedMapping{
|
result[i] = NormalizedMapping{
|
||||||
FromUniverse: m.From.Universe,
|
FromUniverse: m.From.Universe,
|
||||||
FromChannel: m.From.ChannelStart - 1,
|
FromChannel: m.From.ChannelStart - 1,
|
||||||
FromProto: m.FromProto,
|
FromProto: m.From.Protocol,
|
||||||
ToUniverse: m.To.Universe,
|
ToUniverse: m.To.Universe,
|
||||||
ToChannel: m.To.ChannelStart - 1,
|
ToChannel: m.To.ChannelStart - 1,
|
||||||
Count: m.From.Count(),
|
Count: m.From.Count(),
|
||||||
Protocol: m.Protocol,
|
Protocol: m.To.Protocol,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -315,7 +359,7 @@ func (c *Config) Normalize() []NormalizedMapping {
|
|||||||
func (c *Config) SACNSourceUniverses() []uint16 {
|
func (c *Config) SACNSourceUniverses() []uint16 {
|
||||||
seen := make(map[uint16]bool)
|
seen := make(map[uint16]bool)
|
||||||
for _, m := range c.Mappings {
|
for _, m := range c.Mappings {
|
||||||
if m.FromProto == ProtocolSACN {
|
if m.From.Protocol == ProtocolSACN {
|
||||||
seen[uint16(m.From.Universe)] = true
|
seen[uint16(m.From.Universe)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,12 +369,3 @@ func (c *Config) SACNSourceUniverses() []uint16 {
|
|||||||
}
|
}
|
||||||
return result
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
134
main.go
134
main.go
@@ -31,46 +31,40 @@ 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")
|
||||||
listenAddr := flag.String("listen", ":6454", "listen address (host:port, host, or :port)")
|
artnetListen := flag.String("artnet-listen", ":6454", "artnet listen address (empty to disable)")
|
||||||
debug := flag.Bool("debug", false, "log ArtNet packets")
|
debug := flag.Bool("debug", false, "log incoming/outgoing dmx packets")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Parse listen address
|
|
||||||
addr, err := parseListenAddr(*listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("invalid listen address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
cfg, err := config.Load(*configPath)
|
cfg, err := config.Load(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to load config: %v", err)
|
log.Fatalf("config error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("loaded %d mappings", len(cfg.Mappings))
|
log.Printf("loaded mappings=%d", len(cfg.Mappings))
|
||||||
|
|
||||||
// Create remapping engine
|
// Create remapping engine
|
||||||
engine := remap.NewEngine(cfg.Normalize())
|
engine := remap.NewEngine(cfg.Normalize())
|
||||||
|
|
||||||
// Log mappings
|
// Log mappings
|
||||||
for _, m := range cfg.Mappings {
|
for _, m := range cfg.Mappings {
|
||||||
toEnd := m.To.ChannelStart + m.From.Count() - 1
|
log.Printf(" %s -> %s", m.From, m.To)
|
||||||
log.Printf(" [%s] %s:%d-%d -> [%s] %s:%d-%d",
|
|
||||||
m.FromProto, m.From.Universe, m.From.ChannelStart, m.From.ChannelEnd,
|
|
||||||
m.Protocol, m.To.Universe, m.To.ChannelStart, toEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse targets
|
// Parse targets
|
||||||
targets := make(map[artnet.Universe]*net.UDPAddr)
|
targets := make(map[artnet.Universe]*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 {
|
||||||
|
continue // only artnet targets need addresses
|
||||||
|
}
|
||||||
addr, err := parseTargetAddr(t.Address)
|
addr, err := parseTargetAddr(t.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("invalid target address %q: %v", t.Address, err)
|
log.Fatalf("target error: address=%q err=%v", t.Address, err)
|
||||||
}
|
}
|
||||||
targets[t.Universe.Universe] = addr
|
targets[t.Universe.Universe] = addr
|
||||||
pollTargets[addr.String()] = addr
|
pollTargets[addr.String()] = addr
|
||||||
log.Printf(" target %s -> %s", t.Universe.Universe, addr)
|
log.Printf(" target %s -> %s", t.Universe, addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert poll targets to slice
|
// Convert poll targets to slice
|
||||||
@@ -82,14 +76,14 @@ func main() {
|
|||||||
// Create ArtNet sender
|
// Create ArtNet sender
|
||||||
artSender, err := artnet.NewSender()
|
artSender, err := artnet.NewSender()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create artnet sender: %v", err)
|
log.Fatalf("artnet sender error: %v", err)
|
||||||
}
|
}
|
||||||
defer artSender.Close()
|
defer artSender.Close()
|
||||||
|
|
||||||
// Create sACN sender
|
// Create sACN sender
|
||||||
sacnSender, err := sacn.NewSender("artmap")
|
sacnSender, err := sacn.NewSender("artmap")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create sacn sender: %v", err)
|
log.Fatalf("sacn sender error: %v", err)
|
||||||
}
|
}
|
||||||
defer sacnSender.Close()
|
defer sacnSender.Close()
|
||||||
|
|
||||||
@@ -108,38 +102,45 @@ func main() {
|
|||||||
debug: *debug,
|
debug: *debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ArtNet receiver
|
// Create ArtNet receiver if enabled
|
||||||
artReceiver, err := artnet.NewReceiver(addr, app)
|
if *artnetListen != "" {
|
||||||
if err != nil {
|
addr, err := parseListenAddr(*artnetListen)
|
||||||
log.Fatalf("failed to create artnet receiver: %v", err)
|
if err != nil {
|
||||||
|
log.Fatalf("artnet listen error: %v", err)
|
||||||
|
}
|
||||||
|
artReceiver, err := artnet.NewReceiver(addr, app)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("artnet receiver error: %v", err)
|
||||||
|
}
|
||||||
|
app.artReceiver = artReceiver
|
||||||
|
artReceiver.Start()
|
||||||
|
log.Printf("artnet listening addr=%s", addr)
|
||||||
}
|
}
|
||||||
app.artReceiver = artReceiver
|
|
||||||
|
|
||||||
// Create sACN receiver if needed
|
// Create sACN receiver if needed
|
||||||
sacnUniverses := cfg.SACNSourceUniverses()
|
sacnUniverses := cfg.SACNSourceUniverses()
|
||||||
if len(sacnUniverses) > 0 {
|
if len(sacnUniverses) > 0 {
|
||||||
sacnReceiver, err := sacn.NewReceiver(sacnUniverses, app.HandleSACN)
|
sacnReceiver, err := sacn.NewReceiver(sacnUniverses, app.HandleSACN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to create sacn receiver: %v", err)
|
log.Fatalf("sacn receiver error: %v", err)
|
||||||
}
|
}
|
||||||
app.sacnReceiver = sacnReceiver
|
app.sacnReceiver = sacnReceiver
|
||||||
sacnReceiver.Start()
|
sacnReceiver.Start()
|
||||||
log.Printf("listening for sACN on universes %v", sacnUniverses)
|
log.Printf("sacn listening universes=%v", sacnUniverses)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start everything
|
// Start discovery
|
||||||
artReceiver.Start()
|
|
||||||
discovery.Start()
|
discovery.Start()
|
||||||
|
|
||||||
log.Printf("listening for ArtNet on %s", addr)
|
|
||||||
|
|
||||||
// Wait for interrupt
|
// Wait for interrupt
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigChan
|
<-sigChan
|
||||||
|
|
||||||
log.Println("shutting down...")
|
log.Println("shutting down")
|
||||||
artReceiver.Stop()
|
if app.artReceiver != nil {
|
||||||
|
app.artReceiver.Stop()
|
||||||
|
}
|
||||||
if app.sacnReceiver != nil {
|
if app.sacnReceiver != nil {
|
||||||
app.sacnReceiver.Stop()
|
app.sacnReceiver.Stop()
|
||||||
}
|
}
|
||||||
@@ -149,58 +150,17 @@ func main() {
|
|||||||
// HandleDMX implements artnet.PacketHandler
|
// HandleDMX implements artnet.PacketHandler
|
||||||
func (a *App) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {
|
func (a *App) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("recv ArtNet from %s: universe=%s seq=%d len=%d",
|
log.Printf("[<-artnet] src=%s universe=%s seq=%d len=%d",
|
||||||
src.IP, pkt.Universe, pkt.Sequence, pkt.Length)
|
src.IP, pkt.Universe, pkt.Sequence, pkt.Length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply remapping
|
a.sendOutputs(a.engine.Remap(config.ProtocolArtNet, pkt.Universe, pkt.Data))
|
||||||
outputs := a.engine.Remap(config.ProtocolArtNet, pkt.Universe, pkt.Data)
|
|
||||||
|
|
||||||
// Send remapped outputs
|
|
||||||
for _, out := range outputs {
|
|
||||||
switch out.Protocol {
|
|
||||||
case config.ProtocolSACN:
|
|
||||||
if a.debug {
|
|
||||||
log.Printf("send sACN multicast: universe=%d", uint16(out.Universe))
|
|
||||||
}
|
|
||||||
if err := a.sacnSender.SendDMX(uint16(out.Universe), out.Data[:]); err != nil {
|
|
||||||
log.Printf("failed to send sACN: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default: // ArtNet
|
|
||||||
nodes := a.discovery.GetNodesForUniverse(out.Universe)
|
|
||||||
|
|
||||||
if len(nodes) > 0 {
|
|
||||||
for _, node := range nodes {
|
|
||||||
addr := &net.UDPAddr{
|
|
||||||
IP: node.IP,
|
|
||||||
Port: int(node.Port),
|
|
||||||
}
|
|
||||||
if a.debug {
|
|
||||||
log.Printf("send ArtNet to %s: universe=%s", node.IP, out.Universe)
|
|
||||||
}
|
|
||||||
if err := a.artSender.SendDMX(addr, out.Universe, out.Data[:]); err != nil {
|
|
||||||
log.Printf("failed to send to %s: %v", node.IP, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if target, ok := a.targets[out.Universe]; ok {
|
|
||||||
if a.debug {
|
|
||||||
log.Printf("send ArtNet to %s: universe=%s", target.IP, out.Universe)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlePoll implements artnet.PacketHandler
|
// HandlePoll implements artnet.PacketHandler
|
||||||
func (a *App) HandlePoll(src *net.UDPAddr, pkt *artnet.PollPacket) {
|
func (a *App) HandlePoll(src *net.UDPAddr, pkt *artnet.PollPacket) {
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("recv Poll from %s", src.IP)
|
log.Printf("[<-artnet] poll src=%s", src.IP)
|
||||||
}
|
}
|
||||||
a.discovery.HandlePoll(src)
|
a.discovery.HandlePoll(src)
|
||||||
}
|
}
|
||||||
@@ -208,7 +168,7 @@ func (a *App) HandlePoll(src *net.UDPAddr, pkt *artnet.PollPacket) {
|
|||||||
// HandlePollReply implements artnet.PacketHandler
|
// HandlePollReply implements artnet.PacketHandler
|
||||||
func (a *App) HandlePollReply(src *net.UDPAddr, pkt *artnet.PollReplyPacket) {
|
func (a *App) HandlePollReply(src *net.UDPAddr, pkt *artnet.PollReplyPacket) {
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("recv PollReply from %s", src.IP)
|
log.Printf("[<-artnet] pollreply src=%s", src.IP)
|
||||||
}
|
}
|
||||||
a.discovery.HandlePollReply(src, pkt)
|
a.discovery.HandlePollReply(src, pkt)
|
||||||
}
|
}
|
||||||
@@ -216,21 +176,21 @@ func (a *App) HandlePollReply(src *net.UDPAddr, pkt *artnet.PollReplyPacket) {
|
|||||||
// HandleSACN handles incoming sACN DMX data
|
// HandleSACN handles incoming sACN DMX data
|
||||||
func (a *App) HandleSACN(universe uint16, data [512]byte) {
|
func (a *App) HandleSACN(universe uint16, data [512]byte) {
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("recv sACN: universe=%d", universe)
|
log.Printf("[<-sacn] universe=%d", universe)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply remapping
|
a.sendOutputs(a.engine.Remap(config.ProtocolSACN, artnet.Universe(universe), data))
|
||||||
outputs := a.engine.Remap(config.ProtocolSACN, artnet.Universe(universe), data)
|
}
|
||||||
|
|
||||||
// Send remapped outputs
|
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:
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("send sACN multicast: universe=%d", uint16(out.Universe))
|
log.Printf("[->sacn] universe=%d", uint16(out.Universe))
|
||||||
}
|
}
|
||||||
if err := a.sacnSender.SendDMX(uint16(out.Universe), out.Data[:]); err != nil {
|
if err := a.sacnSender.SendDMX(uint16(out.Universe), out.Data[:]); err != nil {
|
||||||
log.Printf("failed to send sACN: %v", err)
|
log.Printf("[->sacn] error: universe=%d err=%v", uint16(out.Universe), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
default: // ArtNet
|
default: // ArtNet
|
||||||
@@ -243,21 +203,21 @@ func (a *App) HandleSACN(universe uint16, data [512]byte) {
|
|||||||
Port: int(node.Port),
|
Port: int(node.Port),
|
||||||
}
|
}
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("send ArtNet to %s: universe=%s", node.IP, out.Universe)
|
log.Printf("[->artnet] dst=%s universe=%s", node.IP, out.Universe)
|
||||||
}
|
}
|
||||||
if err := a.artSender.SendDMX(addr, out.Universe, out.Data[:]); err != nil {
|
if err := a.artSender.SendDMX(addr, out.Universe, out.Data[:]); err != nil {
|
||||||
log.Printf("failed to send to %s: %v", node.IP, err)
|
log.Printf("[->artnet] error: dst=%s err=%v", node.IP, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if target, ok := a.targets[out.Universe]; ok {
|
} else if target, ok := a.targets[out.Universe]; ok {
|
||||||
if a.debug {
|
if a.debug {
|
||||||
log.Printf("send ArtNet to %s: universe=%s", target.IP, out.Universe)
|
log.Printf("[->artnet] dst=%s universe=%s", target.IP, out.Universe)
|
||||||
}
|
}
|
||||||
if err := a.artSender.SendDMX(target, out.Universe, out.Data[:]); err != nil {
|
if err := a.artSender.SendDMX(target, out.Universe, out.Data[:]); err != nil {
|
||||||
log.Printf("failed to send to %s: %v", target.IP, err)
|
log.Printf("[->artnet] error: dst=%s err=%v", target.IP, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("no target configured for universe %s", out.Universe)
|
log.Printf("[->artnet] no target: universe=%s", out.Universe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user