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:
@@ -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"
|
||||||
|
|||||||
212
config/config.go
212
config/config.go
@@ -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
11
main.go
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user