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:
Ian Gulliver
2025-12-22 18:22:09 -08:00
parent 86403f1ff8
commit e823d9838d
4 changed files with 160 additions and 174 deletions

View File

@@ -17,28 +17,36 @@ type Config struct {
// Target represents a target address for an output universe
type Target struct {
Universe TargetUniverse `toml:"universe"`
Address string `toml:"address"`
Universe TargetAddr `toml:"universe"`
Address string `toml:"address"`
}
// TargetUniverse is a universe that can be parsed from string or int
type TargetUniverse struct {
artnet.Universe
// TargetAddr is a protocol-prefixed universe address
type TargetAddr struct {
Protocol Protocol
Universe artnet.Universe
}
func (t *TargetUniverse) UnmarshalTOML(data interface{}) error {
func (t *TargetAddr) UnmarshalTOML(data interface{}) error {
switch v := data.(type) {
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 {
return err
}
t.Universe = u
return nil
case int64:
t.Protocol = ProtocolArtNet
t.Universe = artnet.Universe(v)
return nil
case float64:
t.Protocol = ProtocolArtNet
t.Universe = artnet.Universe(int64(v))
return nil
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
type Protocol string
@@ -56,14 +68,13 @@ const (
// Mapping represents a single channel mapping rule
type Mapping struct {
From FromAddr `toml:"from"`
FromProto Protocol `toml:"from_proto"`
To ToAddr `toml:"to"`
Protocol Protocol `toml:"proto"`
From FromAddr `toml:"from"`
To ToAddr `toml:"to"`
}
// FromAddr represents a source universe address with channel range
type FromAddr struct {
Protocol Protocol
Universe artnet.Universe
ChannelStart int // 1-indexed
ChannelEnd int // 1-indexed
@@ -74,11 +85,13 @@ func (a *FromAddr) UnmarshalTOML(data interface{}) error {
case string:
return a.parse(v)
case int64:
a.Protocol = ProtocolArtNet
a.Universe = artnet.Universe(v)
a.ChannelStart = 1
a.ChannelEnd = 512
return nil
case float64:
a.Protocol = ProtocolArtNet
a.Universe = artnet.Universe(int64(v))
a.ChannelStart = 1
a.ChannelEnd = 512
@@ -88,15 +101,21 @@ func (a *FromAddr) UnmarshalTOML(data interface{}) error {
}
}
// parse parses address formats:
// - "0.0.1" - all channels
// - "0.0.1:50" - single channel
// - "0.0.1:50-" - channel 50 through end
// - "0.0.1:50-100" - channel range
// parse parses address formats with protocol prefix:
// - "artnet:0.0.1" - all channels
// - "sacn:64:50" - single channel
// - "artnet:0.0.1:50-" - channel 50 through end
// - "sacn:1:50-100" - channel range
func (a *FromAddr) parse(s string) error {
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)
if err != nil {
@@ -141,12 +160,23 @@ func (a *FromAddr) parse(s string) error {
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 {
return a.ChannelEnd - a.ChannelStart + 1
}
// ToAddr represents a destination universe address with starting channel
type ToAddr struct {
Protocol Protocol
Universe artnet.Universe
ChannelStart int // 1-indexed
}
@@ -156,10 +186,12 @@ func (a *ToAddr) UnmarshalTOML(data interface{}) error {
case string:
return a.parse(v)
case int64:
a.Protocol = ProtocolArtNet
a.Universe = artnet.Universe(v)
a.ChannelStart = 1
return nil
case float64:
a.Protocol = ProtocolArtNet
a.Universe = artnet.Universe(int64(v))
a.ChannelStart = 1
return nil
@@ -168,13 +200,19 @@ func (a *ToAddr) UnmarshalTOML(data interface{}) error {
}
}
// parse parses address formats:
// - "0.0.1" - starting at channel 1
// - "0.0.1:50" - starting at channel 50
// parse parses address formats with protocol prefix:
// - "artnet:0.0.1" - starting at channel 1
// - "sacn:1:50" - starting at channel 50
func (a *ToAddr) parse(s string) error {
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)
if err != nil {
@@ -200,6 +238,23 @@ func (a *ToAddr) parse(s string) error {
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) {
if idx := strings.LastIndex(s, ":"); idx != -1 {
return s[:idx], s[idx+1:]
@@ -250,8 +305,7 @@ func Load(path string) (*Config, error) {
}
}
for i := range cfg.Mappings {
m := &cfg.Mappings[i]
for i, m := range cfg.Mappings {
if m.From.ChannelStart < 1 || m.From.ChannelStart > 512 {
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 {
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
@@ -301,11 +345,11 @@ func (c *Config) Normalize() []NormalizedMapping {
result[i] = NormalizedMapping{
FromUniverse: m.From.Universe,
FromChannel: m.From.ChannelStart - 1,
FromProto: m.FromProto,
FromProto: m.From.Protocol,
ToUniverse: m.To.Universe,
ToChannel: m.To.ChannelStart - 1,
Count: m.From.Count(),
Protocol: m.Protocol,
Protocol: m.To.Protocol,
}
}
return result
@@ -315,7 +359,7 @@ func (c *Config) Normalize() []NormalizedMapping {
func (c *Config) SACNSourceUniverses() []uint16 {
seen := make(map[uint16]bool)
for _, m := range c.Mappings {
if m.FromProto == ProtocolSACN {
if m.From.Protocol == ProtocolSACN {
seen[uint16(m.From.Universe)] = true
}
}
@@ -325,12 +369,3 @@ 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
}