202 lines
5.1 KiB
Go
202 lines
5.1 KiB
Go
|
|
package config
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/BurntSushi/toml"
|
||
|
|
"github.com/gopatchy/artmap/artnet"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Config represents the application configuration
|
||
|
|
type Config struct {
|
||
|
|
Settings Settings `toml:"settings"`
|
||
|
|
Mappings []Mapping `toml:"mapping"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Settings contains global configuration options
|
||
|
|
type Settings struct {
|
||
|
|
ListenPort int `toml:"listen_port"`
|
||
|
|
BroadcastAddr string `toml:"broadcast_addr"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mapping represents a single channel mapping rule
|
||
|
|
type Mapping struct {
|
||
|
|
// Source
|
||
|
|
From UniverseAddr `toml:"from"`
|
||
|
|
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
|
||
|
|
type UniverseAddr struct {
|
||
|
|
Universe artnet.Universe
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *UniverseAddr) UnmarshalText(text []byte) error {
|
||
|
|
s := string(text)
|
||
|
|
universe, err := ParseUniverseAddr(s)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
u.Universe = universe
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *UniverseAddr) UnmarshalTOML(data interface{}) error {
|
||
|
|
switch v := data.(type) {
|
||
|
|
case string:
|
||
|
|
universe, err := ParseUniverseAddr(v)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
u.Universe = universe
|
||
|
|
return nil
|
||
|
|
case int64:
|
||
|
|
// Universe number only (0-32767)
|
||
|
|
u.Universe = artnet.Universe(v)
|
||
|
|
return nil
|
||
|
|
case float64:
|
||
|
|
// TOML sometimes parses integers as floats
|
||
|
|
u.Universe = artnet.Universe(int64(v))
|
||
|
|
return nil
|
||
|
|
default:
|
||
|
|
return fmt.Errorf("unsupported universe address type: %T", data)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParseUniverseAddr parses various universe address formats:
|
||
|
|
// - "0.0.1" or "0.0.1" - Net.Subnet.Universe
|
||
|
|
// - "0:0:1" - Net:Subnet:Universe
|
||
|
|
// - "1" - Universe number only
|
||
|
|
func ParseUniverseAddr(s string) (artnet.Universe, error) {
|
||
|
|
s = strings.TrimSpace(s)
|
||
|
|
|
||
|
|
// Try Net.Subnet.Universe format
|
||
|
|
if strings.Contains(s, ".") {
|
||
|
|
parts := strings.Split(s, ".")
|
||
|
|
if len(parts) == 3 {
|
||
|
|
net, err := strconv.Atoi(parts[0])
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid net: %w", err)
|
||
|
|
}
|
||
|
|
subnet, err := strconv.Atoi(parts[1])
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid subnet: %w", err)
|
||
|
|
}
|
||
|
|
universe, err := strconv.Atoi(parts[2])
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid universe: %w", err)
|
||
|
|
}
|
||
|
|
return artnet.NewUniverse(uint8(net), uint8(subnet), uint8(universe)), nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try Net:Subnet:Universe format
|
||
|
|
if strings.Contains(s, ":") {
|
||
|
|
parts := strings.Split(s, ":")
|
||
|
|
if len(parts) == 3 {
|
||
|
|
net, err := strconv.Atoi(parts[0])
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid net: %w", err)
|
||
|
|
}
|
||
|
|
subnet, err := strconv.Atoi(parts[1])
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid subnet: %w", err)
|
||
|
|
}
|
||
|
|
universe, err := strconv.Atoi(parts[2])
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid universe: %w", err)
|
||
|
|
}
|
||
|
|
return artnet.NewUniverse(uint8(net), uint8(subnet), uint8(universe)), nil
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try plain universe number
|
||
|
|
u, err := strconv.Atoi(s)
|
||
|
|
if err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid universe address format: %s", s)
|
||
|
|
}
|
||
|
|
return artnet.Universe(u), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load loads configuration from a TOML file
|
||
|
|
func Load(path string) (*Config, error) {
|
||
|
|
var cfg Config
|
||
|
|
|
||
|
|
// Set defaults
|
||
|
|
cfg.Settings.ListenPort = artnet.Port
|
||
|
|
cfg.Settings.BroadcastAddr = "2.255.255.255"
|
||
|
|
|
||
|
|
if _, err := toml.DecodeFile(path, &cfg); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate and normalize mappings
|
||
|
|
for i := range cfg.Mappings {
|
||
|
|
m := &cfg.Mappings[i]
|
||
|
|
|
||
|
|
// Default from_channel to 1 (start of universe)
|
||
|
|
if m.FromChannel == 0 {
|
||
|
|
m.FromChannel = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default to_channel to same as from_channel
|
||
|
|
if m.ToChannel == 0 {
|
||
|
|
m.ToChannel = m.FromChannel
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default count to all remaining channels
|
||
|
|
if m.Count == 0 {
|
||
|
|
m.Count = 512 - m.FromChannel + 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate ranges
|
||
|
|
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 {
|
||
|
|
return nil, fmt.Errorf("mapping %d: to_channel must be 1-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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return &cfg, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// NormalizedMapping is a processed mapping ready for the remapper
|
||
|
|
type NormalizedMapping struct {
|
||
|
|
FromUniverse artnet.Universe
|
||
|
|
FromChannel int // 0-indexed
|
||
|
|
ToUniverse artnet.Universe
|
||
|
|
ToChannel int // 0-indexed
|
||
|
|
Count int
|
||
|
|
}
|
||
|
|
|
||
|
|
// Normalize converts config mappings to normalized form (0-indexed channels)
|
||
|
|
func (c *Config) Normalize() []NormalizedMapping {
|
||
|
|
result := make([]NormalizedMapping, len(c.Mappings))
|
||
|
|
for i, m := range c.Mappings {
|
||
|
|
result[i] = NormalizedMapping{
|
||
|
|
FromUniverse: m.From.Universe,
|
||
|
|
FromChannel: m.FromChannel - 1, // Convert to 0-indexed
|
||
|
|
ToUniverse: m.To.Universe,
|
||
|
|
ToChannel: m.ToChannel - 1, // Convert to 0-indexed
|
||
|
|
Count: m.Count,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result
|
||
|
|
}
|