Add proper Universe type with protocol-aware formatting
This commit is contained in:
4
CLAUDE.md
Normal file
4
CLAUDE.md
Normal 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
|
||||||
@@ -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"
|
||||||
|
|||||||
313
config/config.go
313
config/config.go
@@ -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
66
main.go
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user