Simplify config: encode channels in address strings

- from: "0.0.1:50-100" specifies channel range
- to: "0.0.1:50" specifies starting channel only (range implied by from)
- Remove from_channel, to_channel, count fields
- Support plain universe numbers with channels: "1:50-100"

🤖 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 09:37:09 -08:00
parent 7743836d53
commit d88ef4ccee
3 changed files with 169 additions and 90 deletions

View File

@@ -1,33 +1,37 @@
# artmap configuration # artmap configuration
# Run with: go run . -config config.toml [-port 6454] [-broadcast 2.255.255.255] # Run with: go run . -config config.toml [-port 6454] [-broadcast 2.255.255.255]
# Universe address formats: # Address format:
# "0.0.1" - Net.Subnet.Universe # from: universe[:channels] - range specifies which channels to read
# 1 - Universe number only (net=0, subnet=0) # to: universe[:channel] - single channel specifies where to start writing
#
# From examples:
# "0.0.1" - all channels (1-512)
# "0.0.1:50" - channel 50 only
# "0.0.1:50-" - channels 50-512
# "0.0.1:50-100" - channels 50-100
#
# To examples:
# "0.0.1" - starting at channel 1
# "0.0.1:50" - starting at channel 50
# Example: Remap entire universe # Remap entire universe
# Maps all 512 channels from universe 0 to universe 5
[[mapping]] [[mapping]]
from = "0.0.0" from = "0.0.0"
to = "0.0.5" to = "0.0.5"
# Example: Channel-level remap for fixture spillover # Channel-level remap for fixture spillover
# Maps channels 450-512 from universe 0 to channels 1-63 of universe 1 # Channels 450-512 from universe 0 -> channels 1-63 of universe 1
# Use case: A fixture at the end of universe 0 spills into universe 1
[[mapping]] [[mapping]]
from = "0.0.0" from = "0.0.0:450-512"
from_channel = 450 # 1-512 (1-indexed, like DMX) to = "0.0.1:1"
to = "0.0.1"
to_channel = 1
count = 63 # Number of channels to remap
# Example: Using plain universe numbers # Using plain universe numbers
[[mapping]] [[mapping]]
from = 2 from = 2
to = 10 to = 10
# Example: Multiple outputs from same source # Multiple outputs from same source
# The same source channels can be mapped to multiple destinations
[[mapping]] [[mapping]]
from = "0.0.3" from = "0.0.3"
to = "0.0.7" to = "0.0.7"

View File

@@ -16,66 +16,160 @@ type Config struct {
// Mapping represents a single channel mapping rule // Mapping represents a single channel mapping rule
type Mapping struct { type Mapping struct {
// Source From FromAddr `toml:"from"`
From UniverseAddr `toml:"from"` To ToAddr `toml:"to"`
FromChannel int `toml:"from_channel"` // 1-512, 0 means all channels
// Destination
To UniverseAddr `toml:"to"`
ToChannel int `toml:"to_channel"` // 1-512, 0 means same as from_channel
// Range
Count int `toml:"count"` // Number of channels, 0 means all remaining
} }
// UniverseAddr handles multiple universe address formats // FromAddr represents a source universe address with channel range
type UniverseAddr struct { type FromAddr struct {
Universe artnet.Universe Universe artnet.Universe
ChannelStart int // 1-indexed
ChannelEnd int // 1-indexed
} }
func (u *UniverseAddr) UnmarshalText(text []byte) error { func (a *FromAddr) UnmarshalTOML(data interface{}) error {
s := string(text) switch v := data.(type) {
universe, err := ParseUniverseAddr(s) case string:
return a.parse(v)
case int64:
a.Universe = artnet.Universe(v)
a.ChannelStart = 1
a.ChannelEnd = 512
return nil
case float64:
a.Universe = artnet.Universe(int64(v))
a.ChannelStart = 1
a.ChannelEnd = 512
return nil
default:
return fmt.Errorf("unsupported address type: %T", data)
}
}
// 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
func (a *FromAddr) parse(s string) error {
s = strings.TrimSpace(s)
universeStr, channelSpec := splitAddr(s)
universe, err := parseUniverse(universeStr)
if err != nil { if err != nil {
return err return err
} }
u.Universe = universe a.Universe = universe
if channelSpec == "" {
a.ChannelStart = 1
a.ChannelEnd = 512
return nil
}
if idx := strings.Index(channelSpec, "-"); idx != -1 {
startStr := channelSpec[:idx]
endStr := channelSpec[idx+1:]
start, err := strconv.Atoi(startStr)
if err != nil {
return fmt.Errorf("invalid channel start: %w", err)
}
a.ChannelStart = start
if endStr == "" {
a.ChannelEnd = 512
} else {
end, err := strconv.Atoi(endStr)
if err != nil {
return fmt.Errorf("invalid channel end: %w", err)
}
a.ChannelEnd = end
}
} else {
ch, err := strconv.Atoi(channelSpec)
if err != nil {
return fmt.Errorf("invalid channel: %w", err)
}
a.ChannelStart = ch
a.ChannelEnd = ch
}
return nil return nil
} }
func (u *UniverseAddr) UnmarshalTOML(data interface{}) error { func (a *FromAddr) Count() int {
return a.ChannelEnd - a.ChannelStart + 1
}
// ToAddr represents a destination universe address with starting channel
type ToAddr struct {
Universe artnet.Universe
ChannelStart int // 1-indexed
}
func (a *ToAddr) UnmarshalTOML(data interface{}) error {
switch v := data.(type) { switch v := data.(type) {
case string: case string:
universe, err := ParseUniverseAddr(v) return a.parse(v)
if err != nil {
return err
}
u.Universe = universe
return nil
case int64: case int64:
// Universe number only (0-32767) a.Universe = artnet.Universe(v)
u.Universe = artnet.Universe(v) a.ChannelStart = 1
return nil return nil
case float64: case float64:
// TOML sometimes parses integers as floats a.Universe = artnet.Universe(int64(v))
u.Universe = artnet.Universe(int64(v)) a.ChannelStart = 1
return nil return nil
default: default:
return fmt.Errorf("unsupported universe address type: %T", data) return fmt.Errorf("unsupported address type: %T", data)
} }
} }
// ParseUniverseAddr parses universe address formats: // parse parses address formats:
// - "0.0.1" - Net.Subnet.Universe // - "0.0.1" - starting at channel 1
// - "1" - Universe number only // - "0.0.1:50" - starting at channel 50
func ParseUniverseAddr(s string) (artnet.Universe, error) { func (a *ToAddr) parse(s string) error {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
// Try Net.Subnet.Universe format universeStr, channelSpec := splitAddr(s)
universe, err := parseUniverse(universeStr)
if err != nil {
return err
}
a.Universe = universe
if channelSpec == "" {
a.ChannelStart = 1
return nil
}
if strings.Contains(channelSpec, "-") {
return fmt.Errorf("to address cannot contain range; use single channel number")
}
ch, err := strconv.Atoi(channelSpec)
if err != nil {
return fmt.Errorf("invalid channel: %w", err)
}
a.ChannelStart = ch
return nil
}
func splitAddr(s string) (universe, channel string) {
if idx := strings.LastIndex(s, ":"); idx != -1 {
return s[:idx], s[idx+1:]
}
return s, ""
}
func parseUniverse(s string) (artnet.Universe, error) {
if strings.Contains(s, ".") { if strings.Contains(s, ".") {
parts := strings.Split(s, ".") parts := strings.Split(s, ".")
if len(parts) != 3 { if len(parts) != 3 {
return 0, fmt.Errorf("invalid universe address format: %s (expected net.subnet.universe)", s) return 0, fmt.Errorf("invalid universe format: %s (expected net.subnet.universe)", s)
} }
net, err := strconv.Atoi(parts[0]) net, err := strconv.Atoi(parts[0])
if err != nil { if err != nil {
@@ -92,10 +186,9 @@ func ParseUniverseAddr(s string) (artnet.Universe, error) {
return artnet.NewUniverse(uint8(net), uint8(subnet), uint8(universe)), nil return artnet.NewUniverse(uint8(net), uint8(subnet), uint8(universe)), nil
} }
// Plain universe number
u, err := strconv.Atoi(s) u, err := strconv.Atoi(s)
if err != nil { if err != nil {
return 0, fmt.Errorf("invalid universe address format: %s", s) return 0, fmt.Errorf("invalid universe: %s", s)
} }
return artnet.Universe(u), nil return artnet.Universe(u), nil
} }
@@ -108,37 +201,22 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("failed to load config: %w", err) return nil, fmt.Errorf("failed to load config: %w", err)
} }
// Validate and normalize mappings for i, m := range cfg.Mappings {
for i := range cfg.Mappings { if m.From.ChannelStart < 1 || m.From.ChannelStart > 512 {
m := &cfg.Mappings[i] return nil, fmt.Errorf("mapping %d: from channel start must be 1-512", i)
// Default from_channel to 1 (start of universe)
if m.FromChannel == 0 {
m.FromChannel = 1
} }
if m.From.ChannelEnd < 1 || m.From.ChannelEnd > 512 {
// Default to_channel to same as from_channel return nil, fmt.Errorf("mapping %d: from channel end must be 1-512", i)
if m.ToChannel == 0 {
m.ToChannel = m.FromChannel
} }
if m.From.ChannelStart > m.From.ChannelEnd {
// Default count to all remaining channels return nil, fmt.Errorf("mapping %d: from channel start > end", i)
if m.Count == 0 {
m.Count = 512 - m.FromChannel + 1
} }
if m.To.ChannelStart < 1 || m.To.ChannelStart > 512 {
// Validate ranges return nil, fmt.Errorf("mapping %d: to channel must be 1-512", i)
if m.FromChannel < 1 || m.FromChannel > 512 {
return nil, fmt.Errorf("mapping %d: from_channel must be 1-512", i)
} }
if m.ToChannel < 1 || m.ToChannel > 512 { toEnd := m.To.ChannelStart + m.From.Count() - 1
return nil, fmt.Errorf("mapping %d: to_channel must be 1-512", i) if toEnd > 512 {
} return nil, fmt.Errorf("mapping %d: to channels exceed 512", i)
if m.FromChannel+m.Count-1 > 512 {
return nil, fmt.Errorf("mapping %d: from_channel + count exceeds 512", i)
}
if m.ToChannel+m.Count-1 > 512 {
return nil, fmt.Errorf("mapping %d: to_channel + count exceeds 512", i)
} }
} }
@@ -160,10 +238,10 @@ func (c *Config) Normalize() []NormalizedMapping {
for i, m := range c.Mappings { for i, m := range c.Mappings {
result[i] = NormalizedMapping{ result[i] = NormalizedMapping{
FromUniverse: m.From.Universe, FromUniverse: m.From.Universe,
FromChannel: m.FromChannel - 1, // Convert to 0-indexed FromChannel: m.From.ChannelStart - 1,
ToUniverse: m.To.Universe, ToUniverse: m.To.Universe,
ToChannel: m.ToChannel - 1, // Convert to 0-indexed ToChannel: m.To.ChannelStart - 1,
Count: m.Count, Count: m.From.Count(),
} }
} }
return result return result

11
main.go
View File

@@ -41,13 +41,10 @@ func main() {
// Log mappings // Log mappings
for _, m := range cfg.Mappings { for _, m := range cfg.Mappings {
if m.Count == 512 && m.FromChannel == 1 { toEnd := m.To.ChannelStart + m.From.Count() - 1
log.Printf(" %s -> %s (all channels)", m.From.Universe, m.To.Universe) log.Printf(" %s:%d-%d -> %s:%d-%d",
} else { m.From.Universe, m.From.ChannelStart, m.From.ChannelEnd,
log.Printf(" %s[%d-%d] -> %s[%d-%d]", m.To.Universe, m.To.ChannelStart, toEnd)
m.From.Universe, m.FromChannel, m.FromChannel+m.Count-1,
m.To.Universe, m.ToChannel, m.ToChannel+m.Count-1)
}
} }
// Create sender // Create sender