Add proper Universe type with protocol-aware formatting

This commit is contained in:
Ian Gulliver
2026-01-27 15:25:10 -08:00
parent 9a673bcd23
commit 9a694b5178
5 changed files with 232 additions and 246 deletions

4
CLAUDE.md Normal file
View File

@@ -0,0 +1,4 @@
* Don't commit unless told to
* Single-line commit messages, no mention of claude
* Use go vet or go run instead of go build
* No code comments

View File

@@ -1,3 +1,7 @@
[[mapping]]
from = "artnet:32.0.0"
to = "sacn:32"
# lighting-1 port 1 # lighting-1 port 1
[[target]] [[target]]
universe = "artnet:0.0.0" universe = "artnet:0.0.0"

View File

@@ -6,58 +6,8 @@ import (
"strings" "strings"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/gopatchy/artmap/artnet"
) )
// Config represents the application configuration
type Config struct {
Targets []Target `toml:"target"`
Mappings []Mapping `toml:"mapping"`
}
// Target represents a target address for an output universe
type Target struct {
Universe TargetAddr `toml:"universe"`
Address string `toml:"address"`
}
// TargetAddr is a protocol-prefixed universe address
type TargetAddr struct {
Protocol Protocol
Universe artnet.Universe
}
func (t *TargetAddr) UnmarshalTOML(data interface{}) error {
switch v := data.(type) {
case string:
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:
return fmt.Errorf("unsupported universe type: %T", data)
}
}
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
@@ -66,6 +16,104 @@ const (
ProtocolSACN Protocol = "sacn" ProtocolSACN Protocol = "sacn"
) )
// Universe represents a DMX universe with its protocol
type Universe struct {
Protocol Protocol
Number uint16
}
func NewUniverse(proto Protocol, num any) (Universe, error) {
n, err := toUint16(num, proto)
if err != nil {
return Universe{}, err
}
return makeUniverse(proto, n)
}
func ParseUniverse(s string) (Universe, error) {
proto, rest, err := splitProtoPrefix(s)
if err != nil {
return Universe{}, err
}
return NewUniverse(proto, rest)
}
func (u Universe) String() string {
if u.Protocol == ProtocolSACN {
return fmt.Sprintf("sacn:%d", u.Number)
}
net := (u.Number >> 8) & 0x7F
subnet := (u.Number >> 4) & 0x0F
universe := u.Number & 0x0F
return fmt.Sprintf("artnet:%d.%d.%d", net, subnet, universe)
}
func (u *Universe) UnmarshalTOML(data any) error {
switch v := data.(type) {
case string:
parsed, err := ParseUniverse(v)
if err != nil {
return err
}
*u = parsed
return nil
default:
parsed, err := NewUniverse(ProtocolArtNet, data)
if err != nil {
return err
}
*u = parsed
return nil
}
}
func toUint16(v any, proto Protocol) (uint16, error) {
switch n := v.(type) {
case int:
return uint16(n), nil
case int64:
return uint16(n), nil
case uint16:
return n, nil
case uint:
return uint16(n), nil
case float64:
return uint16(n), nil
case string:
return parseUniverseNumber(n, proto)
default:
return 0, fmt.Errorf("unsupported universe type: %T", v)
}
}
func makeUniverse(proto Protocol, n uint16) (Universe, error) {
switch proto {
case ProtocolArtNet:
if n > 0x7FFF {
return Universe{}, fmt.Errorf("artnet universe %d out of range (max 32767)", n)
}
case ProtocolSACN:
if n < 1 || n > 63999 {
return Universe{}, fmt.Errorf("sacn universe %d out of range (1-63999)", n)
}
default:
return Universe{}, fmt.Errorf("unknown protocol: %s", proto)
}
return Universe{Protocol: proto, Number: n}, nil
}
// Config represents the application configuration
type Config struct {
Targets []Target `toml:"target"`
Mappings []Mapping `toml:"mapping"`
}
// Target represents a target address for an output universe
type Target struct {
Universe Universe `toml:"universe"`
Address string `toml:"address"`
}
// 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"`
@@ -74,54 +122,37 @@ type Mapping struct {
// 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 Universe
Universe artnet.Universe
ChannelStart int // 1-indexed ChannelStart int // 1-indexed
ChannelEnd int // 1-indexed ChannelEnd int // 1-indexed
} }
func (a *FromAddr) UnmarshalTOML(data interface{}) error { func (a *FromAddr) UnmarshalTOML(data any) error {
switch v := data.(type) { if s, ok := data.(string); ok {
case string: return a.parse(s)
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
return nil
default:
return fmt.Errorf("unsupported address type: %T", data)
} }
u, err := NewUniverse(ProtocolArtNet, data)
if err != nil {
return err
}
a.Universe = u
a.ChannelStart = 1
a.ChannelEnd = 512
return nil
} }
// 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 { func (a *FromAddr) parse(s string) error {
s = strings.TrimSpace(s) proto, rest, err := splitProtoPrefix(strings.TrimSpace(s))
proto, rest, err := splitProtoPrefix(s)
if err != nil { if err != nil {
return err return err
} }
a.Protocol = proto
universeStr, channelSpec := splitAddr(rest) universeStr, channelSpec := splitAddr(rest)
u, err := NewUniverse(proto, universeStr)
universe, err := parseUniverse(universeStr)
if err != nil { if err != nil {
return err return err
} }
a.Universe = universe a.Universe = u
if channelSpec == "" { if channelSpec == "" {
a.ChannelStart = 1 a.ChannelStart = 1
@@ -129,45 +160,45 @@ func (a *FromAddr) parse(s string) error {
return nil return nil
} }
if idx := strings.Index(channelSpec, "-"); idx != -1 { return parseChannelRange(channelSpec, &a.ChannelStart, &a.ChannelEnd)
startStr := channelSpec[:idx] }
endStr := channelSpec[idx+1:]
start, err := strconv.Atoi(startStr) func parseChannelRange(spec string, start, end *int) error {
if idx := strings.Index(spec, "-"); idx != -1 {
s, err := strconv.Atoi(spec[:idx])
if err != nil { if err != nil {
return fmt.Errorf("invalid channel start: %w", err) return fmt.Errorf("invalid channel start: %w", err)
} }
a.ChannelStart = start *start = s
if endStr == "" { if spec[idx+1:] == "" {
a.ChannelEnd = 512 *end = 512
} else { } else {
end, err := strconv.Atoi(endStr) e, err := strconv.Atoi(spec[idx+1:])
if err != nil { if err != nil {
return fmt.Errorf("invalid channel end: %w", err) return fmt.Errorf("invalid channel end: %w", err)
} }
a.ChannelEnd = end *end = e
} }
} else { } else {
ch, err := strconv.Atoi(channelSpec) ch, err := strconv.Atoi(spec)
if err != nil { if err != nil {
return fmt.Errorf("invalid channel: %w", err) return fmt.Errorf("invalid channel: %w", err)
} }
a.ChannelStart = ch *start = ch
a.ChannelEnd = ch *end = ch
} }
return nil return nil
} }
func (a FromAddr) String() string { func (a FromAddr) String() string {
if a.ChannelStart == 1 && a.ChannelEnd == 512 { if a.ChannelStart == 1 && a.ChannelEnd == 512 {
return fmt.Sprintf("%s:%s", a.Protocol, a.Universe) return a.Universe.String()
} }
if a.ChannelStart == a.ChannelEnd { if a.ChannelStart == a.ChannelEnd {
return fmt.Sprintf("%s:%s:%d", a.Protocol, a.Universe, a.ChannelStart) return fmt.Sprintf("%s:%d", a.Universe, a.ChannelStart)
} }
return fmt.Sprintf("%s:%s:%d-%d", a.Protocol, a.Universe, a.ChannelStart, a.ChannelEnd) return fmt.Sprintf("%s:%d-%d", a.Universe, a.ChannelStart, a.ChannelEnd)
} }
func (a *FromAddr) Count() int { func (a *FromAddr) Count() int {
@@ -176,49 +207,35 @@ func (a *FromAddr) Count() int {
// 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 Universe
Universe artnet.Universe
ChannelStart int // 1-indexed ChannelStart int // 1-indexed
} }
func (a *ToAddr) UnmarshalTOML(data interface{}) error { func (a *ToAddr) UnmarshalTOML(data any) error {
switch v := data.(type) { if s, ok := data.(string); ok {
case string: return a.parse(s)
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
default:
return fmt.Errorf("unsupported address type: %T", data)
} }
u, err := NewUniverse(ProtocolArtNet, data)
if err != nil {
return err
}
a.Universe = u
a.ChannelStart = 1
return nil
} }
// 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 { func (a *ToAddr) parse(s string) error {
s = strings.TrimSpace(s) proto, rest, err := splitProtoPrefix(strings.TrimSpace(s))
proto, rest, err := splitProtoPrefix(s)
if err != nil { if err != nil {
return err return err
} }
a.Protocol = proto
universeStr, channelSpec := splitAddr(rest) universeStr, channelSpec := splitAddr(rest)
u, err := NewUniverse(proto, universeStr)
universe, err := parseUniverse(universeStr)
if err != nil { if err != nil {
return err return err
} }
a.Universe = universe a.Universe = u
if channelSpec == "" { if channelSpec == "" {
a.ChannelStart = 1 a.ChannelStart = 1
@@ -234,15 +251,14 @@ func (a *ToAddr) parse(s string) error {
return fmt.Errorf("invalid channel: %w", err) return fmt.Errorf("invalid channel: %w", err)
} }
a.ChannelStart = ch a.ChannelStart = ch
return nil return nil
} }
func (a ToAddr) String() string { func (a ToAddr) String() string {
if a.ChannelStart == 1 { if a.ChannelStart == 1 {
return fmt.Sprintf("%s:%s", a.Protocol, a.Universe) return a.Universe.String()
} }
return fmt.Sprintf("%s:%s:%d", a.Protocol, a.Universe, a.ChannelStart) return fmt.Sprintf("%s:%d", a.Universe, a.ChannelStart)
} }
func splitProtoPrefix(s string) (Protocol, string, error) { func splitProtoPrefix(s string) (Protocol, string, error) {
@@ -262,8 +278,11 @@ func splitAddr(s string) (universe, channel string) {
return s, "" return s, ""
} }
func parseUniverse(s string) (artnet.Universe, error) { func parseUniverseNumber(s string, proto Protocol) (uint16, error) {
if strings.Contains(s, ".") { if strings.Contains(s, ".") {
if proto == ProtocolSACN {
return 0, fmt.Errorf("sACN universes cannot use net.subnet.universe format")
}
parts := strings.Split(s, ".") parts := strings.Split(s, ".")
if len(parts) != 3 { if len(parts) != 3 {
return 0, fmt.Errorf("invalid universe format: %s (expected net.subnet.universe)", s) return 0, fmt.Errorf("invalid universe format: %s (expected net.subnet.universe)", s)
@@ -280,14 +299,14 @@ func parseUniverse(s string) (artnet.Universe, error) {
if err != nil { if err != nil {
return 0, fmt.Errorf("invalid universe: %w", err) return 0, fmt.Errorf("invalid universe: %w", err)
} }
return artnet.NewUniverse(uint8(net), uint8(subnet), uint8(universe)), nil return uint16(net&0x7F)<<8 | uint16(subnet&0x0F)<<4 | uint16(universe&0x0F), nil
} }
u, err := strconv.Atoi(s) u, err := strconv.Atoi(s)
if err != nil { if err != nil {
return 0, fmt.Errorf("invalid universe: %s", s) return 0, fmt.Errorf("invalid universe: %s", s)
} }
return artnet.Universe(u), nil return uint16(u), nil
} }
// Load loads configuration from a TOML file // Load loads configuration from a TOML file
@@ -329,13 +348,11 @@ func Load(path string) (*Config, error) {
// NormalizedMapping is a processed mapping ready for the remapper // NormalizedMapping is a processed mapping ready for the remapper
type NormalizedMapping struct { type NormalizedMapping struct {
FromUniverse artnet.Universe From Universe
FromChannel int // 0-indexed FromChan int // 0-indexed
FromProto Protocol To Universe
ToUniverse artnet.Universe ToChan int // 0-indexed
ToChannel int // 0-indexed Count int
Count int
Protocol Protocol
} }
// Normalize converts config mappings to normalized form (0-indexed channels) // Normalize converts config mappings to normalized form (0-indexed channels)
@@ -343,24 +360,22 @@ func (c *Config) Normalize() []NormalizedMapping {
result := make([]NormalizedMapping, len(c.Mappings)) result := make([]NormalizedMapping, len(c.Mappings))
for i, m := range c.Mappings { for i, m := range c.Mappings {
result[i] = NormalizedMapping{ result[i] = NormalizedMapping{
FromUniverse: m.From.Universe, From: m.From.Universe,
FromChannel: m.From.ChannelStart - 1, FromChan: m.From.ChannelStart - 1,
FromProto: m.From.Protocol, To: m.To.Universe,
ToUniverse: m.To.Universe, ToChan: m.To.ChannelStart - 1,
ToChannel: m.To.ChannelStart - 1, Count: m.From.Count(),
Count: m.From.Count(),
Protocol: m.To.Protocol,
} }
} }
return result return result
} }
// SACNSourceUniverses returns universes that need sACN input // SACNSourceUniverses returns sACN universe numbers that need input
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.From.Protocol == ProtocolSACN { if m.From.Universe.Protocol == ProtocolSACN {
seen[uint16(m.From.Universe)] = true seen[m.From.Universe.Number] = true
} }
} }
result := make([]uint16, 0, len(seen)) result := make([]uint16, 0, len(seen))

66
main.go
View File

@@ -25,7 +25,7 @@ type App struct {
sacnSender *sacn.Sender sacnSender *sacn.Sender
discovery *artnet.Discovery discovery *artnet.Discovery
engine *remap.Engine engine *remap.Engine
artTargets map[artnet.Universe]*net.UDPAddr artTargets map[uint16]*net.UDPAddr
sacnTargets map[uint16][]*net.UDPAddr sacnTargets map[uint16][]*net.UDPAddr
debug bool debug bool
} }
@@ -54,28 +54,22 @@ func main() {
} }
// Parse targets // Parse targets
artTargets := make(map[artnet.Universe]*net.UDPAddr) artTargets := make(map[uint16]*net.UDPAddr)
sacnTargets := make(map[uint16][]*net.UDPAddr) sacnTargets := make(map[uint16][]*net.UDPAddr)
pollTargets := make(map[string]*net.UDPAddr) // dedupe by address string pollTargets := make(map[string]*net.UDPAddr)
for _, t := range cfg.Targets { for _, t := range cfg.Targets {
addr, err := parseTargetAddr(t.Address, protocolPort(t.Universe.Protocol))
if err != nil {
log.Fatalf("target error: address=%q err=%v", t.Address, err)
}
switch t.Universe.Protocol { switch t.Universe.Protocol {
case config.ProtocolArtNet: case config.ProtocolArtNet:
addr, err := parseTargetAddr(t.Address, artnet.Port) artTargets[t.Universe.Number] = addr
if err != nil {
log.Fatalf("target error: address=%q err=%v", t.Address, err)
}
artTargets[t.Universe.Universe] = addr
pollTargets[addr.String()] = addr pollTargets[addr.String()] = addr
log.Printf("[config] target %s -> %s", t.Universe, addr)
case config.ProtocolSACN: case config.ProtocolSACN:
addr, err := parseTargetAddr(t.Address, sacn.Port) sacnTargets[t.Universe.Number] = append(sacnTargets[t.Universe.Number], addr)
if err != nil {
log.Fatalf("target error: address=%q err=%v", t.Address, err)
}
u := uint16(t.Universe.Universe)
sacnTargets[u] = append(sacnTargets[u], addr)
log.Printf("[config] target %s -> %s", t.Universe, addr)
} }
log.Printf("[config] target %s -> %s", t.Universe, addr)
} }
// Parse broadcast addresses // Parse broadcast addresses
@@ -120,7 +114,11 @@ func main() {
defer sacnSender.Close() defer sacnSender.Close()
// Create discovery // Create discovery
destUniverses := engine.DestUniverses() destNums := engine.DestArtNetUniverses()
destUniverses := make([]artnet.Universe, len(destNums))
for i, n := range destNums {
destUniverses[i] = artnet.Universe(n)
}
discovery := artnet.NewDiscovery(artSender, "artmap", "ArtNet Remapping Proxy", destUniverses, pollTargetSlice) discovery := artnet.NewDiscovery(artSender, "artmap", "ArtNet Remapping Proxy", destUniverses, pollTargetSlice)
// Create app // Create app
@@ -186,8 +184,8 @@ func (a *App) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {
log.Printf("[<-artnet] src=%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)
} }
u := config.Universe{Protocol: config.ProtocolArtNet, Number: uint16(pkt.Universe)}
a.sendOutputs(a.engine.Remap(config.ProtocolArtNet, pkt.Universe, pkt.Data)) a.sendOutputs(a.engine.Remap(u, pkt.Data))
} }
// HandlePoll implements artnet.PacketHandler // HandlePoll implements artnet.PacketHandler
@@ -211,15 +209,15 @@ func (a *App) HandleSACN(universe uint16, data [512]byte) {
if a.debug { if a.debug {
log.Printf("[<-sacn] universe=%d", universe) log.Printf("[<-sacn] universe=%d", universe)
} }
u := config.Universe{Protocol: config.ProtocolSACN, Number: universe}
a.sendOutputs(a.engine.Remap(config.ProtocolSACN, artnet.Universe(universe), data)) a.sendOutputs(a.engine.Remap(u, data))
} }
func (a *App) sendOutputs(outputs []remap.Output) { func (a *App) sendOutputs(outputs []remap.Output) {
for _, out := range outputs { for _, out := range outputs {
switch out.Protocol { switch out.Universe.Protocol {
case config.ProtocolSACN: case config.ProtocolSACN:
u := uint16(out.Universe) u := out.Universe.Number
if a.debug { if a.debug {
log.Printf("[->sacn] universe=%d", u) log.Printf("[->sacn] universe=%d", u)
} }
@@ -235,15 +233,17 @@ func (a *App) sendOutputs(outputs []remap.Output) {
} }
} }
default: // ArtNet case config.ProtocolArtNet:
if target, ok := a.artTargets[out.Universe]; ok { u := out.Universe.Number
artU := artnet.Universe(u)
if target, ok := a.artTargets[u]; ok {
if a.debug { if a.debug {
log.Printf("[->artnet] dst=%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, artU, out.Data[:]); err != nil {
log.Printf("[->artnet] error: dst=%s err=%v", target.IP, err) log.Printf("[->artnet] error: dst=%s err=%v", target.IP, err)
} }
} else if nodes := a.discovery.GetNodesForUniverse(out.Universe); len(nodes) > 0 { } else if nodes := a.discovery.GetNodesForUniverse(artU); len(nodes) > 0 {
for _, node := range nodes { for _, node := range nodes {
addr := &net.UDPAddr{ addr := &net.UDPAddr{
IP: node.IP, IP: node.IP,
@@ -252,7 +252,7 @@ func (a *App) sendOutputs(outputs []remap.Output) {
if a.debug { if a.debug {
log.Printf("[->artnet] dst=%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, artU, out.Data[:]); err != nil {
log.Printf("[->artnet] error: dst=%s err=%v", node.IP, err) log.Printf("[->artnet] error: dst=%s err=%v", node.IP, err)
} }
} }
@@ -305,9 +305,13 @@ func parseListenAddr(s string) (*net.UDPAddr, error) {
return &net.UDPAddr{IP: ip, Port: port}, nil return &net.UDPAddr{IP: ip, Port: port}, nil
} }
// parseTargetAddr parses target address formats: func protocolPort(p config.Protocol) int {
// - "host:port" -> specific host and port if p == config.ProtocolSACN {
// - "host" -> specific host, default port return sacn.Port
}
return artnet.Port
}
func parseTargetAddr(s string, defaultPort int) (*net.UDPAddr, error) { func parseTargetAddr(s string, defaultPort int) (*net.UDPAddr, error) {
var host string var host string
var port int var port int

View File

@@ -3,53 +3,34 @@ package remap
import ( import (
"sync" "sync"
"github.com/gopatchy/artmap/artnet"
"github.com/gopatchy/artmap/config" "github.com/gopatchy/artmap/config"
) )
// Output represents a remapped DMX output // Output represents a remapped DMX output
type Output struct { type Output struct {
Universe artnet.Universe Universe config.Universe
Protocol config.Protocol
Data [512]byte Data [512]byte
} }
// outputKey uniquely identifies an output destination
type outputKey struct {
Universe artnet.Universe
Protocol config.Protocol
}
// sourceKey uniquely identifies an input source
type sourceKey struct {
Universe artnet.Universe
Protocol config.Protocol
}
// Engine handles DMX channel remapping // Engine handles DMX channel remapping
type Engine struct { type Engine struct {
mappings []config.NormalizedMapping mappings []config.NormalizedMapping
// Index mappings by source universe and protocol for faster lookup bySource map[config.Universe][]config.NormalizedMapping
bySource map[sourceKey][]config.NormalizedMapping state map[config.Universe]*[512]byte
// Persistent state for each output universe (merged from all sources) stateMu sync.Mutex
state map[outputKey]*[512]byte
stateMu sync.Mutex
} }
// NewEngine creates a new remapping engine // NewEngine creates a new remapping engine
func NewEngine(mappings []config.NormalizedMapping) *Engine { func NewEngine(mappings []config.NormalizedMapping) *Engine {
bySource := make(map[sourceKey][]config.NormalizedMapping) bySource := make(map[config.Universe][]config.NormalizedMapping)
for _, m := range mappings { for _, m := range mappings {
key := sourceKey{Universe: m.FromUniverse, Protocol: m.FromProto} bySource[m.From] = append(bySource[m.From], m)
bySource[key] = append(bySource[key], m)
} }
// Initialize state for all output universes state := make(map[config.Universe]*[512]byte)
state := make(map[outputKey]*[512]byte)
for _, m := range mappings { for _, m := range mappings {
key := outputKey{Universe: m.ToUniverse, Protocol: m.Protocol} if _, ok := state[m.To]; !ok {
if _, ok := state[key]; !ok { state[m.To] = &[512]byte{}
state[key] = &[512]byte{}
} }
} }
@@ -61,9 +42,8 @@ func NewEngine(mappings []config.NormalizedMapping) *Engine {
} }
// Remap applies mappings to incoming DMX data and returns outputs // Remap applies mappings to incoming DMX data and returns outputs
func (e *Engine) Remap(srcProto config.Protocol, srcUniverse artnet.Universe, srcData [512]byte) []Output { func (e *Engine) Remap(src config.Universe, srcData [512]byte) []Output {
key := sourceKey{Universe: srcUniverse, Protocol: srcProto} mappings, ok := e.bySource[src]
mappings, ok := e.bySource[key]
if !ok { if !ok {
return nil return nil
} }
@@ -71,62 +51,41 @@ func (e *Engine) Remap(srcProto config.Protocol, srcUniverse artnet.Universe, sr
e.stateMu.Lock() e.stateMu.Lock()
defer e.stateMu.Unlock() defer e.stateMu.Unlock()
// Track which outputs are affected by this input affected := make(map[config.Universe]bool)
affected := make(map[outputKey]bool)
for _, m := range mappings { for _, m := range mappings {
outKey := outputKey{Universe: m.ToUniverse, Protocol: m.Protocol} affected[m.To] = true
affected[outKey] = true outState := e.state[m.To]
// Update state for this output
outState := e.state[outKey]
// Copy channels into persistent state
for i := 0; i < m.Count; i++ { for i := 0; i < m.Count; i++ {
srcChan := m.FromChannel + i srcChan := m.FromChan + i
dstChan := m.ToChannel + i dstChan := m.ToChan + i
if srcChan < 512 && dstChan < 512 { if srcChan < 512 && dstChan < 512 {
outState[dstChan] = srcData[srcChan] outState[dstChan] = srcData[srcChan]
} }
} }
} }
// Return outputs for all affected universes
result := make([]Output, 0, len(affected)) result := make([]Output, 0, len(affected))
for outKey := range affected { for u := range affected {
result = append(result, Output{ result = append(result, Output{
Universe: outKey.Universe, Universe: u,
Protocol: outKey.Protocol, Data: *e.state[u],
Data: *e.state[outKey],
}) })
} }
return result return result
} }
// SourceUniverses returns all universes that have mappings // DestArtNetUniverses returns destination ArtNet universe numbers (for discovery)
func (e *Engine) SourceUniverses() []artnet.Universe { func (e *Engine) DestArtNetUniverses() []uint16 {
seen := make(map[artnet.Universe]bool) seen := make(map[uint16]bool)
for key := range e.bySource {
seen[key.Universe] = true
}
result := make([]artnet.Universe, 0, len(seen))
for u := range seen {
result = append(result, u)
}
return result
}
// DestUniverses returns all destination universes (for ArtNet discovery)
func (e *Engine) DestUniverses() []artnet.Universe {
seen := make(map[artnet.Universe]bool)
for _, m := range e.mappings { for _, m := range e.mappings {
if m.Protocol == config.ProtocolArtNet { if m.To.Protocol == config.ProtocolArtNet {
seen[m.ToUniverse] = true seen[m.To.Number] = true
} }
} }
result := make([]uint16, 0, len(seen))
result := make([]artnet.Universe, 0, len(seen))
for u := range seen { for u := range seen {
result = append(result, u) result = append(result, u)
} }