Initial implementation of ArtNet remapping proxy

Channel-level DMX remapping between ArtNet universes with:
- TOML configuration with multiple address formats (net.subnet.universe, plain number)
- ArtPoll discovery for output nodes
- Configurable channel ranges for fixture spillover handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2025-12-22 09:27:20 -08:00
commit a709e5498b
11 changed files with 1194 additions and 0 deletions

103
artnet/receiver.go Normal file
View File

@@ -0,0 +1,103 @@
package artnet
import (
"log"
"net"
)
// PacketHandler is called when a packet is received
type PacketHandler interface {
HandleDMX(src *net.UDPAddr, pkt *DMXPacket)
HandlePoll(src *net.UDPAddr, pkt *PollPacket)
HandlePollReply(src *net.UDPAddr, pkt *PollReplyPacket)
}
// Receiver listens for ArtNet packets
type Receiver struct {
conn *net.UDPConn
handler PacketHandler
done chan struct{}
}
// NewReceiver creates a new ArtNet receiver
func NewReceiver(port int, handler PacketHandler) (*Receiver, error) {
addr := &net.UDPAddr{
Port: port,
IP: net.IPv4zero,
}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return nil, err
}
return &Receiver{
conn: conn,
handler: handler,
done: make(chan struct{}),
}, nil
}
// Start begins receiving packets
func (r *Receiver) Start() {
go r.receiveLoop()
}
// Stop stops the receiver
func (r *Receiver) Stop() {
close(r.done)
r.conn.Close()
}
func (r *Receiver) receiveLoop() {
buf := make([]byte, 1024)
for {
select {
case <-r.done:
return
default:
}
n, src, err := r.conn.ReadFromUDP(buf)
if err != nil {
select {
case <-r.done:
return
default:
log.Printf("read error: %v", err)
continue
}
}
r.handlePacket(src, buf[:n])
}
}
func (r *Receiver) handlePacket(src *net.UDPAddr, data []byte) {
opCode, pkt, err := ParsePacket(data)
if err != nil {
// Silently ignore invalid packets
return
}
switch opCode {
case OpDmx:
if dmx, ok := pkt.(*DMXPacket); ok {
r.handler.HandleDMX(src, dmx)
}
case OpPoll:
if poll, ok := pkt.(*PollPacket); ok {
r.handler.HandlePoll(src, poll)
}
case OpPollReply:
if reply, ok := pkt.(*PollReplyPacket); ok {
r.handler.HandlePollReply(src, reply)
}
}
}
// LocalAddr returns the local address the receiver is bound to
func (r *Receiver) LocalAddr() net.Addr {
return r.conn.LocalAddr()
}