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

92
artnet/sender.go Normal file
View File

@@ -0,0 +1,92 @@
package artnet
import (
"net"
"sync"
)
// Sender sends ArtNet packets
type Sender struct {
conn *net.UDPConn
broadcastAddr *net.UDPAddr
sequences map[Universe]uint8
seqMu sync.Mutex
}
// NewSender creates a new ArtNet sender
func NewSender(broadcastAddr string) (*Sender, error) {
// Create a UDP socket for sending
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return nil, err
}
// Enable broadcast
if err := conn.SetWriteBuffer(65536); err != nil {
conn.Close()
return nil, err
}
broadcast, err := net.ResolveUDPAddr("udp4", broadcastAddr+":"+string(rune(Port)))
if err != nil {
// Try parsing as IP:Port
broadcast, err = net.ResolveUDPAddr("udp4", broadcastAddr)
if err != nil {
broadcast = &net.UDPAddr{
IP: net.ParseIP(broadcastAddr),
Port: Port,
}
}
}
return &Sender{
conn: conn,
broadcastAddr: broadcast,
sequences: make(map[Universe]uint8),
}, nil
}
// SendDMX sends a DMX packet to a specific address
func (s *Sender) SendDMX(addr *net.UDPAddr, universe Universe, data []byte) error {
s.seqMu.Lock()
seq := s.sequences[universe]
seq++
if seq == 0 {
seq = 1 // Skip 0
}
s.sequences[universe] = seq
s.seqMu.Unlock()
pkt := BuildDMXPacket(universe, seq, data)
_, err := s.conn.WriteToUDP(pkt, addr)
return err
}
// SendDMXBroadcast sends a DMX packet to the broadcast address
func (s *Sender) SendDMXBroadcast(universe Universe, data []byte) error {
return s.SendDMX(s.broadcastAddr, universe, data)
}
// SendPoll sends an ArtPoll packet to the broadcast address
func (s *Sender) SendPoll() error {
pkt := BuildPollPacket()
_, err := s.conn.WriteToUDP(pkt, s.broadcastAddr)
return err
}
// SendPollReply sends an ArtPollReply to a specific address
func (s *Sender) SendPollReply(addr *net.UDPAddr, localIP [4]byte, shortName, longName string, universes []Universe) error {
pkt := BuildPollReplyPacket(localIP, shortName, longName, universes)
_, err := s.conn.WriteToUDP(pkt, addr)
return err
}
// Close closes the sender
func (s *Sender) Close() error {
return s.conn.Close()
}
// BroadcastAddr returns the configured broadcast address
func (s *Sender) BroadcastAddr() *net.UDPAddr {
return s.broadcastAddr
}