Files
artmap/remap/engine.go

163 lines
3.6 KiB
Go
Raw Normal View History

package remap
import (
"sync"
"sync/atomic"
"github.com/gopatchy/artmap/config"
)
// Output represents a remapped DMX output
type Output struct {
Universe config.Universe
Data [512]byte
}
// sourceEntry holds mappings and stats for a source universe
type sourceEntry struct {
mappings []config.NormalizedMapping
counter atomic.Uint64
}
// universeBuffer holds per-output-universe state with its own lock
type universeBuffer struct {
mu sync.Mutex
data [512]byte
dirty bool
}
// Engine handles DMX channel remapping
type Engine struct {
mappings []config.NormalizedMapping
bySource map[config.Universe]*sourceEntry
outputs map[config.Universe]*universeBuffer
}
// NewEngine creates a new remapping engine
func NewEngine(mappings []config.NormalizedMapping) *Engine {
bySource := map[config.Universe]*sourceEntry{}
for _, m := range mappings {
entry := bySource[m.From]
if entry == nil {
entry = &sourceEntry{}
bySource[m.From] = entry
}
entry.mappings = append(entry.mappings, m)
}
outputs := map[config.Universe]*universeBuffer{}
for _, m := range mappings {
if _, ok := outputs[m.To]; !ok {
outputs[m.To] = &universeBuffer{}
}
}
return &Engine{
mappings: mappings,
bySource: bySource,
outputs: outputs,
}
}
// Remap applies mappings to incoming DMX data and marks affected outputs dirty
func (e *Engine) Remap(src config.Universe, srcData [512]byte) {
entry := e.bySource[src]
if entry == nil {
return
}
entry.counter.Add(1)
for _, m := range entry.mappings {
e.applyMapping(m, srcData)
}
}
func (e *Engine) applyMapping(m config.NormalizedMapping, srcData [512]byte) {
buf := e.outputs[m.To]
buf.mu.Lock()
defer buf.mu.Unlock()
for i := 0; i < m.Count; i++ {
srcChan := m.FromChan + i
dstChan := m.ToChan + i
if srcChan < 512 && dstChan < 512 {
buf.data[dstChan] = srcData[srcChan]
}
}
buf.dirty = true
}
// GetDirtyOutputs returns outputs that have been modified since last call
func (e *Engine) GetDirtyOutputs() []Output {
var result []Output
for u, buf := range e.outputs {
if out, ok := e.getDirtyOutput(u, buf); ok {
result = append(result, out)
}
}
return result
}
func (e *Engine) getDirtyOutput(u config.Universe, buf *universeBuffer) (Output, bool) {
buf.mu.Lock()
defer buf.mu.Unlock()
if !buf.dirty {
return Output{}, false
}
buf.dirty = false
return Output{Universe: u, Data: buf.data}, true
}
// SwapStats returns packet counts per source universe since last call and resets them
func (e *Engine) SwapStats() map[config.Universe]uint64 {
result := map[config.Universe]uint64{}
for u, entry := range e.bySource {
result[u] = entry.counter.Swap(0)
}
return result
}
// SourceArtNetUniverses returns source ArtNet universe numbers (for discovery)
func (e *Engine) SourceArtNetUniverses() []uint16 {
seen := make(map[uint16]bool)
for _, m := range e.mappings {
if m.From.Protocol == config.ProtocolArtNet {
seen[m.From.Number] = true
}
}
result := make([]uint16, 0, len(seen))
for u := range seen {
result = append(result, u)
}
return result
}
func (e *Engine) DestArtNetUniverses() []uint16 {
seen := make(map[uint16]bool)
for _, m := range e.mappings {
if m.To.Protocol == config.ProtocolArtNet {
seen[m.To.Number] = true
}
}
result := make([]uint16, 0, len(seen))
for u := range seen {
result = append(result, u)
}
return result
}
func (e *Engine) DestSACNUniverses() []uint16 {
seen := make(map[uint16]bool)
for _, m := range e.mappings {
if m.To.Protocol == config.ProtocolSACN {
seen[m.To.Number] = true
}
}
result := make([]uint16, 0, len(seen))
for u := range seen {
result = append(result, u)
}
return result
}