2025-12-22 09:27:20 -08:00
|
|
|
package remap
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-22 12:32:41 -08:00
|
|
|
"sync"
|
2026-01-30 12:11:37 -08:00
|
|
|
"sync/atomic"
|
2025-12-22 12:32:41 -08:00
|
|
|
|
2025-12-22 09:27:20 -08:00
|
|
|
"github.com/gopatchy/artmap/config"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Output represents a remapped DMX output
|
|
|
|
|
type Output struct {
|
2026-01-27 15:25:10 -08:00
|
|
|
Universe config.Universe
|
2025-12-22 09:27:20 -08:00
|
|
|
Data [512]byte
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 12:11:37 -08:00
|
|
|
// sourceEntry holds mappings and stats for a source universe
|
|
|
|
|
type sourceEntry struct {
|
|
|
|
|
mappings []config.NormalizedMapping
|
|
|
|
|
counter atomic.Uint64
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
// universeBuffer holds per-output-universe state with its own lock
|
|
|
|
|
type universeBuffer struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
data [512]byte
|
|
|
|
|
dirty bool
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 09:27:20 -08:00
|
|
|
// Engine handles DMX channel remapping
|
|
|
|
|
type Engine struct {
|
|
|
|
|
mappings []config.NormalizedMapping
|
2026-01-30 12:11:37 -08:00
|
|
|
bySource map[config.Universe]*sourceEntry
|
2026-01-31 07:31:12 -08:00
|
|
|
outputs map[config.Universe]*universeBuffer
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewEngine creates a new remapping engine
|
|
|
|
|
func NewEngine(mappings []config.NormalizedMapping) *Engine {
|
2026-01-30 12:11:37 -08:00
|
|
|
bySource := map[config.Universe]*sourceEntry{}
|
2025-12-22 12:32:41 -08:00
|
|
|
for _, m := range mappings {
|
2026-01-30 12:11:37 -08:00
|
|
|
entry := bySource[m.From]
|
|
|
|
|
if entry == nil {
|
|
|
|
|
entry = &sourceEntry{}
|
|
|
|
|
bySource[m.From] = entry
|
|
|
|
|
}
|
|
|
|
|
entry.mappings = append(entry.mappings, m)
|
2025-12-22 12:32:41 -08:00
|
|
|
}
|
|
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
outputs := map[config.Universe]*universeBuffer{}
|
2025-12-22 09:27:20 -08:00
|
|
|
for _, m := range mappings {
|
2026-01-31 07:31:12 -08:00
|
|
|
if _, ok := outputs[m.To]; !ok {
|
|
|
|
|
outputs[m.To] = &universeBuffer{}
|
2025-12-22 12:32:41 -08:00
|
|
|
}
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Engine{
|
|
|
|
|
mappings: mappings,
|
|
|
|
|
bySource: bySource,
|
2026-01-31 07:31:12 -08:00
|
|
|
outputs: outputs,
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
// Remap applies mappings to incoming DMX data and marks affected outputs dirty
|
|
|
|
|
func (e *Engine) Remap(src config.Universe, srcData [512]byte) {
|
2026-01-30 12:11:37 -08:00
|
|
|
entry := e.bySource[src]
|
|
|
|
|
if entry == nil {
|
2026-01-31 07:31:12 -08:00
|
|
|
return
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
2026-01-30 12:11:37 -08:00
|
|
|
entry.counter.Add(1)
|
2025-12-22 09:27:20 -08:00
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
for _, m := range entry.mappings {
|
|
|
|
|
e.applyMapping(m, srcData)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-22 12:32:41 -08:00
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
func (e *Engine) applyMapping(m config.NormalizedMapping, srcData [512]byte) {
|
|
|
|
|
buf := e.outputs[m.To]
|
|
|
|
|
buf.mu.Lock()
|
|
|
|
|
defer buf.mu.Unlock()
|
2025-12-22 09:27:20 -08:00
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
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]
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-31 07:31:12 -08:00
|
|
|
buf.dirty = true
|
|
|
|
|
}
|
2025-12-22 09:27:20 -08:00
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
// 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)
|
|
|
|
|
}
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-31 07:31:12 -08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 12:11:37 -08:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 15:54:32 -08:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 15:25:10 -08:00
|
|
|
func (e *Engine) DestArtNetUniverses() []uint16 {
|
|
|
|
|
seen := make(map[uint16]bool)
|
2025-12-22 09:27:20 -08:00
|
|
|
for _, m := range e.mappings {
|
2026-01-27 15:25:10 -08:00
|
|
|
if m.To.Protocol == config.ProtocolArtNet {
|
|
|
|
|
seen[m.To.Number] = true
|
2025-12-22 12:32:41 -08:00
|
|
|
}
|
2025-12-22 09:27:20 -08:00
|
|
|
}
|
2026-01-27 15:25:10 -08:00
|
|
|
result := make([]uint16, 0, len(seen))
|
2025-12-22 09:27:20 -08:00
|
|
|
for u := range seen {
|
|
|
|
|
result = append(result, u)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
2026-01-28 21:13:22 -08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|