Track DMX packet senders and expose via API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-30 22:11:34 -08:00
parent d91e5918a4
commit 1ba098ee24
2 changed files with 93 additions and 1 deletions

27
main.go
View File

@@ -17,6 +17,7 @@ import (
"github.com/gopatchy/artnet" "github.com/gopatchy/artnet"
"github.com/gopatchy/artmap/config" "github.com/gopatchy/artmap/config"
"github.com/gopatchy/artmap/remap" "github.com/gopatchy/artmap/remap"
"github.com/gopatchy/artmap/senders"
"github.com/gopatchy/sacn" "github.com/gopatchy/sacn"
) )
@@ -28,6 +29,7 @@ type App struct {
sacnSender *sacn.Sender sacnSender *sacn.Sender
discovery *artnet.Discovery discovery *artnet.Discovery
engine *remap.Engine engine *remap.Engine
senders *senders.UniverseSenders
artTargets map[uint16]*net.UDPAddr artTargets map[uint16]*net.UDPAddr
sacnTargets map[uint16][]*net.UDPAddr sacnTargets map[uint16][]*net.UDPAddr
debug bool debug bool
@@ -144,6 +146,7 @@ func main() {
sacnSender: sacnSender, sacnSender: sacnSender,
discovery: discovery, discovery: discovery,
engine: engine, engine: engine,
senders: senders.New(),
artTargets: artTargets, artTargets: artTargets,
sacnTargets: sacnTargets, sacnTargets: sacnTargets,
debug: *debug, debug: *debug,
@@ -216,6 +219,15 @@ func main() {
} }
}() }()
// Start sender expiration
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
app.senders.Expire(30 * time.Second)
}
}()
// Wait for interrupt // Wait for interrupt
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
@@ -238,6 +250,7 @@ func (a *App) HandleDMX(src *net.UDPAddr, pkt *artnet.DMXPacket) {
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)} u := config.Universe{Protocol: config.ProtocolArtNet, Number: uint16(pkt.Universe)}
a.senders.Record(u, src.IP)
a.sendOutputs(a.engine.Remap(u, pkt.Data)) a.sendOutputs(a.engine.Remap(u, pkt.Data))
} }
@@ -263,6 +276,7 @@ func (a *App) HandleSACN(src *net.UDPAddr, pkt *sacn.DataPacket) {
log.Printf("[<-sacn] src=%s universe=%d seq=%d", src.IP, pkt.Universe, pkt.Sequence) log.Printf("[<-sacn] src=%s universe=%d seq=%d", src.IP, pkt.Universe, pkt.Sequence)
} }
u := config.Universe{Protocol: config.ProtocolSACN, Number: pkt.Universe} u := config.Universe{Protocol: config.ProtocolSACN, Number: pkt.Universe}
a.senders.Record(u, src.IP)
a.sendOutputs(a.engine.Remap(u, pkt.Data)) a.sendOutputs(a.engine.Remap(u, pkt.Data))
} }
@@ -314,10 +328,21 @@ func (a *App) sendOutputs(outputs []remap.Output) {
} }
} }
type configResponse struct {
Targets []config.Target `json:"targets"`
Mappings []config.Mapping `json:"mappings"`
Senders []senders.SenderInfo `json:"senders"`
}
func (a *App) handleConfig(w http.ResponseWriter, r *http.Request) { func (a *App) handleConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Server", "artmap") w.Header().Set("Server", "artmap")
json.NewEncoder(w).Encode(a.cfg) resp := configResponse{
Targets: a.cfg.Targets,
Mappings: a.cfg.Mappings,
Senders: a.senders.GetAll(),
}
json.NewEncoder(w).Encode(resp)
} }
func (a *App) printStats() { func (a *App) printStats() {

67
senders/senders.go Normal file
View File

@@ -0,0 +1,67 @@
package senders
import (
"net"
"sync"
"time"
"github.com/gopatchy/artmap/config"
)
type SenderInfo struct {
Universe config.Universe `json:"universe"`
IP string `json:"ip"`
}
type senderKey struct {
protocol config.Protocol
universe uint16
ip string
}
type UniverseSenders struct {
mu sync.Mutex
entries map[senderKey]time.Time
}
func New() *UniverseSenders {
return &UniverseSenders{
entries: map[senderKey]time.Time{},
}
}
func (s *UniverseSenders) Record(u config.Universe, ip net.IP) {
key := senderKey{
protocol: u.Protocol,
universe: u.Number,
ip: ip.String(),
}
s.mu.Lock()
s.entries[key] = time.Now()
s.mu.Unlock()
}
func (s *UniverseSenders) Expire(maxAge time.Duration) {
cutoff := time.Now().Add(-maxAge)
s.mu.Lock()
for k, t := range s.entries {
if t.Before(cutoff) {
delete(s.entries, k)
}
}
s.mu.Unlock()
}
func (s *UniverseSenders) GetAll() []SenderInfo {
s.mu.Lock()
defer s.mu.Unlock()
result := make([]SenderInfo, 0, len(s.entries))
for k := range s.entries {
result = append(result, SenderInfo{
Universe: config.Universe{Protocol: k.protocol, Number: k.universe},
IP: k.ip,
})
}
return result
}